• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tjcsl / tin / 17450891849

04 Sep 2025 01:49AM UTC coverage: 58.858% (+0.2%) from 58.654%
17450891849

Pull #92

github

web-flow
Merge 8c9f1d1d3 into 564991635
Pull Request #92: Allow choosing custom versions of python for graders

470 of 971 branches covered (48.4%)

Branch coverage included in aggregate %.

59 of 81 new or added lines in 12 files covered. (72.84%)

1 existing line in 1 file now uncovered.

1746 of 2794 relevant lines covered (62.49%)

0.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.16
/tin/apps/assignments/forms.py
1
from __future__ import annotations
1✔
2

3
from collections.abc import Iterable
1✔
4
from logging import getLogger
1✔
5

6
from django import forms
1✔
7
from django.conf import settings
1✔
8
from django.core.exceptions import ValidationError
1✔
9
from django.db.models import Q
1✔
10

11
from ..submissions.models import Submission
1✔
12
from .models import Assignment, Folder, Language, MossResult, SubmissionCap
1✔
13

14
logger = getLogger(__name__)
1✔
15

16

17
class AssignmentForm(forms.ModelForm):
1✔
18
    due = forms.DateTimeInput()
1✔
19

20
    submission_cap = forms.IntegerField(min_value=1, required=False)
1✔
21
    submission_cap_after_due = forms.IntegerField(min_value=1, required=False)
1✔
22

23
    def __init__(self, course, *args, **kwargs):
1✔
24
        super().__init__(*args, **kwargs)
1✔
25
        self.fields["folder"].queryset = Folder.objects.filter(course=course)
1✔
26

27
        instance = getattr(self, "instance", None)
1✔
28
        if instance and instance.pk:
1!
29
            # allow them to change from a deprecated language to a non-deprecated one
30
            # but it must be the same (e.g. python -> python, or java -> java)
NEW
31
            self.fields["language_details"].queryset = Language.objects.filter(
×
32
                Q(is_deprecated=False) & Q(language=instance.language_details.language)
33
                # just nicer UI to show the deprecated language choice
34
                | Q(id=instance.language_details.id)
35
            )
36

37
            cap = instance.submission_caps.filter(student__isnull=True).first()
×
38
            if cap is not None:
×
39
                self.fields["submission_cap"].initial = cap.submission_cap
×
40
                self.fields["submission_cap_after_due"].initial = cap.submission_cap_after_due
×
41

42
        else:
43
            self.fields["language_details"].queryset = Language.objects.filter(is_deprecated=False)
1✔
44
        self.fields[
1✔
45
            "language_details"
46
        ].help_text = (
47
            "Keep in mind you cannot swap between languages after the assignment has been created."
48
        )
49

50
        # prevent description from getting too big
51
        self.fields["description"].widget.attrs.update({"id": "description"})
1✔
52

53
    def get_sections(self) -> Iterable[dict[str, str | tuple[str, ...] | bool]]:
1✔
54
        """This is used in templates to find which fields should be in a dropdown div."""
55
        for section in self.Meta.sections:
×
56
            if section["name"]:
×
57
                # operate on copy so errors on refresh don't happen
58
                new_section = section.copy()
×
59
                new_section["fields"] = tuple(self[field] for field in new_section["fields"])
×
60
                yield new_section
×
61

62
    def get_main_section(self) -> dict[str, str | tuple[str, ...]]:
1✔
63
        """This is the section that is NOT in a dropdown"""
64
        for section in self.Meta.sections:
×
65
            if section["name"] == "":
×
66
                new_section = section.copy()
×
67
                new_section["fields"] = tuple(self[field] for field in new_section["fields"])
×
68
                return new_section
×
69
        logger.error(f"Could not find main section for assignment {self}")
×
70
        return {"fields": ()}
×
71

72
    class Meta:
1✔
73
        model = Assignment
1✔
74
        fields = [
1✔
75
            "name",
76
            "description",
77
            "markdown",
78
            "folder",
79
            "language_details",
80
            "filename",
81
            "venv",
82
            "points_possible",
83
            "due",
84
            "hidden",
85
            "enable_grader_timeout",
86
            "grader_timeout",
87
            "grader_has_network_access",
88
            "has_network_access",
89
            "submission_limit_count",
90
            "submission_limit_interval",
91
            "submission_limit_cooldown",
92
            "is_quiz",
93
            "quiz_action",
94
            "quiz_autocomplete_enabled",
95
            "quiz_description",
96
            "quiz_description_markdown",
97
        ]
98
        labels = {
1✔
99
            "markdown": "Use markdown?",
100
            "venv": "Virtual environment",
101
            "hidden": "Hide assignment from students?",
102
            "enable_grader_timeout": "Set a timeout for the grader?",
103
            "grader_timeout": "Grader timeout (seconds):",
104
            "grader_has_network_access": "Give the grader internet access?",
105
            "has_network_access": "Give submissions internet access?",
106
            "submission_limit_count": "Rate limit count",
107
            "submission_limit_interval": "Rate limit interval (minutes)",
108
            "submission_limit_cooldown": "Rate limit cooldown period (minutes)",
109
            "is_quiz": "Is this a quiz?",
110
            "quiz_autocomplete_enabled": "Enable code autocompletion?",
111
            "quiz_description_markdown": "Use markdown?",
112
            "language_details": "Grader language",
113
        }
114
        sections = (
1✔
115
            {
116
                "name": "",
117
                "fields": (
118
                    "name",
119
                    "description",
120
                    "markdown",
121
                    "due",
122
                    "points_possible",
123
                    "hidden",
124
                ),
125
            },
126
            {
127
                "name": "Environment Setup",
128
                "description": "",
129
                "fields": (
130
                    "folder",
131
                    "language_details",
132
                    "filename",
133
                    "venv",
134
                ),
135
                "collapsed": False,
136
            },
137
            {
138
                "name": "Quiz Options",
139
                "description": "",
140
                "fields": (
141
                    "is_quiz",
142
                    "quiz_action",
143
                    "quiz_autocomplete_enabled",
144
                    "quiz_description",
145
                    "quiz_description_markdown",
146
                ),
147
                "collapsed": False,
148
            },
149
            {
150
                "name": "Other Settings",
151
                "description": "",
152
                "fields": (
153
                    "enable_grader_timeout",
154
                    "grader_timeout",
155
                    "has_network_access",
156
                    "grader_has_network_access",
157
                    "submission_limit_count",
158
                    "submission_limit_interval",
159
                    "submission_limit_cooldown",
160
                    "submission_cap",
161
                    "submission_cap_after_due",
162
                ),
163
                "collapsed": True,
164
            },
165
        )
166
        help_texts = {
1✔
167
            "filename": "Clarify which file students need to upload (including the file "
168
            "extension). For Java assignments, this also sets the name of the "
169
            "saved submission file.",
170
            "markdown": "This allows adding images, code blocks, or hyperlinks to the assignment description.",
171
            "venv": "If set, Tin will run the student's code in this virtual environment.",
172
            "grader_has_network_access": 'If unset, this effectively disables "Give submissions '
173
            'internet access" below. If set, it increases the amount '
174
            "of time it takes to start up the grader (to about 1.5 "
175
            "seconds). This is not recommended unless necessary.",
176
            "submission_cap": "The maximum number of submissions that can be made, or empty for unlimited.",
177
            "submission_cap_after_due": "The maximum number of submissions that can be made after the due date.",
178
            "submission_limit_count": "",
179
            "submission_limit_interval": "Tin sets rate limits on submissions. If a student tries "
180
            "to submit too many submissions in a given interval, "
181
            "Tin will block further submissions until a cooldown "
182
            "period has elapsed since the time of the last "
183
            "submission.",
184
            "submission_limit_cooldown": 'This sets the length of the "cooldown" period after a '
185
            "student exceeds the rate limit for submissions.",
186
            "folder": "If blank, assignment will show on the main classroom page.",
187
            "is_quiz": "This forces students to submit through a page that monitors their actions. The below options "
188
            "have no effect if this is unset.",
189
            "quiz_action": "Tin will take the selected action if a student clicks off of the "
190
            "quiz page.",
191
            "quiz_autocomplete_enabled": "This gives students basic code completion in the quiz editor, including "
192
            "variable names, built-in functions, and keywords. It's recommended for quizzes that focus on code logic "
193
            "and not syntax.",
194
            "quiz_description": "Unlike the assignment description (left) which shows up on the assignment page, the "
195
            "quiz description only appears once the student has started the quiz. This is useful for quiz "
196
            "instructions that need to be hidden until the student enters the monitored quiz environment.",
197
            "quiz_description_markdown": "This allows adding images, code blocks, or hyperlinks to the quiz "
198
            "description.",
199
        }
200
        widgets = {
1✔
201
            "description": forms.Textarea(attrs={"cols": 30, "rows": 8}),
202
            "quiz_description": forms.Textarea(attrs={"cols": 30, "rows": 6}),
203
        }
204

205
    def __str__(self) -> str:
1✔
206
        return f'AssignmentForm("{self["name"].value()}")'
×
207

208
    def save(self) -> Assignment:
1✔
209
        assignment = super().save()
1✔
210
        sub_cap = self.cleaned_data.get("submission_cap")
1✔
211
        sub_cap_after_due = self.cleaned_data.get("submission_cap_after_due")
1✔
212
        if sub_cap is not None or sub_cap_after_due is not None:
1!
213
            SubmissionCap.objects.update_or_create(
×
214
                assignment=assignment,
215
                student=None,
216
                defaults={
217
                    "submission_cap": sub_cap,
218
                    "submission_cap_after_due": sub_cap_after_due,
219
                },
220
            )
221
        elif sub_cap is None and sub_cap_after_due is None:
1!
222
            SubmissionCap.objects.filter(assignment=assignment, student=None).delete()
1✔
223
        return assignment
1✔
224

225

226
class GraderScriptUploadForm(forms.Form):
1✔
227
    grader_file = forms.FileField(
1✔
228
        label="Upload grader",
229
        max_length=settings.SUBMISSION_SIZE_LIMIT,
230
        allow_empty_file=False,
231
        help_text="Size limit is 1MB.",
232
    )
233

234

235
class FileUploadForm(forms.Form):
1✔
236
    upload_file = forms.FileField(
1✔
237
        max_length=settings.SUBMISSION_SIZE_LIMIT,
238
        allow_empty_file=False,
239
        help_text="Size limit is 1MB.",
240
    )
241

242

243
class FileSubmissionForm(forms.Form):
1✔
244
    file = forms.FileField(
1✔
245
        label="",
246
        max_length=settings.SUBMISSION_SIZE_LIMIT,
247
        allow_empty_file=False,
248
        help_text="You can also drag files onto this page to submit them. Size limit is 1MB.",
249
    )
250

251

252
class TextSubmissionForm(forms.ModelForm):
1✔
253
    text = forms.CharField(label="", widget=forms.Textarea(attrs={"cols": 130, "rows": 20}))
1✔
254

255
    class Meta:
1✔
256
        model = Submission
1✔
257
        fields = []
1✔
258

259

260
class MossForm(forms.ModelForm):
1✔
261
    def __init__(self, assignment, period, *args, **kwargs):
1✔
262
        super().__init__(*args, **kwargs)
×
263
        self.fields["period"].queryset = assignment.course.period_set.all()
×
264
        if period:
×
265
            self.fields["period"].initial = period
×
266
        self.fields["language"].initial = (
×
267
            "java" if assignment.filename.endswith(".java") else "python"
268
        )
269

270
    class Meta:
1✔
271
        model = MossResult
1✔
272
        fields = ["period", "language", "base_file", "user_id"]
1✔
273
        help_texts = {
1✔
274
            "period": "Leave blank to run Moss on all students in the course.",
275
            "base_file": "The assignment's shell code (optional).",
276
        }
277

278

279
class FolderForm(forms.ModelForm):
1✔
280
    class Meta:
1✔
281
        model = Folder
1✔
282
        fields = [
1✔
283
            "name",
284
        ]
285
        help_texts = {"name": "Note: Folders are ordered alphabetically."}
1✔
286

287

288
class ImageForm(forms.Form):
1✔
289
    image = forms.FileField()
1✔
290

291
    def clean_image(self):
1✔
292
        image = self.cleaned_data.get("image")
×
293
        if image and image.size > settings.MAX_UPLOADED_IMAGE_SIZE:
×
294
            raise ValidationError("Image size exceeds the maximum limit")
×
295
        return image
×
296

297

298
class SubmissionCapForm(forms.ModelForm):
1✔
299
    class Meta:
1✔
300
        model = SubmissionCap
1✔
301
        fields = ["submission_cap", "submission_cap_after_due"]
1✔
302
        help_texts = {
1✔
303
            "submission_cap_after_due": (
304
                "The submission cap after the due date. "
305
                "By default, this is the same as the submission cap."
306
            ),
307
            "student": "The student to apply the cap to.",
308
        }
309
        labels = {"submission_cap_after_due": "Submission cap after due date"}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc