Passed
Push — master ( 7d61bb...58af22 )
by Peter
01:49
created

Submission.inform_student()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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