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)
Esempio n. 2
0
class CourseStaffViewSet(ListNestedModelViewSet):
    serializer_class = ag_serializers.UserSerializer
    permission_classes = (
        ag_permissions.is_admin_or_read_only_staff_or_handgrader(), )

    model_manager = ag_models.Course.objects
    reverse_to_one_field_name = 'staff'

    api_tags = [APITags.permissions]

    @swagger_auto_schema(responses={'204': ''},
                         request_body_parameters=_add_staff_params)
    @transaction.atomic()
    @method_decorator(require_body_params('new_staff'))
    def post(self, request, *args, **kwargs):
        course = self.get_object()
        self.add_staff(course, request.data['new_staff'])

        return response.Response(status=status.HTTP_204_NO_CONTENT)

    @swagger_auto_schema(responses={'204': ''},
                         request_body_parameters=_remove_staff_params)
    @transaction.atomic()
    @method_decorator(require_body_params('remove_staff'))
    def patch(self, request, *args, **kwargs):
        course = self.get_object()
        self.remove_staff(course, request.data['remove_staff'])

        return response.Response(status=status.HTTP_204_NO_CONTENT)

    def add_staff(self, course, usernames):
        staff_to_add = [
            User.objects.get_or_create(username=username)[0]
            for username in usernames
        ]
        course.staff.add(*staff_to_add)

    def remove_staff(self, course, users_json):
        staff_to_remove = User.objects.filter(
            pk__in=[user['pk'] for user in users_json])
        course.staff.remove(*staff_to_remove)

    @classmethod
    def as_view(cls, actions=None, **initkwargs):
        return super().as_view(actions={
            'get': 'list',
            'post': 'post',
            'patch': 'patch'
        },
                               **initkwargs)
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',
            in_='body',
            type='List[string]',
            required=True,
            description=
            'The usernames to invite to be in a group with the current user.')
    ])
    @method_decorator(require_body_params('invited_usernames'))
    @transaction.atomic()
    def create(self, *args, **kwargs):
        for key in self.request.data:
            if key != 'invited_usernames':
                raise exceptions.ValidationError({'invalid_fields': [key]})

        invited_users = [
            User.objects.get_or_create(username=username)[0]
            for username in self.request.data.pop('invited_usernames')
        ]

        utils.lock_users(itertools.chain([self.request.user], invited_users))

        self.request.data['invitation_creator'] = self.request.user
        self.request.data['invited_users'] = invited_users
        return super().create(self.request, *args, **kwargs)
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)
Esempio n. 5
0
class UserLateDaysView(AlwaysIsAuthenticatedMixin, APIView):
    swagger_schema = AGModelViewAutoSchema

    api_tags = [APITags.courses]

    @swagger_auto_schema(manual_parameters=[
        Parameter('course_pk', in_='query', type='integer', required=True)
    ],
                         responses={
                             '200':
                             Schema(type='object',
                                    properties=[
                                        Parameter('late_days_remaining',
                                                  in_='body',
                                                  type='integer')
                                    ])
                         })
    @method_decorator(require_query_params('course_pk'))
    def get(self, request: Request, *args, **kwargs):
        try:
            user = get_object_or_404(User.objects,
                                     pk=int(kwargs['username_or_pk']))
        except ValueError:
            user = get_object_or_404(User.objects,
                                     username=kwargs['username_or_pk'])

        course = get_object_or_404(ag_models.Course.objects,
                                   pk=request.query_params['course_pk'])
        remaining = ag_models.LateDaysRemaining.objects.get_or_create(
            user=user, course=course)[0]

        self._check_read_permissions(remaining)

        return response.Response(
            {'late_days_remaining': remaining.late_days_remaining})

    @swagger_auto_schema(manual_parameters=[
        Parameter('course_pk', in_='query', type='integer', required=True)
    ],
                         request_body_parameters=[
                             Parameter('late_days_remaining',
                                       in_='body',
                                       type='integer',
                                       required=True)
                         ],
                         responses={
                             '200':
                             Schema(type='object',
                                    properties=[
                                        Parameter('late_days_remaining',
                                                  in_='body',
                                                  type='integer')
                                    ])
                         })
    @method_decorator(require_body_params('late_days_remaining'))
    def put(self, request: Request, *args, **kwargs):
        try:
            user = get_object_or_404(User.objects,
                                     pk=int(kwargs['username_or_pk']))
        except ValueError:
            user = get_object_or_404(User.objects,
                                     username=kwargs['username_or_pk'])

        course = get_object_or_404(ag_models.Course.objects,
                                   pk=request.query_params['course_pk'])

        with transaction.atomic():
            remaining = ag_models.LateDaysRemaining.objects.select_for_update(
            ).get_or_create(user=user, course=course)[0]

            self._check_read_permissions(remaining)
            self._check_write_permissions(remaining)

            remaining.late_days_remaining = request.data['late_days_remaining']
            remaining.save()

            return response.Response(
                {'late_days_remaining': remaining.late_days_remaining})

    def _check_read_permissions(self, remaining: ag_models.LateDaysRemaining):
        user = self.request.user
        if user == remaining.user:
            return

        if remaining.course.is_staff(user):
            return

        raise PermissionDenied

    def _check_write_permissions(self, remaining: ag_models.LateDaysRemaining):
        if not remaining.course.is_admin(self.request.user):
            raise PermissionDenied
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)