Skip to content

Commit 5ac0a6f

Browse files
authored
Merge pull request #603 from ryantrinh05/master
Crib Sheet Portal and Drive Storage
2 parents e39741d + b11a3b0 commit 5ac0a6f

18 files changed

Lines changed: 850 additions & 3 deletions

File tree

hknweb/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Committee,
1515
Election,
1616
Committeeship,
17+
DriveFolderID,
1718
)
1819
from hknweb.forms import ProvisionCandidatesForm
1920

@@ -155,3 +156,4 @@ class CommitteeshipAdmin(admin.ModelAdmin):
155156
admin.site.register(CandidateProvisioningPassword)
156157
admin.site.register(Committee)
157158
admin.site.register(Election)
159+
admin.site.register(DriveFolderID)

hknweb/google_drive_utils.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
from functools import wraps
2+
import google.oauth2.service_account as service_account
3+
from googleapiclient.discovery import build
4+
from googleapiclient.errors import HttpError
5+
from googleapiclient.http import MediaIoBaseUpload
6+
from django.core.exceptions import ImproperlyConfigured
7+
8+
9+
from pathlib import Path
10+
import os
11+
import json
12+
13+
# Thank you Brian Yu as much of this code and framework is taken from his work on the google calendar
14+
15+
# Alter the path to a server environment path
16+
17+
18+
def get_credentials(): # pragma: no cover
19+
"""
20+
Gets the google drive service account's credentials from the server.
21+
Should not be called anywhere else.
22+
23+
Returns:
24+
google.oauth2.service_acount.Credentials
25+
"""
26+
SCOPE = ["https://www.googleapis.com/auth/drive"]
27+
28+
json_env = os.getenv("GOOGLE_DRIVE_CREDENTIALS_JSON")
29+
30+
if not json_env:
31+
raise ImproperlyConfigured(
32+
"No Drive credentials found in environment variables."
33+
)
34+
35+
try:
36+
info = json.loads(json_env)
37+
except json.JSONDecodeError:
38+
raise ImproperlyConfigured("env doesn't contain a valid JSON")
39+
40+
creds = service_account.Credentials.from_service_account_info(info, scopes=SCOPE)
41+
42+
return creds
43+
44+
45+
def check_credentials_wrapper(fn):
46+
@wraps(fn)
47+
def new_fn(*args, **kwargs):
48+
get_credentials()
49+
return fn(*args, **kwargs)
50+
51+
return new_fn
52+
53+
54+
@check_credentials_wrapper
55+
def get_service(): # pragma: no cover
56+
creds = get_credentials()
57+
service = build("drive", "v3", credentials=creds)
58+
59+
return service
60+
61+
62+
def create_metadata(
63+
name: str,
64+
mimeType: str,
65+
parents: list,
66+
description: str,
67+
) -> dict:
68+
data = dict()
69+
data["name"] = name
70+
data["mimeType"] = mimeType
71+
72+
if description is not None:
73+
data["description"] = description
74+
75+
if parents is not None:
76+
data["parents"] = parents
77+
78+
return data
79+
80+
81+
def create_folder(
82+
name: str,
83+
parents: list = None,
84+
description: str = None,
85+
) -> dict: # pragma: no cover
86+
folder_resource = create_metadata(
87+
name=name,
88+
mimeType="application/vnd.google-apps.folder",
89+
description=description,
90+
parents=parents,
91+
)
92+
93+
try:
94+
folder = (
95+
get_service()
96+
.files()
97+
.create(body=folder_resource, fields="id", supportsAllDrives=True)
98+
.execute()
99+
)
100+
101+
return {"status": True, "result": folder["id"]}
102+
except HttpError as e:
103+
if e.resp.status == 403:
104+
return {
105+
"status": False,
106+
"result": "Service Account: Insufficent Permissions",
107+
}
108+
else:
109+
raise
110+
111+
112+
def create_pdf(
113+
name: str,
114+
file,
115+
parents: list = None,
116+
description: str = None,
117+
) -> dict: # pragma: no cover
118+
pdf_resource = create_metadata(
119+
name=name,
120+
mimeType="application/pdf",
121+
description=description,
122+
parents=parents,
123+
)
124+
125+
try:
126+
file.seek(0)
127+
128+
media = MediaIoBaseUpload(file, mimetype="application/pdf")
129+
130+
pdf = (
131+
get_service()
132+
.files()
133+
.create(
134+
body=pdf_resource, media_body=media, fields="id", supportsAllDrives=True
135+
)
136+
.execute()
137+
)
138+
139+
return {"status": True, "result": pdf["id"]}
140+
except HttpError as e:
141+
if e.resp.status == 403:
142+
return {
143+
"status": False,
144+
"result": "Service Account: Insufficient Permissions",
145+
}
146+
else:
147+
raise
148+
149+
150+
def create_permission(
151+
fileID: str,
152+
typeID: str,
153+
role: str,
154+
emailAddress: str = None,
155+
domain: str = None,
156+
) -> dict: # pragma: no cover
157+
body = {
158+
"type": typeID,
159+
"role": role,
160+
}
161+
if typeID in ["user", "group"]:
162+
if not emailAddress:
163+
raise ValueError(
164+
"Email Address required for 'user' and 'group' permissions"
165+
)
166+
body["emailAddress"] = emailAddress
167+
elif typeID == "domain":
168+
if not domain:
169+
raise ValueError("Domain required for 'domain' permissions")
170+
body["domain"] = domain
171+
body["allowFileDiscovery"] = False
172+
elif typeID == "anyone":
173+
body["allowFileDiscovery"] = False
174+
175+
try:
176+
permission = (
177+
get_service()
178+
.permissions()
179+
.create(fileId=fileID, body=body, fields="id", supportsAllDrives=True)
180+
.execute()
181+
)
182+
return {"status": True, "id": permission["id"]}
183+
except HttpError as e:
184+
if e.resp.status == 403:
185+
return {"status": False}
186+
else:
187+
raise
188+
189+
190+
def delete_permission(
191+
fileID: str, typeID: str, role: str, emailAddress: str = None, domain: str = None
192+
) -> dict: # pragma: no cover
193+
permissionID = get_permission_id(fileID, typeID, role, emailAddress, domain)
194+
if not permissionID:
195+
return {"status": False, "result": "No permission found"}
196+
try:
197+
deletion = (
198+
get_service()
199+
.permissions()
200+
.delete(fileId=fileID, permissionId=permissionID, supportsAllDrives=True)
201+
.execute()
202+
)
203+
return {"status": True}
204+
except HttpError as e:
205+
if e.resp.status == 403:
206+
return {
207+
"status": False,
208+
"result": "Service Account: Insufficent Permissions",
209+
}
210+
else:
211+
raise
212+
213+
214+
def update_permission(
215+
fileID: str,
216+
typeID: str,
217+
role: str,
218+
new_role: str,
219+
emailAddress: str = None,
220+
domain: str = None,
221+
) -> dict: # pragma: no cover
222+
permissionID = get_permission_id(fileID, typeID, role, emailAddress, domain)
223+
if not permissionID:
224+
return {"status": False, "result": "No permission found"}
225+
try:
226+
update = (
227+
get_service()
228+
.permissions()
229+
.update(
230+
fileId=fileID,
231+
permissionId=permissionID,
232+
supportsAllDrives=True,
233+
body={"role": new_role},
234+
)
235+
.execute()
236+
)
237+
return {"status": True}
238+
except HttpError as e:
239+
if e.resp.status == 403:
240+
return {
241+
"status": False,
242+
"result": "Service Account: Insufficent Permissions",
243+
}
244+
else:
245+
raise
246+
247+
248+
def get_permission_id(
249+
fileID: str, typeID: str, role: str, emailAddress: str = None, domain: str = None
250+
) -> str: # pragma: no cover
251+
permissions = (
252+
get_service()
253+
.permissions()
254+
.list(
255+
fileId=fileID,
256+
fields="permissions(id,type,role,emailAddress,domain)",
257+
supportsAllDrives=True,
258+
)
259+
.execute()
260+
.get("permissions", [])
261+
)
262+
263+
for p in permissions:
264+
if p["type"] != typeID:
265+
continue
266+
if p["role"] != role:
267+
continue
268+
if typeID in ["user", "group"] and p["emailAddress"] != emailAddress:
269+
continue
270+
if typeID == "domain" and p["domain"] != domain:
271+
continue
272+
return p["id"]
273+
return None
274+
275+
276+
def get_files(folderID: str, mimeType: str = None) -> str: # pragma: no cover
277+
query = f"'{folderID}' in parents and trashed = false"
278+
279+
if mimeType:
280+
query += f" and mimeType = '{mimeType}'"
281+
282+
files = (
283+
get_service()
284+
.files()
285+
.list(
286+
q=query,
287+
fields="files(id, name, mimeType)",
288+
supportsAllDrives=True,
289+
includeItemsFromAllDrives=True,
290+
spaces="drive",
291+
)
292+
.execute()
293+
)
294+
295+
return files.get("files", [])
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 4.2.17 on 2026-01-28 19:39
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("hknweb", "0024_profile_preferred_courses"),
9+
]
10+
11+
operations = [
12+
migrations.CreateModel(
13+
name="DriveFolderID",
14+
fields=[
15+
(
16+
"id",
17+
models.AutoField(
18+
auto_created=True,
19+
primary_key=True,
20+
serialize=False,
21+
verbose_name="ID",
22+
),
23+
),
24+
("title", models.CharField(max_length=100)),
25+
("folderID", models.CharField(max_length=50)),
26+
],
27+
),
28+
]

hknweb/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,12 @@ def people(self) -> Dict[str, "QuerySet[User]"]:
133133
"Assistant Officer": self.assistant_officers.all(),
134134
"Committee Member": self.committee_members.all(),
135135
}
136+
137+
138+
class DriveFolderID(models.Model):
139+
title = models.CharField(max_length=100)
140+
141+
folderID = models.CharField(max_length=50)
142+
143+
def __str__(self) -> str: # pragma: no cover
144+
return self.title

hknweb/studentservices/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class CourseDescriptionAdmin(admin.ModelAdmin):
7878
fields = (
7979
"title",
8080
"slug",
81+
"folderID",
8182
"description",
8283
"quick_links",
8384
"topics_covered",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.17 on 2026-01-28 19:39
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("studentservices", "0013_alter_coursedescription_description_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="coursedescription",
14+
name="folderID",
15+
field=models.CharField(blank=True, max_length=50),
16+
),
17+
]

hknweb/studentservices/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ class CourseDescription(models.Model):
107107
topics_covered = MarkdownxField(max_length=2000, blank=True)
108108
more_info = MarkdownxField(max_length=10000, blank=True)
109109

110+
folderID = models.CharField(max_length=50, blank=True)
111+
110112
created_at = models.DateTimeField(auto_now_add=True)
111113
updated_at = models.DateTimeField(auto_now=True)
112114

0 commit comments

Comments
 (0)