|
1
|
|
|
# Assignment admin interface |
|
2
|
|
|
|
|
3
|
|
|
from collections import defaultdict |
|
4
|
|
|
from django.contrib.admin import ModelAdmin |
|
5
|
|
|
from django.utils.translation import ugettext_lazy as _ |
|
6
|
|
|
from django.db.models import Q |
|
7
|
|
|
from opensubmit.models import Course |
|
8
|
|
|
from django import forms |
|
9
|
|
|
from django.core.exceptions import ValidationError |
|
10
|
|
|
from django.utils.safestring import mark_safe |
|
11
|
|
|
from django.core.urlresolvers import reverse |
|
12
|
|
|
from django.utils.html import format_html |
|
13
|
|
|
|
|
14
|
|
|
class AssignmentAdminForm(forms.ModelForm): |
|
15
|
|
|
def __init__(self, *args, **kwargs): |
|
16
|
|
|
''' |
|
17
|
|
|
The intention here is to correct the shown download URL for already existing test script uploads, |
|
18
|
|
|
which is suprisingly hard. |
|
19
|
|
|
The URL comes as read-only field from the underlying storage system implementation, which |
|
20
|
|
|
generates it from the relative file path and MEDIA_URL. Since we want to control all media file downloads, |
|
21
|
|
|
MEDIA_URL is not given in OpenSubmit, so the resulting URL does not exist. Since test scripts are not |
|
22
|
|
|
a separate model as submission files (*sick*), we cannot use the elegant get_absolute_url() override to |
|
23
|
|
|
fix the download URL for a test script. Instead, we hack the widget rendering here. |
|
24
|
|
|
''' |
|
25
|
|
|
super(AssignmentAdminForm, self).__init__(*args, **kwargs) |
|
26
|
|
|
if self.instance.pk: # makes only sense if this is not a new assignment to be created |
|
27
|
|
|
if self.instance.validity_test_url(): |
|
28
|
|
|
self.fields['attachment_test_validity'].widget.template_with_initial = ( |
|
29
|
|
|
'%(initial_text)s: <a href="'+self.instance.validity_test_url()+'">%(initial)s</a> ' |
|
30
|
|
|
'%(clear_template)s<br />%(input_text)s: %(input)s' |
|
31
|
|
|
) |
|
32
|
|
|
else: |
|
33
|
|
|
self.fields['attachment_test_validity'].widget.template_with_initial = ( |
|
34
|
|
|
'%(initial_text)s: %(clear_template)s<br />%(input_text)s: %(input)s' |
|
35
|
|
|
) |
|
36
|
|
|
if self.instance.full_test_url(): |
|
37
|
|
|
self.fields['attachment_test_full'].widget.template_with_initial = ( |
|
38
|
|
|
'%(initial_text)s: <a href="'+self.instance.full_test_url()+'">%(initial)s</a> ' |
|
39
|
|
|
'%(clear_template)s<br />%(input_text)s: %(input)s' |
|
40
|
|
|
) |
|
41
|
|
|
else: |
|
42
|
|
|
self.fields['attachment_test_full'].widget.template_with_initial = ( |
|
43
|
|
|
'%(initial_text)s: %(clear_template)s<br />%(input_text)s: %(input)s' |
|
44
|
|
|
) |
|
45
|
|
|
if self.instance.url(): |
|
46
|
|
|
self.fields['description'].widget.template_with_initial = ( |
|
47
|
|
|
'%(initial_text)s: <a href="'+self.instance.url()+'">%(initial)s</a> ' |
|
48
|
|
|
'%(clear_template)s<br />%(input_text)s: %(input)s' |
|
49
|
|
|
) |
|
50
|
|
|
else: |
|
51
|
|
|
self.fields['description'].widget.template_with_initial = ( |
|
52
|
|
|
'%(initial_text)s: %(clear_template)s<br />%(input_text)s: %(input)s' |
|
53
|
|
|
) |
|
54
|
|
|
|
|
55
|
|
|
def clean(self): |
|
56
|
|
|
''' |
|
57
|
|
|
Check if such an assignment configuration makes sense, and reject it otherwise. |
|
58
|
|
|
This mainly relates to interdependencies between the different fields, since |
|
59
|
|
|
single field constraints are already clatified by the Django model configuration. |
|
60
|
|
|
''' |
|
61
|
|
|
super(AssignmentAdminForm, self).clean() |
|
62
|
|
|
d = defaultdict(lambda: False) |
|
63
|
|
|
d.update(self.cleaned_data) |
|
64
|
|
|
# Having validation or full test enabled demands file upload |
|
65
|
|
|
if d['attachment_test_validity'] and not d['has_attachment']: |
|
66
|
|
|
raise ValidationError('You cannot have a validation script without allowing file upload.') |
|
67
|
|
|
if d['attachment_test_full'] and not d['has_attachment']: |
|
68
|
|
|
raise ValidationError('You cannot have a full test script without allowing file upload.') |
|
69
|
|
|
# Having validation or full test enabled demands a test machine |
|
70
|
|
|
if d['attachment_test_validity'] and 'test_machines' in d and not len(d['test_machines'])>0: |
|
71
|
|
|
raise ValidationError('You cannot have a validation script without specifying test machines.') |
|
72
|
|
|
if d['attachment_test_full'] and 'test_machines' in d and not len(d['test_machines'])>0: |
|
73
|
|
|
raise ValidationError('You cannot have a full test script without specifying test machines.') |
|
74
|
|
|
if d['download'] and d['description']: |
|
75
|
|
|
raise ValidationError('You can only have a description link OR a description file.') |
|
76
|
|
|
if not d['download'] and not d['description']: |
|
77
|
|
|
raise ValidationError('You need a description link OR a description file.') |
|
78
|
|
|
# Having test machines demands compilation or validation scripts |
|
79
|
|
|
if 'test_machines' in d and len(d['test_machines'])>0 \ |
|
80
|
|
|
and not 'attachment_test_validity' in d \ |
|
81
|
|
|
and not 'attachment_test_full' in d: |
|
82
|
|
|
raise ValidationError('For using test machines, you need to enable validation or full test.') |
|
83
|
|
|
|
|
84
|
|
|
def course(obj): |
|
85
|
|
|
''' Course name as string.''' |
|
86
|
|
|
return str(obj.course) |
|
87
|
|
|
|
|
88
|
|
|
def num_subm(obj): |
|
89
|
|
|
return obj.valid_submissions().count() |
|
90
|
|
|
num_subm.short_description = "Submissions" |
|
91
|
|
|
|
|
92
|
|
|
def num_authors(obj): |
|
93
|
|
|
return obj.authors().count() |
|
94
|
|
|
num_authors.short_description = "Authors" |
|
95
|
|
|
|
|
96
|
|
|
def num_finished(obj): |
|
97
|
|
|
return obj.graded_submissions().count() |
|
98
|
|
|
num_finished.short_description = "Grading finished" |
|
99
|
|
|
|
|
100
|
|
|
def num_unfinished(obj): |
|
101
|
|
|
unfinished=obj.grading_unfinished_submissions().count() |
|
102
|
|
|
gradable =obj.gradable_submissions().count() |
|
103
|
|
|
return "%u (%u)"%(gradable, unfinished) |
|
104
|
|
|
num_unfinished.short_description = "To be graded (unfinished)" |
|
105
|
|
|
|
|
106
|
|
|
def view_links(obj): |
|
107
|
|
|
''' Link to performance data and duplicate overview.''' |
|
108
|
|
|
result=format_html('') |
|
109
|
|
|
result+=format_html('<a href="%s" style="white-space: nowrap">Show duplicates</a><br/>'%reverse('duplicates', args=(obj.pk,))) |
|
110
|
|
|
result+=format_html('<a href="%s" style="white-space: nowrap">Show submissions</a><br/>'%obj.grading_url()) |
|
111
|
|
|
result+=format_html('<a href="%s" style="white-space: nowrap">Download submissions</a>'%reverse('assarchive', args=(obj.pk,))) |
|
112
|
|
|
return result |
|
113
|
|
|
view_links.short_description = "" |
|
114
|
|
|
|
|
115
|
|
|
class AssignmentAdmin(ModelAdmin): |
|
116
|
|
|
list_display = ['title', course, 'soft_deadline', 'hard_deadline', 'gradingScheme', num_authors, num_subm, num_finished, num_unfinished, view_links] |
|
117
|
|
|
change_list_template = "admin/change_list_filter_sidebar.html" |
|
118
|
|
|
|
|
119
|
|
|
class Media: |
|
120
|
|
|
css = {'all': ('css/teacher.css',)} |
|
121
|
|
|
js = ('js/opensubmit.js',) |
|
122
|
|
|
|
|
123
|
|
|
form = AssignmentAdminForm |
|
124
|
|
|
|
|
125
|
|
|
fieldsets = ( |
|
126
|
|
|
('', |
|
127
|
|
|
{'fields': (('title','course'), 'gradingScheme', 'max_authors', 'has_attachment')}), |
|
128
|
|
|
('Description', |
|
129
|
|
|
{ 'fields': ('download', 'description')}), |
|
130
|
|
|
('Time', |
|
131
|
|
|
{ 'fields': ('publish_at', ('soft_deadline', 'hard_deadline'))}), |
|
132
|
|
|
('File Upload Validation', |
|
133
|
|
|
{ 'fields': (('attachment_test_validity', 'validity_script_download'), \ |
|
134
|
|
|
'attachment_test_full', \ |
|
135
|
|
|
('test_machines', 'attachment_test_timeout') )}, |
|
136
|
|
|
) |
|
137
|
|
|
) |
|
138
|
|
|
|
|
139
|
|
|
def get_queryset(self, request): |
|
140
|
|
|
''' Restrict the listed assignments for the current user.''' |
|
141
|
|
|
qs = super(AssignmentAdmin, self).get_queryset(request) |
|
142
|
|
|
if not request.user.is_superuser: |
|
143
|
|
|
qs = qs.filter(course__active=True).filter(Q(course__tutors__pk=request.user.pk) | Q(course__owner=request.user)).distinct() |
|
144
|
|
|
return qs.order_by('title') |
|
145
|
|
|
|
|
146
|
|
|
def formfield_for_foreignkey(self, db_field, request, **kwargs): |
|
147
|
|
|
if db_field.name == "course": |
|
148
|
|
|
kwargs["queryset"] = Course.valid_ones |
|
149
|
|
|
return super(AssignmentAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) |