Passed
Push — master ( a505d5...8178ea )
by Peter
01:25
created

archive()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 5
rs 9.4285
c 1
b 0
f 0
cc 1
1
import json
2
import io
3
import zipfile
4
import csv
5
6
from datetime import datetime
7
8
from django.contrib import messages
9
from django.contrib.auth.decorators import login_required
10
from django.contrib.auth.models import User
11
from django.core.exceptions import PermissionDenied
12
from django.core.mail import send_mass_mail
13
from django.core.urlresolvers import reverse
14
from django.http import HttpResponse
15
from django.shortcuts import get_object_or_404, redirect, render
16
from django.views.decorators.http import require_POST
17
from django.contrib.admin.views.decorators import staff_member_required
18
from django.forms.models import model_to_dict
19
from blti import lti_provider
20
21
from .forms import SettingsForm, getSubmissionForm, SubmissionFileUpdateForm, MailForm
22
from .models import SubmissionFile, Submission, Assignment, TestMachine, Course, UserProfile
23
from .models.userprofile import db_fixes, move_user_data
24
from .models.course import lti_secret
25
from .settings import MAIN_URL
26
from .social import passthrough
27
28
import logging
29
logger = logging.getLogger('OpenSubmit')
30
31
32
@login_required
33
def new(request, ass_id):
34
    ass = get_object_or_404(Assignment, pk=ass_id)
35
36
    # Check whether submissions are allowed.
37
    if not ass.can_create_submission(user=request.user):
38
        raise PermissionDenied(
39
            "You are not allowed to create a submission for this assignment")
40
41
    # get submission form according to the assignment type
42
    SubmissionForm = getSubmissionForm(ass)
43
44
    # Analyze submission data
45
    if request.POST:
46
        # we need to fill all forms here,
47
        # so that they can be rendered on validation errors
48
        submissionForm = SubmissionForm(request.user,
49
                                        ass,
50
                                        request.POST,
51
                                        request.FILES)
52
        if submissionForm.is_valid():
53
            # commit=False to set submitter in the instance
54
            submission = submissionForm.save(commit=False)
55
            submission.submitter = request.user
56
            submission.assignment = ass
57
            submission.state = submission.get_initial_state()
58
            # take uploaded file from extra field
59
            if ass.has_attachment:
60
                upload_file = request.FILES['attachment']
61
                submissionFile = SubmissionFile(
62
                    attachment=submissionForm.cleaned_data['attachment'],
63
                    original_filename=upload_file.name)
64
                submissionFile.save()
65
                submission.file_upload = submissionFile
66
            submission.save()
67
            # because of commit=False, we first need to add
68
            # the form-given authors
69
            submissionForm.save_m2m()
70
            submission.save()
71
            messages.info(request, "New submission saved.")
72
            if submission.state == Submission.SUBMITTED:
73
                # Initial state is SUBMITTED,
74
                # which means that there is no validation
75
                if not submission.assignment.is_graded():
76
                    # No validation, no grading. We are done.
77
                    submission.state = Submission.CLOSED
78
                    submission.save()
79
            return redirect('dashboard')
80
        else:
81
            messages.error(
82
                request, "Please correct your submission information.")
83
    else:
84
        submissionForm = SubmissionForm(request.user, ass)
85
    return render(request, 'new.html', {'submissionForm': submissionForm,
86
                                        'assignment': ass})
87
88
89
@login_required
90
def update(request, subm_id):
91
    # Submission should only be editable by their creators
92
    submission = get_object_or_404(Submission, pk=subm_id)
93
    # Somebody may bypass the template check by sending direct POST form data.
94
    # This check also fails when the student performs a simple reload on the /update page
95
    # without form submission after the deadline.
96
    # In practice, the latter happens all the time, so we skip the Exception raising here,
97
    # which lead to endless mails for the admin user in the past.
98
    if not submission.can_reupload():
99
        return redirect('dashboard')
100
        #raise SuspiciousOperation("Update of submission %s is not allowed at this time." % str(subm_id))
101
    if request.user not in submission.authors.all():
102
        return redirect('dashboard')
103
    if request.POST:
104
        updateForm = SubmissionFileUpdateForm(request.POST, request.FILES)
105
        if updateForm.is_valid():
106
            upload_file = request.FILES['attachment']
107
            new_file = SubmissionFile(
108
                attachment=updateForm.files['attachment'],
109
                original_filename=upload_file.name)
110
            new_file.save()
111
            # fix status of old uploaded file
112
            submission.file_upload.replaced_by = new_file
113
            submission.file_upload.save()
114
            # store new file for submissions
115
            submission.file_upload = new_file
116
            submission.state = submission.get_initial_state()
117
            submission.notes = updateForm.data['notes']
118
            submission.save()
119
            messages.info(request, 'Submission files successfully updated.')
120
            return redirect('dashboard')
121
    else:
122
        updateForm = SubmissionFileUpdateForm(instance=submission)
123
    return render(request, 'update.html', {'submissionFileUpdateForm': updateForm,
124
                                           'submission': submission})
125
126
127
@login_required
128
@staff_member_required
129
def mergeusers(request):
130
    '''
131
        Offers an intermediate admin view to merge existing users.
132
    '''
133
    if request.method == 'POST':
134
        primary = get_object_or_404(User, pk=request.POST['primary_id'])
135
        secondary = get_object_or_404(User, pk=request.POST['secondary_id'])
136
        try:
137
            move_user_data(primary, secondary)
138
            messages.info(request, 'Submissions moved to user %u.' %
139
                          (primary.pk))
140
        except:
141
            messages.error(
142
                request, 'Error during data migration, nothing changed.')
143
            return redirect('admin:index')
144
        messages.info(request, 'User %u deleted.' % (secondary.pk))
145
        secondary.delete()
146
        return redirect('admin:index')
147
    primary = get_object_or_404(User, pk=request.GET['primary_id'])
148
    secondary = get_object_or_404(User, pk=request.GET['secondary_id'])
149
    # Determine data to be migrated
150
    return render(request, 'mergeusers.html', {'primary': primary, 'secondary': secondary})
151
152
153
@login_required
154
@staff_member_required
155
def preview(request, subm_id):
156
    '''
157
        Renders a preview of the uploaded student file.
158
        This is only intended for the grading procedure, so staff status is needed.
159
    '''
160
    submission = get_object_or_404(Submission, pk=subm_id)
161
    return render(request, 'file_preview.html', {'submission': submission})
162
163
164
@login_required
165
@staff_member_required
166
def perftable(request, ass_id):
167
    assignment = get_object_or_404(Assignment, pk=ass_id)
168
    response = HttpResponse(content_type='text/csv')
169
    response['Content-Disposition'] = 'attachment; filename="perf_assignment%u.csv"' % assignment.pk
170
    writer = csv.writer(response, delimiter=';')
171
    writer.writerow(['Assignment', 'Submission ID',
172
                     'Authors', 'Performance Data'])
173
    for sub in assignment.submissions.all():
174
        result = sub.get_fulltest_result()
175
        if result:
176
            row = [str(sub.assignment), str(sub.pk), ", ".join(sub.authors.values_list(
177
                'username', flat=True).order_by('username')), str(result.perf_data)]
178
            writer.writerow(row)
179
    return response
180
181
182
@login_required
183
@staff_member_required
184
def duplicates(request, ass_id):
185
    ass = get_object_or_404(Assignment, pk=ass_id)
186
    return render(request, 'duplicates.html', {
187
        'duplicates': ass.duplicate_files(),
188
        'assignment': ass
189
    })
190
191
192
@login_required
193
@staff_member_required
194
def gradingtable(request, course_id):
195
    author_submissions = {}
196
    course = get_object_or_404(Course, pk=course_id)
197
    assignments = course.assignments.all().order_by('title')
198
    # find all gradings per author and assignment
199
    for assignment in assignments:
200
        for submission in assignment.submissions.all().filter(state=Submission.CLOSED):
201
            for author in submission.authors.all():
202
                # author_submissions is a dict mapping authors to another dict
203
                # This second dict maps assignments to submissions (for this author)
204
                # A tuple as dict key does not help here, since we want to iterate over the assignments later
205
                if author not in list(author_submissions.keys()):
206
                    author_submissions[author] = {assignment.pk: submission}
207
                else:
208
                    author_submissions[author][assignment.pk] = submission
209
    resulttable = []
210
    for author, ass2sub in list(author_submissions.items()):
211
        columns = []
212
        numpassed = 0
213
        numgraded = 0
214
        pointsum = 0
215
        columns.append(author.last_name if author.last_name else '')
216
        columns.append(author.first_name if author.first_name else '')
217
        columns.append(
218
            author.profile.student_id if author.profile.student_id else '')
219
        columns.append(
220
            author.profile.study_program if author.profile.study_program else '')
221
        # Process all assignments in the table order, once per author (loop above)
222
        for assignment in assignments:
223
            if assignment.pk in ass2sub:
224
                # Ok, we have a submission for this author in this assignment
225
                submission = ass2sub[assignment.pk]
226
                if assignment.is_graded():
227
                    # is graded, make part of statistics
228
                    numgraded += 1
229
                    if submission.grading_means_passed():
230
                        numpassed += 1
231
                        try:
232
                            pointsum += int(str(submission.grading))
233
                        except Exception:
234
                            pass
235
                # considers both graded and ungraded assignments
236
                columns.append(submission.grading_value_text())
237
            else:
238
                # No submission for this author in this assignment
239
                # This may or may not be bad, so we keep it neutral here
240
                columns.append('-')
241
        columns.append("%s / %s" % (numpassed, numgraded))
242
        columns.append("%u" % pointsum)
243
        resulttable.append(columns)
244
    return render(request, 'gradingtable.html',
245
                  {'course': course, 'assignments': assignments,
246
                   'resulttable': sorted(resulttable)})
247
248
249
def _mail_form(request, users_qs):
250
    receivers_qs = users_qs.order_by('email').distinct().values('first_name', 'last_name', 'email')
251
    receivers = [receiver for receiver in receivers_qs]
252
    request.session['mail_receivers'] = receivers
253
    return render(request, 'mail_form.html', {'receivers': receivers, 'mailform': MailForm()})
254
255
256
@login_required
257
@staff_member_required
258
def mail_course(request, course_id):
259
    course = get_object_or_404(Course, pk=course_id)
260
    users = User.objects.filter(profile__courses__pk=course.pk)
261
    if users.count() == 0:
262
        messages.warning(request, 'No students in this course.')
263
        return redirect('teacher:index')
264
    else:
265
        return _mail_form(request, users)
266
267
268
@login_required
269
@staff_member_required
270
def mail_students(request, student_ids):
271
    id_list = [int(val) for val in student_ids.split(',')]
272
    users = User.objects.filter(pk__in=id_list).distinct()
273
    return _mail_form(request, users)
274
275
276
@login_required
277
@staff_member_required
278
def mail_preview(request):
279
    def _replace_placeholders(text, user):
280
        return text.replace("#FIRSTNAME#", user['first_name'].strip()) \
281
                   .replace("#LASTNAME#", user['last_name'].strip())
282
283
    if 'mail_receivers' in request.session and \
284
       'subject' in request.POST and \
285
       'message' in request.POST:
286
        data = [{'subject': _replace_placeholders(request.POST['subject'], receiver),
287
                 'message': _replace_placeholders(request.POST['message'], receiver),
288
                 'to': receiver['email']
289
                 } for receiver in request.session['mail_receivers']]
290
291
        request.session['mail_data'] = data
292
        del request.session['mail_receivers']
293
        return render(request, 'mail_preview.html', {'data': data})
294
    else:
295
        messages.error(request, 'Error while rendering mail preview.')
296
        return redirect('teacher:index')
297
298
299
@login_required
300
@staff_member_required
301
def mail_send(request):
302
    if 'mail_data' in request.session:
303
        tosend = [[d['subject'],
304
                   d['message'],
305
                   request.user.email,
306
                   [d['to']]]
307
                  for d in request.session['mail_data']]
308
        sent = send_mass_mail(tosend, fail_silently=True)
309
        messages.add_message(request, messages.INFO,
310
                             '%u message(s) sent.' % sent)
311
        del request.session['mail_data']
312
    else:
313
        messages.error(request, 'Error while preparing mail sending.')
314
    return redirect('teacher:index')
315
316
317
@login_required
318
@staff_member_required
319
def coursearchive(request, course_id):
320
    '''
321
        Provides all course submissions and their information as archive download.
322
        For archiving purposes, since withdrawn submissions are included.
323
    '''
324
    output = io.BytesIO()
325
    z = zipfile.ZipFile(output, 'w')
326
327
    course = get_object_or_404(Course, pk=course_id)
328
    assignments = course.assignments.order_by('title')
329
    for ass in assignments:
330
        ass.add_to_zipfile(z)
331
        subs = ass.submissions.all().order_by('submitter')
332
        for sub in subs:
333
            sub.add_to_zipfile(z)
334
335
    z.close()
336
    # go back to start in ZIP file so that Django can deliver it
337
    output.seek(0)
338
    response = HttpResponse(
339
        output, content_type="application/x-zip-compressed")
340
    response['Content-Disposition'] = 'attachment; filename=%s.zip' % course.directory_name()
341
    return response
342
343
344
@login_required
345
@staff_member_required
346
def assarchive(request, ass_id):
347
    '''
348
        Provides all non-withdrawn submissions for an assignment as download.
349
        Intented for supporting offline correction.
350
    '''
351
    output = io.BytesIO()
352
    z = zipfile.ZipFile(output, 'w')
353
354
    ass = get_object_or_404(Assignment, pk=ass_id)
355
    ass.add_to_zipfile(z)
356
    subs = Submission.valid_ones.filter(assignment=ass).order_by('submitter')
357
    for sub in subs:
358
        sub.add_to_zipfile(z)
359
360
    z.close()
361
    # go back to start in ZIP file so that Django can deliver it
362
    output.seek(0)
363
    response = HttpResponse(
364
        output, content_type="application/x-zip-compressed")
365
    response['Content-Disposition'] = 'attachment; filename=%s.zip' % ass.directory_name()
366
    return response
367
368
369
370
@login_required
371
def withdraw(request, subm_id):
372
    # submission should only be deletable by their creators
373
    submission = get_object_or_404(Submission, pk=subm_id)
374
    if not submission.can_withdraw(user=request.user):
375
        raise PermissionDenied(
376
            "Withdrawal for this assignment is no longer possible, or you are unauthorized to access that submission.")
377
    if "confirm" in request.POST:
378 View Code Duplication
        submission.state = Submission.WITHDRAWN
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
379
        submission.save()
380
        messages.info(request, 'Submission successfully withdrawn.')
381
        return redirect('dashboard')
382
    else:
383
        return render(request, 'withdraw.html', {'submission': submission})
384
385
386
@lti_provider(consumer_lookup=lti_secret, site_url=MAIN_URL)
387
@require_POST
388
def lti(request, post_params, consumer_key, *args, **kwargs):
389
    '''
390
        Entry point for LTI consumers.
391
392
        This view is protected by the BLTI package decorator,
393
        which performs all the relevant OAuth signature checking.
394
        It also makes sure that the LTI consumer key and secret were ok.
395
        The latter ones are supposed to be configured in the admin interface.
396
397
        We can now trust on the provided data to be from the LTI provider.
398
399
        If everything worked out, we store the information the session for
400
        the Python Social passthrough provider, which is performing
401
        user creation and database storage.
402
    '''
403
    data = {}
404 View Code Duplication
    data['ltikey'] = post_params.get('oauth_consumer_key')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
405
    # None of them is mandatory
406
    data['id'] = post_params.get('user_id', None)
407
    data['username'] = post_params.get('custom_username', None)
408
    data['last_name'] = post_params.get('lis_person_name_family', None)
409
    data['email'] = post_params.get('lis_person_contact_email_primary', None)
410
    data['first_name'] = post_params.get('lis_person_name_given', None)
411
    request.session[passthrough.SESSION_VAR] = data
412
    return redirect(reverse('social:begin', args=['lti']))
413