Skip to content

Commit 84eafaa

Browse files
Self-host images for assignments (#105)
ImgBB is blocked on fcps computers. --------- Co-authored-by: Krishnan Shankar <krishnans2006@gmail.com>
1 parent d3d73e6 commit 84eafaa

7 files changed

Lines changed: 168 additions & 127 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ media/
77

88
tin/settings/secret.py
99
tin/serve
10+
tin/static/uploaded-media
1011

1112
.idea/
1213
docs/build/

tin/apps/assignments/forms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from django import forms
77
from django.conf import settings
8+
from django.core.exceptions import ValidationError
89

910
from ..submissions.models import Submission
1011
from .models import Assignment, Folder, MossResult
@@ -241,3 +242,13 @@ class Meta:
241242
"name",
242243
]
243244
help_texts = {"name": "Note: Folders are ordered alphabetically."}
245+
246+
247+
class ImageForm(forms.Form):
248+
image = forms.FileField()
249+
250+
def clean_image(self):
251+
image = self.cleaned_data.get("image")
252+
if image and image.size > settings.MAX_UPLOADED_IMAGE_SIZE:
253+
raise ValidationError("Image size exceeds the maximum limit")
254+
return image

tin/apps/assignments/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@
5656
views.delete_folder_view,
5757
name="delete_folder",
5858
),
59+
path("upload", views.upload_image, name="upload_image"),
5960
]

tin/apps/assignments/views.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import logging
66
import os
77
import subprocess
8+
import uuid
89
import zipfile
910
from io import BytesIO
11+
from pathlib import Path
1012

1113
import celery
1214
from django import http
@@ -16,6 +18,7 @@
1618
from django.urls import reverse
1719
from django.utils.text import slugify
1820
from django.utils.timezone import now
21+
from django.views.decorators.http import require_POST
1922

2023
from ... import sandboxing
2124
from ..auth.decorators import login_required, teacher_or_superuser_required
@@ -29,6 +32,7 @@
2932
FileUploadForm,
3033
FolderForm,
3134
GraderScriptUploadForm,
35+
ImageForm,
3236
MossForm,
3337
TextSubmissionForm,
3438
)
@@ -236,7 +240,6 @@ def create_view(request, course_id):
236240
"course": course,
237241
"nav_item": "Create assignment",
238242
"action": "add",
239-
"imgbb_api_key": settings.IMGBB_API_KEY,
240243
},
241244
)
242245

@@ -272,7 +275,6 @@ def edit_view(request, assignment_id):
272275
"assignment": assignment,
273276
"nav_item": "Edit",
274277
"action": "edit",
275-
"imgbb_api_key": settings.IMGBB_API_KEY,
276278
},
277279
)
278280

@@ -1181,3 +1183,30 @@ def delete_folder_view(request, course_id, folder_id):
11811183

11821184
folder.delete()
11831185
return redirect("courses:show", course.id)
1186+
1187+
1188+
@require_POST
1189+
@teacher_or_superuser_required
1190+
def upload_image(request):
1191+
form = ImageForm(request.POST, request.FILES)
1192+
if form.is_valid():
1193+
image = form.cleaned_data["image"]
1194+
image_name = Path(image.name)
1195+
uuid_image_name = image_name.with_stem(f"{image_name.stem}-{uuid.uuid4()}")
1196+
static_location = Path("uploaded-media") / uuid_image_name
1197+
dest = (
1198+
Path(settings.STATICFILES_DIRS[0]) if settings.DEBUG else Path(settings.STATIC_ROOT)
1199+
) / static_location
1200+
dest.parent.mkdir(exist_ok=True)
1201+
with dest.open("wb+") as f:
1202+
for chunk in image.chunks():
1203+
f.write(chunk)
1204+
return http.JsonResponse(
1205+
{
1206+
"url": request.build_absolute_uri(
1207+
f"{settings.STATIC_URL.removesuffix('/')}/{static_location}"
1208+
),
1209+
"title": image_name.stem,
1210+
}
1211+
)
1212+
return http.JsonResponse(form.errors, status=400)

tin/settings/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,8 @@
344344
# Threshold for log messages being issues
345345
QUIZ_ISSUE_THRESHOLD = 5
346346

347-
# ImgBB API key (set in secret.py)
348-
IMGBB_API_KEY = ""
347+
# Maximum size of uploaded images in bytes
348+
MAX_UPLOADED_IMAGE_SIZE = float("inf")
349349

350350
# Sandboxing
351351

tin/static/js/markdown-image-dragdrop.js

Lines changed: 119 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -19,144 +19,142 @@ function insertAtCursor(myField, myValue) {
1919
}
2020

2121
$(function () {
22-
if (IMGBB_API_KEY) {
23-
const markdownToggle = $('#id_markdown');
24-
const description = $('#description');
25-
const descriptionParent = description.parent();
22+
const markdownToggle = $('#id_markdown');
23+
const description = $('#description');
24+
const descriptionParent = description.parent();
2625

27-
descriptionParent.css({
28-
position: 'relative',
29-
});
26+
descriptionParent.css({
27+
position: 'relative',
28+
});
3029

31-
const uploadOverlay = $('<div>')
32-
.css({
33-
display: 'none',
34-
position: 'absolute',
35-
top: 0,
36-
left: 0,
37-
width: '97%',
38-
height: '100%',
39-
'background-color': 'rgba(0, 0, 0, 0.2)',
40-
'z-index': 1000,
41-
'text-align': 'center',
42-
border: '10px dashed #000',
43-
padding: '30px 20px 0px 20px',
44-
'font-size': '250%',
45-
'box-sizing': 'border-box',
46-
})
47-
.text('Drop to embed')
48-
.appendTo(descriptionParent);
30+
const uploadOverlay = $('<div>')
31+
.css({
32+
display: 'none',
33+
position: 'absolute',
34+
top: 0,
35+
left: 0,
36+
width: '97%',
37+
height: '100%',
38+
'background-color': 'rgba(0, 0, 0, 0.2)',
39+
'z-index': 1000,
40+
'text-align': 'center',
41+
border: '10px dashed #000',
42+
padding: '30px 20px 0px 20px',
43+
'font-size': '250%',
44+
'box-sizing': 'border-box',
45+
})
46+
.text('Drop to embed')
47+
.appendTo(descriptionParent);
4948

50-
descriptionParent.on({
51-
dragover: function (e) {
52-
if (markdownToggle.prop('checked')) {
53-
e.preventDefault();
49+
descriptionParent.on({
50+
dragover: function (e) {
51+
if (markdownToggle.prop('checked')) {
52+
e.preventDefault();
53+
}
54+
},
55+
dragenter: function (e) {
56+
if (markdownToggle.prop('checked')) {
57+
const dt = e.originalEvent.dataTransfer;
58+
if (dt.types.includes('Files')) {
59+
uploadOverlay.stop().fadeIn(150);
5460
}
55-
},
56-
dragenter: function (e) {
57-
if (markdownToggle.prop('checked')) {
58-
const dt = e.originalEvent.dataTransfer;
59-
if (dt.types.includes('Files')) {
60-
uploadOverlay.stop().fadeIn(150);
61+
}
62+
},
63+
drop: function (e) {
64+
if (markdownToggle.prop('checked')) {
65+
const dt = e.originalEvent.dataTransfer;
66+
e.preventDefault();
67+
if (dt && dt.files.length) {
68+
if (dt.files.length != 1) {
69+
alert('Please only upload one file at a time.');
70+
return;
6171
}
62-
}
63-
},
64-
drop: function (e) {
65-
if (markdownToggle.prop('checked')) {
66-
const dt = e.originalEvent.dataTransfer;
67-
e.preventDefault();
68-
if (dt && dt.files.length) {
69-
if (dt.files.length != 1) {
70-
alert('Please only upload one file at a time.');
71-
return;
72-
}
7372

74-
let data = new FormData();
75-
data.append('key', IMGBB_API_KEY);
76-
data.append('image', dt.files[0]);
73+
let data = new FormData();
74+
data.append('image', dt.files[0]);
75+
data.append('csrfmiddlewaretoken', Cookies.get('csrftoken'));
7776

78-
$.ajax({
79-
url: 'https://api.imgbb.com/1/upload',
80-
method: 'POST',
81-
data: data,
82-
processData: false,
83-
contentType: false,
84-
}).done(function (data) {
85-
const name = data.data.title;
86-
const url = data.data.url;
87-
insertAtCursor(description[0], `![${name}](${url})`);
88-
});
89-
}
90-
uploadOverlay.stop().fadeOut(150);
77+
$.ajax({
78+
url: '/assignments/upload',
79+
method: 'POST',
80+
data: data,
81+
processData: false,
82+
contentType: false,
83+
}).done(function (data) {
84+
const name = data.title;
85+
const url = data.url;
86+
insertAtCursor(description[0], `![${name}](${url})`);
87+
});
9188
}
92-
},
93-
});
89+
uploadOverlay.stop().fadeOut(150);
90+
}
91+
},
92+
});
9493

95-
uploadOverlay.on('dragleave', function () {
96-
uploadOverlay.stop().fadeOut(150);
97-
});
94+
uploadOverlay.on('dragleave', function () {
95+
uploadOverlay.stop().fadeOut(150);
96+
});
9897

99-
// This is basically a copy-paste of the above code, but for the quiz description
100-
// I'm not sure how to refactor this to completely avoid code duplication, but I've made an attempt
101-
const quizMarkdownToggle = $('#id_quiz_description_markdown');
102-
const quizDescription = $('#id_quiz_description');
103-
const quizDescriptionParent = quizDescription.parent();
98+
// This is basically a copy-paste of the above code, but for the quiz description
99+
// I'm not sure how to refactor this to completely avoid code duplication, but I've made an attempt
100+
const quizMarkdownToggle = $('#id_quiz_description_markdown');
101+
const quizDescription = $('#id_quiz_description');
102+
const quizDescriptionParent = quizDescription.parent();
104103

105-
quizDescriptionParent.css({
106-
position: 'relative',
107-
});
104+
quizDescriptionParent.css({
105+
position: 'relative',
106+
});
108107

109-
const quizUploadOverlay = uploadOverlay
110-
.clone()
111-
.appendTo(quizDescriptionParent);
108+
const quizUploadOverlay = uploadOverlay
109+
.clone()
110+
.appendTo(quizDescriptionParent);
112111

113-
quizDescriptionParent.on({
114-
dragover: function (e) {
115-
if (quizMarkdownToggle.prop('checked')) {
116-
e.preventDefault();
112+
quizDescriptionParent.on({
113+
dragover: function (e) {
114+
if (quizMarkdownToggle.prop('checked')) {
115+
e.preventDefault();
116+
}
117+
},
118+
dragenter: function (e) {
119+
if (quizMarkdownToggle.prop('checked')) {
120+
const dt = e.originalEvent.dataTransfer;
121+
if (dt.types.includes('Files')) {
122+
quizUploadOverlay.stop().fadeIn(150);
117123
}
118-
},
119-
dragenter: function (e) {
120-
if (quizMarkdownToggle.prop('checked')) {
121-
const dt = e.originalEvent.dataTransfer;
122-
if (dt.types.includes('Files')) {
123-
quizUploadOverlay.stop().fadeIn(150);
124+
}
125+
},
126+
drop: function (e) {
127+
if (quizMarkdownToggle.prop('checked')) {
128+
const dt = e.originalEvent.dataTransfer;
129+
e.preventDefault();
130+
if (dt && dt.files.length) {
131+
if (dt.files.length != 1) {
132+
alert('Please only upload one file at a time.');
133+
return;
124134
}
125-
}
126-
},
127-
drop: function (e) {
128-
if (quizMarkdownToggle.prop('checked')) {
129-
const dt = e.originalEvent.dataTransfer;
130-
e.preventDefault();
131-
if (dt && dt.files.length) {
132-
if (dt.files.length != 1) {
133-
alert('Please only upload one file at a time.');
134-
return;
135-
}
136135

137-
let data = new FormData();
138-
data.append('key', IMGBB_API_KEY);
139-
data.append('image', dt.files[0]);
136+
let data = new FormData();
137+
data.append('image', dt.files[0]);
138+
data.append('csrfmiddlewaretoken', Cookies.get('csrftoken'));
140139

141-
$.ajax({
142-
url: 'https://api.imgbb.com/1/upload',
143-
method: 'POST',
144-
data: data,
145-
processData: false,
146-
contentType: false,
147-
}).done(function (data) {
148-
const name = data.data.title;
149-
const url = data.data.url;
150-
insertAtCursor(quizDescription[0], `![${name}](${url})`);
151-
});
152-
}
153-
quizUploadOverlay.stop().fadeOut(150);
140+
$.ajax({
141+
url: '/assignments/upload',
142+
method: 'POST',
143+
data: data,
144+
processData: false,
145+
contentType: false,
146+
}).done(function (data) {
147+
const name = data.title;
148+
const url = data.url;
149+
insertAtCursor(quizDescription[0], `![${name}](${url})`);
150+
});
154151
}
155-
},
156-
});
152+
quizUploadOverlay.stop().fadeOut(150);
153+
}
154+
},
155+
});
157156

158-
quizUploadOverlay.on('dragleave', function () {
159-
quizUploadOverlay.stop().fadeOut(150);
160-
});
161-
}
157+
quizUploadOverlay.on('dragleave', function () {
158+
quizUploadOverlay.stop().fadeOut(150);
159+
});
162160
});

0 commit comments

Comments
 (0)