Passed
Push — master ( b1ab39...9c18ff )
by Peter
01:21
created

mergeusers()   B

Complexity

Conditions 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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