forms.py 6.42 KB
Newer Older
Nimisha Asthagiri committed
1 2 3 4 5 6 7 8 9 10 11
"""
Course API Forms
"""
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.forms import Form, CharField, ChoiceField, IntegerField
from django.http import Http404
from rest_framework.exceptions import PermissionDenied

from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
12
from openedx.core.djangoapps.util.forms import ExtendedNullBooleanField, MultiValueField
Nimisha Asthagiri committed
13 14
from xmodule.modulestore.django import modulestore

15
from . import permissions
Nimisha Asthagiri committed
16 17 18 19 20 21


class BlockListGetForm(Form):
    """
    A form to validate query parameters in the block list retrieval endpoint
    """
22
    all_blocks = ExtendedNullBooleanField(required=False)
Nimisha Asthagiri committed
23 24 25
    block_counts = MultiValueField(required=False)
    depth = CharField(required=False)
    nav_depth = IntegerField(required=False, min_value=0)
26
    requested_fields = MultiValueField(required=False)
Nimisha Asthagiri committed
27 28 29 30
    return_type = ChoiceField(
        required=False,
        choices=[(choice, choice) for choice in ['dict', 'list']],
    )
31 32 33
    student_view_data = MultiValueField(required=False)
    usage_key = CharField(required=True)
    username = CharField(required=False)
Nimisha Asthagiri committed
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

    def clean_depth(self):
        """
        Get the appropriate depth.  No provided value will be treated as a
        depth of 0, while a value of "all" will be treated as unlimited depth.
        """
        value = self.cleaned_data['depth']
        if not value:
            return 0
        elif value == "all":
            return None
        try:
            return int(value)
        except ValueError:
            raise ValidationError("'{}' is not a valid depth value.".format(value))

50
    def clean_requested_fields(self):
Nimisha Asthagiri committed
51
        """
52 53
        Return a set of `requested_fields`, merged with defaults of `type`
        and `display_name`
Nimisha Asthagiri committed
54
        """
55
        requested_fields = self.cleaned_data['requested_fields']
Nimisha Asthagiri committed
56

57 58
        # add default requested_fields
        return (requested_fields or set()) | {'type', 'display_name'}
Nimisha Asthagiri committed
59 60 61 62 63 64 65

    def clean_return_type(self):
        """
        Return valid 'return_type' or default value of 'dict'
        """
        return self.cleaned_data['return_type'] or 'dict'

66
    def clean_usage_key(self):
Nimisha Asthagiri committed
67
        """
68
        Ensure a valid `usage_key` was provided.
Nimisha Asthagiri committed
69
        """
70
        usage_key = self.cleaned_data['usage_key']
Nimisha Asthagiri committed
71

72 73 74 75
        try:
            usage_key = UsageKey.from_string(usage_key)
        except InvalidKeyError:
            raise ValidationError("'{}' is not a valid usage key.".format(unicode(usage_key)))
Nimisha Asthagiri committed
76

77
        return usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
Nimisha Asthagiri committed
78 79 80

    def clean(self):
        """
81
        Return cleaned data, including additional requested fields.
Nimisha Asthagiri committed
82 83 84
        """
        cleaned_data = super(BlockListGetForm, self).clean()

85 86
        # Add additional requested_fields that are specified as separate
        # parameters, if they were requested.
Nimisha Asthagiri committed
87 88 89 90 91 92 93 94 95 96 97 98 99 100
        additional_requested_fields = [
            'student_view_data',
            'block_counts',
            'nav_depth',
        ]
        for additional_field in additional_requested_fields:
            field_value = cleaned_data.get(additional_field)
            if field_value or field_value == 0:  # allow 0 as a requested value
                cleaned_data['requested_fields'].add(additional_field)

        usage_key = cleaned_data.get('usage_key')
        if not usage_key:
            return

101
        cleaned_data['user'] = self._clean_requested_user(cleaned_data, usage_key.course_key)
Nimisha Asthagiri committed
102
        return cleaned_data
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169

    def _clean_requested_user(self, cleaned_data, course_key):
        """
        Validates and returns the requested_user, while checking permissions.
        """
        requesting_user = self.initial['requesting_user']
        requested_username = cleaned_data.get('username', None)

        if not requested_username:
            return self._verify_no_user(requesting_user, cleaned_data, course_key)
        elif requesting_user.username.lower() == requested_username.lower():
            return self._verify_requesting_user(requesting_user, course_key)
        else:
            return self._verify_other_user(requesting_user, requested_username, course_key)

    @staticmethod
    def _verify_no_user(requesting_user, cleaned_data, course_key):
        """
        Verifies form for when no username is specified, including permissions.
        """
        # Verify that access to all blocks is requested
        # (and not unintentionally requested).
        if not cleaned_data.get('all_blocks', None):
            raise ValidationError({'username': ['This field is required unless all_blocks is requested.']})

        # Verify all blocks can be accessed for the course.
        if not permissions.can_access_all_blocks(requesting_user, course_key):
            raise PermissionDenied(
                "'{requesting_username}' does not have permission to access all blocks in '{course_key}'."
                .format(requesting_username=requesting_user.username, course_key=unicode(course_key))
            )

        # return None for user
        return None

    @staticmethod
    def _verify_requesting_user(requesting_user, course_key):
        """
        Verifies whether the requesting user can access blocks in the course.
        """
        if not permissions.can_access_self_blocks(requesting_user, course_key):
            raise PermissionDenied(
                "Course blocks for '{requesting_username}' cannot be accessed."
                .format(requesting_username=requesting_user.username)
            )
        return requesting_user

    @staticmethod
    def _verify_other_user(requesting_user, requested_username, course_key):
        """
        Verifies whether the requesting user can access another user's view of
        the blocks in the course.
        """
        # Verify requesting user can access the user's blocks.
        if not permissions.can_access_others_blocks(requesting_user, course_key):
            raise PermissionDenied(
                "'{requesting_username}' does not have permission to access view for '{requested_username}'."
                .format(requesting_username=requesting_user.username, requested_username=requested_username)
            )

        # Verify user exists.
        try:
            return User.objects.get(username=requested_username)
        except User.DoesNotExist:
            raise Http404(
                "Requested user '{requested_username}' does not exist.".format(requested_username=requested_username)
            )