TNL-1897 Implement Course Team API
This commit is contained in:
22
openedx/core/lib/api/fields.py
Normal file
22
openedx/core/lib/api/fields.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Fields useful for edX API implementations."""
|
||||
|
||||
from rest_framework.serializers import Field
|
||||
|
||||
|
||||
class ExpandableField(Field):
|
||||
"""Field that can dynamically use a more detailed serializer based on a user-provided "expand" parameter."""
|
||||
def __init__(self, **kwargs):
|
||||
"""Sets up the ExpandableField with the collapsed and expanded versions of the serializer."""
|
||||
assert 'collapsed_serializer' in kwargs and 'expanded_serializer' in kwargs
|
||||
self.collapsed = kwargs.pop('collapsed_serializer')
|
||||
self.expanded = kwargs.pop('expanded_serializer')
|
||||
super(ExpandableField, self).__init__(**kwargs)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
"""Converts obj to a native representation, using the expanded serializer if the context requires it."""
|
||||
if 'expand' in self.context and field_name in self.context['expand']:
|
||||
self.expanded.initialize(self, field_name)
|
||||
return self.expanded.field_to_native(obj, field_name)
|
||||
else:
|
||||
self.collapsed.initialize(self, field_name)
|
||||
return self.collapsed.field_to_native(obj, field_name)
|
||||
@@ -2,6 +2,8 @@ from django.conf import settings
|
||||
from rest_framework import permissions
|
||||
from django.http import Http404
|
||||
|
||||
from student.roles import CourseStaffRole
|
||||
|
||||
|
||||
class ApiKeyHeaderPermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
@@ -74,3 +76,13 @@ class IsUserInUrlOrStaff(IsUserInUrl):
|
||||
return True
|
||||
|
||||
return super(IsUserInUrlOrStaff, self).has_permission(request, view)
|
||||
|
||||
|
||||
class IsStaffOrReadOnly(permissions.BasePermission):
|
||||
"""Permission that checks to see if the user is global or course
|
||||
staff, permitting only read-only access if they are not.
|
||||
"""
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return (request.user.is_staff or
|
||||
CourseStaffRole(obj.course_id).has_user(request.user) or
|
||||
request.method in permissions.SAFE_METHODS)
|
||||
|
||||
@@ -6,3 +6,40 @@ class PaginationSerializer(pagination.PaginationSerializer):
|
||||
Custom PaginationSerializer to include num_pages field
|
||||
"""
|
||||
num_pages = serializers.Field(source='paginator.num_pages')
|
||||
|
||||
|
||||
class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""Serializes arbitrary models in a collapsed format, with just an id and url."""
|
||||
id = serializers.CharField(read_only=True) # pylint: disable=invalid-name
|
||||
url = serializers.HyperlinkedIdentityField(view_name='')
|
||||
|
||||
def __init__(self, model_class, view_name, id_source='id', lookup_field=None, *args, **kwargs):
|
||||
"""Configures the serializer.
|
||||
|
||||
Args:
|
||||
model_class (class): Model class to serialize.
|
||||
view_name (string): Name of the Django view used to lookup the
|
||||
model.
|
||||
id_source (string): Optional name of the id field on the model.
|
||||
Defaults to 'id'.
|
||||
lookup_field (string): Optional name of the model field used to
|
||||
lookup the model in the view. Defaults to the value of
|
||||
id_source.
|
||||
"""
|
||||
if not lookup_field:
|
||||
lookup_field = id_source
|
||||
|
||||
self.Meta.model = model_class
|
||||
|
||||
super(CollapsedReferenceSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['id'].source = id_source
|
||||
self.fields['url'].view_name = view_name
|
||||
self.fields['url'].lookup_field = lookup_field
|
||||
|
||||
class Meta(object):
|
||||
"""Defines meta information for the ModelSerializer.
|
||||
|
||||
model is set dynamically in __init__.
|
||||
"""
|
||||
fields = ("id", "url")
|
||||
|
||||
@@ -4,10 +4,13 @@ Utilities related to API views
|
||||
import functools
|
||||
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
||||
from django.http import Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from rest_framework import status, response
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
|
||||
from rest_framework.generics import GenericAPIView
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -122,3 +125,51 @@ def view_auth_classes(is_user=False):
|
||||
func_or_class.permission_classes += (IsUserInUrl,)
|
||||
return func_or_class
|
||||
return _decorator
|
||||
|
||||
|
||||
def add_serializer_errors(serializer, data, field_errors):
|
||||
"""Adds errors from serializer validation to field_errors. data is the original data to deserialize."""
|
||||
if not serializer.is_valid(): # pylint: disable=maybe-no-member
|
||||
errors = serializer.errors # pylint: disable=maybe-no-member
|
||||
for key, error in errors.iteritems():
|
||||
field_errors[key] = {
|
||||
'developer_message': u"Value '{field_value}' is not valid for field '{field_name}': {error}".format(
|
||||
field_value=data.get(key, ''), field_name=key, error=error
|
||||
),
|
||||
'user_message': _(u"This value is invalid."),
|
||||
}
|
||||
return field_errors
|
||||
|
||||
|
||||
class RetrievePatchAPIView(RetrieveModelMixin, UpdateModelMixin, GenericAPIView):
|
||||
"""Concrete view for retrieving and updating a model instance.
|
||||
|
||||
Like DRF's RetrieveUpdateAPIView, but without PUT and with automatic validation errors in the edX format.
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Retrieves the specified resource using the RetrieveModelMixin."""
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""Checks for validation errors, then updates the model using the UpdateModelMixin."""
|
||||
field_errors = self._validate_patch(request.DATA)
|
||||
if field_errors:
|
||||
return Response({'field_errors': field_errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return self.partial_update(request, *args, **kwargs)
|
||||
|
||||
def _validate_patch(self, patch):
|
||||
"""Validates a JSON merge patch. Captures DRF serializer errors and converts them to edX's standard format."""
|
||||
field_errors = {}
|
||||
serializer = self.get_serializer(self.get_object_or_none(), data=patch, partial=True)
|
||||
fields = self.get_serializer().get_fields() # pylint: disable=maybe-no-member
|
||||
|
||||
for key in patch:
|
||||
if key in fields and fields[key].read_only:
|
||||
field_errors[key] = {
|
||||
'developer_message': "This field is not editable",
|
||||
'user_message': _("This field is not editable"),
|
||||
}
|
||||
|
||||
add_serializer_errors(serializer, patch, field_errors)
|
||||
|
||||
return field_errors
|
||||
|
||||
Reference in New Issue
Block a user