from functools import wraps import json import decimal 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(EDXJSONEncoder, self).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) except ValueError: return JsonResponseBadRequest({"error": "Invalid JSON"}) else: request.json = {} return view_function(request, *args, **kwargs) return parse_json_into_request class JsonResponse(HttpResponse): """ Django HttpResponse subclass that has sensible defaults for outputting JSON. """ def __init__(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=False) kwargs.setdefault("content_type", "application/json") if status: kwargs["status"] = status super(JsonResponse, self).__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): 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(JsonResponseBadRequest, self).__init__(content, *args, **kwargs)