SubmissionAssignmentFilter   A
last analyzed

Complexity

Total Complexity 4

Size/Duplication

Total Lines 19
Duplicated Lines 94.74 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 4
c 1
b 0
f 0
dl 18
loc 19
rs 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
A lookups() 4 4 2
A queryset() 4 5 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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