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) |