TNL-1897 Implement Course Team API
This commit is contained in:
@@ -10,6 +10,8 @@ from student.models import User, UserProfile, Registration
|
||||
from student import views as student_views
|
||||
from util.model_utils import emit_setting_changed_event
|
||||
|
||||
from openedx.core.lib.api.view_utils import add_serializer_errors
|
||||
|
||||
from ..errors import (
|
||||
AccountUpdateError, AccountValidationError, AccountUsernameInvalid, AccountPasswordInvalid,
|
||||
AccountEmailInvalid, AccountUserAlreadyExists,
|
||||
@@ -170,7 +172,7 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
legacy_profile_serializer = AccountLegacyProfileSerializer(existing_user_profile, data=update)
|
||||
|
||||
for serializer in user_serializer, legacy_profile_serializer:
|
||||
field_errors = _add_serializer_errors(update, serializer, field_errors)
|
||||
field_errors = add_serializer_errors(serializer, update, field_errors)
|
||||
|
||||
# If the user asked to change email, validate it.
|
||||
if changing_email:
|
||||
@@ -250,27 +252,6 @@ def _get_user_and_profile(username):
|
||||
return existing_user, existing_user_profile
|
||||
|
||||
|
||||
def _add_serializer_errors(update, serializer, field_errors):
|
||||
"""
|
||||
Helper method that adds any validation errors that are present in the serializer to
|
||||
the supplied field_errors dict.
|
||||
"""
|
||||
if not serializer.is_valid():
|
||||
errors = serializer.errors
|
||||
for key, error in errors.iteritems():
|
||||
field_value = update[key]
|
||||
field_errors[key] = {
|
||||
"developer_message": u"Value '{field_value}' is not valid for field '{field_name}': {error}".format(
|
||||
field_value=field_value, field_name=key, error=error
|
||||
),
|
||||
"user_message": _(u"This value is invalid.").format(
|
||||
field_value=field_value, field_name=key
|
||||
),
|
||||
}
|
||||
|
||||
return field_errors
|
||||
|
||||
|
||||
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
|
||||
@transaction.commit_on_success
|
||||
def create_account(username, password, email):
|
||||
|
||||
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