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

Assignment.is_graded()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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