def is_admin_or_read_only_can_view_project( get_project_fn: GetProjectFnType=_get_project ) -> Type[permissions.BasePermission]: return ( P(is_admin(lambda obj: get_project_fn(obj).course)) | (P(IsReadOnly) & can_view_project(get_project_fn)) )
class CourseAdminViewSet(ListNestedModelViewSet): serializer_class = ag_serializers.UserSerializer permission_classes = ( P(ag_permissions.IsSuperuser) | P(ag_permissions.is_admin_or_read_only_staff_or_handgrader()), ) model_manager = ag_models.Course.objects reverse_to_one_field_name = 'admins' api_tags = [APITags.permissions] @swagger_auto_schema(responses={'204': ''}, request_body_parameters=_add_admins_params) @transaction.atomic() @method_decorator(require_body_params('new_admins')) def post(self, request, *args, **kwargs): course = self.get_object() self.add_admins(course, request.data['new_admins']) return response.Response(status=status.HTTP_204_NO_CONTENT) @swagger_auto_schema(responses={'204': ''}, request_body_parameters=_remove_admins_params) @transaction.atomic() @method_decorator(require_body_params('remove_admins')) def patch(self, request, *args, **kwargs): course = self.get_object() self.remove_admins(course, request.data['remove_admins']) return response.Response(status=status.HTTP_204_NO_CONTENT) def add_admins(self, course: ag_models.Course, usernames): users_to_add = [ User.objects.get_or_create(username=username)[0] for username in usernames ] course.admins.add(*users_to_add) def remove_admins(self, course: ag_models.Course, users_json): users_to_remove = User.objects.filter( pk__in=[user['pk'] for user in users_json]) if self.request.user in users_to_remove: raise exceptions.ValidationError({ 'remove_admins': ["You cannot remove your own admin privileges."] }) course.admins.remove(*users_to_remove) @classmethod def as_view(cls, actions=None, **initkwargs): return super().as_view(actions={ 'get': 'list', 'post': 'post', 'patch': 'patch' }, **initkwargs)
def test_and(self): table = [ (T, T, True), (T, F, False), (F, T, False), (F, F, False), ] for c1, c2, outcome in table: combined = P(c1) & P(c2) self.assertEqual(combined.has_permission(None, None), outcome) self.assertEqual(combined.has_object_permission(None, None, None), outcome)
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)
def test_not(self): table = [ (T, False), (F, True), ] for c1, outcome in table: combined = ~P(c1) self.assertEqual(combined.has_permission(None, None), outcome) self.assertEqual(combined.has_object_permission(None, None, None), outcome)
class HandgradingRubricRetrieveCreateViewSet(RetrieveCreateNestedModelViewSet): serializer_class = handgrading_serializers.HandgradingRubricSerializer permission_classes = [(P(is_admin_or_read_only) | (P(is_handgrader) & P(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 = 'handgrading_rubric' api_tags = [APITags.handgrading_rubrics] def retrieve(self, *args, **kwargs): project = self.get_object() try: handgrading_rubric = project.handgrading_rubric except ObjectDoesNotExist: raise Http404( 'Project {} has no HandgradingRubric (handgrading not enabled)' .format(project.pk)) serializer = self.get_serializer(handgrading_rubric) return response.Response(serializer.data)
class HandgradingResultView(AGModelGenericViewSet): serializer_class = handgrading_serializers.HandgradingResultSerializer permission_classes = [ (P(is_admin) | P(is_staff) | P(is_handgrader) | student_permission) ] pk_key = 'group_pk' model_manager = ag_models.Group.objects.select_related( 'project__course' ) one_to_one_field_name = 'group' reverse_one_to_one_field_name = 'handgrading_result' api_tags = [APITags.handgrading_results] @swagger_auto_schema( manual_parameters=[ Parameter( name='filename', in_='query', type='string', description='The name of a submitted file. When this parameter is included, ' 'this endpoint will return the contents of the requested file.') ] ) @handle_object_does_not_exist_404 def retrieve(self, request, *args, **kwargs): group = self.get_object() # type: ag_models.Group if 'filename' not in request.query_params: return response.Response(self.get_serializer(group.handgrading_result).data) submission = group.handgrading_result.submission filename = request.query_params['filename'] return FileResponse(submission.get_file(filename)) @transaction.atomic() def create(self, *args, **kwargs): """ Creates a new HandgradingResult for the specified Group, or returns an already existing one. """ group = self.get_object() try: handgrading_rubric = group.project.handgrading_rubric except ObjectDoesNotExist: raise exceptions.ValidationError( {'handgrading_rubric': 'Project {} has not enabled handgrading'.format(group.project.pk)}) ultimate_submission = get_ultimate_submission(group) if not ultimate_submission: raise exceptions.ValidationError( {'num_submissions': 'Group {} has no submissions'.format(group.pk)}) handgrading_result, created = handgrading_models.HandgradingResult.objects.get_or_create( defaults={'submission': ultimate_submission}, handgrading_rubric=handgrading_rubric, group=group) for criterion in handgrading_rubric.criteria.all(): handgrading_models.CriterionResult.objects.get_or_create( defaults={'selected': False}, criterion=criterion, handgrading_result=handgrading_result, ) serializer = self.get_serializer(handgrading_result) return response.Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK) @transaction.atomic() @handle_object_does_not_exist_404 def partial_update(self, request, *args, **kwargs): group = self.get_object() # type: ag_models.Group is_admin = group.project.course.is_admin(request.user) is_staff = group.project.course.is_staff(request.user) can_adjust_points = ( is_admin or is_staff or group.project.course.is_handgrader(request.user) and group.project.handgrading_rubric.handgraders_can_adjust_points) if 'points_adjustment' in self.request.data and not can_adjust_points: raise PermissionDenied handgrading_result = group.handgrading_result handgrading_result.validate_and_update(**request.data) return response.Response(self.get_serializer(handgrading_result).data)
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 HandgradingResultView(AGModelGenericViewSet): serializer_class = handgrading_serializers.HandgradingResultSerializer permission_classes = [ (P(is_admin) | P(is_staff) | P(is_handgrader) | student_permission) ] pk_key = 'group_pk' model_manager = ag_models.Group.objects.select_related( 'project__course'
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)
def has_object_permission(self, request, view, project: ag_models.Project): if (project.disallow_group_registration and not project.course.is_staff(request.user)): return False if (project.course.is_handgrader(request.user) and not project.course.is_student(request.user) and not project.course.is_staff(request.user)): return False return True list_create_invitation_permissions = ( # Only staff can list invitations. (P(ag_permissions.IsReadOnly)) & P(ag_permissions.is_staff()) | (~P(ag_permissions.IsReadOnly) & P(ag_permissions.can_view_project()) & P(CanSendInvitation))) class ListCreateGroupInvitationViewSet(ListCreateNestedModelViewSet): serializer_class = ag_serializers.SubmissionGroupInvitationSerializer permission_classes = (list_create_invitation_permissions, ) model_manager = ag_models.Project.objects to_one_field_name = 'project' reverse_to_one_field_name = 'group_invitations' @swagger_auto_schema(request_body_parameters=[ Parameter( name='invited_usernames',
'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)
from rest_framework import decorators, mixins, response from rest_framework import permissions from rest_framework import status from rest_framework.request import Request 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'
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)
from django.utils import timezone from django.utils.decorators import method_decorator from drf_composable_permissions.p import P from drf_yasg.openapi import Parameter from drf_yasg.utils import swagger_auto_schema from rest_framework import decorators, exceptions, mixins, response, status import autograder.core.models as ag_models import autograder.rest_api.permissions as ag_permissions import autograder.rest_api.serializers as ag_serializers import autograder.utils.testing as test_ut from autograder.rest_api import transaction_mixins from autograder.rest_api.views.ag_model_views import ( AGModelGenericViewSet, ListCreateNestedModelViewSet, require_query_params) can_view_group = (P(ag_permissions.IsReadOnly) & P(ag_permissions.can_view_project()) & P(ag_permissions.is_staff_or_group_member())) can_submit = (~P(ag_permissions.IsReadOnly) & P(ag_permissions.can_view_project()) & P(ag_permissions.is_group_member())) list_create_submission_permissions = can_view_group | can_submit class ListCreateSubmissionViewSet(ListCreateNestedModelViewSet): serializer_class = ag_serializers.SubmissionSerializer permission_classes = (list_create_submission_permissions, ) model_manager = ag_models.Group.objects.select_related('project__course')