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

_mail_form()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 1
Ratio 20 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 1
loc 5
rs 9.4285
c 1
b 0
f 0
cc 2
1
import json
2
import io
3
import zipfile
4
import csv
5
6
from datetime import datetime
7
8
from django.contrib import auth, 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 modelform_factory
19
from django.forms.models import model_to_dict
20
from blti import lti_provider
21
22
from .forms import SettingsForm, getSubmissionForm, SubmissionFileUpdateForm, MailForm
23
from .models import SubmissionFile, Submission, Assignment, TestMachine, Course, UserProfile
24
from .models.userprofile import db_fixes, move_user_data
25
from .models.course import lti_secret
26
from .settings import MAIN_URL
27
from .social import passthrough
28
29
import logging
30
logger = logging.getLogger('OpenSubmit')
31
32
33
def index(request):
34
    if request.user.is_authenticated():
35
        return redirect('dashboard')
36
37
    return render(request, 'index.html', {})
38
39
40
@login_required
41
def logout(request):
42
    auth.logout(request)
43
    return redirect('index')
44
45
46
@login_required
47
def settings(request):
48
    if request.POST:
49
        settingsForm = SettingsForm(request.POST, instance=request.user)
50
        if settingsForm.is_valid():
51
            settingsForm.save()
52
            messages.info(request, 'User settings saved.')
53
            return redirect('dashboard')
54
    else:
55
        settingsForm = SettingsForm(instance=request.user)
56
    return render(request, 'settings.html', {'settingsForm': settingsForm})
57
58
59
@login_required
60
def courses(request):
61
    UserProfileForm = modelform_factory(UserProfile, fields=['courses'])
62
    profile = UserProfile.objects.get(user=request.user)
63
    if request.POST:
64
        coursesForm = UserProfileForm(request.POST, instance=profile)
65
        if coursesForm.is_valid():
66
            coursesForm.save()
67
            messages.info(request, 'You choice was saved.')
68
            return redirect('dashboard')
69
    else:
70
        coursesForm = UserProfileForm(instance=profile)
71
    return render(request, 'courses.html', {'coursesForm': coursesForm, 'courses': request.user.profile.user_courses()})
72
73
74
@login_required
75
def archive(request):
76
    archived = request.user.authored.all().exclude(assignment__course__active=False).filter(
77
        state=Submission.WITHDRAWN).order_by('-created')
78
    return render(request, 'archive.html', {'archived': archived})
79
80
81
@login_required
82
def dashboard(request):
83
    # Fix database on lower levels for the current user
84
    db_fixes(request.user)
85
    profile = request.user.profile
86
87
    # If this is pass-through authentication,
88
    # we can determine additional information
89
    if 'passthroughauth' in request.session:
90
        if 'ltikey' in request.session['passthroughauth']:
91
            # User coming through LTI.
92
            # Check the course having this LTI key and
93
            # enable it for the user.
94
            try:
95
                ltikey = request.session['passthroughauth']['ltikey']
96
                request.session['ui_disable_logout'] = True
97
                course = Course.objects.get(lti_key=ltikey)
98
                profile.courses.add(course)
99
                profile.save()
100
            except Exception:
101
                # This is only a comfort function,
102
                # so we should not crash the app if that goes wrong
103
                pass
104
105
    # This is the first view than can check
106
    # if the user information is complete.
107
    # If not, then we drop annyoing popups until he gives up.
108
    settingsform = SettingsForm(model_to_dict(
109
        request.user), instance=request.user)
110
    if not settingsform.is_valid():
111
        messages.error(request, "Your user settings are incomplete.")
112
113
    # Student submissions under validation / grading
114
    subs_in_progress = request.user.authored.all(). \
115
        exclude(assignment__course__active=False). \
116
        exclude(state=Submission.RECEIVED). \
117
        exclude(state=Submission.WITHDRAWN). \
118
        exclude(state=Submission.CLOSED). \
119
        exclude(state=Submission.CLOSED_TEST_FULL_PENDING). \
120
        order_by('-created')
121
122
    # Closed student submissions, graded ones first
123
    subs_finished = request.user.authored.all(). \
124
        exclude(assignment__course__active=False). \
125
        filter(state__in=[Submission.CLOSED, Submission.CLOSED_TEST_FULL_PENDING]). \
126
        order_by('-assignment__gradingScheme', '-created')
127
128
    # Assignments the student missed
129
    assign_missed = request.user.profile.gone_assignments()
130
131
    username = request.user.get_full_name() + " <" + request.user.email + ">"
132
    assignments = request.user.profile.open_assignments()
133
    return render(request, 'dashboard.html', {
134
        'subs_in_progress': subs_in_progress,
135
        'subs_finished': subs_finished,
136
        'user': request.user,
137
        'username': username,
138
        'courses': request.user.profile.user_courses(),
139
        'assignments': assignments,
140
        'assign_missed': assign_missed,
141
        'machines': TestMachine.objects.filter(enabled=True),
142
        'today': datetime.now()}
143
    )
144
145
146
@login_required
147
def details(request, subm_id):
148
    subm = get_object_or_404(Submission, pk=subm_id)
149
    # only authors should be able to look into submission details
150
    if not (request.user in subm.authors.all() or request.user.is_staff):
151
        raise PermissionDenied()
152
    return render(request, 'details.html', {
153
        'submission': subm}
154
    )
155
156
157
@login_required
158
def new(request, ass_id):
159
    ass = get_object_or_404(Assignment, pk=ass_id)
160
161
    # Check whether submissions are allowed.
162
    if not ass.can_create_submission(user=request.user):
163
        raise PermissionDenied(
164
            "You are not allowed to create a submission for this assignment")
165
166
    # get submission form according to the assignment type
167
    SubmissionForm = getSubmissionForm(ass)
168
169
    # Analyze submission data
170
    if request.POST:
171
        # we need to fill all forms here,
172
        # so that they can be rendered on validation errors
173
        submissionForm = SubmissionForm(request.user,
174
                                        ass,
175
                                        request.POST,
176
                                        request.FILES)
177
        if submissionForm.is_valid():
178
            # commit=False to set submitter in the instance
179
            submission = submissionForm.save(commit=False)
180
            submission.submitter = request.user
181
            submission.assignment = ass
182
            submission.state = submission.get_initial_state()
183
            # take uploaded file from extra field
184
            if ass.has_attachment:
185
                upload_file = request.FILES['attachment']
186
                submissionFile = SubmissionFile(
187
                    attachment=submissionForm.cleaned_data['attachment'],
188
                    original_filename=upload_file.name)
189
                submissionFile.save()
190
                submission.file_upload = submissionFile
191
            submission.save()
192
            # because of commit=False, we first need to add
193
            # the form-given authors
194
            submissionForm.save_m2m()
195
            submission.save()
196
            messages.info(request, "New submission saved.")
197
            if submission.state == Submission.SUBMITTED:
198
                # Initial state is SUBMITTED,
199
                # which means that there is no validation
200
                if not submission.assignment.is_graded():
201
                    # No validation, no grading. We are done.
202
                    submission.state = Submission.CLOSED
203
                    submission.save()
204
            return redirect('dashboard')
205
        else:
206
            messages.error(
207
                request, "Please correct your submission information.")
208
    else:
209
        submissionForm = SubmissionForm(request.user, ass)
210
    return render(request, 'new.html', {'submissionForm': submissionForm,
211
                                        'assignment': ass})
212
213
214
@login_required
215
def update(request, subm_id):
216
    # Submission should only be editable by their creators
217
    submission = get_object_or_404(Submission, pk=subm_id)
218
    # Somebody may bypass the template check by sending direct POST form data.
219
    # This check also fails when the student performs a simple reload on the /update page
220
    # without form submission after the deadline.
221
    # In practice, the latter happens all the time, so we skip the Exception raising here,
222
    # which lead to endless mails for the admin user in the past.
223
    if not submission.can_reupload():
224
        return redirect('dashboard')
225
        #raise SuspiciousOperation("Update of submission %s is not allowed at this time." % str(subm_id))
226
    if request.user not in submission.authors.all():
227
        return redirect('dashboard')
228
    if request.POST:
229
        updateForm = SubmissionFileUpdateForm(request.POST, request.FILES)
230
        if updateForm.is_valid():
231
            upload_file = request.FILES['attachment']
232
            new_file = SubmissionFile(
233
                attachment=updateForm.files['attachment'],
234
                original_filename=upload_file.name)
235
            new_file.save()
236
            # fix status of old uploaded file
237
            submission.file_upload.replaced_by = new_file
238
            submission.file_upload.save()
239
            # store new file for submissions
240
            submission.file_upload = new_file
241
            submission.state = submission.get_initial_state()
242
            submission.notes = updateForm.data['notes']
243
            submission.save()
244
            messages.info(request, 'Submission files successfully updated.')
245
            return redirect('dashboard')
246
    else:
247
        updateForm = SubmissionFileUpdateForm(instance=submission)
248
    return render(request, 'update.html', {'submissionFileUpdateForm': updateForm,
249
                                           'submission': submission})
250
251
252
@login_required
253
@staff_member_required
254
def mergeusers(request):
255
    '''
256
        Offers an intermediate admin view to merge existing users.
257
    '''
258
    if request.method == 'POST':
259
        primary = get_object_or_404(User, pk=request.POST['primary_id'])
260
        secondary = get_object_or_404(User, pk=request.POST['secondary_id'])
261
        try:
262
            move_user_data(primary, secondary)
263
            messages.info(request, 'Submissions moved to user %u.' %
264
                          (primary.pk))
265
        except:
266
            messages.error(
267
                request, 'Error during data migration, nothing changed.')
268
            return redirect('admin:index')
269
        messages.info(request, 'User %u deleted.' % (secondary.pk))
270
        secondary.delete()
271
        return redirect('admin:index')
272
    primary = get_object_or_404(User, pk=request.GET['primary_id'])
273
    secondary = get_object_or_404(User, pk=request.GET['secondary_id'])
274
    # Determine data to be migrated
275
    return render(request, 'mergeusers.html', {'primary': primary, 'secondary': secondary})
276
277
278
@login_required
279
@staff_member_required
280
def preview(request, subm_id):
281
    '''
282
        Renders a preview of the uploaded student file.
283
        This is only intended for the grading procedure, so staff status is needed.
284
    '''
285
    submission = get_object_or_404(Submission, pk=subm_id)
286
    return render(request, 'file_preview.html', {'submission': submission})
287
288
289
@login_required
290
@staff_member_required
291
def perftable(request, ass_id):
292
    assignment = get_object_or_404(Assignment, pk=ass_id)
293
    response = HttpResponse(content_type='text/csv')
294
    response['Content-Disposition'] = 'attachment; filename="perf_assignment%u.csv"' % assignment.pk
295
    writer = csv.writer(response, delimiter=';')
296
    writer.writerow(['Assignment', 'Submission ID',
297
                     'Authors', 'Performance Data'])
298
    for sub in assignment.submissions.all():
299
        result = sub.get_fulltest_result()
300
        if result:
301
            row = [str(sub.assignment), str(sub.pk), ", ".join(sub.authors.values_list(
302
                'username', flat=True).order_by('username')), str(result.perf_data)]
303
            writer.writerow(row)
304
    return response
305
306
307
@login_required
308
@staff_member_required
309
def duplicates(request, ass_id):
310
    ass = get_object_or_404(Assignment, pk=ass_id)
311
    return render(request, 'duplicates.html', {
312
        'duplicates': ass.duplicate_files(),
313
        'assignment': ass
314
    })
315
316
317
@login_required
318
@staff_member_required
319
def gradingtable(request, course_id):
320
    author_submissions = {}
321
    course = get_object_or_404(Course, pk=course_id)
322
    assignments = course.assignments.all().order_by('title')
323
    # find all gradings per author and assignment
324
    for assignment in assignments:
325
        for submission in assignment.submissions.all().filter(state=Submission.CLOSED):
326
            for author in submission.authors.all():
327
                # author_submissions is a dict mapping authors to another dict
328
                # This second dict maps assignments to submissions (for this author)
329
                # A tuple as dict key does not help here, since we want to iterate over the assignments later
330
                if author not in list(author_submissions.keys()):
331
                    author_submissions[author] = {assignment.pk: submission}
332
                else:
333
                    author_submissions[author][assignment.pk] = submission
334
    resulttable = []
335
    for author, ass2sub in list(author_submissions.items()):
336
        columns = []
337
        numpassed = 0
338
        numgraded = 0
339
        pointsum = 0
340
        columns.append(author.last_name if author.last_name else '')
341
        columns.append(author.first_name if author.first_name else '')
342
        columns.append(
343
            author.profile.student_id if author.profile.student_id else '')
344
        columns.append(
345
            author.profile.study_program if author.profile.study_program else '')
346
        # Process all assignments in the table order, once per author (loop above)
347
        for assignment in assignments:
348
            if assignment.pk in ass2sub:
349
                # Ok, we have a submission for this author in this assignment
350
                submission = ass2sub[assignment.pk]
351
                if assignment.is_graded():
352
                    # is graded, make part of statistics
353
                    numgraded += 1
354
                    if submission.grading_means_passed():
355
                        numpassed += 1
356
                        try:
357
                            pointsum += int(str(submission.grading))
358
                        except Exception:
359
                            pass
360
                # considers both graded and ungraded assignments
361
                columns.append(submission.grading_value_text())
362
            else:
363
                # No submission for this author in this assignment
364
                # This may or may not be bad, so we keep it neutral here
365
                columns.append('-')
366
        columns.append("%s / %s" % (numpassed, numgraded))
367
        columns.append("%u" % pointsum)
368
        resulttable.append(columns)
369
    return render(request, 'gradingtable.html',
370
                  {'course': course, 'assignments': assignments,
371
                   'resulttable': sorted(resulttable)})
372
373
374
def _mail_form(request, users_qs):
375
    receivers_qs = users_qs.order_by('email').distinct().values('first_name', 'last_name', 'email')
376
    receivers = [receiver for receiver in receivers_qs]
377
    request.session['mail_receivers'] = receivers
378 View Code Duplication
    return render(request, 'mail_form.html', {'receivers': receivers, 'mailform': MailForm()})
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
379
380
381
@login_required
382
@staff_member_required
383
def mail_course(request, course_id):
384
    course = get_object_or_404(Course, pk=course_id)
385
    users = User.objects.filter(profile__courses__pk=course.pk)
386
    if users.count() == 0:
387
        messages.warning(request, 'No students in this course.')
388
        return redirect('teacher:index')
389
    else:
390
        return _mail_form(request, users)
391
392
393
@login_required
394
@staff_member_required
395
def mail_students(request, student_ids):
396
    id_list = [int(val) for val in student_ids.split(',')]
397
    users = User.objects.filter(pk__in=id_list).distinct()
398
    return _mail_form(request, users)
399
400
401
@login_required
402
@staff_member_required
403
def mail_preview(request):
404 View Code Duplication
    def _replace_placeholders(text, user):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
405
        return text.replace("#FIRSTNAME#", user['first_name'].strip()) \
406
                   .replace("#LASTNAME#", user['last_name'].strip())
407
408
    if 'mail_receivers' in request.session and \
409
       'subject' in request.POST and \
410
       'message' in request.POST:
411
        data = [{'subject': _replace_placeholders(request.POST['subject'], receiver),
412
                 'message': _replace_placeholders(request.POST['message'], receiver),
413
                 'to': receiver['email']
414
                 } for receiver in request.session['mail_receivers']]
415
416
        request.session['mail_data'] = data
417
        del request.session['mail_receivers']
418
        return render(request, 'mail_preview.html', {'data': data})
419
    else:
420
        messages.error(request, 'Error while rendering mail preview.')
421
        return redirect('teacher:index')
422
423
424
@login_required
425
@staff_member_required
426
def mail_send(request):
427
    if 'mail_data' in request.session:
428
        tosend = [[d['subject'],
429
                   d['message'],
430
                   request.user.email,
431
                   [d['to']]]
432
                  for d in request.session['mail_data']]
433
        sent = send_mass_mail(tosend, fail_silently=True)
434
        messages.add_message(request, messages.INFO,
435
                             '%u message(s) sent.' % sent)
436
        del request.session['mail_data']
437
    else:
438
        messages.error(request, 'Error while preparing mail sending.')
439
    return redirect('teacher:index')
440
441
442
@login_required
443
@staff_member_required
444
def coursearchive(request, course_id):
445
    '''
446
        Provides all course submissions and their information as archive download.
447
        For archiving purposes, since withdrawn submissions are included.
448
    '''
449
    output = io.BytesIO()
450
    z = zipfile.ZipFile(output, 'w')
451
452
    course = get_object_or_404(Course, pk=course_id)
453
    assignments = course.assignments.order_by('title')
454
    for ass in assignments:
455
        ass.add_to_zipfile(z)
456
        subs = ass.submissions.all().order_by('submitter')
457
        for sub in subs:
458
            sub.add_to_zipfile(z)
459
460
    z.close()
461
    # go back to start in ZIP file so that Django can deliver it
462
    output.seek(0)
463
    response = HttpResponse(
464
        output, content_type="application/x-zip-compressed")
465
    response['Content-Disposition'] = 'attachment; filename=%s.zip' % course.directory_name()
466
    return response
467
468
469
@login_required
470
@staff_member_required
471
def assarchive(request, ass_id):
472
    '''
473
        Provides all non-withdrawn submissions for an assignment as download.
474
        Intented for supporting offline correction.
475
    '''
476
    output = io.BytesIO()
477
    z = zipfile.ZipFile(output, 'w')
478
479
    ass = get_object_or_404(Assignment, pk=ass_id)
480
    ass.add_to_zipfile(z)
481
    subs = Submission.valid_ones.filter(assignment=ass).order_by('submitter')
482
    for sub in subs:
483
        sub.add_to_zipfile(z)
484
485
    z.close()
486
    # go back to start in ZIP file so that Django can deliver it
487
    output.seek(0)
488
    response = HttpResponse(
489
        output, content_type="application/x-zip-compressed")
490
    response['Content-Disposition'] = 'attachment; filename=%s.zip' % ass.directory_name()
491
    return response
492
493
494
@login_required
495
def machine(request, machine_id):
496
    machine = get_object_or_404(TestMachine, pk=machine_id)
497
    try:
498
        config = json.loads(machine.config)
499
    except:
500
        config = []
501
    queue = Submission.pending_student_tests.all()
502
    additional = len(Submission.pending_full_tests.all())
503
    return render(request, 'machine.html', {'machine': machine, 'queue': queue, 'additional': additional, 'config': config})
504
505
506
@login_required
507
def withdraw(request, subm_id):
508
    # submission should only be deletable by their creators
509
    submission = get_object_or_404(Submission, pk=subm_id)
510
    if not submission.can_withdraw(user=request.user):
511
        raise PermissionDenied(
512
            "Withdrawal for this assignment is no longer possible, or you are unauthorized to access that submission.")
513
    if "confirm" in request.POST:
514
        submission.state = Submission.WITHDRAWN
515
        submission.save()
516
        messages.info(request, 'Submission successfully withdrawn.')
517
        return redirect('dashboard')
518
    else:
519
        return render(request, 'withdraw.html', {'submission': submission})
520
521
522
@lti_provider(consumer_lookup=lti_secret, site_url=MAIN_URL)
523
@require_POST
524
def lti(request, post_params, consumer_key, *args, **kwargs):
525
    '''
526
        Entry point for LTI consumers.
527
528
        This view is protected by the BLTI package decorator,
529
        which performs all the relevant OAuth signature checking.
530
        It also makes sure that the LTI consumer key and secret were ok.
531
        The latter ones are supposed to be configured in the admin interface.
532
533
        We can now trust on the provided data to be from the LTI provider.
534
535
        If everything worked out, we store the information the session for
536
        the Python Social passthrough provider, which is performing
537
        user creation and database storage.
538
    '''
539
    data = {}
540
    data['ltikey'] = post_params.get('oauth_consumer_key')
541
    # None of them is mandatory
542
    data['id'] = post_params.get('user_id', None)
543
    data['username'] = post_params.get('custom_username', None)
544
    data['last_name'] = post_params.get('lis_person_name_family', None)
545
    data['email'] = post_params.get('lis_person_contact_email_primary', None)
546
    data['first_name'] = post_params.get('lis_person_name_given', None)
547
    request.session[passthrough.SESSION_VAR] = data
548
    return redirect(reverse('social:begin', args=['lti']))
549