Assignment.graded_submissions()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
1
from django.db import models
2
from django.utils import timezone
3
from django.conf import settings
4
from django.urls 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
        '''
104
        if self.pk and self.has_validity_test():
105
            return settings.HOST + reverse('validity_script_secret', args=[self.pk, settings.JOB_EXECUTOR_SECRET])
106
        else:
107
            return None
108
109
    def full_test_url(self):
110
        '''
111
            Return absolute download URL for full test script.
112
            Using reverse() seems to be broken with FORCE_SCRIPT in use, so we use direct URL formulation.
113
        '''
114
        if self.pk and self.has_full_test():
115
            return settings.HOST + reverse('full_testscript_secret', args=[self.pk, settings.JOB_EXECUTOR_SECRET])
116
        else:
117
            return None
118
119
    def url(self):
120
        '''
121
            Return absolute URL for assignment description.
122
        '''
123
        if self.pk:
124
            if self.has_description():
125
                return settings.HOST + reverse('assignment_description_file', args=[self.pk])
126
            else:
127
                return self.download
128
        else:
129
            return None
130
131
    def has_validity_test(self):
132
        return str(self.attachment_test_validity).strip() != ""
133
134
    def has_full_test(self):
135
        return str(self.attachment_test_full).strip() != ""
136
137
    def has_description(self):
138
        return str(self.description).strip() != ""
139
140
    def attachment_is_tested(self):
141
        return self.has_validity_test() or self.has_full_test()
142
143
    def can_create_submission(self, user=None):
144
        '''
145
            Central access control for submitting things related to assignments.
146
        '''
147
        if user:
148
            # Super users, course owners and tutors should be able to test their validations
149
            # before the submission is officially possible.
150
            # They should also be able to submit after the deadline.
151
            if user.is_superuser or user is self.course.owner or self.course.tutors.filter(pk=user.pk).exists():
152
                return True
153
            if self.course not in user.profile.user_courses():
154
                # The user is not enrolled in this assignment's course.
155
                logger.debug('Submission not possible, user not enrolled in the course.')
156
                return False
157
158
            if user.authored.filter(assignment=self).exclude(state=Submission.WITHDRAWN).count() > 0:
159
                # User already has a valid submission for this assignment.
160
                logger.debug('Submission not possible, user already has one for this assignment.')
161
                return False
162
163
        if self.hard_deadline and self.hard_deadline < timezone.now():
164
            # Hard deadline has been reached.
165
            logger.debug('Submission not possible, hard deadline passed.')
166
            return False
167
168
        if self.publish_at > timezone.now() and not user.profile.can_see_future():
169
            # The assignment has not yet been published.
170
            logger.debug('Submission not possible, assignment has not yet been published.')
171
            return False
172
173
        return True
174
175
    def add_to_zipfile(self, z):
176
        if self.description:
177
            sourcepath = settings.MEDIA_ROOT + self.description.name
178
            targetpath = self.directory_name_with_course()
179
            targetname = os.path.basename(self.description.name)
180
            z.write(sourcepath, targetpath + os.sep + targetname)
181
182
    def duplicate_files(self):
183
        '''
184
        Search for duplicates of submission file uploads for this assignment.
185
        This includes the search in other course, whether inactive or not.
186
        Returns a list of lists, where each latter is a set of duplicate submissions
187
        with at least on of them for this assignment
188
        '''
189
        result=list()
190
        files = SubmissionFile.valid_ones.order_by('md5')
191
192
        for key, dup_group in groupby(files, lambda f: f.md5):
193
            file_list=[entry for entry in dup_group]
194
            if len(file_list)>1:
195
                for entry in file_list:
196
                    if entry.submissions.filter(assignment=self).count()>0:
197
                        result.append([key, file_list])
198
                        break
199
        return result
200
201
202