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)
Exemple #3
0
    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)
Exemple #5
0
    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'
Exemple #9
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)
    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)
Exemple #13
0
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')