"""
**Why no AJAXMixin?**
An AJAX mixin similar to Braces' AjaxResponseMixin simply allows a view for "fork" behavior based on
``request.is_ajax()``. We feel a better practice is to create a view that specifically handles AJAX requests because
this is much more obvious, easier to test, and reduces the code complexity of the view.
"""
# Imports
from django.conf import settings
from django.core import serializers
from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponse, HttpResponseBadRequest
import json
import logging
logger = logging.getLogger(__name__)
# Exports
__all__ = (
"JSONMixin",
)
# Mixins
[docs]class JSONMixin(object):
"""Handle JSON requests/responses.
.. code-block:: python
from superdjango.views import GenericView, JSONMixin
class MyAjaxView(JSONMixin, GenericView):
def post(self, request, *args, **kwargs):
# process the request that produces a dict or queryset or model instance.
data = self.get_some_data()
return self.get_json_response(data)
"""
ajax_required = not settings.DEBUG
"""Indicates requests must be made via AJAX. Defaults to ``False`` in development."""
content_type = "application/json"
"""The expected content type of the request or response."""
decoder_class = None
"""Optional class used to decode the request. It takes the current request instance as the only parameter and must
implement a load method which returns a ``dict``.
"""
encoder_class = DjangoJSONEncoder
"""The class used to encode JSON responses."""
json_required = False
"""Indicates that requests must be made using JSON."""
# noinspection PyAttributeOutsideInit
[docs] def dispatch(self, request, *args, **kwargs):
"""Set ``request_json`` and validate the request before continuing with the dispatch."""
# IF AJAX is required.
if not request.is_ajax() and self.ajax_required:
return self.dispatch_bad_request(error={'error': "An AJAX request is required."})
# Assign for later, possible use.
self.args = args
self.kwargs = kwargs
self.request = request
# Get the JSON, if any, sent to the view.
self.json_request = self.get_json_request()
# Validate the request.
conditions = [request.method != "OPTIONS", self.json_required, self.json_request is None]
if all(conditions):
return self.dispatch_bad_request()
# Continue dispatching the request.
# noinspection PyUnresolvedReferences
return super().dispatch(request, *args, **kwargs)
[docs] def dispatch_bad_request(self, error=None, **kwargs):
"""Handle a bad request.
:param error: A dictionary describing the error in the form of ``{'error': "Description."}``. Defaults to
``{'error': "Bad request."}``
:type error: dict
Keyword arguments are passed to ``json.dumps()``.
"""
if error is None:
error = {'error': "Bad request."}
kwargs.setdefault("ensure_ascii", False)
json_data = json.dumps(error, cls=self.encoder_class, **kwargs).encode("utf-8")
return HttpResponseBadRequest(json_data, content_type=self.get_content_type())
[docs] def get_content_type(self):
"""Get the content type for the request/response.
:rtype: str
"""
return self.content_type
[docs] def get_json_request(self):
"""Get JSON from the current request.
:rtype: dict | None
"""
if self.decoder_class is not None:
decoder_class = self.decoder_class
decoder = decoder_class(self.request)
return decoder.load()
try:
return json.loads(self.request.body.decode("utf-8"))
except json.JSONDecodeError as e:
if self.json_required:
logger.warning("%s failed to get JSON from request: %s" % (self.__class__.__name__, e))
return None
[docs] def get_json_response(self, data, status=200, **kwargs):
"""Get the JSON response.
:param data: The data to be serialized. If this is a dictionary, simple serialization is used using the
``encoder_class`` attribute. If a QuerySet is given, Django's built-in serializer is used.
:type data: dict | list | tuple | django.db.models.QuerySet
:param status: The HTTP status code.
:type status: int
Additional keyword arguments are passed to ``json.dumps()`` or Django's serializer.
:rtype: HttpResponse
.. warning::
If ``data`` is not provided as a ``dict``, ``list``, or ``tuple`` it is assumed to be a QuerySet. This will
produce unexpected results if you fail to provide the supported data type.
"""
if type(data) in (dict, list, tuple):
kwargs.setdefault("ensure_ascii", False)
json_data = json.dumps(data, cls=self.encoder_class, **kwargs).encode("utf-8")
else:
json_data = serializers.serialize("json", data, **kwargs)
return HttpResponse(json_data, content_type=self.get_content_type(), status=status)