Submission.authorized_users()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
1
from django.db import models
2
from django.contrib.auth.models import User
3
from django.utils import timezone
4
from django.core.urlresolvers import reverse
5
6
from datetime import datetime
7
import tempfile
8
import zipfile
9
import tarfile
10
11
from opensubmit import mails
12
13
from .submissionfile import upload_path, SubmissionFile
14
from .submissiontestresult import SubmissionTestResult
15
16
import logging
17
import shutil
18
import os
19
logger = logging.getLogger('OpenSubmit')
20
21
22
class ValidSubmissionsManager(models.Manager):
23
    '''
24
        A model manager used by the Submission model. It returns a sorted list
25
        of submissions that are not withdrawn.
26
    '''
27
28
    def get_queryset(self):
29
        submissions = Submission.objects.exclude(
30
            state__in=[Submission.WITHDRAWN, Submission.RECEIVED]).order_by('pk')
31
        return submissions
32
33
34
class PendingStudentTestsManager(models.Manager):
35
    '''
36
        A model manager used by the Submission model. It returns a sorted list
37
        of executor work to be done that relates to compilation and
38
        validation test jobs for students.
39
        The basic approach is that compilation should happen before validation in FIFO order,
40
        under the assumption is that the time effort is increasing.
41
    '''
42
43
    def get_queryset(self):
44
        jobs = Submission.objects.filter(
45
            state=Submission.TEST_VALIDITY_PENDING).order_by('-modified')
46
        return jobs
47
48
49
class PendingFullTestsManager(models.Manager):
50
    '''
51
        A model manager used by the Submission model. It returns a sorted list
52
        of full test executor work to be done.
53
        The basic approach is that non-graded job validation wins over closed job
54
        re-evaluation triggered by the teachers,
55
        under the assumption is that the time effort is increasing.
56
    '''
57
58
    def get_queryset(self):
59
        jobs = Submission.objects.filter(
60
            state__in=[Submission.TEST_FULL_PENDING,
61
                       Submission.CLOSED_TEST_FULL_PENDING]
62
        ).order_by('-state').order_by('-modified')
63
        return jobs
64
65
66
class PendingTestsManager(models.Manager):
67
    '''
68
        A combination of both, with focus on getting the full tests later than the validity tests.
69
    '''
70
71
    def get_queryset(self):
72
        jobs = Submission.objects.filter(
73
            state__in=[Submission.TEST_FULL_PENDING,           # PF
74
                       Submission.CLOSED_TEST_FULL_PENDING,    # CT
75
                       Submission.TEST_VALIDITY_PENDING]      # PV
76
        ).order_by('-state').order_by('-modified')
77
        return jobs
78
79
80
class Submission(models.Model):
81
    '''
82
        A student submission for an assignment.
83
    '''
84
85
    RECEIVED = 'R'                   # Only for initialization, this should never persist
86
    WITHDRAWN = 'W'                  # Withdrawn by the student
87
    SUBMITTED = 'S'                  # Submitted, no tests so far
88
    TEST_VALIDITY_PENDING = 'PV'     # Submitted, validity test planned
89
    TEST_VALIDITY_FAILED = 'FV'      # Submitted, validity test failed
90
    TEST_FULL_PENDING = 'PF'         # Submitted, full test planned
91
    TEST_FULL_FAILED = 'FF'          # Submitted, full test failed
92
    SUBMITTED_TESTED = 'ST'          # Submitted, all tests performed, grading planned
93
    GRADING_IN_PROGRESS = 'GP'       # Grading in progress, but not finished
94
    GRADED = 'G'                     # Graded, student notification not done
95
    CLOSED = 'C'                     # Graded, student notification done
96
    CLOSED_TEST_FULL_PENDING = 'CT'  # Keep grading status, full test planned
97
98
    # Docs start: States
99
    # State description in teacher backend
100
    STATES = (
101
102
        # The submission is currently uploaded,
103
        # some internal processing still takes place.
104
        (RECEIVED, 'Received'),
105
106
        # The submission was withdrawn by the student
107
        # before the deadline. No further automated action
108
        # will take place with this submission.
109
        (WITHDRAWN, 'Withdrawn'),
110
111
        # The submission is completely uploaded.
112
        # If code validation is configured, the state will
113
        # directly change to TEST_VALIDITY_PENDING.
114
        (SUBMITTED, 'Submitted'),
115
116
        # The submission is waiting to be validated with the
117
        # validation script on one of the test machines.
118
        # The submission remains in this state until some
119
        # validation result was sent from the test machines.
120
        (TEST_VALIDITY_PENDING, 'Validity test pending'),
121
122
        # The validation of the student sources on the
123
        # test machine failed. No further automated action will
124
        # take place with this submission.
125
        # The students get informed by email.
126
        (TEST_VALIDITY_FAILED, 'Validity test failed'),
127
128
        # The submission is waiting to be checked with the
129
        # full test script on one of the test machines.
130
        # The submission remains in this state until
131
        # some result was sent from the test machines.
132
        (TEST_FULL_PENDING, 'Full test pending'),
133
134
        # The (compilation and) validation of the student
135
        # sources on the test machine worked, only the full test
136
        # failed. No further automated action will take place with
137
        # this submission.
138
        (TEST_FULL_FAILED, 'All but full test passed, grading pending'),
139
140
        # The compilation (if configured) and the validation and
141
        #  the full test (if configured) of the submission were
142
        # successful. No further automated action will take
143
        # place with this submission.
144
        (SUBMITTED_TESTED, 'All tests passed, grading pending'),
145
146
        # Some grading took place in the teacher backend,
147
        # and the submission was explicitly marked with
148
        # 'grading not finished'. This allows correctors to have
149
        # multiple runs over the submissions and see which
150
        # of the submissions were already investigated.
151
        (GRADING_IN_PROGRESS, 'Grading not finished'),
152
153
        # Some grading took place in the teacher backend,
154
        # and the submission was explicitly marked with
155
        # 'grading not finished'. This allows correctors
156
        # to have multiple runs over the submissions and
157
        #  see which of the submissions were already investigated.
158
        (GRADED, 'Grading finished'),
159
160
        # The submission is closed, meaning that in the
161
        # teacher backend, the submission was marked
162
        # as closed to trigger the student notification
163
        # for their final assignment grades.
164
        # Students are notified by email.
165
        (CLOSED, 'Closed, student notified'),
166
167
        # The submission is closed, but marked for
168
        # another full test run.
169
        # This is typically used to have some post-assignment
170
        # analysis of student submissions
171
        # by the help of full test scripts.
172
        # Students never get any notification about this state.
173
        (CLOSED_TEST_FULL_PENDING, 'Closed, full test pending')
174
    )
175
176
    # State description in student dashboard
177
    STUDENT_STATES = (
178
        (RECEIVED, 'Received'),
179
        (WITHDRAWN, 'Withdrawn'),
180
        (SUBMITTED, 'Waiting for grading'),
181
        (TEST_VALIDITY_PENDING, 'Waiting for validation test'),
182
        (TEST_VALIDITY_FAILED, 'Validation failed'),
183
        (TEST_FULL_PENDING, 'Waiting for grading'),
184
        (TEST_FULL_FAILED, 'Waiting for grading'),
185
        (SUBMITTED_TESTED, 'Waiting for grading'),
186
        (GRADING_IN_PROGRESS, 'Waiting for grading'),
187
        (GRADED, 'Waiting for grading'),
188
        (CLOSED, 'Done'),
189
        (CLOSED_TEST_FULL_PENDING, 'Done')
190
    )
191
    # Docs end: States    
192
193
    assignment = models.ForeignKey('Assignment', related_name='submissions')
194
    submitter = models.ForeignKey(User, related_name='submitted')
195
    # includes also submitter, see submission_post_save() handler
196
    authors = models.ManyToManyField(User, related_name='authored')
197
    notes = models.TextField(max_length=200, blank=True)
198
    file_upload = models.ForeignKey(
199
        'SubmissionFile', related_name='submissions', blank=True, null=True, verbose_name='New upload')
200
    created = models.DateTimeField(auto_now_add=True, editable=False)
201
    modified = models.DateTimeField(
202
        auto_now=True, editable=False, blank=True, null=True)
203
    grading = models.ForeignKey('Grading', blank=True, null=True)
204
    grading_notes = models.TextField(max_length=10000, blank=True, null=True,
205
                                     help_text="Specific notes about the grading for this submission.")
206
    grading_file = models.FileField(upload_to=upload_path, blank=True, null=True,
207
                                    help_text="Additional information about the grading as file.")
208
    state = models.CharField(max_length=2, choices=STATES, default=RECEIVED)
209
210
    objects = models.Manager()
211
    pending_student_tests = PendingStudentTestsManager()
212
    pending_full_tests = PendingFullTestsManager()
213
    pending_tests = PendingTestsManager()
214
    valid_ones = ValidSubmissionsManager()
215
216
    class Meta:
217
        app_label = 'opensubmit'
218
219
    @staticmethod
220
    def qs_valid(qs):
221
        '''
222
            A filtering of the given Submission queryset for all submissions that were successfully validated. This includes the following cases:
223
224
            - The submission was submitted and there are no tests.
225
            - The submission was successfully validity-tested, regardless of the full test status (not existent / failed / success).
226
            - The submission was graded or the grading was already started.
227
            - The submission was closed.
228
229
            The idea is to get all submissions that were a valid solution, regardless of the point in time where you check the list.
230
        '''
231
        return qs.filter(state__in=[Submission.SUBMITTED, Submission.SUBMITTED_TESTED, Submission.TEST_FULL_FAILED, Submission.GRADING_IN_PROGRESS, Submission.GRADED, Submission.CLOSED, Submission.CLOSED_TEST_FULL_PENDING])
232
233
    @staticmethod
234
    def qs_tobegraded(qs):
235
        '''
236
            A filtering of the given Submission queryset for all submissions that are gradeable. This includes the following cases:
237
238
            - The submission was submitted and there are no tests.
239
            - The submission was successfully validity-tested, regardless of the full test status (not existent / failed / success).
240
            - The grading was already started, but not finished.
241
242
            The idea is to get a list of work to be done for the correctors.
243
        '''
244
        return qs.filter(state__in=[Submission.SUBMITTED, Submission.SUBMITTED_TESTED, Submission.TEST_FULL_FAILED, Submission.GRADING_IN_PROGRESS])
245
246
    @staticmethod
247
    def qs_notified(qs):
248
        '''
249
            A filtering of the given Submission queryset for all submissions were students already got notification about their grade. This includes the following cases:
250
251
            - The submission was closed.
252
            - The submission was closed and full tests were re-started.
253
254
            The idea is to get indirectly a list of emails that went out.
255
        '''
256
        return qs.filter(state__in=[Submission.CLOSED, Submission.CLOSED_TEST_FULL_PENDING])
257
258
    @staticmethod
259
    def qs_notwithdrawn(qs):
260
        '''
261
            A filtering of the given Submission queryset for all submissions that were not withdrawn. This excludes the following cases:
262
263
            - The submission was withdrawn.
264
            - The submission was received and never left that status due to an error.
265
266
            The idea is a list of all submissions, but without the garbage.
267
        '''
268
        return qs.exclude(state__in=[Submission.WITHDRAWN, Submission.RECEIVED])
269
270
    def __str__(self):
271
        if self.pk:
272
            return str(self.pk)
273
        else:
274
            return "New Submission instance"
275
276
    def author_list(self):
277
        ''' The list of authors als text, for admin submission list overview.'''
278
        author_list = [self.submitter] + \
279
            [author for author in self.authors.all().exclude(pk=self.submitter.pk)]
280
        return ",\n".join([author.get_full_name() for author in author_list])
281
    author_list.admin_order_field = 'submitter'
282
283
    def course(self):
284
        ''' The course of this submission as text, for admin submission list overview.'''
285
        return self.assignment.course
286
    course.admin_order_field = 'assignment__course'
287
288
    def grading_status_text(self):
289
        '''
290
        A rendering of the grading that is an answer on the question
291
        "Is grading finished?".
292
        Used in duplicate view and submission list on the teacher backend.
293
        '''
294
        if self.assignment.is_graded():
295
            if self.is_grading_finished():
296
                return str('Yes ({0})'.format(self.grading))
297
            else:
298
                return str('No')
299
        else:
300
            return str('Not graded')
301
    grading_status_text.admin_order_field = 'grading__title'
302
    grading_status_text.short_description = "Grading finished?"
303
304
    def has_grading_notes(self):
305
        ''' Determines if the submission has grading notes.
306
            Used for submission list overview in teacher backend.
307
        '''
308
        if self.grading_notes is not None and len(self.grading_notes) > 0:
309
            return True
310
        else:
311
            return False
312
    has_grading_notes.short_description = "Grading notes?"
313
    has_grading_notes.admin_order_field = 'grading_notes'
314
    has_grading_notes.boolean = True            # show nice little icon
315
316
    def grading_value_text(self):
317
        '''
318
        A rendering of the grading that is an answer to the question
319
        "What is the grade?".
320
        '''
321
        if self.assignment.is_graded():
322
            if self.is_grading_finished():
323
                return str(self.grading)
324
            else:
325
                return str('pending')
326
        else:
327
            if self.is_grading_finished():
328
                return str('done')
329
            else:
330
                return str('not done')
331
332
    def grading_means_passed(self):
333
        '''
334
        Information if the given grading means passed.
335
        Non-graded assignments are always passed.
336
        '''
337
        if self.assignment.is_graded():
338
            if self.grading and self.grading.means_passed:
339
                return True
340
            else:
341
                return False
342
        else:
343
            return True
344
345
    def log(self, level, format_string, *args, **kwargs):
346
        level_mapping = {
347
            'CRITICAL': logging.CRITICAL,
348
            'ERROR': logging.ERROR,
349
            'WARNING': logging.WARNING,
350
            'INFO': logging.INFO,
351
            'DEBUG': logging.DEBUG,
352
            'NOTSET': logging.NOTSET,
353
        }
354
        level_numeric = level_mapping[level] if level in level_mapping else level_mapping['NOTSET']
355
        if self.pk:
356
            log_prefix = "<{} pk={}>".format(self.__class__.__name__, self.pk)
357
        else:
358
            log_prefix = "<{} new>".format(self.__class__.__name__)
359
        return logger.log(level_numeric, "{} {}".format(log_prefix, format_string.format(*args, **kwargs)))
360
361
    def can_modify(self, user=None):
362
        """Determines whether the submission can be modified.
363
        Returns a boolean value.
364
        The 'user' parameter is optional and additionally checks whether
365
        the given user is authorized to perform these actions.
366
367
        This function checks the submission states and assignment deadlines."""
368
369
        # The user must be authorized to commit these actions.
370
        if user and not self.user_can_modify(user):
371
            #self.log('DEBUG', "Submission cannot be modified, user is not an authorized user ({!r} not in {!r})", user, self.authorized_users)
372
            return False
373
374
        # Modification of submissions, that are withdrawn, graded or currently being graded, is prohibited.
375
        if self.state in [self.WITHDRAWN, self.GRADED, self.GRADING_IN_PROGRESS, ]:
376
            #self.log('DEBUG', "Submission cannot be modified, is in state '{}'", self.state)
377
            return False
378
379
        # Modification of closed submissions is prohibited.
380
        if self.is_closed():
381
            if self.assignment.is_graded():
382
                # There is a grading procedure, so taking it back would invalidate the tutors work
383
                #self.log('DEBUG', "Submission cannot be modified, it is closed and graded")
384
                return False
385
            else:
386
                #self.log('DEBUG', "Closed submission can be modified, since it has no grading scheme.")
387
                return True
388
389
        # Submissions, that are executed right now, cannot be modified
390
        if self.state in [self.TEST_VALIDITY_PENDING, self.TEST_FULL_PENDING]:
391
            if not self.file_upload:
392
                self.log(
393
                    'CRITICAL', "Submission is in invalid state! State is '{}', but there is no file uploaded!", self.state)
394
                raise AssertionError()
395
                return False
396
            if self.file_upload.is_executed():
397
                # The above call informs that the uploaded file is being executed, or execution has been completed.
398
                # Since the current state is 'PENDING', the execution cannot yet be completed.
399
                # Thus, the submitted file is being executed right now.
400
                return False
401
402
        # Submissions must belong to an assignment.
403
        if not self.assignment:
404
            self.log('CRITICAL', "Submission does not belong to an assignment!")
405
            raise AssertionError()
406
407
        # Submissions, that belong to an assignment where the hard deadline has passed,
408
        # cannot be modified.
409
        if self.assignment.hard_deadline and timezone.now() > self.assignment.hard_deadline:
410
            #self.log('DEBUG', "Submission cannot be modified - assignment's hard deadline has passed (hard deadline is: {})", self.assignment.hard_deadline)
411
            return False
412
413
        # The soft deadline has no effect (yet).
414
        if self.assignment.soft_deadline:
415
            if timezone.now() > self.assignment.soft_deadline:
416
                # The soft deadline has passed
417
                pass  # do nothing.
418
419
        #self.log('DEBUG', "Submission can be modified.")
420
        return True
421
422
    def can_withdraw(self, user=None):
423
        """Determines whether a submisison can be withdrawn.
424
        Returns a boolean value.
425
426
        Requires: can_modify.
427
428
        Currently, the conditions for modifications and withdrawal are the same."""
429
        return self.can_modify(user=user)
430
431
    def can_reupload(self, user=None):
432
        """Determines whether a submission can be re-uploaded.
433
        Returns a boolean value.
434
435
        Requires: can_modify.
436
437
        Re-uploads are allowed only when test executions have failed."""
438
439
        # Re-uploads are allowed only when test executions have failed.
440
        if self.state not in (self.TEST_VALIDITY_FAILED, self.TEST_FULL_FAILED):
441
            return False
442
443
        # It must be allowed to modify the submission.
444
        if not self.can_modify(user=user):
445
            return False
446
447
        return True
448
449
    def user_can_modify(self, user):
450
        """Determines whether a user is allowed to modify a specific submission in general.
451
        Returns a boolean value.
452
453
        A user is authorized when he is part of the authorized users (submitter and authors)."""
454
        return user in self.authorized_users
455
456
    @property
457
    def authorized_users(self):
458
        """Returns a list of all authorized users (submitter and additional authors)."""
459
        return [self.submitter, ] + list(self.authors.all())
460
461
    def is_withdrawn(self):
462
        return self.state == self.WITHDRAWN
463
464
    def is_closed(self):
465
        return self.state in [self.CLOSED, self.CLOSED_TEST_FULL_PENDING]
466
467
    def is_grading_finished(self):
468
        return self.state in [self.GRADED, self.CLOSED, self.CLOSED_TEST_FULL_PENDING]
469
470
    def show_grading(self):
471
        return self.assignment.gradingScheme != None and self.is_closed()
472
473
    def get_initial_state(self):
474
        '''
475
            Return first state for this submission after upload,
476
            which depends on the kind of assignment.
477
        '''
478
        if not self.assignment.attachment_is_tested():
479
            return Submission.SUBMITTED
480
        else:
481
            if self.assignment.attachment_test_validity:
482
                return Submission.TEST_VALIDITY_PENDING
483
            elif self.assignment.attachment_test_full:
484
                return Submission.TEST_FULL_PENDING
485
486
    def state_for_students(self):
487
        '''
488
            Return human-readable description of current state for students.
489
        '''
490
        try:
491
            return dict(self.STUDENT_STATES)[self.state]
492
        except:
493
            # deal with old databases that have pre 0.7 states, such as "FC"
494
            return "Unknown"
495
496
    def state_for_tutors(self):
497
        '''
498
            Return human-readable description of current state for tutors.
499
        '''
500
        return dict(self.STATES)[self.state]
501
502
    def grading_file_url(self):
503
        # to implement access protection, we implement our own download
504
        # this implies that the Apache media serving is disabled
505
        return reverse('submission_grading_file', args=(self.pk,))
506
507
    def _save_test_result(self, machine, text_student, text_tutor, kind, perf_data):
508
        result = SubmissionTestResult(
509
            result=text_student,
510
            result_tutor=text_tutor,
511
            machine=machine,
512
            kind=kind,
513
            perf_data=perf_data,
514
            submission_file=self.file_upload)
515
        result.save()
516
517
    def _get_test_result(self, kind):
518
        try:
519
            return self.file_upload.test_results.filter(kind=kind).order_by('-created')[0]
520
        except:
521
            return None
522
523
    def save_fetch_date(self):
524
        SubmissionFile.objects.filter(
525
            pk=self.file_upload.pk).update(fetched=datetime.now())
526
527
    def get_fetch_date(self):
528
        return self.file_upload.fetched
529
530
    def clean_fetch_date(self):
531
        SubmissionFile.objects.filter(
532
            pk=self.file_upload.pk).update(fetched=None)
533
534
    def save_validation_result(self, machine, text_student, text_tutor, perf_data=None):
535
        self._save_test_result(
536
            machine, text_student, text_tutor, SubmissionTestResult.VALIDITY_TEST, perf_data)
537
538
    def save_fulltest_result(self, machine, text_tutor, perf_data=None):
539
        self._save_test_result(
540
            machine, None, text_tutor, SubmissionTestResult.FULL_TEST, perf_data)
541
542
    def get_validation_result(self):
543
        '''
544
            Return the most recent validity test result object for this submission.
545
        '''
546
        return self._get_test_result(SubmissionTestResult.VALIDITY_TEST)
547
548
    def get_fulltest_result(self):
549
        '''
550
            Return the most recent full test result object for this submission.
551
        '''
552
        return self._get_test_result(SubmissionTestResult.FULL_TEST)
553
554
    def inform_student(self, state):
555
        '''
556
            We hand-in explicitely about which new state we want to inform,
557
            since this may not be reflected in the model at the moment.
558
        '''
559
        mails.inform_student(self, state)
560
561
    def info_file(self, delete=True):
562
        '''
563
            Prepares an open temporary file with information about the submission.
564
            Closing it will delete it, which must be considered by the caller.
565
            This file is not readable, since the tempfile library wants either readable or writable files.
566
        '''
567
        info = tempfile.NamedTemporaryFile(
568
            mode='wt', encoding='utf-8', delete=delete)
569
        info.write("Submission ID:\t%u\n" % self.pk)
570
        info.write("Submitter:\t%s (%u)\n" %
571
                   (self.submitter.get_full_name(), self.submitter.pk))
572
        info.write("Authors:\n")
573
        for auth in self.authors.all():
574
            info.write("\t%s (%u)\n" % (auth.get_full_name(), auth.pk))
575
        info.write("\n")
576
        info.write("Creation:\t%s\n" % str(self.created))
577
        info.write("Last modification:\t%s\n" % str(self.modified))
578
        info.write("Status:\t%s\n" % self.state_for_students())
579
        if self.grading:
580
            info.write("Grading:\t%s\n" % str(self.grading))
581
        if self.notes:
582
            notes = self.notes
583
            info.write("Author notes:\n-------------\n%s\n\n" % notes)
584
        if self.grading_notes:
585
            notes = self.grading_notes
586
            info.write("Grading notes:\n--------------\n%s\n\n" % notes)
587
        info.flush()    # no closing here, because it disappears then
588
        return info
589
590
    def copy_file_upload(self, targetdir):
591
        '''
592
            Copies the currently valid file upload into the given directory.
593
            If possible, the content is un-archived in the target directory.
594
        '''
595
        assert(self.file_upload)
596
        # unpack student data to temporary directory
597
        # os.chroot is not working with tarfile support
598
        tempdir = tempfile.mkdtemp()
599
        try:
600
            if zipfile.is_zipfile(self.file_upload.absolute_path()):
601
                f = zipfile.ZipFile(self.file_upload.absolute_path(), 'r')
602
                f.extractall(targetdir)
603
            elif tarfile.is_tarfile(self.file_upload.absolute_path()):
604
                tar = tarfile.open(self.file_upload.absolute_path())
605
                tar.extractall(targetdir)
606
                tar.close()
607
            else:
608
                # unpacking not possible, just copy it
609
                shutil.copyfile(self.file_upload.absolute_path(),
610
                                targetdir + "/" + self.file_upload.basename())
611
        except IOError:
612
            logger.error("I/O exception while accessing %s." %
613
                         (self.file_upload.absolute_path()))
614
            pass
615
616
    def add_to_zipfile(self, z):
617
        submitter = "user" + str(self.submitter.pk)
618
        assdir = self.assignment.directory_name_with_course()
619
        if self.modified:
620
            modified = self.modified.strftime("%Y_%m_%d_%H_%M_%S")
621
        else:
622
            modified = self.created.strftime("%Y_%m_%d_%H_%M_%S")
623
        state = self.state_for_students().replace(" ", "_").lower()
624
        submdir = "%s/%u_%s/" % (assdir, self.pk, state)
625
        if self.file_upload:
626
            # Copy student upload
627
            tempdir = tempfile.mkdtemp()
628
            self.copy_file_upload(tempdir)
629
            # Add content to final ZIP file
630
            allfiles = [(subdir, files)
631
                        for (subdir, dirs, files) in os.walk(tempdir)]
632
            for subdir, files in allfiles:
633
                for f in files:
634
                    zip_relative_dir = subdir.replace(tempdir, "")
635
                    zip_relative_file = '%s/%s' % (zip_relative_dir, f)
636
                    z.write(subdir + "/" + f, submdir + 'student_files/%s' %
637
                            zip_relative_file, zipfile.ZIP_DEFLATED)
638
        # add text file with additional information
639
        info = self.info_file()
640
        z.write(info.name, submdir + "info.txt")
641