TNL-1897 Implement Course Team API

This commit is contained in:
Ben McMorran
2015-05-15 20:05:56 +00:00
parent 0db8bac7c4
commit 9d336c33eb
20 changed files with 1561 additions and 47 deletions

View File

@@ -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):

View 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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -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