コード例 #1
0
class ExpectedStudentFile(AutograderModel):
    """
    These objects describe Unix-style shell patterns that files
    submitted by students can or should match.
    """
    class Meta:
        unique_together = ('pattern', 'project')

    SERIALIZABLE_FIELDS = (
        'pk',
        'project',
        'pattern',
        'min_num_matches',
        'max_num_matches',
    )

    EDITABLE_FIELDS = (
        'pattern',
        'min_num_matches',
        'max_num_matches',
    )

    project = models.ForeignKey(Project, on_delete=models.CASCADE,
                                related_name='expected_student_files')

    pattern = ag_fields.ShortStringField(
        validators=[core_ut.check_filename],
        help_text='''A shell-style file pattern suitable for
            use with Python's fnmatch.fnmatch()
            function (https://docs.python.org/3.5/library/fnmatch.html)
            This string must be a legal UNIX filename and may not be
            '..' or '.'.
            NOTE: Patterns for a given project must not overlap,
                otherwise the behavior is undefined.''')

    min_num_matches = models.IntegerField(
        default=1,
        validators=[validators.MinValueValidator(0)],
        help_text='''The minimum number of submitted student files that
            should match the pattern. Must be non-negative.''')

    max_num_matches = models.IntegerField(
        default=1,
        help_text='''The maximum number of submitted student files that
            can match the pattern. Must be >= min_num_matches''')

    def clean(self):
        if self.max_num_matches < self.min_num_matches:
            raise exceptions.ValidationError(
                {'max_num_matches': (
                    'Maximum number of matches must be greater than or '
                    'equal to minimum number of matches')})
コード例 #2
0
class AGTestCase(AutograderModel):
    """
    An AGTestCase consists of a series of commands to be run together.
    An AGTestCase must belong to exactly one AGTestSuite.
    """
    class Meta:
        unique_together = ('name', 'ag_test_suite')
        order_with_respect_to = 'ag_test_suite'

    name = ag_fields.ShortStringField(
        help_text='''The name used to identify this autograder test.
                     Must be non-empty and non-null.
                     Must be unique among autograder tests that belong to the same suite.
                     This field is REQUIRED.''')

    ag_test_suite = models.ForeignKey(
        AGTestSuite,
        related_name='ag_test_cases',
        on_delete=models.CASCADE,
        help_text='''The suite this autograder test belongs to.
                     This field is REQUIRED.''')

    old_normal_fdbk_config = models.OneToOneField(
        AGTestCaseFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_test_fdbk,
        related_name='+',
        help_text='Feedback settings for a normal Submission.')
    old_ultimate_submission_fdbk_config = models.OneToOneField(
        AGTestCaseFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_test_fdbk,
        related_name='+',
        help_text='Feedback settings for an ultimate Submission.')
    old_past_limit_submission_fdbk_config = models.OneToOneField(
        AGTestCaseFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_test_fdbk,
        related_name='+',
        help_text=
        'Feedback settings for a Submission that is past the daily limit.')
    old_staff_viewer_fdbk_config = models.OneToOneField(
        AGTestCaseFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_test_fdbk,
        related_name='+',
        help_text=
        'Feedback settings for a staff member viewing a Submission from another group.'
    )

    normal_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestCaseFeedbackConfig, default=NewAGTestCaseFeedbackConfig)
    ultimate_submission_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestCaseFeedbackConfig, default=NewAGTestCaseFeedbackConfig)
    past_limit_submission_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestCaseFeedbackConfig, default=NewAGTestCaseFeedbackConfig)
    staff_viewer_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestCaseFeedbackConfig, default=NewAGTestCaseFeedbackConfig)

    @transaction.atomic
    def validate_and_update(self,
                            ag_test_suite: Optional[Union[int,
                                                          AGTestSuite]] = None,
                            **kwargs):
        """
        :param ag_test_suite:
            An AGTestSuite (or its primary key) that this AGTestCase
            should be moved to.
            It is legal to assign an AGTestCase to a different
            AGTestSuite as long as the old and new suites belong to
            the same Project.
        """
        move_suite = False
        if ag_test_suite is not None:
            if isinstance(ag_test_suite, int):
                move_suite = self.ag_test_suite.pk != ag_test_suite
            else:
                move_suite = self.ag_test_suite != ag_test_suite

        if not move_suite:
            super().validate_and_update(**kwargs)
            return

        if isinstance(ag_test_suite, int):
            ag_test_suite = AGTestSuite.objects.get(pk=ag_test_suite)

        if ag_test_suite.project != self.ag_test_suite.project:
            raise exceptions.ValidationError({
                'ag_test_suite':
                'AGTestCases can only be moved to AGTestSuites within the same Project.'
            })

        # Update all the AGTestCaseResult objects that belong to this
        # AGTestCase so that they belong to AGTestSuiteResults that
        # belong to the destination AGTestSuite.

        from .ag_test_suite_result import AGTestSuiteResult

        for ag_test_case_result in self.related_ag_test_case_results.select_related(
                'ag_test_suite_result__submission').all():
            dest_suite_result = AGTestSuiteResult.objects.get_or_create(
                ag_test_suite=ag_test_suite,
                submission=ag_test_case_result.ag_test_suite_result.submission
            )[0]
            ag_test_case_result.ag_test_suite_result = dest_suite_result
            ag_test_case_result.save()

        self.ag_test_suite = ag_test_suite

        super().validate_and_update(**kwargs)

    @transaction.atomic()
    def delete(self, *args, **kwargs):
        with connection.cursor() as cursor:
            cursor.execute(
                '''UPDATE core_submission
                SET denormalized_ag_test_results =
                    denormalized_ag_test_results #- '{%s,ag_test_case_results,%s}'
                WHERE core_submission.project_id = %s
                ''', (self.ag_test_suite_id, self.pk,
                      self.ag_test_suite.project_id))

        return super().delete()

    SERIALIZABLE_FIELDS = (
        'pk',
        'name',
        'last_modified',
        'ag_test_suite',
        'ag_test_commands',
        'normal_fdbk_config',
        'ultimate_submission_fdbk_config',
        'past_limit_submission_fdbk_config',
        'staff_viewer_fdbk_config',
    )

    SERIALIZE_RELATED = ('ag_test_commands', )

    EDITABLE_FIELDS = (
        'name',
        'ag_test_suite',
        'normal_fdbk_config',
        'ultimate_submission_fdbk_config',
        'past_limit_submission_fdbk_config',
        'staff_viewer_fdbk_config',
    )
コード例 #3
0
class AGCommandBase(AutograderModel):
    """
    An abstract base class that contains the core information needed to run
    a command during grading.
    """
    class Meta:
        abstract = True

    name = ag_fields.ShortStringField(
        blank=True, help_text="An optional name for this command.")

    cmd = models.CharField(
        max_length=constants.MAX_COMMAND_LENGTH,
        help_text='''A string containing the command to be run.
                     Note: This string will be inserted into ['bash', '-c', <cmd>]
                        in order to be executed.
                     Note: This string defaults to the "true" command
                     (which does nothing and returns 0) so that AGCommands are
                     default-creatable.''')

    time_limit = models.IntegerField(
        default=constants.DEFAULT_SUBPROCESS_TIMEOUT,
        validators=[
            MinValueValidator(1),
            MaxValueValidator(constants.MAX_SUBPROCESS_TIMEOUT)
        ],
        help_text='''The time limit in seconds to be placed on the
            command.
            Must be > 0
            Must be <= autograder.shared.global_constants
                                 .MAX_SUBPROCESS_TIMEOUT''')

    stack_size_limit = models.IntegerField(
        default=constants.DEFAULT_STACK_SIZE_LIMIT,
        validators=[
            MinValueValidator(1),
            MaxValueValidator(constants.MAX_STACK_SIZE_LIMIT)
        ],
        help_text='''
        stack_size_limit -- The maximum stack size in bytes.
            Must be > 0
            Must be <= autograder.shared.global_constants.MAX_STACK_SIZE_LIMIT
            NOTE: Setting this value too low may cause the command to crash prematurely.'''
    )

    virtual_memory_limit = models.BigIntegerField(
        default=constants.DEFAULT_VIRTUAL_MEM_LIMIT,
        validators=[
            MinValueValidator(1),
            MaxValueValidator(constants.MAX_VIRTUAL_MEM_LIMIT)
        ],
        help_text='''The maximum amount of virtual memory
            (in bytes) the command can use.
            Must be > 0
            Must be <= autograder.shared.global_constants.MAX_VIRTUAL_MEM_LIMIT
            NOTE: Setting this value too low may cause the command to crash prematurely.'''
    )

    process_spawn_limit = models.IntegerField(
        default=constants.DEFAULT_PROCESS_LIMIT,
        validators=[
            MinValueValidator(0),
            MaxValueValidator(constants.MAX_PROCESS_LIMIT)
        ],
        help_text=
        '''The maximum number of processes that the command is allowed to spawn.
            Must be >= 0
            Must be <= autograder.shared.global_constants.MAX_PROCESS_LIMIT
            NOTE: This limit applies cumulatively to the processes
                    spawned by the main program being run. i.e. If a
                    spawned process spawns it's own child process, both
                    of those processes will count towards the main
                    program's process limit.''')
コード例 #4
0
class Project(AutograderModel):
    """
    Represents a programming project for which students can
    submit solutions and have them evaluated.

    Related object fields:
        instructor_files -- Resource files to be used in project test
            cases. In the API, this field is hidden from non-staff.

        expected_student_files -- Patterns that
            student-submitted files can or should match.

        groups -- The submission groups registered for this
            Project.

        group_invitations -- The pending submission group
            invitations belonging to this Project.
    """
    class Meta:
        unique_together = ('name', 'course')

    name = ag_fields.ShortStringField(
        help_text="""The name used to identify this project.
            Must be non-empty and non-null.
            Must be unique among Projects associated with
            a given course.
            This field is REQUIRED.""")

    course = models.ForeignKey(Course,
                               related_name='projects',
                               on_delete=models.CASCADE,
                               help_text="""The Course this project belongs to.
            This field is REQUIRED.""")

    visible_to_students = models.BooleanField(
        default=False,
        help_text="""Whether information about this Project can
            be viewed by students.""")

    closing_time = models.DateTimeField(
        default=None,
        null=True,
        blank=True,
        help_text="""The date and time that this project should stop
            accepting submissions.
            A value of None indicates that this project should
            stay open.
            In the API, this field is hidden from non-admins.""")

    soft_closing_time = models.DateTimeField(
        default=None,
        null=True,
        blank=True,
        help_text="""The date and time that should be displayed as the
            due date for this project. Unlike closing_time,
            soft_closing_time does not affect whether submissions are
            actually accepted.
            If not None and closing_time is not None, this value must be
            less than (before) closing_time.""")

    disallow_student_submissions = models.BooleanField(
        default=False,
        help_text="""A hard override that indicates that students should
            be prevented from submitting even if visible_to_students is
            True and it is before closing_time.""")

    disallow_group_registration = models.BooleanField(
        default=False,
        help_text="""A hard override that indicates that students should
            not be able to send, accept, or reject group
            invitations.""")

    guests_can_submit = models.BooleanField(
        default=False,
        help_text="""By default, only admins, staff, and students
            for a given Course can view and submit to its Projects.
            When True, submissions will be accepted from guests
            with the following caveats:
                - Guests must be given a direct link to the project.
                - When group work is allowed, guests can
                only be in groups with other guests.""")

    min_group_size = models.IntegerField(
        default=1,
        validators=[validators.MinValueValidator(1)],
        help_text="""The minimum number of students that can work in a
            group on this project.
            Must be >= 1.
            Must be <= max_group_size.""")

    max_group_size = models.IntegerField(
        default=1,
        validators=[validators.MinValueValidator(1)],
        help_text="""The maximum number of students that can work in a
            group on this project.
            Must be >= 1.
            Must be >= min_group_size.""")

    submission_limit_per_day = models.IntegerField(
        default=None,
        null=True,
        blank=True,
        validators=[validators.MinValueValidator(1)],
        help_text="""The number of submissions each group is allowed per
            day before either reducing feedback or preventing further
            submissions. A value of None indicates no limit.""")

    groups_combine_daily_submissions = models.BooleanField(
        default=False,
        blank=True,
        help_text="""If True, group members can "pool" their daily submissions.
            For example, if submission_limit_per_day is 3,
            a group with 2 members would get 6 submissions per day.""")

    allow_submissions_past_limit = models.BooleanField(
        default=True,
        blank=True,
        help_text="""Whether to allow additional submissions after a
            group has submitted submission_limit_per_day times.""")

    submission_limit_reset_time = models.TimeField(
        default=datetime.time,
        help_text="""The time that marks the beginning and end of the 24
            hour period during which submissions should be counted
            towards the daily limit. Defaults to 0:0:0.""")

    submission_limit_reset_timezone = TimeZoneField(
        default='UTC',
        help_text="""The timezone to use when computing how many
            submissions a group has made in a 24 hour period.""")

    num_bonus_submissions = models.IntegerField(
        default=0, validators=[validators.MinValueValidator(0)])

    total_submission_limit = models.IntegerField(
        default=None,
        blank=True,
        null=True,
        validators=[validators.MinValueValidator(1)],
        help_text="""The maximum number of times a Group can submit to
            this Project EVER.""")

    allow_late_days = models.BooleanField(
        default=False,
        help_text="""Whether to allow the use of late days for submitting
            past the deadline.""")

    ultimate_submission_policy = ag_fields.EnumField(
        UltimateSubmissionPolicy,
        default=UltimateSubmissionPolicy.most_recent,
        blank=True,
        help_text="""The "ultimate" submission for a group is the one
            that will be used for final grading. This field specifies
            how the ultimate submission should be determined.""")

    hide_ultimate_submission_fdbk = models.BooleanField(
        default=True,
        blank=True,
        help_text="""A hard override that indicates that ultimate
            submission feedback should not be shown, even if the
            appropriate criteria are met.""")

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        project_root_dir = core_ut.get_project_root_dir(self)
        project_files_dir = core_ut.get_project_files_dir(self)
        project_submissions_dir = core_ut.get_project_groups_dir(self)

        if not os.path.isdir(project_root_dir):
            # Since the database is in charge of validating the
            # uniqueness of this project, we can assume at this point
            # that creating the project directories will succeed.
            # If for some reason it fails, this will be considered a
            # more severe error, and the OSError thrown by os.makedirs
            # will be handled at a higher level.

            os.makedirs(project_root_dir)
            os.mkdir(project_files_dir)
            os.mkdir(project_submissions_dir)

    def clean(self):
        super().clean()

        if self.max_group_size < self.min_group_size:
            raise exceptions.ValidationError({
                'max_group_size': ('Maximum group size must be greater than '
                                   'or equal to minimum group size')
            })

        if self.closing_time is not None and self.soft_closing_time is not None:
            if self.closing_time < self.soft_closing_time:
                raise exceptions.ValidationError({
                    'soft_closing_time':
                    ('Soft closing time must be before hard closing time')
                })

    def to_dict(self):
        result = super().to_dict()
        result['submission_limit_reset_timezone'] = (
            self.submission_limit_reset_timezone.tzname(None))
        return result

    SERIALIZABLE_FIELDS = (
        'pk',
        'name',
        'last_modified',
        'course',
        'visible_to_students',
        'closing_time',
        'soft_closing_time',
        'disallow_student_submissions',
        'disallow_group_registration',
        'guests_can_submit',
        'min_group_size',
        'max_group_size',
        'submission_limit_per_day',
        'allow_submissions_past_limit',
        'groups_combine_daily_submissions',
        'submission_limit_reset_time',
        'submission_limit_reset_timezone',
        'num_bonus_submissions',
        'total_submission_limit',
        'allow_late_days',
        'ultimate_submission_policy',
        'hide_ultimate_submission_fdbk',
        'instructor_files',
        'expected_student_files',
    )

    SERIALIZE_RELATED = (
        'instructor_files',
        'expected_student_files',
    )

    EDITABLE_FIELDS = (
        'name',
        'visible_to_students',
        'closing_time',
        'soft_closing_time',
        'disallow_student_submissions',
        'disallow_group_registration',
        'guests_can_submit',
        'min_group_size',
        'max_group_size',
        'submission_limit_per_day',
        'allow_submissions_past_limit',
        'groups_combine_daily_submissions',
        'submission_limit_reset_time',
        'submission_limit_reset_timezone',
        'num_bonus_submissions',
        'total_submission_limit',
        'allow_late_days',
        'ultimate_submission_policy',
        'hide_ultimate_submission_fdbk',
    )
コード例 #5
0
class StudentTestSuite(AutograderModel):
    """
    A StudentTestSuite defines a way of grading student-submitted
    test cases against a set of intentionally buggy implementations
    of instructor code.
    """
    class Meta:
        unique_together = ('name', 'project')
        order_with_respect_to = 'project'

    STUDENT_TEST_NAME_PLACEHOLDER = r'${student_test_name}'
    BUGGY_IMPL_NAME_PLACEHOLDER = r'${buggy_impl_name}'

    name = ag_fields.ShortStringField(
        help_text="""The name used to identify this StudentTestSuite.
                     Must be non-empty and non-null.""")
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        related_name='student_test_suites',
        help_text="The Project that this student test suite belongs to.")

    instructor_files_needed = models.ManyToManyField(
        InstructorFile,
        help_text=
        """The project files that will be copied into the sandbox before the suite
                     is graded.""")

    read_only_instructor_files = models.BooleanField(
        default=True,
        help_text=
        """When True, project files needed for this suite will be read-only when this
                     suite is graded.""")

    student_files_needed = models.ManyToManyField(
        ExpectedStudentFile,
        help_text=
        '''Student-submitted files matching these patterns will be copied into the
                     sandbox before the suite is graded.''')

    buggy_impl_names = ag_fields.StringArrayField(
        strip_strings=True,
        blank=True,
        default=list,
        help_text=
        "The names of buggy implementations that student tests should be run against."
    )

    use_setup_command = models.BooleanField(default=False)
    setup_command = models.OneToOneField(
        AGCommand,
        on_delete=models.PROTECT,
        related_name='+',
        default=make_default_setup_cmd,
        help_text="""A command to be run after student and project files have
                     been added to the sandbox but before any other commands are run.
                     The AGCommand's 'cmd' field must not be blank. To indicate that no
                     setup command should be run, set use_setup_command to False."""
    )
    get_student_test_names_command = models.OneToOneField(
        AGCommand,
        on_delete=models.PROTECT,
        related_name='+',
        blank=True,
        default=make_default_get_student_test_names_cmd,
        help_text=
        """This required command should print out a whitespace-separated
                     list of detected student names. The output of this command will
                     be parsed using Python's str.split().
                     NOTE: This AGCommand's 'cmd' field must not be blank.""")

    DEFAULT_STUDENT_TEST_MAX = 25
    MAX_STUDENT_TEST_MAX = 50

    max_num_student_tests = models.IntegerField(
        default=DEFAULT_STUDENT_TEST_MAX,
        validators=[
            MinValueValidator(0),
            MaxValueValidator(MAX_STUDENT_TEST_MAX)
        ],
        help_text=
        """The maximum number of test cases students are allowed to submit.
                     If more than this many tests are discovered by the
                     get_student_test_names_command, test names will be discarded
                     from the end of that list.""")

    student_test_validity_check_command = models.OneToOneField(
        AGCommand,
        on_delete=models.PROTECT,
        related_name='+',
        blank=True,
        default=make_default_validity_check_command,
        help_text=
        """This command will be run once for each detected student test case.
                     An exit status of zero indicates that a student test case is valid,
                     whereas a nonzero exit status indicates that a student test case
                     is invalid.
                     This command must contain the placeholder {} at least once. That
                     placeholder will be replaced with the name of the student test case
                     that is to be checked for validity.
                     NOTE: This AGCommand's 'cmd' field must not be blank.
                     """.format(STUDENT_TEST_NAME_PLACEHOLDER))

    grade_buggy_impl_command = models.OneToOneField(
        AGCommand,
        on_delete=models.PROTECT,
        related_name='+',
        blank=True,
        default=make_default_grade_buggy_impl_command,
        help_text=
        """This command will be run once for every (buggy implementation, valid test)
                    pair.
                     A nonzero exit status indicates that the valid student tests exposed the
                     buggy impl, whereas an exit status of zero indicates that the student
                     tests did not expose the buggy impl.
                     This command must contain the placeholders {0} and {1}. The placeholder
                     {0} will be replaced with the name of a valid student test case.
                     The placeholder {1} will be replaced with the name of
                     the buggy impl that the student test is being run against.
                     NOTE: This AGCommand's 'cmd' field must not be blank.
                     """.format(STUDENT_TEST_NAME_PLACEHOLDER,
                                BUGGY_IMPL_NAME_PLACEHOLDER))

    points_per_exposed_bug = models.DecimalField(
        decimal_places=2,
        max_digits=4,
        default=0,
        validators=[MinValueValidator(0)],
        help_text=
        """The number of points to be awarded per buggy implementation exposed by
                     the student test cases. This field is limited to 4 digits total and a maximum
                     of 2 decimal places.""")
    max_points = models.IntegerField(
        null=True,
        default=None,
        blank=True,
        validators=[MinValueValidator(0)],
        help_text=
        """An optional ceiling on the number of points to be awarded.""")

    deferred = models.BooleanField(
        default=False,
        help_text=
        '''If true, this student test suite can be graded asynchronously.
                     Deferred suites that have yet to be graded do not prevent members
                     of a group from submitting again.''')

    docker_image_to_use = ag_fields.EnumField(
        constants.SupportedImages,
        default=constants.SupportedImages.default,
        help_text=
        "An identifier for the Docker image that the sandbox should be created from."
    )

    allow_network_access = models.BooleanField(
        default=False,
        help_text=
        '''Specifies whether the sandbox should allow commands run inside of it to
                     make network calls outside of the sandbox.''')

    normal_fdbk_config = models.OneToOneField(
        StudentTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_command_fdbk,
        related_name='+',
        help_text='Feedback settings for a normal Submission.')
    ultimate_submission_fdbk_config = models.OneToOneField(
        StudentTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_ultimate_submission_command_fdbk,
        related_name='+',
        help_text='Feedback settings for an ultimate Submission.')
    past_limit_submission_fdbk_config = models.OneToOneField(
        StudentTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_past_limit_student_suite_fdbk,
        related_name='+',
        help_text=
        'Feedback settings for a Submission that is past the daily limit.')
    staff_viewer_fdbk_config = models.OneToOneField(
        StudentTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_max_student_suite_fdbk,
        related_name='+',
        help_text=
        'Feedback settings for a staff member viewing a Submission from another group.'
    )

    def clean(self):
        if self.pk is None:
            return

        errors = {}

        for instructor_file in self.instructor_files_needed.all():
            if instructor_file.project != self.project:
                errors['instructor_files_needed'] = (
                    'File {} does not belong to the project "{}".'.format(
                        instructor_file.name, self.project.name))

        for student_file in self.student_files_needed.all():
            if student_file.project != self.project:
                errors['student_files_needed'] = (
                    'Student file pattern {} does not belong to the project "{}".'
                    .format(student_file.pattern, self.project.name))

        for cmd_field in [
                'setup_command', 'get_student_test_names_command',
                'student_test_validity_check_command',
                'grade_buggy_impl_command'
        ]:
            cmd = getattr(self, cmd_field)  # type: AGCommand
            if cmd is None:
                continue

        if self.STUDENT_TEST_NAME_PLACEHOLDER not in self.student_test_validity_check_command.cmd:
            errors['student_test_validity_check_command'] = (
                'Validity check command missing placeholder "{}"'.format(
                    self.STUDENT_TEST_NAME_PLACEHOLDER))

        if self.STUDENT_TEST_NAME_PLACEHOLDER not in self.grade_buggy_impl_command.cmd:
            errors['grade_buggy_impl_command'] = (
                'Grade buggy impl command missing placeholder "{}"'.format(
                    self.STUDENT_TEST_NAME_PLACEHOLDER))

        if self.BUGGY_IMPL_NAME_PLACEHOLDER not in self.grade_buggy_impl_command.cmd:
            errors['grade_buggy_impl_command'] = (
                'Grade buggy impl command missing placeholder "{}"'.format(
                    self.BUGGY_IMPL_NAME_PLACEHOLDER))

        if errors:
            raise exceptions.ValidationError(errors)

    SERIALIZABLE_FIELDS = (
        'pk',
        'name',
        'project',
        'instructor_files_needed',
        'read_only_instructor_files',
        'student_files_needed',
        'buggy_impl_names',
        'use_setup_command',
        'setup_command',
        'get_student_test_names_command',
        'max_num_student_tests',
        'student_test_validity_check_command',
        'grade_buggy_impl_command',
        'points_per_exposed_bug',
        'max_points',
        'deferred',
        'docker_image_to_use',
        'allow_network_access',
        'normal_fdbk_config',
        'ultimate_submission_fdbk_config',
        'past_limit_submission_fdbk_config',
        'staff_viewer_fdbk_config',
        'last_modified',
    )

    EDITABLE_FIELDS = (
        'name',
        'instructor_files_needed',
        'read_only_instructor_files',
        'student_files_needed',
        'buggy_impl_names',
        'use_setup_command',
        'setup_command',
        'get_student_test_names_command',
        'max_num_student_tests',
        'student_test_validity_check_command',
        'grade_buggy_impl_command',
        'points_per_exposed_bug',
        'max_points',
        'deferred',
        'docker_image_to_use',
        'allow_network_access',
        'normal_fdbk_config',
        'ultimate_submission_fdbk_config',
        'past_limit_submission_fdbk_config',
        'staff_viewer_fdbk_config',
        'last_modified',
    )

    SERIALIZE_RELATED = (
        'instructor_files_needed',
        'student_files_needed',
    )

    TRANSPARENT_TO_ONE_FIELDS = (
        'setup_command',
        'get_student_test_names_command',
        'student_test_validity_check_command',
        'grade_buggy_impl_command',
        'normal_fdbk_config',
        'ultimate_submission_fdbk_config',
        'past_limit_submission_fdbk_config',
        'staff_viewer_fdbk_config',
    )
コード例 #6
0
class AGTestSuite(AutograderModel):
    """
    A group of autograder test cases to be run inside the same sandbox.
    """
    class Meta:
        unique_together = ('name', 'project')
        order_with_respect_to = 'project'

    name = ag_fields.ShortStringField(
        help_text='''The name used to identify this suite.
                     Must be non-empty and non-null.
                     Must be unique among suites that belong to the same project.
                     This field is REQUIRED.''')

    project = models.ForeignKey(Project,
                                related_name='ag_test_suites',
                                on_delete=models.CASCADE,
                                help_text='''The project this suite belongs to.
                                             This field is REQUIRED.''')

    instructor_files_needed = models.ManyToManyField(
        InstructorFile,
        help_text=
        '''The project files that will be copied into the sandbox before the suite's
                     tests are run.''')

    read_only_instructor_files = models.BooleanField(
        default=True,
        help_text=
        """When True, project files needed for this suite will be read-only when this
                     suite is run.""")

    student_files_needed = models.ManyToManyField(
        ExpectedStudentFile,
        help_text=
        '''Student-submitted files matching these patterns will be copied into the
                     sandbox before the suite's tests are run.''')

    setup_suite_cmd = models.CharField(
        max_length=constants.MAX_COMMAND_LENGTH,
        blank=True,
        help_text="""A command to be run before this suite's tests are run.
                     This command is only run once at the beginning of the suite.
                     This command will be run after the student and project files
                     have been added to the sandbox.""")

    setup_suite_cmd_name = ag_fields.ShortStringField(
        blank=True, help_text="""The name of this suite's setup command.""")

    docker_image_to_use = ag_fields.EnumField(
        constants.SupportedImages,
        default=constants.SupportedImages.default,
        help_text=
        "An identifier for the Docker image that the sandbox should be created from."
    )

    allow_network_access = models.BooleanField(
        default=False,
        help_text=
        '''Specifies whether the sandbox should allow commands run inside of it to
                     make network calls outside of the sandbox.''')

    deferred = models.BooleanField(
        default=False,
        help_text=
        '''If true, this test suite can be graded asynchronously. Deferred suites that
                     have yet to be graded do not prevent members of a group from submitting
                     again.''')

    old_normal_fdbk_config = models.OneToOneField(
        AGTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_suite_fdbk,
        related_name='+',
        help_text='Feedback settings for a normal submission.')
    old_ultimate_submission_fdbk_config = models.OneToOneField(
        AGTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_suite_fdbk,
        related_name='+',
        help_text='Feedback settings for an ultimate submission.')
    old_past_limit_submission_fdbk_config = models.OneToOneField(
        AGTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_suite_fdbk,
        related_name='+',
        help_text=
        'Feedback settings for a submission that is past the daily limit.')
    old_staff_viewer_fdbk_config = models.OneToOneField(
        AGTestSuiteFeedbackConfig,
        on_delete=models.PROTECT,
        default=make_default_suite_fdbk,
        related_name='+',
        help_text=
        'Feedback settings for a staff member viewing a submission from another group.'
    )

    normal_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestSuiteFeedbackConfig, default=NewAGTestSuiteFeedbackConfig)
    ultimate_submission_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestSuiteFeedbackConfig, default=NewAGTestSuiteFeedbackConfig)
    past_limit_submission_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestSuiteFeedbackConfig, default=NewAGTestSuiteFeedbackConfig)
    staff_viewer_fdbk_config = ag_fields.ValidatedJSONField(
        NewAGTestSuiteFeedbackConfig, default=NewAGTestSuiteFeedbackConfig)

    def clean(self):
        if self.pk is None:
            return

        errors = {}

        for instructor_file in self.instructor_files_needed.all():
            if instructor_file.project != self.project:
                errors['instructor_files_needed'] = (
                    'File {} does not belong to the project "{}".'.format(
                        instructor_file.name, self.project.name))

        for student_file in self.student_files_needed.all():
            if student_file.project != self.project:
                errors['student_files_needed'] = (
                    'Student file pattern {} does not belong to the project "{}".'
                    .format(student_file.pattern, self.project.name))

        if errors:
            raise exceptions.ValidationError(errors)

    def delete(self, *args, **kwargs):
        with connection.cursor() as cursor:
            cursor.execute(
                '''UPDATE core_submission
                SET denormalized_ag_test_results = denormalized_ag_test_results #- '{%s}'
                WHERE core_submission.project_id = %s
                ''', (self.pk, self.project_id))

        return super().delete()

    SERIALIZABLE_FIELDS = (
        'pk',
        'name',
        'project',
        'last_modified',
        'instructor_files_needed',
        'read_only_instructor_files',
        'student_files_needed',
        'ag_test_cases',
        'setup_suite_cmd',
        'setup_suite_cmd_name',
        'docker_image_to_use',
        'allow_network_access',
        'deferred',
        'normal_fdbk_config',
        'ultimate_submission_fdbk_config',
        'past_limit_submission_fdbk_config',
        'staff_viewer_fdbk_config',
    )

    SERIALIZE_RELATED = (
        'instructor_files_needed',
        'student_files_needed',
        'ag_test_cases',
    )

    EDITABLE_FIELDS = ('name', 'instructor_files_needed',
                       'read_only_instructor_files', 'student_files_needed',
                       'setup_suite_cmd', 'setup_suite_cmd_name',
                       'allow_network_access', 'deferred',
                       'docker_image_to_use', 'normal_fdbk_config',
                       'ultimate_submission_fdbk_config',
                       'past_limit_submission_fdbk_config',
                       'staff_viewer_fdbk_config')
コード例 #7
0
class Submission(ag_model_base.AutograderModel):
    """
    This model stores a set of files submitted by a student for grading.
    """
    objects = _SubmissionManager()

    class Meta:
        ordering = ['-pk']

    class GradingStatus:
        # The submission has been accepted and saved to the database
        received = 'received'

        # The submission has been queued is waiting to be graded
        queued = 'queued'

        being_graded = 'being_graded'

        # Non-deferred test cases have finished and the group can submit
        # again.
        waiting_for_deferred = 'waiting_for_deferred'
        # All test cases have finished grading.
        finished_grading = 'finished_grading'

        # A student removed their submission from the queue before it
        # started being graded.
        removed_from_queue = 'removed_from_queue'

        # Something unexpected occurred during the grading process.
        error = 'error'

        values = [
            received,
            queued,
            being_graded,
            waiting_for_deferred,
            finished_grading,
            removed_from_queue,
            error,
        ]

        # These statuses bar users from making another submission
        # while the current one is active.
        active_statuses = [received, queued, being_graded]

        # A submission should only be counted towards the daily limit if
        # it has one of these statuses.
        count_towards_limit_statuses = [
            received, queued, being_graded, waiting_for_deferred,
            finished_grading
        ]

    # -------------------------------------------------------------------------

    group = models.ForeignKey('core.Group',
                              related_name='submissions',
                              on_delete=models.CASCADE,
                              help_text="""
            The SubmissionGroup that this submission belongs to. Note
            that this field indirectly links this Submission object to a
            Project.
            This field is REQUIRED.""")

    project = models.ForeignKey(
        'core.Project',
        related_name='submissions',
        # Project will cascade-delete groups, groups will cascade-delete
        # submissions.
        on_delete=models.DO_NOTHING,
        help_text='A shortcut for submission.group.project.',
    )

    timestamp = models.DateTimeField(default=timezone.now)

    submitter = ag_fields.ShortStringField(
        blank=True,
        help_text="""The name of the user who made this submission""")

    @property
    def submitted_files(self):
        """
        An iterable of the files included in this submission.
        """
        return (self.get_file(filename)
                for filename in self.submitted_filenames)

    submitted_filenames = ag_fields.StringArrayField(
        blank=True,
        default=list,
        help_text="""The names of files that were submitted,
                     excluding those that were discarded.""")

    discarded_files = ag_fields.StringArrayField(
        default=list,
        blank=True,
        help_text=
        """The names of files that were discarded when this Submission was created."""
    )

    missing_files = pg_fields.JSONField(
        default=dict,
        blank=True,
        help_text="""Stores missing filenames and the additional number
            of files needed to satisfy a file pattern requirement.
            Stored as key-value pairs of the form:
            {pattern: num_additional_needed}""")

    status = models.CharField(
        max_length=const.MAX_CHAR_FIELD_LEN,
        default=GradingStatus.received,
        choices=zip(GradingStatus.values, GradingStatus.values),
        help_text="""The grading status of this submission see
            Submission.GradingStatus for details on allowed values.""")

    count_towards_daily_limit = models.BooleanField(
        default=True,
        help_text="""Indicates whether this submission should count
            towards the daily submission limit.""")

    is_past_daily_limit = models.BooleanField(
        default=False,
        help_text="Whether this submission is past the daily submission limit."
    )

    is_bonus_submission = models.BooleanField(
        default=False,
        help_text="""When True, indicates that the group that made this
            submission should be able to request normal feedback for
            this submission's results.
            Note: If this field is True, is_past_daily_limit should be
            False.""")

    count_towards_total_limit = models.BooleanField(
        default=True,
        help_text=
        "Whether this submission should count towards the total submission limit."
    )

    does_not_count_for = pg_fields.ArrayField(
        models.CharField(max_length=constants.MAX_USERNAME_LEN),
        default=list,
        blank=True,
        help_text="""A list of users for whom this submission will NOT
            count as their final graded submission. Users are added to
            this list if they are out of late days and another group
            member (who still has late days remaining) uses their own
            late day to submit.""")

    error_msg = models.TextField(
        blank=True,
        help_text=
        """If status is "error", an error message will be stored here.""")

    denormalized_ag_test_results = pg_fields.JSONField(
        default=dict,
        blank=True,
        help_text="""Stores denormalized AG test results in order to avoid
                     expensive joins when getting submission result feedback.
                     To update this field, use
                     autograder.core.submission_feedback.update_denormalized_ag_test_results

                     Data format:
{
    "<ag test suite pk>": {
        <ag test suite result data>,
        "ag_test_case_results": {
            "<ag test case pk>": {
                <ag test case result data>,
                "ag_test_command_results": <ag test command result data>
            }
        }
    }
}
        """)

    @property
    def position_in_queue(self) -> int:
        """
        Returns this submission's position in the queue of submissions
        to be graded for the associated project.
        """
        if self.status != Submission.GradingStatus.queued:
            return 0

        return Submission.objects.filter(
            status=Submission.GradingStatus.queued,
            group__project=self.group.project,
            pk__lte=self.pk).count()

    # -------------------------------------------------------------------------

    def get_file(self, filename, mode='rb'):
        """
        Returns a Django File object containing the submitted file with
        the given name. The file is opened using the specified mode
        (mode can be any valid value for the same argument to the Python
        open() function).
        If the file doesn't exist, ObjectDoesNotExist will be raised.
        """
        self._check_file_exists(filename)
        return File(open(self._get_submitted_file_dir(filename), mode),
                    name=os.path.basename(filename))

    def _check_file_exists(self, filename):
        if filename not in self.submitted_filenames:
            raise exceptions.ObjectDoesNotExist()

    def _get_submitted_file_dir(self, filename):
        return os.path.join(core_ut.get_submission_dir(self), filename)

    def get_submitted_file_basenames(self):
        return self.submitted_filenames

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        # result_output_dir is a subdir of the submission dir
        result_output_dir = core_ut.get_result_output_dir(self)
        if not os.path.isdir(result_output_dir):
            os.makedirs(result_output_dir, exist_ok=True)

    SERIALIZABLE_FIELDS = (
        'pk',
        'group',
        'timestamp',
        'submitter',
        'submitted_filenames',
        'discarded_files',
        'missing_files',
        'status',
        'count_towards_daily_limit',
        'is_past_daily_limit',
        'is_bonus_submission',
        'count_towards_total_limit',
        'does_not_count_for',
        'position_in_queue',
    )

    EDITABLE_FIELDS = ('count_towards_daily_limit',
                       'count_towards_total_limit')