* feat: add xblock endpoint for updating an xblock fix: remove debugger feat: make function call more generic refactor: just use request.json for request data as before refactor: extract method fix: revert wrong method change fix: refactor correct method feat: use handle_xblock method so that we can do more than update xblocks fix: usage_key_string defaults to None add all CRUD operations fix usage key parameter refactor: create /views folder refactor: move xblock view functions to xblock_services fix: tests fix: tests refactor: move xblock API endpoint to contentstore * docs: add explanatory comment to new xblock_service * feat: add feature flag for enabling content editing api * feat: raise 404 if studio content api is disabled * tests: test xblock endpoint * test: make all post tests work * test: check that xblock_handler receives correct args * refactor: create util mixin for course factories with staff * refactor: extract course staff authorization tests * refactor: extract tests to api view testcase class * test: add get tests * test: fix tests * test: fix tests * test: fix tests * test: add all crud tests * fix: refactor to fix tests * fix: merge conflict * fix: merge conflict * fix: tests after merge * fix: json request decorator * fix: lint * fix: lint * fix: lint * fix: lint * fix: new test files * fix: lint * fix: lint and apply PR suggestions * fix: lint * fix: lint * fix: lint * fix: lint * fix: lint * fix: lint
128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
# lint-amnesty, pylint: disable=missing-module-docstring
|
|
|
|
import decimal
|
|
import json
|
|
from functools import wraps
|
|
|
|
from django.core.serializers import serialize
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
from django.db.models.query import QuerySet
|
|
from django.http import HttpResponse, HttpResponseBadRequest
|
|
|
|
|
|
class EDXJSONEncoder(DjangoJSONEncoder):
|
|
"""
|
|
Encoder for Decimal object, other objects will be encoded as per DjangoJSONEncoder default implementation.
|
|
|
|
NOTE:
|
|
Please see https://docs.djangoproject.com/en/1.8/releases/1.5/#system-version-of-simplejson-no-longer-used
|
|
DjangoJSONEncoder will now use the Python's json module but Python's json module don't know about how to
|
|
encode Decimal object, so as per default implementation Decimal objects will be encoded to `str` which we don't
|
|
want and also this is different from Django 1.4, In Django 1.4 if Decimal object has zeros after the decimal
|
|
point then object will be serialized as `int` else `float`, so we are keeping this behavior.
|
|
"""
|
|
|
|
def default(self, o): # pylint: disable=method-hidden
|
|
"""
|
|
Encode Decimal objects. If decimal object has zeros after the
|
|
decimal point then object will be serialized as `int` else `float`
|
|
"""
|
|
if isinstance(o, decimal.Decimal):
|
|
if o == o.to_integral():
|
|
return int(o)
|
|
return float(o)
|
|
else:
|
|
return super().default(o)
|
|
|
|
|
|
def expect_json(view_function):
|
|
"""
|
|
View decorator for simplifying handing of requests that expect json. If the request's
|
|
CONTENT_TYPE is application/json, parses the json dict from request.body, and updates
|
|
request.POST with the contents.
|
|
"""
|
|
|
|
@wraps(view_function)
|
|
def parse_json_into_request(request, *args, **kwargs):
|
|
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
|
|
# e.g. 'charset', so we can't do a direct string compare
|
|
if "application/json" in request.META.get("CONTENT_TYPE", "") and request.body:
|
|
try:
|
|
request.json = json.loads(request.body.decode("utf8"))
|
|
except ValueError:
|
|
return JsonResponseBadRequest({"error": "Invalid JSON"})
|
|
else:
|
|
request.json = {}
|
|
|
|
return view_function(request, *args, **kwargs)
|
|
return parse_json_into_request
|
|
|
|
|
|
def expect_json_in_class_view(view):
|
|
"""
|
|
Class-based View decorator for simplifying handing of requests that expect json. If the request's
|
|
CONTENT_TYPE is application/json, parses the json dict from request.body, and updates
|
|
request.POST with the contents.
|
|
"""
|
|
|
|
def _wrapper_view(self, request, *args, **kwargs):
|
|
if "application/json" in request.META.get("CONTENT_TYPE", "") and request.body:
|
|
try:
|
|
request.json = json.loads(request.body.decode("utf8"))
|
|
except ValueError:
|
|
return JsonResponseBadRequest({"error": "Invalid JSON"})
|
|
else:
|
|
request.json = {}
|
|
return view(self, request, *args, **kwargs)
|
|
|
|
return _wrapper_view
|
|
|
|
|
|
class JsonResponse(HttpResponse):
|
|
"""
|
|
Django HttpResponse subclass that has sensible defaults for outputting JSON.
|
|
"""
|
|
|
|
def __init__( # lint-amnesty, pylint: disable=keyword-arg-before-vararg
|
|
self,
|
|
resp_obj=None,
|
|
status=None,
|
|
encoder=EDXJSONEncoder,
|
|
*args,
|
|
**kwargs
|
|
):
|
|
if resp_obj in (None, ""):
|
|
content = ""
|
|
status = status or 204
|
|
elif isinstance(resp_obj, QuerySet):
|
|
content = serialize("json", resp_obj)
|
|
else:
|
|
content = json.dumps(resp_obj, cls=encoder, indent=2, ensure_ascii=True)
|
|
kwargs.setdefault("content_type", "application/json")
|
|
if status:
|
|
kwargs["status"] = status
|
|
super().__init__(content, *args, **kwargs)
|
|
|
|
|
|
class JsonResponseBadRequest(HttpResponseBadRequest):
|
|
"""
|
|
Subclass of HttpResponseBadRequest that defaults to outputting JSON.
|
|
Use this to send BadRequestResponse & some Json object along with it.
|
|
|
|
Defaults:
|
|
dictionary: empty dictionary
|
|
status: 400
|
|
encoder: DjangoJSONEncoder
|
|
"""
|
|
|
|
def __init__(
|
|
self, obj=None, status=400, encoder=DjangoJSONEncoder, *args, **kwargs
|
|
): # lint-amnesty, pylint: disable=keyword-arg-before-vararg
|
|
if obj in (None, ""):
|
|
content = ""
|
|
else:
|
|
content = json.dumps(obj, cls=encoder, indent=2, ensure_ascii=False)
|
|
kwargs.setdefault("content_type", "application/json")
|
|
kwargs["status"] = status
|
|
super().__init__(content, *args, **kwargs)
|