From 15f6617c2f9d559031707a9c9dbfea02cd223a3c Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 14 Mar 2016 12:32:53 +0100 Subject: [PATCH] embed jsonresponse into the package (#10283) --- README | 9 + debian/control | 1 - passerelle/apps/bdp/views.py | 6 +- passerelle/apps/choosit/views.py | 7 +- passerelle/apps/clicrdv/views.py | 16 +- passerelle/datasources/views.py | 2 +- passerelle/utils.py | 183 ------------------- passerelle/utils/__init__.py | 144 +++++++++++++++ passerelle/utils/jsonresponse.py | 384 +++++++++++++++++++++++++++++++++++++++ setup.py | 1 - tests/test_jsonresponse.py | 32 ++++ 11 files changed, 581 insertions(+), 204 deletions(-) delete mode 100644 passerelle/utils.py create mode 100644 passerelle/utils/__init__.py create mode 100644 passerelle/utils/jsonresponse.py create mode 100644 tests/test_jsonresponse.py diff --git a/README b/README index 37fb153..516fca0 100644 --- a/README +++ b/README @@ -89,3 +89,12 @@ icon-concerto.svg license: Creative Commons – Attribution (CC BY 3.0) http://creativecommons.org/licenses/by/3.0/us/ "Family" designed by Ahmed Elzahra http://www.thenounproject.com/trochilidae/ from the Noun Project http://www.thenounproject.com/ + + +Copyright +--------- + +django-jsonresponse (https://github.com/jjay/django-jsonresponse) + # Files: passerelle/utils/jsonresponse.py + # Copyright (c) 2012 Yasha Borevich + # Licensed under the BSD license diff --git a/debian/control b/debian/control index 9fb568a..8b918c5 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,6 @@ Depends: ${python:Depends}, ${misc:Depends}, python-django (>= 1.7), python-gadjo, - python-django-jsonresponse, python-django-model-utils, python-requests, python-setuptools, diff --git a/passerelle/apps/bdp/views.py b/passerelle/apps/bdp/views.py index 4f13826..f04e881 100644 --- a/passerelle/apps/bdp/views.py +++ b/passerelle/apps/bdp/views.py @@ -6,8 +6,6 @@ from django.views.generic.base import View from django.views.generic.detail import SingleObjectMixin, DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView -from jsonresponse import to_json - from passerelle import utils from .models import Bdp @@ -21,7 +19,7 @@ class ResourcesView(View, SingleObjectMixin): model = Bdp @utils.protected_api('can_access') - @to_json('api') + @utils.to_json('api') def get(self, request, *args, **kwargs): text_key = request.GET.get('text_key') id_key = request.GET.get('id_key') @@ -44,7 +42,7 @@ class PostAdherentView(View, SingleObjectMixin): raise Http404 @utils.protected_api('can_access') - @to_json('api') + @utils.to_json('api') def post(self, request, *args, **kwargs): data = json.loads(request.body) # JSON w.c.s. formdata date_de_naissance = data['fields'].get('date_de_naissance') diff --git a/passerelle/apps/choosit/views.py b/passerelle/apps/choosit/views.py index 6db06a0..f0c304c 100644 --- a/passerelle/apps/choosit/views.py +++ b/passerelle/apps/choosit/views.py @@ -1,9 +1,6 @@ import json -from jsonresponse import to_json - from django.core.urlresolvers import reverse - from django.views.generic.edit import CreateView, UpdateView, DeleteView, View from django.views.generic.detail import SingleObjectMixin from django.utils.decorators import method_decorator @@ -79,14 +76,14 @@ class ChoositRegisterView(View, SingleObjectMixin): def dispatch(self, request, *args, **kwargs): return super(ChoositRegisterView, self).dispatch(request, *args, **kwargs) - @to_json('api') + @utils.to_json('api') @method_decorator(csrf_exempt) def get(self, request, *args, **kwargs): user = request.GET.get('user') assert user, 'missing user parameter' return self.get_object().get_list(user) - @to_json('api') + @utils.to_json('api') def post(self, request, *args, **kwargs): user = request.GET.get('user') assert user, 'missing user parameter' diff --git a/passerelle/apps/clicrdv/views.py b/passerelle/apps/clicrdv/views.py index ad1b98e..9460ee2 100644 --- a/passerelle/apps/clicrdv/views.py +++ b/passerelle/apps/clicrdv/views.py @@ -7,8 +7,6 @@ from django.views.generic.base import View, RedirectView from django.views.generic.detail import SingleObjectMixin, DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView -from jsonresponse import to_json - from passerelle import utils from passerelle.base.views import ResourceView @@ -51,7 +49,7 @@ class InterventionSetsView(View, SingleObjectMixin): model = ClicRdv - @to_json('api') + @utils.to_json('api') def get(self, request, *args, **kwargs): return self.get_object().get_interventionsets() @@ -66,7 +64,7 @@ class InterventionsView(View, SingleObjectMixin): """ model = ClicRdv - @to_json('api') + @utils.to_json('api') def get(self, request, set_id, *args, **kwargs): return self.get_object().get_interventions(set_id) @@ -78,7 +76,7 @@ class DateTimesView(View, SingleObjectMixin): """ model = ClicRdv - @to_json('api') + @utils.to_json('api') def get(self, request, intervention_id, *args, **kwargs): return self.get_object().get_datetimes(intervention_id) @@ -93,7 +91,7 @@ class DatesView(View, SingleObjectMixin): """ model = ClicRdv - @to_json('api') + @utils.to_json('api') def get(self, request, intervention_id, *args, **kwargs): return self.get_object().get_dates(intervention_id) @@ -108,7 +106,7 @@ class TimesView(View, SingleObjectMixin): """ model = ClicRdv - @to_json('api') + @utils.to_json('api') def get(self, request, intervention_id, date, *args, **kwargs): return self.get_object().get_times(intervention_id, date) @@ -140,7 +138,7 @@ class CreateAppointmentView(View, SingleObjectMixin): model = ClicRdv @utils.protected_api('can_manage_appointment') - @to_json('api') + @utils.to_json('api') def post(self, request, intervention_id=None, *args, **kwargs): if intervention_id is None: intervention_id = self.request.GET.get('intervention') @@ -161,6 +159,6 @@ class CancelAppointmentView(View, SingleObjectMixin): model = ClicRdv @utils.protected_api('can_manage_appointment') - @to_json('api') + @utils.to_json('api') def get(self, request, appointment_id, *args, **kwargs): return self.get_object().cancel(appointment_id) diff --git a/passerelle/datasources/views.py b/passerelle/datasources/views.py index cc477c3..837bb2b 100644 --- a/passerelle/datasources/views.py +++ b/passerelle/datasources/views.py @@ -1,9 +1,9 @@ import json from django.views.decorators.csrf import csrf_exempt -from jsonresponse import to_json from models import BaseDataSource +from passerelle.utils import to_json def get_data(request, slug, id=None): diff --git a/passerelle/utils.py b/passerelle/utils.py deleted file mode 100644 index a3f8cd7..0000000 --- a/passerelle/utils.py +++ /dev/null @@ -1,183 +0,0 @@ -from functools import wraps -import json -import re -import logging - -from requests import Session as RequestSession -from jsonresponse import to_json as jsonresponse_to_json - -from django.conf import settings -from django.core.exceptions import PermissionDenied, ObjectDoesNotExist -from django.core.serializers.json import DjangoJSONEncoder -from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest -from django.template import Template, Context -from django.utils.decorators import available_attrs -from django.views.generic.detail import SingleObjectMixin - -from django.contrib.contenttypes.models import ContentType - -from .base.context_processors import template_vars -from .base.models import ApiUser, AccessRight -from .base.signature import check_query - -class to_json(jsonresponse_to_json): - def __init__(self, serializer_type, error_code=500, **kwargs): - super(to_json, self).__init__(serializer_type, error_code, **kwargs) - if 'cls' not in self.kwargs: - self.kwargs['cls'] = DjangoJSONEncoder - - def api(self, func, req, *args, **kwargs): - """ - Raises the exceptions provided by "raises" argument, else wraps the - error message in json. Exceptions can define the error code returned in - JSON, and HTTP status: - - class BlockedAccount(Exception): - err_code = 100 - http_status = 403 - """ - if req.GET.get('raise'): - return super(to_json, self).api(func, req, *args, **kwargs) - # force raise=1 to handle exceptions here - req.GET = req.GET.copy() - req.GET.update({'raise': 1}) - try: - return super(to_json, self).api(func, req, *args, **kwargs) - except Exception as e: - data = self.err_to_response(e) - if getattr(e, 'err_code', None): - data['err'] = e.err_code - if getattr(e, 'http_status', None): - status = e.http_status - elif isinstance(e, ObjectDoesNotExist): - status = 404 - elif isinstance(e, PermissionDenied): - status = 403 - else: - status = self.error_code - return self.render_data(req, data, status) - - -def get_template_vars(): - """ - returns the template vars as dict, to be used in apps code - """ - from django.http import HttpRequest - return template_vars(HttpRequest()) - -def render_template_vars(value): - """ - renders the template vars in a string - """ - template = Template(value) - return template.render(Context(get_template_vars())) - - -def response_for_json(request, data): - response = HttpResponse(content_type='application/json') - json_str = json.dumps(data) - for variable in ('jsonpCallback', 'callback'): - if variable in request.GET: - identifier = request.GET[variable] - if not re.match(r'^[$A-Za-z_][0-9A-Za-z_$]*$', identifier): - return HttpResponseBadRequest('invalid JSONP callback name') - json_str = '%s(%s);' % (identifier, json_str) - break - response.write(json_str) - return response - - -def get_request_users(request): - users = [] - - users.extend(ApiUser.objects.filter(keytype='')) - - if 'orig' in request.GET and 'signature' in request.GET: - orig = request.GET['orig'] - query = request.META['QUERY_STRING'] - signature_users = ApiUser.objects.filter(keytype='SIGN', username=orig) - for signature_user in signature_users: - if check_query(query, signature_user.key): - users.append(signature_user) - - elif 'apikey' in request.GET: - users.extend(ApiUser.objects.filter(keytype='API', - key=request.GET['apikey'])) - - elif request.META.has_key('HTTP_AUTHORIZATION'): - (scheme, param) = request.META['HTTP_AUTHORIZATION'].split(' ',1) - if scheme.lower() == 'basic': - username, password = param.strip().decode('base64').split(':',1) - users.extend(ApiUser.objects.filter(keytype='SIGN', - username=username, key=password)) - - def ip_match(ip, match): - if not ip: - return True - if ip == match: - return True - return False - - users = [x for x in users if ip_match(x.ipsource, request.META.get('REMOTE_ADDR'))] - return users - - -def is_authorized(request, obj, perm): - resource_type = ContentType.objects.get_for_model(obj) - rights = AccessRight.objects.filter(resource_type=resource_type, - resource_pk=obj.id, codename=perm) - users = [x.apiuser for x in rights] - return set(users).intersection(get_request_users(request)) - - -def protected_api(perm): - def decorator(view_func): - @wraps(view_func, assigned=available_attrs(view_func)) - def _wrapped_view(instance, request, *args, **kwargs): - if not isinstance(instance, SingleObjectMixin): - raise Exception("protected_api must be applied on a method of a class based view") - obj = instance.get_object() - if not is_authorized(request, obj, perm): - raise PermissionDenied() - return view_func(instance, request, *args, **kwargs) - return _wrapped_view - return decorator - - -# Wrapper around requests.Session -# logging requests input and output data - -class LoggedRequest(RequestSession): - - def __init__(self, *args, **kwargs): - self.logger = kwargs.pop('logger') - super(LoggedRequest, self).__init__(*args, **kwargs) - - def request(self, method, url, **kwargs): - self.logger.info('%s %s' % (method, url), - extra={'requests_url': url} - ) - - response = super(LoggedRequest, self).request(method, url, **kwargs) - - self.logger.debug('Request Headers: {}'.format(''.join([ - '%s: %s | ' % (k,v) for k,v in response.request.headers.items() - ]))) - if response.request.body: - self.logger.info('Request Payload: %r' %(response.request.body), - extra={'requests_request_payload': '%r' %response.request.body}) - self.logger.info('Status code: %r' %(response.status_code), - extra={'requests_response_status': response.status_code}) - resp_headers = ''.join([ - '%s: %s | ' % (k,v) for k,v in response.headers.items() - ]) - self.logger.debug('Response Headers: %r' %resp_headers, extra={ - 'requests_response_headers': resp_headers}) - content = response.content[:getattr(settings, - 'REQUESTS_RESPONSE_CONTENT_MAX_LENGTH',5000)] - self.logger.debug('Response Content: %r' % content, - extra={'requests_response_content': content}) - - return response - - diff --git a/passerelle/utils/__init__.py b/passerelle/utils/__init__.py new file mode 100644 index 0000000..31e937f --- /dev/null +++ b/passerelle/utils/__init__.py @@ -0,0 +1,144 @@ +from functools import wraps +import json +import re +import logging + +from requests import Session as RequestSession +from jsonresponse import to_json as jsonresponse_to_json + +from django.conf import settings +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from django.core.serializers.json import DjangoJSONEncoder +from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest +from django.template import Template, Context +from django.utils.decorators import available_attrs +from django.views.generic.detail import SingleObjectMixin +from django.contrib.contenttypes.models import ContentType + +from passerelle.base.context_processors import template_vars +from passerelle.base.models import ApiUser, AccessRight +from passerelle.base.signature import check_query + +from .jsonresponse import to_json + +def get_template_vars(): + """ + returns the template vars as dict, to be used in apps code + """ + from django.http import HttpRequest + return template_vars(HttpRequest()) + +def render_template_vars(value): + """ + renders the template vars in a string + """ + template = Template(value) + return template.render(Context(get_template_vars())) + + +def response_for_json(request, data): + response = HttpResponse(content_type='application/json') + json_str = json.dumps(data) + for variable in ('jsonpCallback', 'callback'): + if variable in request.GET: + identifier = request.GET[variable] + if not re.match(r'^[$A-Za-z_][0-9A-Za-z_$]*$', identifier): + return HttpResponseBadRequest('invalid JSONP callback name') + json_str = '%s(%s);' % (identifier, json_str) + break + response.write(json_str) + return response + + +def get_request_users(request): + users = [] + + users.extend(ApiUser.objects.filter(keytype='')) + + if 'orig' in request.GET and 'signature' in request.GET: + orig = request.GET['orig'] + query = request.META['QUERY_STRING'] + signature_users = ApiUser.objects.filter(keytype='SIGN', username=orig) + for signature_user in signature_users: + if check_query(query, signature_user.key): + users.append(signature_user) + + elif 'apikey' in request.GET: + users.extend(ApiUser.objects.filter(keytype='API', + key=request.GET['apikey'])) + + elif request.META.has_key('HTTP_AUTHORIZATION'): + (scheme, param) = request.META['HTTP_AUTHORIZATION'].split(' ',1) + if scheme.lower() == 'basic': + username, password = param.strip().decode('base64').split(':',1) + users.extend(ApiUser.objects.filter(keytype='SIGN', + username=username, key=password)) + + def ip_match(ip, match): + if not ip: + return True + if ip == match: + return True + return False + + users = [x for x in users if ip_match(x.ipsource, request.META.get('REMOTE_ADDR'))] + return users + + +def is_authorized(request, obj, perm): + resource_type = ContentType.objects.get_for_model(obj) + rights = AccessRight.objects.filter(resource_type=resource_type, + resource_pk=obj.id, codename=perm) + users = [x.apiuser for x in rights] + return set(users).intersection(get_request_users(request)) + + +def protected_api(perm): + def decorator(view_func): + @wraps(view_func, assigned=available_attrs(view_func)) + def _wrapped_view(instance, request, *args, **kwargs): + if not isinstance(instance, SingleObjectMixin): + raise Exception("protected_api must be applied on a method of a class based view") + obj = instance.get_object() + if not is_authorized(request, obj, perm): + raise PermissionDenied() + return view_func(instance, request, *args, **kwargs) + return _wrapped_view + return decorator + + +# Wrapper around requests.Session +# logging requests input and output data + +class LoggedRequest(RequestSession): + + def __init__(self, *args, **kwargs): + self.logger = kwargs.pop('logger') + super(LoggedRequest, self).__init__(*args, **kwargs) + + def request(self, method, url, **kwargs): + self.logger.info('%s %s' % (method, url), + extra={'requests_url': url} + ) + + response = super(LoggedRequest, self).request(method, url, **kwargs) + + self.logger.debug('Request Headers: {}'.format(''.join([ + '%s: %s | ' % (k,v) for k,v in response.request.headers.items() + ]))) + if response.request.body: + self.logger.info('Request Payload: %r' %(response.request.body), + extra={'requests_request_payload': '%r' %response.request.body}) + self.logger.info('Status code: %r' %(response.status_code), + extra={'requests_response_status': response.status_code}) + resp_headers = ''.join([ + '%s: %s | ' % (k,v) for k,v in response.headers.items() + ]) + self.logger.debug('Response Headers: %r' %resp_headers, extra={ + 'requests_response_headers': resp_headers}) + content = response.content[:getattr(settings, + 'REQUESTS_RESPONSE_CONTENT_MAX_LENGTH',5000)] + self.logger.debug('Response Content: %r' % content, + extra={'requests_response_content': content}) + + return response diff --git a/passerelle/utils/jsonresponse.py b/passerelle/utils/jsonresponse.py new file mode 100644 index 0000000..36dccc8 --- /dev/null +++ b/passerelle/utils/jsonresponse.py @@ -0,0 +1,384 @@ +# This module is a modified copy of code of Yasha's Borevich library +# django-jsonresponse (https://github.com/jjay/django-jsonresponse) distributed +# under BSD license + +import json +import functools +import logging +from collections import Iterable + +from django.http import HttpResponse +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.core.serializers.json import DjangoJSONEncoder + +DEFAULT_DEBUG = getattr(settings, 'JSONRESPONSE_DEFAULT_DEBUG', False) +CALLBACK_NAME = getattr(settings, 'JSONRESPONSE_CALLBACK_NAME', 'callback') + + +class to_json(object): + """ + Wrap view functions to render python native and custom + objects to json + + >>> from django.test.client import RequestFactory + >>> requests = RequestFactory() + + Simple wrap returning data into json + + >>> @to_json('plain') + ... def hello(request): + ... return dict(hello='world') + + >>> resp = hello(requests.get('/hello/')) + >>> print resp.status_code + 200 + >>> print resp.content + {"hello": "world"} + + Result can be wraped in some api manier + + >>> @to_json('api') + ... def goodbye(request): + ... return dict(good='bye') + >>> resp = goodbye(requests.get('/goodbye', {'debug': 1})) + >>> print resp.status_code + 200 + >>> print resp.content + { + "data": { + "good": "bye" + }, + "err": 0 + } + + Automaticaly error handling + + >>> @to_json('api') + ... def error(request): + ... raise Exception('Wooot!??') + + >>> resp = error(requests.get('/error', {'debug': 1})) + >>> print resp.status_code + 500 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + { + "err_class": "Exception", + "err_desc": "Wooot!??", + "data": null, + "err": 1 + } + + >>> from django.core.exceptions import ObjectDoesNotExist + >>> @to_json('api') + ... def error_404(request): + ... raise ObjectDoesNotExist('Not found') + + >>> resp = error_404(requests.get('/error', {'debug': 1})) + >>> print resp.status_code + 404 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + { + "err_class": "django.core.exceptions.ObjectDoesNotExist", + "err_desc": "Not found", + "data": null, + "err": 1 + } + + + You can serialize not only pure python data types. + Implement `serialize` method on toplevel object or + each element of toplevel array. + + >>> class User(object): + ... def __init__(self, name, age): + ... self.name = name + ... self.age = age + ... + ... def serialize(self, request): + ... if request.GET.get('with_age', False): + ... return dict(name=self.name, age=self.age) + ... else: + ... return dict(name=self.name) + + >>> @to_json('objects') + ... def users(request): + ... return [User('Bob', 10), User('Anna', 12)] + + >>> resp = users(requests.get('users', { 'debug': 1 })) + >>> print resp.status_code + 200 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + { + "data": [ + { + "name": "Bob" + }, + { + "name": "Anna" + } + ], + "err": 0 + } + + You can pass extra args for serialization: + + >>> resp = users(requests.get('users', + ... { 'debug':1, 'with_age':1 })) + >>> print resp.status_code + 200 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + { + "data": [ + { + "age": 10, + "name": "Bob" + }, + { + "age": 12, + "name": "Anna" + } + ], + "err": 0 + } + + It is easy to use jsonp, just pass format=jsonp + + >>> resp = users(requests.get('users', + ... { 'debug':1, 'format': 'jsonp' })) + >>> print resp.status_code + 200 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + callback({ + "data": [ + { + "name": "Bob" + }, + { + "name": "Anna" + } + ], + "err": 0 + }); + + You can override the name of callback method using + JSONRESPONSE_CALLBACK_NAME option or query arg callback=another_callback + + >>> resp = users(requests.get('users', + ... { 'debug':1, 'format': 'jsonp', 'callback': 'my_callback' })) + >>> print resp.status_code + 200 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + my_callback({ + "data": [ + { + "name": "Bob" + }, + { + "name": "Anna" + } + ], + "err": 0 + }); + + You can pass raise=1 to raise exceptions in debug purposes + instead of passing info to json response + + >>> @to_json('api') + ... def error(request): + ... raise Exception('Wooot!??') + + >>> resp = error(requests.get('/error', + ... {'debug': 1, 'raise': 1})) + Traceback (most recent call last): + Exception: Wooot!?? + + You can wraps both methods and functions + + >>> class View(object): + ... @to_json('plain') + ... def render(self, request): + ... return dict(data='ok') + ... @to_json('api') + ... def render_api(self, request): + ... return dict(data='ok') + + + >>> view = View() + >>> resp = view.render(requests.get('/render')) + >>> print resp.status_code + 200 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + {"data": "ok"} + + Try it one more + + >>> resp = view.render(requests.get('/render')) + >>> print resp.status_code + 200 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + {"data": "ok"} + + Try it one more with api + + >>> resp = view.render_api(requests.get('/render')) + >>> print resp.status_code + 200 + >>> print resp.content # doctest: +NORMALIZE_WHITESPACE + {"data": {"data": "ok"}, "err": 0} + + + You can pass custom kwargs to json.dumps, + just give them to constructor + + >>> @to_json('plain', separators=(', ', ': ')) + ... def custom_kwargs(request): + ... return ['a', { 'b': 1 }] + >>> resp = custom_kwargs(requests.get('/render')) + >>> print resp.status_code + 200 + >>> print resp.content + ["a", {"b": 1}] + """ + def __init__(self, serializer_type, error_code=500, **kwargs): + """ + serializer_types: + * api - serialize buildin objects (dict, list, etc) in strict api + * objects - serialize list of region in strict api + * plain - just serialize result of function, do not wrap response and do not handle exceptions + """ + self.serializer_type = serializer_type + self.method = None + self.error_code=error_code + self.kwargs = kwargs + if 'cls' not in self.kwargs: + self.kwargs['cls'] = DjangoJSONEncoder + + def __call__(self, f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + if self.method: + return self.method(f, *args, **kwargs) + + if not args: + if self.serializer_type == 'plain': + self.method = self.plain_func + else: + self.method = self.api_func + + if getattr(getattr(args[0], f.__name__, None), "im_self", False): + if self.serializer_type == 'plain': + self.method = self.plain_method + else: + self.method = self.api_method + else: + if self.serializer_type == 'plain': + self.method = self.plain_func + else: + self.method = self.api_func + + return self.method(f, *args, **kwargs) + + return wrapper + + def obj_to_response(self, req, obj): + if self.serializer_type == 'objects': + if isinstance(obj, Iterable): + obj = [o.serialize(req) if obj else None for o in obj] + elif obj: + obj = obj.serialize(req) + else: + obj = None + + return { "err": 0, "data": obj } + + def err_to_response(self, err): + if hasattr(err, "__module__"): + err_module = err.__module__ + "." + else: + err_module = "" + + if hasattr(err, "owner"): + err_module += err.owner.__name__ + "." + + err_class = err_module + err.__class__.__name__ + + err_desc = str(err) + + return { + "err": 1, + "err_class": err_class, + "err_desc": err_desc, + "data": None + } + + def render_data(self, req, data, status=200): + debug = DEFAULT_DEBUG + debug = debug or req.GET.get('debug', 'false').lower() in ('true', 't', '1', 'on') + debug = debug or req.GET.get('decode', '0').lower() in ('true', 't', '1', 'on') + format = req.GET.get('format', 'json') + jsonp_cb = req.GET.get('callback', CALLBACK_NAME) + content_type = "application/json" + + kwargs = dict(self.kwargs) + if debug: + kwargs["indent"] = 4 + kwargs["ensure_ascii"] = False + kwargs["encoding"] = "utf8" + + plain = json.dumps(data, **kwargs) + if format == 'jsonp': + plain = "%s(%s);" % (jsonp_cb, plain) + content_type = "application/javascript" + + return HttpResponse(plain, content_type="%s; charset=UTF-8" % content_type, status=status) + + def api_func(self, f, *args, **kwargs): + return self.api(f, args[0], *args, **kwargs) + + def api_method(self, f, *args, **kwargs): + return self.api(f, args[1], *args, **kwargs) + + def api(self, f, req, *args, **kwargs): + logger = logging.getLogger('passerelle.jsonresponse') + try: + resp = f(*args, **kwargs) + if isinstance(resp, HttpResponse): + return resp + + data = self.obj_to_response(req, resp) + status = 200 + except Exception as e: + extras = {'method': req.method} + if req.method == 'POST': + extras.update({'body': req.body}) + logger.exception("Error occurred while processing request", extra=extras) + if int(req.GET.get('raise', 0)): + raise + + data = self.err_to_response(e) + if getattr(e, 'err_code', None): + data['err'] = e.err_code + if getattr(e, 'http_status', None): + status = e.http_status + elif isinstance(e, ObjectDoesNotExist): + status = 404 + elif isinstance(e, PermissionDenied): + status = 403 + else: + status = self.error_code + return self.render_data(req, data, status) + + def plain_method(self, f, *args, **kwargs): + data = f(*args, **kwargs) + if isinstance(data, HttpResponse): + return data + + return self.render_data(args[1], data) + + def plain_func(self, f, *args, **kwargs): + data = f(*args, **kwargs) + if isinstance(data, HttpResponse): + return data + + return self.render_data(args[0], data) diff --git a/setup.py b/setup.py index 335add4..8b88468 100755 --- a/setup.py +++ b/setup.py @@ -87,7 +87,6 @@ setup(name='passerelle', install_requires=[ 'django >= 1.7, <1.8', 'django-model-utils', - 'django-jsonresponse==0.10', 'django-jsonfield >= 0.9.3', 'requests', 'gadjo', diff --git a/tests/test_jsonresponse.py b/tests/test_jsonresponse.py new file mode 100644 index 0000000..6441f31 --- /dev/null +++ b/tests/test_jsonresponse.py @@ -0,0 +1,32 @@ +import logging +import pytest +import json + +from django.test.client import RequestFactory + + +from passerelle.utils import to_json + +class WrappedException(Exception): + pass + + +@to_json('api') +def wrapped_exception(req, *args, **kwargs): + raise WrappedException + + +def test_jsonresponselog_get(caplog): + request = RequestFactory() + wrapped_exception(request.get('/')) + post_payload = {'data': 'plop'} + with pytest.raises(WrappedException): + wrapped_exception(request.post('/?raise=1', post_payload)) + + for record in caplog.records(): + assert record.name == 'passerelle.jsonresponse' + assert record.levelno == logging.ERROR + assert hasattr(record, 'method') + if record.method == 'POST': + assert hasattr(record, 'body') + assert "Error occurred while processing request" in record.message -- 2.8.0.rc3