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)
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)
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)
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)
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)
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)