Example #1
0
class DownloadTaskDetailViewSet(mixins.RetrieveModelMixin,
                                AGModelGenericViewSet):
    permission_classes = (
        ag_permissions.is_admin(lambda task: task.project.course), )
    serializer_class = ag_serializers.DownloadTaskSerializer

    model_manager = ag_models.DownloadTask.objects

    swagger_schema = None

    @decorators.detail_route()
    def result(self, *args, **kwargs):
        task = self.get_object()
        if task.progress != 100:
            return response.Response(data={'in_progress': task.progress},
                                     status=status.HTTP_400_BAD_REQUEST)
        if task.error_msg:
            return response.Response(data={'task_error': task.error_msg},
                                     status=status.HTTP_400_BAD_REQUEST)

        content_type = self._get_content_type(task.download_type)
        return FileResponse(open(task.result_filename, 'rb'),
                            content_type=content_type)

    def _get_content_type(self, download_type: ag_models.DownloadType):
        if (download_type == ag_models.DownloadType.all_scores or download_type
                == ag_models.DownloadType.final_graded_submission_scores):
            return 'text/csv'

        if (download_type == ag_models.DownloadType.all_submission_files
                or download_type
                == ag_models.DownloadType.final_graded_submission_files):
            return 'application/zip'
class RerunSubmissionsTaskDetailVewSet(mixins.RetrieveModelMixin,
                                       AGModelGenericViewSet):
    permission_classes = [
        ag_permissions.is_admin(lambda rerun_task: rerun_task.project.course)
    ]
    serializer_class = ag_serializers.RerunSubmissionTaskSerializer

    model_manager = ag_models.RerunSubmissionsTask.objects
class GroupsViewSet(ListCreateNestedModelViewSet):
    serializer_class = ag_serializers.SubmissionGroupSerializer
    permission_classes = (
        P(ag_permissions.is_admin())
        | ((P(ag_permissions.is_staff()) | P(ag_permissions.is_handgrader()))
           & ag_permissions.IsReadOnly), )

    pk_key = 'project_pk'
    model_manager = ag_models.Project.objects.select_related('course')
    to_one_field_name = 'project'
    reverse_to_one_field_name = 'groups'

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.request.method.lower() == 'get':
            queryset = queryset.prefetch_related('submissions')

        return queryset

    @swagger_auto_schema(request_body_parameters=[
        Parameter(name='member_names',
                  in_='body',
                  description='Usernames to add to the new Group.',
                  type='List[string]',
                  required=True)
    ])
    @transaction.atomic()
    @method_decorator(require_body_params('member_names'))
    def create(self, request, *args, **kwargs):
        project = self.get_object()
        request.data['project'] = project

        users = [
            User.objects.get_or_create(username=username)[0]
            for username in request.data.pop('member_names')
        ]

        utils.lock_users(users)
        # Keep this hook immediately after locking the users.
        test_ut.mocking_hook()

        request.data['members'] = users
        request.data['check_group_size_limits'] = (not project.course.is_admin(
            request.user))

        return super().create(request, *args, **kwargs)
Example #4
0
class CopyProjectView(AGModelGenericViewSet):
    api_tags = [APITags.projects]

    pk_key = 'project_pk'
    model_manager = ag_models.Project.objects

    serializer_class = ag_serializers.ProjectSerializer
    permission_classes = (ag_permissions.is_admin(), )

    @swagger_auto_schema(
        operation_description="""Makes a copy of the specified project and
            all of its instructor file, expected student file, test case,
            and handgrading data.
            Note that groups, submissions, and results (test case, handgrading,
            etc.) are NOT copied.
        """,
        request_body_parameters=[
            Parameter('new_project_name',
                      in_='query',
                      type='string',
                      required=False),
        ])
    @convert_django_validation_error
    @transaction.atomic()
    def copy_project(self, request: Request, *args, **kwargs):
        project: ag_models.Project = self.get_object()

        target_course = get_object_or_404(ag_models.Course.objects,
                                          pk=kwargs['target_course_pk'])
        if not target_course.is_admin(request.user):
            return response.Response(status=status.HTTP_403_FORBIDDEN)

        new_project_name = request.query_params.get('new_project_name', None)
        new_project = copy_project(project=project,
                                   target_course=target_course,
                                   new_project_name=new_project_name)

        return response.Response(status=status.HTTP_201_CREATED,
                                 data=new_project.to_dict())

    @classmethod
    def as_view(cls, actions=None, **initkwargs):
        return super().as_view(actions={'post': 'copy_project'}, **initkwargs)
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission
from rest_framework import response, mixins, exceptions
from drf_composable_permissions.p import P

import autograder.core.models as ag_models
from autograder.core.models.get_ultimate_submissions import get_ultimate_submission
import autograder.handgrading.models as handgrading_models
import autograder.handgrading.serializers as handgrading_serializers
import autograder.rest_api.permissions as ag_permissions
from autograder.rest_api.views.ag_model_views import (
    handle_object_does_not_exist_404, AGModelAPIView, AGModelGenericViewSet)
from autograder import utils
from autograder.rest_api.views.schema_generation import APITags, AGModelSchemaBuilder

is_admin = ag_permissions.is_admin(lambda group: group.project.course)
is_staff = ag_permissions.is_staff(lambda group: group.project.course)
is_handgrader = ag_permissions.is_handgrader(lambda group: group.project.course)
can_view_project = ag_permissions.can_view_project(lambda group: group.project)


class HandgradingResultsPublished(BasePermission):
    def has_object_permission(self, request, view, group: ag_models.Group):
        return group.project.handgrading_rubric.show_grades_and_rubric_to_students


student_permission = (
    P(ag_permissions.IsReadOnly)
    & P(can_view_project)
    & P(ag_permissions.is_group_member())
    & P(HandgradingResultsPublished)
Example #6
0
class AllUltimateSubmissionResults(AGModelAPIView):
    permission_classes = (
        P(ag_permissions.is_admin())
        | (P(ag_permissions.is_staff()) & P(_UltimateSubmissionsAvailable)), )
    model_manager = ag_models.Project
    pk_key = 'project_pk'

    api_tags = (APITags.submissions, )

    @swagger_auto_schema(manual_parameters=[
        Parameter(name='page', type='integer', in_='query'),
        Parameter(name='groups_per_page',
                  type='integer',
                  in_='query',
                  default=UltimateSubmissionPaginator.page_size,
                  description='Max groups per page: {}'.format(
                      UltimateSubmissionPaginator.max_page_size)),
        Parameter(name='full_results',
                  type='string',
                  enum=['true', 'false'],
                  in_='query',
                  description=
                  'When true, includes all SubmissionResultFeedback fields. '
                  'Defaults to false.'),
        Parameter(name='include_staff',
                  type='string',
                  enum=['true', 'false'],
                  in_='query',
                  description='When false, excludes staff and admin users '
                  'from the results. Defaults to true.')
    ],
                         responses={
                             '200': _all_ultimate_submission_results_schema
                         })
    def get(self, *args, **kwargs):
        project: ag_models.Project = self.get_object()

        include_staff = self.request.query_params.get('include_staff',
                                                      'true') == 'true'
        if include_staff:
            groups = project.groups.all()
        else:
            staff = list(
                itertools.chain(project.course.staff.all(),
                                project.course.admins.all()))
            groups = project.groups.exclude(members__in=staff)

        full_results = self.request.query_params.get('full_results') == 'true'

        paginator = UltimateSubmissionPaginator()
        page = paginator.paginate_queryset(queryset=groups,
                                           request=self.request,
                                           view=self)

        ag_test_preloader = AGTestPreLoader(project)
        ultimate_submissions = get_ultimate_submissions(
            project, filter_groups=page, ag_test_preloader=ag_test_preloader)

        results = serialize_ultimate_submission_results(
            ultimate_submissions, full_results=full_results)

        return paginator.get_paginated_response(results)
            'members': [request.user],
            'check_group_size_limits':
            (not project.course.is_staff(request.user))
        }
        serializer = self.get_serializer(data=data)
        serializer.is_valid()
        serializer.save()

        return response.Response(serializer.data,
                                 status=status.HTTP_201_CREATED)


is_staff_or_member = ag_permissions.is_staff_or_group_member()
can_view_project = ag_permissions.can_view_project(lambda group: group.project)
group_permissions = (
    P(ag_permissions.is_admin())
    | (P(ag_permissions.IsReadOnly) & can_view_project & is_staff_or_member))


class _UltimateSubmissionPermissions(permissions.BasePermission):
    def has_object_permission(self, request, view, group):
        project = group.project
        course = group.project.course
        is_staff = course.is_staff(request.user)

        # Staff and higher can always view their own ultimate submission
        if is_staff and group.members.filter(pk=request.user.pk).exists():
            return True

        closing_time = (project.closing_time if group.extended_due_date is None
                        else group.extended_due_date)
class CopyCourseView(AGModelGenericViewSet):
    api_tags = [APITags.courses]

    pk_key = 'course_pk'
    model_manager = ag_models.Course.objects

    serializer_class = ag_serializers.CourseSerializer

    permission_classes = (
        (P(ag_permissions.IsSuperuser) |
         (P(ag_permissions.is_admin()) & P(CanCreateCourses))), )

    @swagger_auto_schema(
        operation_description=
        """Makes a copy of the given course and all its projects.
            The projects and all of their  instructor file,
            expected student file, test case, and handgrading data.
            Note that groups, submissions, and results (test case, handgrading,
            etc.) are NOT copied.
            The admin list is copied to the new project, but other permissions
            (staff, students, etc.) are not.
        """,
        request_body_parameters=[
            Parameter('new_name', in_='body', type='string', required=True),
            Parameter(
                'new_semester',
                in_='body',
                type='string',
                required=True,
                description='Must be one of: ' +
                f'{", ".join((semester.value for semester in ag_models.Semester))}'
            ),
            Parameter('new_year', in_='body', type='integer', required=True)
        ],
    )
    @transaction.atomic()
    @convert_django_validation_error
    @method_decorator(
        require_body_params('new_name', 'new_semester', 'new_year'))
    def copy_course(self, request: Request, *args, **kwargs):
        course: ag_models.Course = self.get_object()

        new_semester = request.data['new_semester']
        try:
            new_semester = ag_models.Semester(new_semester)
        except ValueError:
            return response.Response(
                status=status.HTTP_400_BAD_REQUEST,
                data=f'"{new_semester}" is not a valid semester.')

        new_course = copy_course(course=course,
                                 new_course_name=request.data['new_name'],
                                 new_course_semester=new_semester,
                                 new_course_year=request.data['new_year'])

        return response.Response(status=status.HTTP_201_CREATED,
                                 data=new_course.to_dict())

    @classmethod
    def as_view(cls, actions=None, **initkwargs):
        return super().as_view(actions={'post': 'copy_course'}, **initkwargs)
Example #9
0
class EditBonusSubmissionsView(AGModelGenericViewSet):
    serializer_class = ag_serializers.ProjectSerializer
    permission_classes = (ag_permissions.is_admin(), )

    model_manager = ag_models.Project.objects.select_related('course')
    pk_key = 'project_pk'

    @swagger_auto_schema(
        responses={'204': ''},
        request_body_parameters=[
            Parameter(
                'add',
                'body',
                type='integer',
                description=
                """How many bonus submissions to add to each group's total.
                               Mutually exclusive with "subtract"."""),
            Parameter(
                'subtract',
                'body',
                type='integer',
                description=
                """How many bonus submissions to subtract from each group's total.
                       Mutually exclusive with "add".""")
        ],
        manual_parameters=[
            Parameter(
                'group_pk',
                'query',
                type='integer',
                description=
                """Instead of modifying the bonus submission totals for every group,
                               only modify the group with the specified primary key."""
            )
        ])
    @transaction.atomic()
    def partial_update(self, *args, **kwargs):
        project: ag_models.Project = self.get_object()

        if len(self.request.data) > 1 or len(self.request.data) == 0:
            return response.Response(
                status=status.HTTP_400_BAD_REQUEST,
                data='Please provide exactly one of: "add", "subtract".')

        queryset = project.groups
        if 'group_pk' in self.request.query_params:
            queryset = queryset.filter(
                pk=self.request.query_params.get('group_pk'))

        if 'add' in self.request.data:
            queryset.update(
                bonus_submissions_remaining=(F('bonus_submissions_remaining') +
                                             self.request.data.get('add')))
        if 'subtract' in self.request.data:
            queryset.update(bonus_submissions_remaining=(
                F('bonus_submissions_remaining') -
                self.request.data.get('subtract')))

        return response.Response(status=status.HTTP_204_NO_CONTENT)

    @classmethod
    def as_view(cls, actions=None, **initkwargs):
        return super().as_view(actions={'patch': 'partial_update'},
                               **initkwargs)
Example #10
0
class ProjectDetailViewSet(mixins.RetrieveModelMixin,
                           transaction_mixins.TransactionPartialUpdateMixin,
                           AGModelGenericViewSet):
    model_manager = ag_models.Project.objects.select_related('course')

    serializer_class = ag_serializers.ProjectSerializer
    permission_classes = (project_detail_permissions, )

    @decorators.detail_route()
    def num_queued_submissions(self, *args, **kwargs):
        project = self.get_object()
        num_queued_submissions = ag_models.Submission.objects.filter(
            status=ag_models.Submission.GradingStatus.queued,
            group__project=project).count()

        return response.Response(data=num_queued_submissions)

    @swagger_auto_schema(auto_schema=None)
    @decorators.detail_route(
        methods=['POST'],
        permission_classes=[
            permissions.IsAuthenticated,
            ag_permissions.is_admin(lambda project: project.course)
        ])
    def all_submission_files(self, *args, **kwargs):
        # IMPORTANT: Do NOT add the task to the queue before completing this transaction!
        with transaction.atomic():
            project = self.get_object()  # type: ag_models.Project
            include_staff = self.request.query_params.get(
                'include_staff', None) == 'true'
            task = ag_models.DownloadTask.objects.validate_and_create(
                project=project,
                creator=self.request.user,
                download_type=ag_models.DownloadType.all_submission_files)

        from autograder.celery import app
        api_tasks.all_submission_files_task.apply_async(
            (project.pk, task.pk, include_staff), connection=app.connection())

        return response.Response(status=status.HTTP_202_ACCEPTED,
                                 data=task.to_dict())

    @swagger_auto_schema(auto_schema=None)
    @decorators.detail_route(
        methods=['POST'],
        permission_classes=[
            permissions.IsAuthenticated,
            ag_permissions.is_admin(lambda project: project.course)
        ])
    def ultimate_submission_files(self, *args, **kwargs):
        # IMPORTANT: Do NOT add the task to the queue before completing this transaction!
        with transaction.atomic():
            project = self.get_object()
            include_staff = self.request.query_params.get(
                'include_staff', None) == 'true'
            task = ag_models.DownloadTask.objects.validate_and_create(
                project=project,
                creator=self.request.user,
                download_type=ag_models.DownloadType.
                final_graded_submission_files)

        from autograder.celery import app
        api_tasks.ultimate_submission_files_task.apply_async(
            (project.pk, task.pk, include_staff), connection=app.connection())

        return response.Response(status=status.HTTP_202_ACCEPTED,
                                 data=task.to_dict())

    @swagger_auto_schema(auto_schema=None)
    @decorators.detail_route(
        methods=['POST'],
        permission_classes=[
            permissions.IsAuthenticated,
            ag_permissions.is_admin(lambda project: project.course)
        ])
    def all_submission_scores(self, *args, **kwargs):
        # IMPORTANT: Do NOT add the task to the queue before completing this transaction!
        with transaction.atomic():
            project = self.get_object()  # type: ag_models.Project
            include_staff = self.request.query_params.get(
                'include_staff', None) == 'true'
            task = ag_models.DownloadTask.objects.validate_and_create(
                project=project,
                creator=self.request.user,
                download_type=ag_models.DownloadType.all_scores)

        from autograder.celery import app
        api_tasks.all_submission_scores_task.apply_async(
            (project.pk, task.pk, include_staff), connection=app.connection())

        return response.Response(status=status.HTTP_202_ACCEPTED,
                                 data=task.to_dict())

    @swagger_auto_schema(auto_schema=None)
    @decorators.detail_route(
        methods=['POST'],
        permission_classes=[
            permissions.IsAuthenticated,
            ag_permissions.is_admin(lambda project: project.course)
        ])
    def ultimate_submission_scores(self, *args, **kwargs):
        # IMPORTANT: Do NOT add the task to the queue before completing this transaction!
        with transaction.atomic():
            project = self.get_object()  # type: ag_models.Project
            include_staff = self.request.query_params.get(
                'include_staff', None) == 'true'
            task = ag_models.DownloadTask.objects.validate_and_create(
                project=project,
                creator=self.request.user,
                download_type=ag_models.DownloadType.
                final_graded_submission_scores)

        from autograder.celery import app
        api_tasks.ultimate_submission_scores_task.apply_async(
            (project.pk, task.pk, include_staff), connection=app.connection())

        return response.Response(status=status.HTTP_202_ACCEPTED,
                                 data=task.to_dict())

    @swagger_auto_schema(auto_schema=None)
    @decorators.detail_route(permission_classes=[
        permissions.IsAuthenticated,
        ag_permissions.is_admin(lambda project: project.course)
    ])
    def download_tasks(self, *args, **kwargs):
        project = self.get_object()
        queryset = project.download_tasks.all()
        serializer = ag_serializers.DownloadTaskSerializer(queryset, many=True)
        return response.Response(data=serializer.data)

    @decorators.detail_route(
        methods=['DELETE'],
        permission_classes=[
            permissions.IsAuthenticated,
            ag_permissions.is_admin(lambda project: project.course)
        ])
    def results_cache(self, *args, **kwargs):
        with transaction.atomic():
            project = self.get_object()

        cache.delete_pattern('project_{}_submission_normal_results_*'.format(
            project.pk))
        return response.Response(status=status.HTTP_204_NO_CONTENT)
Example #11
0
import autograder.core.models as ag_models
import autograder.rest_api.permissions as ag_permissions
import autograder.rest_api.serializers as ag_serializers
from autograder.core.models.copy_project_and_course import copy_project
from autograder.rest_api import tasks as api_tasks, transaction_mixins
from autograder.rest_api.views.ag_model_views import (
    AGModelGenericViewSet, convert_django_validation_error)
from autograder.rest_api.views.ag_model_views import ListCreateNestedModelViewSet
from autograder.rest_api.views.schema_generation import APITags

can_list_projects = (P(ag_permissions.IsReadOnly)
                     & (P(ag_permissions.is_staff())
                        | P(ag_permissions.is_student())
                        | P(ag_permissions.is_handgrader())))
list_create_project_permissions = P(
    ag_permissions.is_admin()) | can_list_projects


class ListCreateProjectView(ListCreateNestedModelViewSet):
    serializer_class = ag_serializers.ProjectSerializer
    permission_classes = (list_create_project_permissions, )

    model_manager = ag_models.Course.objects
    to_one_field_name = 'course'
    reverse_to_one_field_name = 'projects'

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.request.method not in permissions.SAFE_METHODS:
            return queryset
class RerunSubmissionsTaskListCreateView(ListCreateNestedModelViewSet):
    serializer_class = ag_serializers.RerunSubmissionTaskSerializer
    permission_classes = [
        ag_permissions.is_admin(lambda project: project.course)
    ]

    pk_key = 'project_pk'
    model_manager = ag_models.Project.objects.select_related('course')
    to_one_field_name = 'project'
    reverse_to_one_field_name = 'rerun_submission_tasks'

    @transaction.atomic()
    def create(self, request: Request, *args, **kwargs):
        project = self.get_object()  # type: ag_models.Project

        serializer = self.get_serializer(data=request.data)
        serializer.is_valid()
        rerun_task = serializer.save(
            project=project,
            creator=request.user)  # type: ag_models.RerunSubmissionsTask

        submissions = ag_models.Submission.objects.filter(
            group__project=project)
        if not request.data.get('rerun_all_submissions', True):
            submissions = submissions.filter(
                pk__in=request.data.get('submission_pks', []))

        ag_test_suites = project.ag_test_suites.all()
        ag_suites_data = request.data.get('ag_test_suite_data', {})
        if not request.data.get('rerun_all_ag_test_suites', True):
            ag_test_suites = ag_test_suites.filter(
                pk__in=ag_suites_data.keys())

        student_suites = project.student_test_suites.all()
        if not request.data.get('rerun_all_student_test_suites', True):
            student_suites = student_suites.filter(
                pk__in=request.data.get('student_suite_pks', []))

        signatures = []
        for submission in submissions:
            ag_suite_sigs = [
                rerun_ag_test_suite.s(
                    rerun_task.pk, submission.pk, ag_suite.pk,
                    *ag_suites_data.get(str(ag_suite.pk), []))
                for ag_suite in ag_test_suites
            ]

            student_suite_sigs = [
                rerun_student_test_suite.s(rerun_task.pk, submission.pk,
                                           student_suite.pk)
                for student_suite in student_suites
            ]

            signatures += ag_suite_sigs
            signatures += student_suite_sigs

        from autograder.celery import app
        if signatures:
            group_result = celery.group(signatures).apply_async(
                connection=app.connection())

            # Save the group result in the Celery results backend,
            # and save the group result ID in the database.
            group_result.save(backend=app.backend)

            # In case any of the subtasks finish before we reach this line
            # (definitely happens in testing), make sure we don't
            # accidentally overwrite the task's progress or error messages.
            ag_models.RerunSubmissionsTask.objects.filter(
                pk=rerun_task.pk).update(
                    celery_group_result_id=group_result.id)

        return response.Response(self.get_serializer(rerun_task).data,
                                 status=status.HTTP_201_CREATED)
class SubmissionDetailViewSet(mixins.RetrieveModelMixin,
                              transaction_mixins.TransactionPartialUpdateMixin,
                              AGModelGenericViewSet):
    model_manager = ag_models.Submission.objects.select_related(
        'group__project__course')

    serializer_class = ag_serializers.SubmissionSerializer
    permission_classes = ((P(ag_permissions.is_admin())
                           | P(ag_permissions.IsReadOnly)),
                          ag_permissions.can_view_project(),
                          ag_permissions.is_staff_or_group_member())

    @swagger_auto_schema(manual_parameters=[
        Parameter(name='filename',
                  in_='query',
                  description='The name of the file to return.',
                  required=True,
                  type='str')
    ],
                         responses={'200': 'Returns the file contents.'})
    @method_decorator(require_query_params('filename'))
    @decorators.detail_route()
    def file(self, request, *args, **kwargs):
        submission = self.get_object()
        filename = request.query_params['filename']
        try:
            return FileResponse(submission.get_file(filename))
        except ObjectDoesNotExist:
            return response.Response('File "{}" not found'.format(filename),
                                     status=status.HTTP_404_NOT_FOUND)

    @swagger_auto_schema(
        responses={'204': 'The submission has been removed from the queue.'},
        request_body_parameters=[])
    @transaction.atomic()
    @decorators.detail_route(
        methods=['post'],
        # NOTE: Only group members can remove their own submissions from the queue.
        permission_classes=(ag_permissions.can_view_project(),
                            ag_permissions.is_group_member()))
    def remove_from_queue(self, request, *args, **kwargs):
        """
        Remove this submission from the grading queue.
        """
        submission: ag_models.Submission = self.get_object()
        removeable_statuses = [
            ag_models.Submission.GradingStatus.received,
            ag_models.Submission.GradingStatus.queued
        ]
        if submission.status not in removeable_statuses:
            return response.Response('This submission is not currently queued',
                                     status=status.HTTP_400_BAD_REQUEST)

        refund_bonus_submission = submission.is_bonus_submission
        if refund_bonus_submission:
            ag_models.Group.objects.select_for_update(
            ).filter(pk=submission.group_id).update(
                bonus_submissions_remaining=F('bonus_submissions_remaining') +
                1)

        submission.status = (
            ag_models.Submission.GradingStatus.removed_from_queue)
        submission.is_bonus_submission = False
        submission.save()

        return response.Response(status=status.HTTP_204_NO_CONTENT)