Passed
Push — master ( 3684b3...aca5bb )
by Peter
02:15 queued 40s
created

SubmissionAdmin.sendMailAction()   B

Complexity

Conditions 5

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 8.5454
c 0
b 0
f 0
cc 5
1
# Submission admin interface
2
3
from django.contrib.admin import SimpleListFilter, ModelAdmin
4
from django.utils.translation import ugettext_lazy as _
5
from django.db.models import Q
6
from django.shortcuts import redirect
7
from django.utils.safestring import mark_safe
8
from django.http import HttpResponse
9
from django.utils.html import format_html
10
from opensubmit.models import Assignment, Submission, Grading
11
from django.utils import timesince
12
13
import io
14
import zipfile
15
16
17
def grading_file(submission):
18
    ''' Determines if the submission has a grading file,
19
        leads to nice little icon in the submission overview.
20
    '''
21
    if submission.grading_file.name:
22
        return True
23
    else:
24
        return False
25
grading_file.boolean = True            # show nice little icon
26
27
28
def test_results(submission):
29
    return "Foo"
30
31
32
class SubmissionStateFilter(SimpleListFilter):
33
34
    ''' This custom filter allows to filter the submissions according to their state.
35
        Additionally, only submissions the user is tutor for are shown.
36
    '''
37
    title = _('Submission Status')
38
    parameter_name = 'statefilter'
39
40
    def lookups(self, request, model_admin):
41
        return (
42
            ('notwithdrawn', _('All, except withdrawn')),
43
            ('valid', _('Tested')),
44
            ('tobegraded', _('Grading needed')),
45
            ('gradingunfinished', _('Grading not finished')),
46
            ('graded', _('Grading finished')),
47
            ('closed', _('Closed, student notified')),
48
        )
49
50
    def queryset(self, request, qs):
51
        qs = qs.filter(assignment__course__in=list(request.user.profile.tutor_courses()))
52
        if   self.value() == 'notwithdrawn':
53
            return Submission.qs_notwithdrawn(qs)
54
        elif self.value() == 'valid':
55
            return Submission.qs_valid(qs)
56
        elif self.value() == 'tobegraded':
57
            return Submission.qs_tobegraded(qs)
58
        elif self.value() == 'gradingunfinished':
59
            return qs.filter(state__in=[Submission.GRADING_IN_PROGRESS])
60
        elif self.value() == 'graded':
61
            return qs.filter(state__in=[Submission.GRADED])
62
        elif self.value() == 'closed':
63
            return Submission.qs_notified(qs)
64
        else:
65
            return qs
66
67 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
68
class SubmissionAssignmentFilter(SimpleListFilter):
69
70
    ''' This custom filter allows to filter the submissions according to their
71
        assignment. Only submissions from courses were the user is tutor are
72
        considered.
73
    '''
74
    title = _('Assignment')
75
    parameter_name = 'assignmentfilter'
76
77
    def lookups(self, request, model_admin):
78
        tutor_assignments = Assignment.objects.filter(course__in=list(request.user.profile.tutor_courses())).order_by('title')
79
        return ((ass.pk, ass.title) for ass in tutor_assignments)
80
81
    def queryset(self, request, qs):
82
        if self.value():
83
            return qs.filter(assignment__exact=self.value())
84
        else:
85
            return qs.filter(assignment__course__in=list(request.user.profile.tutor_courses()))
86
87 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
88
class SubmissionCourseFilter(SimpleListFilter):
89
90
    ''' This custom filter allows to filter the submissions according to
91
        the course they belong to. Additionally, only submission that the
92
        user is a tutor for are returned in any of the filter settings.
93
    '''
94
    title = _('Course')
95
    parameter_name = 'coursefilter'
96
97
    def lookups(self, request, model_admin):
98
        return ((c.pk, c.title) for c in list(request.user.profile.tutor_courses()))
99
100
    def queryset(self, request, qs):
101
        if self.value():
102
            return qs.filter(assignment__course__exact=self.value())
103
        else:
104
            return qs.filter(assignment__course__in=list(request.user.profile.tutor_courses()))
105
106
class SubmissionAdmin(ModelAdmin):
107
108
    ''' This is our version of the admin view for a single submission.
109
    '''
110
    list_display = ['__str__', 'created', 'modified', 'author_list', 'course', 'assignment', 'state', 'grading_status_text', 'has_grading_notes']
111
    list_filter = (SubmissionStateFilter, SubmissionCourseFilter, SubmissionAssignmentFilter)
112
    filter_horizontal = ('authors',)
113
    actions = ['downloadArchiveAction', 'setInitialStateAction', 'setFullPendingStateAction','setGradingNotFinishedStateAction', 'setGradingFinishedStateAction', 'closeAndNotifyAction', 'sendMailAction', 'notifyAction', 'getPerformanceResultsAction']
114
    search_fields = ['=id', '=authors__email', '=authors__first_name', '=authors__last_name', '=authors__username', '=notes']
115
    change_list_template = "admin/change_list_filter_sidebar.html"
116
117
    class Media:
118
        css = {'all': ('css/teacher.css',)}
119
120
121
    fieldsets = (
122
            ('General',
123
                {'fields': ('assignment', 'assignment_info', ('submitter','modified')),}),
124
            ('Authors',
125
                {   'fields': ('authors',),
126
                    'classes': ('grp-collapse grp-closed',)
127
                }),
128
            ('Submission and test results',
129
                {   'fields': (('file_link', 'file_upload', 'notes') ,'validation_result','fulltest_result'),
130
                }),
131
            ('Grading',
132
                {'fields': (('grading', 'grading_status'), 'grading_notes', 'grading_file',),}),
133
    )
134
135
    def assignment_info(self, instance):
136
        message = 'Course: %s<br/>'%instance.assignment.course
137
        message += 'Deadline: %s (%s ago)'%(instance.assignment.hard_deadline, timesince.timesince(instance.assignment.hard_deadline))
138
        if instance.can_modify(instance.submitter):
139
            message += '''<p style="color: red">Warning: Assignment is still open. Saving grading information will disable withdrawal and re-upload for the authors.</p>'''
140
        return mark_safe(message)
141
    assignment_info.short_description = "Details"
142
143
    def file_link(self, instance):
144
        '''
145
            Renders the link to the student upload file.
146
        '''
147
        sfile = instance.file_upload
148
        if not sfile:
149
            return mark_safe('No file submitted by student.')
150
        else:
151
            return mark_safe('<a href="%s">%s</a><br/>(<a href="%s" target="_new">Preview</a>)' % (sfile.get_absolute_url(), sfile.basename(), sfile.get_preview_url()))
152
    file_link.short_description = "Stored upload"
153
154
    def _render_test_result(self, result_obj, enabled):
155
        if not result_obj:
156
            if enabled:
157
                return mark_safe('Enabled, no results.')
158
            else:
159
                return mark_safe('Not enabled.')
160
        else:
161
            return format_html("Test output from {0}:<br/><pre>{1}</pre>", result_obj.machine, result_obj.result)
162
163
    def validation_result(self, instance):
164
        result_obj = instance.get_validation_result()
165
        return self._render_test_result(result_obj, instance.assignment.attachment_test_validity)
166
    validation_result.short_description = "Validation test"
167
    validation_result.allow_tags = True
168
169
    def fulltest_result(self, instance):
170
        result_obj = instance.get_fulltest_result()
171
        return self._render_test_result(result_obj, instance.assignment.attachment_test_full)
172
    fulltest_result.short_description = "Full test"
173
    fulltest_result.allow_tags = True
174
175
    def grading_status(self, instance):
176
        message = '''<input type="radio" name="newstate" value="unfinished">&nbsp;Grading not finished</input>
177
            <input type="radio" name="newstate" value="finished">&nbsp;Grading finished</input>
178
        '''
179
        return mark_safe(message)
180
    grading_status.short_description = "Status"
181
182
183
    def get_queryset(self, request):
184
        ''' Restrict the listed submission for the current user.'''
185
        qs = super(SubmissionAdmin, self).get_queryset(request)
186
        if request.user.is_superuser:
187
            return qs
188
        else:
189
            return qs.filter(Q(assignment__course__tutors__pk=request.user.pk) | Q(assignment__course__owner=request.user)).distinct()
190
191
    def get_readonly_fields(self, request, obj=None):
192
        ''' Make some of the form fields read-only, but only if the view used to
193
            modify an existing submission. Overriding the ModelAdmin getter
194
            is the documented way to do that.
195
        '''
196
        # Pseudo-fields generated by functions always must show up in the readonly list
197
        pseudo_fields = ('file_link', 'validation_result','fulltest_result', 'grading_status', 'assignment_info', 'modified')
198
        if obj:
199
            return ('assignment', 'submitter', 'notes')+pseudo_fields
200
        else:
201
            # New manual submission
202
            return pseudo_fields
203
204
    def formfield_for_dbfield(self, db_field, **kwargs):
205
        ''' Offer grading choices from the assignment definition as potential form
206
            field values for 'grading'.
207
            When no object is given in the form, the this is a new manual submission
208
        '''
209
        if db_field.name == "grading":
210
            submurl = kwargs['request'].path
211
            try:
212
                # Does not work on new submission action by admin or with a change of URLs. The former is expectable.
213
                submid = [int(s) for s in submurl.split('/') if s.isdigit()][0] 
214
                kwargs["queryset"] = Submission.objects.get(pk=submid).assignment.gradingScheme.gradings
215
            except:
216
                kwargs["queryset"] = Grading.objects.none()
217
        return super(SubmissionAdmin, self).formfield_for_dbfield(db_field, **kwargs)
218
219
    def save_model(self, request, obj, form, change):
220
        ''' Our custom addition to the view
221
            adds an easy radio button choice for the new state. This is meant to be for tutors.
222
            We need to peel this choice from the form data and set the state accordingly.
223
            The radio buttons have no default, so that we can keep the existing state
224
            if the user makes no explicit choice.
225
            Everything else can be managed as prepared by the framework.
226
        '''
227
        if 'newstate' in request.POST:
228
            if request.POST['newstate'] == 'finished':
229
                obj.state = Submission.GRADED
230
            elif request.POST['newstate'] == 'unfinished':
231
                obj.state = Submission.GRADING_IN_PROGRESS
232
        obj.save()
233
234
    def setInitialStateAction(self, request, queryset):
235
        for subm in queryset:
236
            subm.state = subm.get_initial_state()
237
            subm.save()
238
    setInitialStateAction.short_description = "Re-run validation test for selection submissions (student visible)"
239
240
    def setGradingNotFinishedStateAction(self, request, queryset):
241
        '''
242
            Set all marked submissions to "grading not finished".
243
            This is intended to support grading corrections on a larger scale.
244
        '''
245
        for subm in queryset:
246
            subm.state = Submission.GRADING_IN_PROGRESS
247
            subm.save()
248
    setGradingNotFinishedStateAction.short_description = "Mark selected submissions as 'Grading not finished'"
249
250
    def setGradingFinishedStateAction(self, request, queryset):
251
        '''
252
            Set all marked submissions to "grading finished".
253
            This is intended to support grading corrections on a larger scale.
254
        '''
255
        for subm in queryset:
256
            subm.state = Submission.GRADED
257
            subm.save()
258
    setGradingFinishedStateAction.short_description = "Mark selected submissions as 'Grading finished'"
259
260
    def setFullPendingStateAction(self, request, queryset):
261
        # do not restart tests for withdrawn solutions
262
        queryset = queryset.exclude(state=Submission.WITHDRAWN)
263
        # do not restart tests for queued things
264
        queryset = queryset.exclude(state=Submission.TEST_VALIDITY_PENDING)
265
        queryset = queryset.exclude(state=Submission.TEST_FULL_PENDING)
266
        numchanged = 0
267
        for subm in queryset:
268
            if subm.assignment.has_full_test():
269
                if subm.state == Submission.CLOSED:
270
                    subm.state = Submission.CLOSED_TEST_FULL_PENDING
271
                else:
272
                    subm.state = Submission.TEST_FULL_PENDING
273
                subm.save()
274
                numchanged += 1
275
        if numchanged == 0:
276
            self.message_user(request, "Nothing changed, no testable submission found.")
277
        else:
278
            self.message_user(request, "Changed status of %u submissions." % numchanged)
279
    setFullPendingStateAction.short_description = "Re-run full test for selected submissions (student invisible)"
280
281
    def closeAndNotifyAction(self, request, queryset):
282
        ''' Close all submissions were the tutor sayed that the grading is finished,
283
            and inform the student. CLosing only graded submissions is a safeguard,
284
            since backend users tend to checkbox-mark all submissions without thinking.
285
        '''
286
        mails = []
287
        qs = queryset.filter(Q(state=Submission.GRADED))
288
        for subm in qs:
289
            subm.inform_student(Submission.CLOSED)
290
            mails.append(str(subm.pk))
291
        qs.update(state=Submission.CLOSED)      # works in bulk because inform_student never fails
292
        if len(mails) == 0:
293
            self.message_user(request, "Nothing closed, no mails sent.")
294
        else:
295
            self.message_user(request, "Mail sent for submissions: " + ",".join(mails))
296
    closeAndNotifyAction.short_description = "Close + send student notification for selected submissions"
297
298
    def sendMailAction(self, request, queryset):
299
        receiver_list = []
300
        for submission in queryset:
301
            for author in submission.authors.all():
302
                if author.pk not in receiver_list:
303
                    receiver_list.append(author.pk)
304
        return redirect('mailstudents', student_ids=','.join([str(pk) for pk in receiver_list]))
305
    sendMailAction.short_description = "Send eMail to authors of selected submissions"
306
307
    def downloadArchiveAction(self, request, queryset):
308
        '''
309
        Download selected submissions as archive, for targeted correction.
310
        '''
311
        output = io.BytesIO()
312
        z = zipfile.ZipFile(output, 'w')
313
314
        for sub in queryset:
315
            sub.add_to_zipfile(z)
316
317
        z.close()
318
        # go back to start in ZIP file so that Django can deliver it
319
        output.seek(0)
320
        response = HttpResponse(output, content_type="application/x-zip-compressed")
321
        response['Content-Disposition'] = 'attachment; filename=submissions.zip'
322
        return response
323
    downloadArchiveAction.short_description = "Download selected submissions as ZIP archive"
324
325
326
327
328
    # ToDo: Must be refactored to consider new performance data storage model
329
    # def getPerformanceResultsAction(self, request, queryset):
330
    #     qs = queryset.exclude(state=Submission.WITHDRAWN)  # avoid accidental addition of withdrawn solutions
331
    #     response = HttpResponse(content_type="text/csv")
332
    #     response.write("Submission ID;Course;Assignment;Authors;Performance Data\n")
333
    #     for subm in qs:
334
    #         if subm.file_upload and subm.file_upload.perf_data is not None:
335
    #             auth = ", ".join([author.get_full_name() for author in subm.authors.all()])
336
    #             response.write("%u;%s;%s;%s;" % (subm.pk, course(subm), subm.assignment, auth))
337
    #             response.write(subm.file_upload.perf_data)
338
    #             response.write("\n")
339
    #     return response
340
    # getPerformanceResultsAction.short_description = "Download performance data as CSV"
341
342