Passed
Push — master ( 45831a...03eabf )
by Peter
01:36
created

Assignment.directory_name_with_course()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
1
from django.db import models
2
from django.utils import timezone
3
from django.conf import settings
4
from django.core.urlresolvers import reverse
5
6
from .submission import Submission
7
from .submissionfile import SubmissionFile
8
from .submissiontestresult import SubmissionTestResult
9
10
import os
11
from itertools import groupby
12
13
import logging
14
logger = logging.getLogger('OpenSubmit')
15
16
17
class Assignment(models.Model):
18
    '''
19
        An assignment for which students can submit their solution.
20
    '''
21
22
    title = models.CharField(max_length=200)
23
    course = models.ForeignKey('Course', related_name='assignments')
24
    download = models.URLField(max_length=200, blank=True, null=True, verbose_name="As link", help_text="External link to the assignment description.")
25
    description = models.FileField(upload_to="assignment_desc", blank=True, null=True, verbose_name='As file', help_text="Uploaded document with the assignment description.")
26
    created = models.DateTimeField(auto_now_add=True, editable=False)
27
    gradingScheme = models.ForeignKey('GradingScheme', related_name="assignments", verbose_name="grading scheme", blank=True, null=True, help_text="Grading scheme for this assignment. Leave empty to have an ungraded assignment.")
28
    publish_at = models.DateTimeField(default=timezone.now, help_text="Shown for students after this point in time. Users with backend rights always see it.")
29
    soft_deadline = models.DateTimeField(blank=True, null=True, help_text="Deadline shown to students. After this point in time, submissions are still possible. Leave empty for only using a hard deadline.")
30
    hard_deadline = models.DateTimeField(blank=True, null=True, help_text="Deadline after which submissions are no longer possible. Can be empty.")
31
    has_attachment = models.BooleanField(default=False, verbose_name="Student file upload ?", help_text="Activate this if the students must upload a (document / ZIP /TGZ) file as solution. Otherwise, they can only provide notes.")
32
    attachment_test_timeout = models.IntegerField(default=30, verbose_name="Timout for tests", help_text="Timeout (in seconds) after which the compilation / validation test / full test is cancelled. The submission is marked as invalid in this case. Intended for student code with deadlocks.")
33
    attachment_test_validity = models.FileField(upload_to="testscripts", blank=True, null=True, verbose_name='Validation script', help_text="If given, the student upload is uncompressed and the script is executed for it on a test machine. Student submissions are marked as valid if this script was successful.")
34
    validity_script_download = models.BooleanField(default=False, verbose_name='Download of validation script ?', help_text='If activated, the students can download the validation script for offline analysis.')
35
    attachment_test_full = models.FileField(upload_to="testscripts", blank=True, null=True, verbose_name='Full test script', help_text='Same as the validation script, but executed AFTER the hard deadline to determine final grading criterias for the submission. Results are not shown to students.')
36
    test_machines = models.ManyToManyField('TestMachine', blank=True, related_name="assignments", help_text="The test machines that will take care of submissions for this assignment.")
37
    max_authors = models.PositiveSmallIntegerField(default=1, help_text="Maximum number of authors (= group size) for this assignment.")
38
39
    class Meta:
40
        app_label = 'opensubmit'
41
42
    def __str__(self):
43
        return self.title
44
45
    def directory_name(self):
46
        ''' The assignment name in a format that is suitable for a directory name.  '''
47
        return self.title.replace(" ", "_").replace("\\", "_").replace(",","").lower()
48
49
    def directory_name_with_course(self):
50
        ''' The assignment name in a format that is suitable for a directory name.  '''
51
        coursename = self.course.directory_name()
52
        assignmentname = self.title.replace(" ", "_").replace("\\", "_").replace(",","").lower()
53
        return coursename + os.sep + assignmentname
54
55
    def gradable_submissions(self):
56
        qs = self.valid_submissions()
57
        qs = qs.filter(state__in=[Submission.GRADING_IN_PROGRESS, Submission.SUBMITTED_TESTED, Submission.TEST_FULL_FAILED, Submission.SUBMITTED])
58
        return qs
59
60
    def grading_unfinished_submissions(self):
61
        qs = self.valid_submissions()
62
        qs = qs.filter(state__in=[Submission.GRADING_IN_PROGRESS])
63
        return qs
64
65
    def graded_submissions(self):
66
        qs = self.valid_submissions().filter(state__in=[Submission.GRADED])
67
        return qs
68
69
    def grading_url(self):
70
        '''
71
            Determines the teacher backend link to the filtered list of gradable submissions for this assignment.
72
        '''
73
        grading_url="%s?coursefilter=%u&assignmentfilter=%u&statefilter=tobegraded"%(
74
                            reverse('teacher:opensubmit_submission_changelist'),
75
                            self.course.pk, self.pk
76
                        )
77
        return grading_url
78
79
    def authors(self):
80
        qs = self.valid_submissions().values_list('authors',flat=True).distinct()
81
        return qs
82
83
    def valid_submissions(self):
84
        qs = self.submissions.exclude(state=Submission.WITHDRAWN)
85
        return qs
86
87
    def has_perf_results(self):
88
        '''
89
            Figure out if any submission for this assignment has performance data being available.
90
        '''
91
        num_results = SubmissionTestResult.objects.filter(perf_data__isnull=False).filter(submission_file__submissions__assignment=self).count()
92
        return num_results != 0
93
94
    def is_graded(self):
95
        '''
96
        Checks if this a graded assignment.
97
        '''
98
        return self.gradingScheme is not None
99
100
    def validity_test_url(self):
101
        '''
102
            Return absolute download URL for validity test script.
103
            Using reverse() seems to be broken with FORCE_SCRIPT in use, so we use direct URL formulation.
104
        '''
105
        if self.pk and self.has_validity_test():
106
            return settings.MAIN_URL + "/download/%u/validity_testscript/secret=%s" % (self.pk, settings.JOB_EXECUTOR_SECRET)
107
        else:
108
            return None
109
110
    def full_test_url(self):
111
        '''
112
            Return absolute download URL for full test script.
113
            Using reverse() seems to be broken with FORCE_SCRIPT in use, so we use direct URL formulation.
114
        '''
115
        if self.pk and self.has_full_test():
116
            return settings.MAIN_URL + "/download/%u/full_testscript/secret=%s" % (self.pk, settings.JOB_EXECUTOR_SECRET)
117
        else:
118
            return None
119
120
    def url(self):
121
        '''
122
            Return absolute URL for assignment description.
123
            Using reverse() seems to be broken with FORCE_SCRIPT in use, so we use direct URL formulation.
124
        '''
125
        if self.pk:
126
            if self.has_description():
127
                return settings.MAIN_URL + "/download/%u/description" % (self.pk)
128
            else:
129
                return self.download
130
        else:
131
            return None
132
133
    def has_validity_test(self):
134
        return str(self.attachment_test_validity).strip() != ""
135
136
    def has_full_test(self):
137
        return str(self.attachment_test_full).strip() != ""
138
139
    def has_description(self):
140
        return str(self.description).strip() != ""
141
142
    def attachment_is_tested(self):
143
        return self.has_validity_test() or self.has_full_test()
144
145
    def can_create_submission(self, user=None):
146
        '''
147
            Central access control for submitting things related to assignments.
148
        '''
149
        if user:
150
            # Super users, course owners and tutors should be able to test their validations
151
            # before the submission is officially possible.
152
            # They should also be able to submit after the deadline.
153
            if user.is_superuser or user is self.course.owner or self.course.tutors.filter(pk=user.pk).exists():
154
                return True
155
            if self.course not in user.profile.user_courses():
156
                # The user is not enrolled in this assignment's course.
157
                logger.debug('Submission not possible, user not enrolled in the course.')
158
                return False
159
160
            if user.authored.filter(assignment=self).exclude(state=Submission.WITHDRAWN).count() > 0:
161
                # User already has a valid submission for this assignment.
162
                logger.debug('Submission not possible, user already has one for this assignment.')
163
                return False
164
165
        if self.hard_deadline and self.hard_deadline < timezone.now():
166
            # Hard deadline has been reached.
167
            logger.debug('Submission not possible, hard deadline passed.')
168
            return False
169
170
        if self.publish_at > timezone.now() and not user.profile.can_see_future():
171
            # The assignment has not yet been published.
172
            logger.debug('Submission not possible, assignment has not yet been published.')
173
            return False
174
175
        return True
176
177
    def add_to_zipfile(self, z):
178
        if self.description:
179
            sourcepath = settings.MEDIA_ROOT + self.description.name
180
            targetpath = self.directory_name_with_course()
181
            targetname = os.path.basename(self.description.name)
182
            z.write(sourcepath, targetpath + os.sep + targetname)
183
184
    def duplicate_files(self):
185
        '''
186
        Search for duplicates of submission file uploads for this assignment.
187
        This includes the search in other course, whether inactive or not.
188
        Returns a list of lists, where each latter is a set of duplicate submissions
189
        with at least on of them for this assignment
190
        '''
191
        result=list()
192
        files = SubmissionFile.valid_ones.order_by('md5')
193
194
        for key, dup_group in groupby(files, lambda f: f.md5):
195
            file_list=[entry for entry in dup_group]
196
            if len(file_list)>1:
197
                for entry in file_list:
198
                    if entry.submissions.filter(assignment=self).count()>0:
199
                        result.append([key, file_list])
200
                        break
201
        return result
202
203
204