Skip to content

Commit d764587

Browse files
committed
Implement quiz descriptions with markdown functionality
1 parent a21d8c9 commit d764587

File tree

6 files changed

+122
-12
lines changed

6 files changed

+122
-12
lines changed

tin/apps/assignments/forms.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class Meta:
7070
"is_quiz",
7171
"quiz_action",
7272
"quiz_autocomplete_enabled",
73+
"quiz_description",
74+
"quiz_description_markdown",
7375
]
7476
labels = {
7577
"markdown": "Use markdown?",
@@ -84,6 +86,7 @@ class Meta:
8486
"submission_limit_cooldown": "Rate limit cooldown period (minutes)",
8587
"is_quiz": "Is this a quiz?",
8688
"quiz_autocomplete_enabled": "Enable code autocompletion?",
89+
"quiz_description_markdown": "Use markdown?",
8790
}
8891
sections = (
8992
{
@@ -115,6 +118,8 @@ class Meta:
115118
"is_quiz",
116119
"quiz_action",
117120
"quiz_autocomplete_enabled",
121+
"quiz_description",
122+
"quiz_description_markdown",
118123
),
119124
"collapsed": False,
120125
},
@@ -159,8 +164,16 @@ class Meta:
159164
"quiz_autocomplete_enabled": "This gives students basic code completion in the quiz editor, including "
160165
"variable names, built-in functions, and keywords. It's recommended for quizzes that focus on code logic "
161166
"and not syntax.",
167+
"quiz_description": "Unlike the assignment description (left) which shows up on the assignment page, the "
168+
"quiz description only appears once the student has started the quiz. This is useful for quiz "
169+
"instructions that need to be hidden until the student enters the monitored quiz environment.",
170+
"quiz_description_markdown": "This allows adding images, code blocks, or hyperlinks to the quiz "
171+
"description.",
172+
}
173+
widgets = {
174+
"description": forms.Textarea(attrs={"cols": 30, "rows": 8}),
175+
"quiz_description": forms.Textarea(attrs={"cols": 30, "rows": 6}),
162176
}
163-
widgets = {"description": forms.Textarea(attrs={"cols": 30, "rows": 4})}
164177

165178
def __str__(self) -> str:
166179
return f"AssignmentForm(\"{self['name'].value()}\")"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.13 on 2024-06-05 02:20
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("assignments", "0031_assignment_quiz_autocomplete_enabled"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="assignment",
15+
name="quiz_description",
16+
field=models.CharField(blank=True, default="", max_length=4096),
17+
),
18+
migrations.AddField(
19+
model_name="assignment",
20+
name="quiz_description_markdown",
21+
field=models.BooleanField(default=False),
22+
),
23+
]

tin/apps/assignments/models.py

+2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ class Assignment(models.Model):
139139
QUIZ_ACTIONS = (("0", "Log only"), ("1", "Color Change"), ("2", "Lock"))
140140
quiz_action = models.CharField(max_length=1, choices=QUIZ_ACTIONS, default="2")
141141
quiz_autocomplete_enabled = models.BooleanField(default=False)
142+
quiz_description = models.CharField(max_length=4096, default="", null=False, blank=True)
143+
quiz_description_markdown = models.BooleanField(default=False)
142144

143145
objects = AssignmentQuerySet.as_manager()
144146

tin/static/css/edit.css

+3-4
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,11 @@ button.collapsible.active:after {
7676

7777
/* Style the description box */
7878
textarea {
79-
width: 90%;
80-
height: 300px;
79+
width: 95%;
8180
resize: vertical;
8281
/* For legacy browsers without CSS 3 support */
83-
min-width: 90%;
84-
max-width: 90%;
82+
min-width: 95%;
83+
max-width: 95%;
8584
}
8685

8786
@media (max-width: 900.02px) {

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

+70-7
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ $(function () {
2828
position: 'relative',
2929
});
3030

31-
const upload_overlay = $('<div>')
31+
const uploadOverlay = $('<div>')
3232
.css({
3333
display: 'none',
3434
position: 'absolute',
3535
top: 0,
3636
left: 0,
37-
width: '92%',
37+
width: '97%',
3838
height: '100%',
3939
'background-color': 'rgba(0, 0, 0, 0.2)',
4040
'z-index': 1000,
4141
'text-align': 'center',
4242
border: '10px dashed #000',
43-
padding: '100px 20px 0px 20px',
43+
padding: '30px 20px 0px 20px',
4444
'font-size': '250%',
4545
'box-sizing': 'border-box',
4646
})
@@ -57,7 +57,7 @@ $(function () {
5757
if (markdownToggle.prop('checked')) {
5858
const dt = e.originalEvent.dataTransfer;
5959
if (dt.types.includes('Files')) {
60-
upload_overlay.stop().fadeIn(150);
60+
uploadOverlay.stop().fadeIn(150);
6161
}
6262
}
6363
},
@@ -87,13 +87,76 @@ $(function () {
8787
insertAtCursor(description[0], `![${name}](${url})`);
8888
});
8989
}
90-
upload_overlay.stop().fadeOut(150);
90+
uploadOverlay.stop().fadeOut(150);
9191
}
9292
},
9393
});
9494

95-
upload_overlay.on('dragleave', function () {
96-
upload_overlay.stop().fadeOut(150);
95+
uploadOverlay.on('dragleave', function () {
96+
uploadOverlay.stop().fadeOut(150);
97+
});
98+
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();
104+
105+
quizDescriptionParent.css({
106+
position: 'relative',
107+
});
108+
109+
const quizUploadOverlay = uploadOverlay
110+
.clone()
111+
.appendTo(quizDescriptionParent);
112+
113+
quizDescriptionParent.on({
114+
dragover: function (e) {
115+
if (quizMarkdownToggle.prop('checked')) {
116+
e.preventDefault();
117+
}
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+
},
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+
}
136+
137+
let data = new FormData();
138+
data.append('key', IMGBB_API_KEY);
139+
data.append('image', dt.files[0]);
140+
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);
154+
}
155+
},
156+
});
157+
158+
quizUploadOverlay.on('dragleave', function () {
159+
quizUploadOverlay.stop().fadeOut(150);
97160
});
98161
}
99162
});

tin/templates/assignments/quiz.html

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% extends "base_wo_head.html" %}
22
{% load static %}
3+
{% load markdownify %}
34

45
{% block title %}
56
Turn-In: {{ assignment.name }}: Take Quiz
@@ -132,6 +133,15 @@ <h2>{{ assignment.name }}: Take Quiz</h2>
132133

133134
{% if assignment.grader_file %}
134135

136+
{% if assignment.quiz_description %}
137+
{% if assignment.quiz_description_markdown %}
138+
{{ assignment.quiz_description | markdownify }}
139+
{% else %}
140+
<p>{{ assignment.quiz_description | linebreaks }}</p>
141+
{% endif %}
142+
{% endif %}
143+
144+
135145
{% if latest_submission %}
136146
<h3 style="border-top:1px solid lightgray;padding-top:15px;">Grader output</h3>
137147
<div id="grader-output" class="code-result{% if not latest_submission.complete %} incomplete{% endif %}"

0 commit comments

Comments
 (0)