diff --git a/.gitignore b/.gitignore index ba1b530..71058aa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ *.egg *.egg-info *.sqlite3 +env/ +py3env/ +.idea/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..44f1265 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: python +notifications: + email: false +env: +- DJANGO_VERSION=1.7.0 +- DJANGO_VERSION=1.8.0 +- DJANGO_VERSION=1.9.0 +python: +- '2.7' +- '3.3' +- '3.4' +- '3.5' +install: +- pip install -r ci.txt django~=$DJANGO_VERSION +- python setup.py install +script: +- make test +matrix: + exclude: + - python: '3.5' + env: DJANGO_VERSION=1.7.0 + - python: '3.3' + env: DJANGO_VERSION=1.9.0 diff --git a/Makefile b/Makefile index 7237b13..48c8b76 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ help: @echo "Please use \`make ' where is one of" - @echo " tests to make a unit test run" + @echo " test to make unit tests run" test: - python tests/manage.py test example + PYTHONPATH=${PWD}:${PYTHONPATH} python tests/manage.py test example + +release: + python setup.py sdist upload diff --git a/README.mkd b/README.mkd index 36492dc..172e1c0 100644 --- a/README.mkd +++ b/README.mkd @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/joestump/django-ajax.png?branch=master)](https://travis-ci.org/joestump/django-ajax) + # Overview This package creates a minimal framework for creating AJAX endpoints of your own in Django without having to create all of the mappings, handling errors, building JSON, etc. @@ -106,7 +108,7 @@ You can then send a POST to: ### Adding Ad-hoc endpoints to ModelEndpoints -You can also add you own custom methods to a ModelEndpoint. Adhoc methods in a ModelEndpoint observe the same rules as the get(), update() and delete() methods - with the noticable exception that self.pk _may_ not be set. +You can also add you own custom methods to a ModelEndpoint. Adhoc methods in a ModelEndpoint observe the same rules as the get(), update() and delete() methods - with the noticeable exception that self.pk _may_ not be set. For example, you could add a method called `about` that will display some info about the Model or Record (just used for illustration: not actually a good idea in real life): diff --git a/ajax/__init__.py b/ajax/__init__.py index 18d3be3..45579a6 100755 --- a/ajax/__init__.py +++ b/ajax/__init__.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from ajax.endpoints import Endpoints from ajax.encoders import Encoders diff --git a/ajax/authentication.py b/ajax/authentication.py new file mode 100644 index 0000000..8d3ab9f --- /dev/null +++ b/ajax/authentication.py @@ -0,0 +1,6 @@ +class BaseAuthentication(object): + def is_authenticated(self, request, application, method): + if request.user.is_authenticated(): + return True + + return False diff --git a/ajax/compat.py b/ajax/compat.py new file mode 100644 index 0000000..7bcf8d6 --- /dev/null +++ b/ajax/compat.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import +import django + +if django.VERSION >= (1, 7): + from django.utils.module_loading import import_string as path_to_import + from importlib import import_module + from logging import getLogger +else: + # 1.4 LTS compatibility + from ajax.utils import import_by_path as path_to_import + from django.utils.importlib import import_module + from django.utils.log import getLogger diff --git a/ajax/conf.py b/ajax/conf.py new file mode 100644 index 0000000..9769cd3 --- /dev/null +++ b/ajax/conf.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import +from django.conf import settings +from appconf import AppConf + + +class AjaxAppConf(AppConf): + AJAX_AUTHENTICATION = 'ajax.authentication.BaseAuthentication' diff --git a/ajax/decorators.py b/ajax/decorators.py index 9546e76..7f0c997 100644 --- a/ajax/decorators.py +++ b/ajax/decorators.py @@ -1,5 +1,6 @@ +from __future__ import absolute_import from django.utils.translation import ugettext as _ -from django.utils.log import getLogger +from ajax.compat import getLogger from django.http import Http404 from django.conf import settings from decorator import decorator @@ -61,7 +62,7 @@ def json_response(f, *args, **kwargs): result = f(*args, **kwargs) if isinstance(result, AJAXError): raise result - except AJAXError, e: + except AJAXError as e: result = e.get_response() request = args[0] @@ -72,9 +73,9 @@ def json_response(f, *args, **kwargs): 'request': request } ) - except Http404, e: + except Http404 as e: result = AJAXError(404, e.__str__()).get_response() - except Exception, e: + except Exception as e: import sys exc_info = sys.exc_info() type, message, trace = exc_info diff --git a/ajax/encoders.py b/ajax/encoders.py index 75bcd34..dc78699 100644 --- a/ajax/encoders.py +++ b/ajax/encoders.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from django.core import serializers from ajax.exceptions import AlreadyRegistered, NotRegistered from django.db.models.fields import FieldDoesNotExist @@ -7,6 +8,7 @@ from django.db.models.query import QuerySet from django.utils.encoding import smart_str import collections +import six # Used to change the field name for the Model's pk. @@ -45,7 +47,7 @@ def to_dict(self, record, expand=False, html_escape=False, fields=None): ret.update(data['fields']) ret[AJAX_PK_ATTR_NAME] = data['pk'] - for field, val in ret.iteritems(): + for field, val in six.iteritems(ret): try: f = record.__class__._meta.get_field(field) if expand and isinstance(f, models.ForeignKey): @@ -58,7 +60,7 @@ def to_dict(self, record, expand=False, html_escape=False, fields=None): new_value = self._encode_value(f, val) ret[smart_str(field)] = new_value - except FieldDoesNotExist, e: + except FieldDoesNotExist as e: pass # Assume extra fields are already safe. if expand and hasattr(record, 'tags') and \ diff --git a/ajax/endpoints.py b/ajax/endpoints.py index 063d867..d5d2873 100644 --- a/ajax/endpoints.py +++ b/ajax/endpoints.py @@ -1,13 +1,18 @@ +from __future__ import absolute_import from django.core.exceptions import ValidationError +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.db import models from django.utils.encoding import smart_str from django.utils.translation import ugettext_lazy as _ + +from ajax.compat import path_to_import +from ajax.conf import settings from ajax.decorators import require_pk from ajax.exceptions import AJAXError, AlreadyRegistered, NotRegistered from ajax.encoders import encoder from ajax.signals import ajax_created, ajax_deleted, ajax_updated -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.conf import settings +from ajax.views import EnvelopedResponse +import six try: from taggit.utils import parse_tags @@ -30,6 +35,8 @@ class ModelEndpoint(object): immutable_fields = [] # List of model fields that are not writable. + authentication = path_to_import(settings.AJAX_AUTHENTICATION)() + def __init__(self, application, model, method, **kwargs): self.application = application self.model = model @@ -71,15 +78,18 @@ def tags(self, request): return encoder.encode(result) - def list(self,request): + def get_queryset(self, request, **kwargs): + return self.model.objects.none() + + def list(self, request): """ - List objects of a model. By default will show page 1 with 20 objects on it. - + List objects of a model. By default will show page 1 with 20 objects on it. + **Usage**:: - + params = {"items_per_page":10,"page":2} //all params are optional $.post("/ajax/{app}/{model}/list.json"),params) - + """ max_items_per_page = getattr(self, 'max_per_page', @@ -88,11 +98,11 @@ def list(self,request): items_per_page = min(max_items_per_page, requested_items_per_page) current_page = request.POST.get("current_page", 1) - if hasattr(self, 'get_queryset'): - objects = self.get_queryset(request.user) - else: - objects = self.model.objects.all() - + if not self.can_list(request.user): + raise AJAXError(403, _("Access to this endpoint is forbidden")) + + objects = self.get_queryset(request) + paginator = Paginator(objects, items_per_page) try: @@ -103,20 +113,22 @@ def list(self,request): except EmptyPage: # If page is out of range (e.g. 9999), return empty list. page = EmptyPageResult() - - return [encoder.encode(record) for record in page.object_list] + + data = [encoder.encode(record) for record in page.object_list] + return EnvelopedResponse(data=data, metadata={'total': paginator.count}) + def _set_tags(self, request, record): tags = self._extract_tags(request) if tags: - record.tags.set(*tags) + record.tags.set(*tags) def _save(self, record): try: record.full_clean() record.save() return record - except ValidationError, e: + except ValidationError as e: raise AJAXError(400, _("Could not save model."), errors=e.message_dict) @@ -124,11 +136,19 @@ def _save(self, record): def update(self, request): record = self._get_record() modified = self._get_record() - for key, val in self._extract_data(request).iteritems(): - setattr(modified, key, val) - if self.can_update(request.user, record, modified=modified): - self._save(modified) + update_record = False + for key, val in six.iteritems(self._extract_data(request)): + + # Only setattr and save the model when a change has happened. + if val != getattr(record, key): + setattr(modified, key, val) + update_record = True + + if self.can_update(request.user, record, modified=modified): + + if update_record: + self._save(modified) try: tags = self._extract_tags(request) @@ -150,9 +170,10 @@ def update(self, request): def delete(self, request): record = self._get_record() if self.can_delete(request.user, record): + payload = {'pk': int(self.pk)} record.delete() - ajax_deleted.send(sender=record.__class__, instance=record) - return {'pk': int(self.pk)} + ajax_deleted.send(sender=record.__class__, instance=record, payload=payload) + return payload else: raise AJAXError(403, _("Access to endpoint is forbidden")) @@ -173,7 +194,7 @@ def _extract_tags(self, request): if raw_tags: try: tags = [t for t in parse_tags(raw_tags) if len(t)] - except Exception, e: + except Exception as e: pass return tags @@ -189,7 +210,7 @@ def _extract_data(self, request): load up that record. """ data = {} - for field, val in request.POST.iteritems(): + for field, val in six.iteritems(request.POST): if field in self.immutable_fields: continue # Ignore immutable fields silently. @@ -202,7 +223,7 @@ def _extract_data(self, request): else: clean_value = field_obj.rel.to.objects.get(pk=val) else: - clean_value = val + clean_value = field_obj.to_python(val) data[smart_str(field)] = clean_value return data @@ -235,6 +256,7 @@ def _user_is_active_or_staff(self, user, record, **kwargs): can_create = _user_is_active_or_staff can_update = _user_is_active_or_staff can_delete = _user_is_active_or_staff + can_list = lambda *args, **kwargs: False def authenticate(self, request, application, method): """Authenticate the AJAX request. @@ -246,10 +268,7 @@ def authenticate(self, request, application, method): Most likely you will want to lock down who can edit and delete various models. To do this, just override this method in your child class. """ - if request.user.is_authenticated(): - return True - - return False + return self.authentication.is_authenticated(request, application, method) class FormEndpoint(object): diff --git a/ajax/exceptions.py b/ajax/exceptions.py index 4baa800..0f4c96c 100644 --- a/ajax/exceptions.py +++ b/ajax/exceptions.py @@ -1,4 +1,6 @@ -from django.utils import simplejson as json +from __future__ import absolute_import +import json + from django.utils.encoding import smart_str from django.http import HttpResponse, HttpResponseNotFound, \ HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseServerError, \ diff --git a/ajax/middleware/DebugToolbar.py b/ajax/middleware/DebugToolbar.py index 6147dd7..048782b 100644 --- a/ajax/middleware/DebugToolbar.py +++ b/ajax/middleware/DebugToolbar.py @@ -1,5 +1,8 @@ +from __future__ import absolute_import +import json + +from django.utils import six from debug_toolbar.middleware import DebugToolbarMiddleware, add_content_handler -from django.utils import simplejson as json from django.core.serializers.json import DjangoJSONEncoder @@ -17,7 +20,10 @@ class AJAXDebugToolbarMiddleware(DebugToolbarMiddleware): the django-debug-toolbar panels. """ def _append_json(self, response, toolbar): - payload = json.loads(response.content) + if isinstance(response.content, six.text_type): + payload = json.loads(response.content) + else: + payload = json.loads(response.content.decode('utf-8')) payload['debug_toolbar'] = { 'sql': toolbar.stats['sql'], 'timer': toolbar.stats['timer'] diff --git a/ajax/signals.py b/ajax/signals.py index c1e1c68..aaf1609 100644 --- a/ajax/signals.py +++ b/ajax/signals.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import django.dispatch ajax_created = django.dispatch.Signal(providing_args=['instance']) diff --git a/ajax/urls.py b/ajax/urls.py index b7ef0bd..790b182 100644 --- a/ajax/urls.py +++ b/ajax/urls.py @@ -1,13 +1,25 @@ -from django.conf.urls.defaults import * +from __future__ import absolute_import +from django.conf.urls import * from django.views.static import serve +from ajax import views +import django import os JAVASCRIPT_PATH = "%s/js" % os.path.dirname(__file__) -urlpatterns = patterns('ajax.views', - (r'^(?P\w+)/(?P\w+).json', 'endpoint_loader'), - (r'^(?P\w+)/(?P\w+)/(?P\w+).json', 'endpoint_loader'), - (r'^(?P\w+)/(?P\w+)/(?P\d+)/(?P\w+)/?(?P(add|remove|set|clear|similar))?.json$', 'endpoint_loader'), - (r'^js/(?P.*)$', serve, - {'document_root': JAVASCRIPT_PATH}), -) +if django.VERSION < (1, 8): + urlpatterns = patterns('ajax.views', + (r'^(?P\w+)/(?P\w+).json', 'endpoint_loader'), + (r'^(?P\w+)/(?P\w+)/(?P\w+).json', 'endpoint_loader'), + (r'^(?P\w+)/(?P\w+)/(?P\d+)/(?P\w+)/?(?P(add|remove|set|clear|similar))?.json$', 'endpoint_loader'), + (r'^js/(?P.*)$', serve, + {'document_root': JAVASCRIPT_PATH}), + ) +else: + urlpatterns = [ + url(http://github.com/r'%5E(?P%3Capplication%3E\w+)/(?P\w+).json', views.endpoint_loader), + url(http://github.com/r'%5E(?P%3Capplication%3E\w+)/(?P\w+)/(?P\w+).json', views.endpoint_loader), + url(http://github.com/r'%5E(?P%3Capplication%3E\w+)/(?P\w+)/(?P\d+)/(?P\w+)/?(?P(add|remove|set|clear|similar))?.json$', views.endpoint_loader), + url(http://github.com/r'%5Ejs/(?P%3Cpath%3E.*)$', serve, + {'document_root': JAVASCRIPT_PATH}), + ] diff --git a/ajax/utils.py b/ajax/utils.py new file mode 100644 index 0000000..d0f59ab --- /dev/null +++ b/ajax/utils.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import +import sys + +from django.core.exceptions import ImproperlyConfigured +from ajax.compat import import_module + + +def import_by_path(dotted_path, error_prefix=''): + """ + Import a dotted module path and return the attribute/class designated by + the last name in the path. Raise ImproperlyConfigured if something goes + wrong. This has come straight from Django 1.6 + """ + try: + module_path, class_name = dotted_path.rsplit('.', 1) + except ValueError: + raise ImproperlyConfigured("%s%s doesn't look like a module path" % ( + error_prefix, dotted_path)) + try: + module = import_module(module_path) + except ImportError as e: + raise ImproperlyConfigured('%sError importing module %s: "%s"' % ( + error_prefix, module_path, e)) + try: + attr = getattr(module, class_name) + except AttributeError: + raise ImproperlyConfigured( + '%sModule "%s" does not define a "%s" attribute/class' % ( + error_prefix, module_path, class_name + ) + ) + return attr diff --git a/ajax/views.py b/ajax/views.py index d8f0545..bbc647a 100755 --- a/ajax/views.py +++ b/ajax/views.py @@ -1,18 +1,33 @@ +from __future__ import absolute_import + +import json from django.conf import settings from django.http import HttpResponse -from django.utils import simplejson as json from django.utils.translation import ugettext as _ -from django.utils.importlib import import_module -from django.utils.log import getLogger +from ajax.compat import getLogger from django.core.serializers.json import DjangoJSONEncoder from ajax.exceptions import AJAXError, NotRegistered from ajax.decorators import json_response +from ajax.compat import import_module import ajax logger = getLogger('django.request') +class EnvelopedResponse(object): + """ + Object used to contain metadata about the request that will be added to + the wrapping json structure (aka the envelope). + + :param: data - The object representation that you want to return + :param: metadata - dict of information which will be merged with the + envelope. + """ + def __init__(self, data, metadata): + self.data = data + self.metadata = metadata + @json_response def endpoint_loader(request, application, model, **kwargs): """Load an AJAX endpoint. @@ -27,7 +42,7 @@ def endpoint_loader(request, application, model, **kwargs): try: module = import_module('%s.endpoints' % application) - except ImportError, e: + except ImportError as e: if settings.DEBUG: raise e else: @@ -60,10 +75,18 @@ def endpoint_loader(request, application, model, **kwargs): data = endpoint(request) if isinstance(data, HttpResponse): return data + + if isinstance(data, EnvelopedResponse): + envelope = data.metadata + payload = data.data else: - payload = { - 'success': True, - 'data': data, - } - return HttpResponse(json.dumps(payload, cls=DjangoJSONEncoder, - separators=(',', ':'))) + envelope = {} + payload = data + + envelope.update({ + 'success': True, + 'data': payload, + }) + + return HttpResponse(json.dumps(envelope, cls=DjangoJSONEncoder, + separators=(',', ':'))) diff --git a/ci.txt b/ci.txt new file mode 100644 index 0000000..462109c --- /dev/null +++ b/ci.txt @@ -0,0 +1,3 @@ +decorator +django-appconf~=1.0.0 +django-jenkins diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed077da --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +-r ./ci.txt +mock diff --git a/setup.py b/setup.py index 77cc91a..3c88e40 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ +from __future__ import absolute_import from setuptools import setup, find_packages setup( - name='django-ajax', - version='0.1.0', + name='ajax', + version='3.0.0', description='A simple framework for creating AJAX endpoints in Django.', long_description='', keywords='django, ajax', @@ -13,8 +14,11 @@ license='BSD', packages=find_packages(), zip_safe=False, - install_requires=['decorator',], - extras_require = { + install_requires=[ + 'decorator', + 'django-appconf>=1.0.0,<1.1', + ], + extras_require={ 'Tagging': ['taggit'] }, include_package_data=True, diff --git a/tests/example/endpoints.py b/tests/example/endpoints.py index b3de90a..37be850 100644 --- a/tests/example/endpoints.py +++ b/tests/example/endpoints.py @@ -1,7 +1,8 @@ +from __future__ import absolute_import from ajax import endpoint from ajax.decorators import login_required from ajax.endpoints import ModelEndpoint -from .models import Widget +from .models import Widget, Category @login_required @@ -13,5 +14,14 @@ def echo(request): class WidgetEndpoint(ModelEndpoint): model = Widget max_per_page = 100 + can_list = lambda *args, **kwargs: True + + def get_queryset(self, request): + return Widget.objects.all() + +class CategoryEndpoint(ModelEndpoint): + model = Category + endpoint.register(Widget, WidgetEndpoint) +endpoint.register(Category, CategoryEndpoint) diff --git a/tests/example/models.py b/tests/example/models.py index 28f2a8d..e25b2fd 100644 --- a/tests/example/models.py +++ b/tests/example/models.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from django.db import models class Category(models.Model): @@ -7,4 +8,4 @@ class Widget(models.Model): category = models.ForeignKey(Category, null=True, blank=True) title = models.CharField(max_length=100) description = models.CharField(max_length=200, null=True, blank=True) - active = models.BooleanField() + active = models.BooleanField(default=True) diff --git a/tests/example/tests.py b/tests/example/tests.py index 30d220b..8888e66 100644 --- a/tests/example/tests.py +++ b/tests/example/tests.py @@ -1,8 +1,19 @@ +from __future__ import absolute_import +from __future__ import print_function +import json + +import mock + from django.test import TestCase from django.contrib.auth.models import User -import json -from .models import Widget -from .endpoints import WidgetEndpoint +from django.utils import six + +from ajax.endpoints import ModelEndpoint +from ajax.exceptions import AJAXError +from ajax.signals import ajax_deleted + +from .models import Widget, Category +from .endpoints import WidgetEndpoint, CategoryEndpoint class BaseTest(TestCase): @@ -31,12 +42,14 @@ def post(self, uri, data={}, debug=False, status_code=200): """ response = self.client.post(uri, data) if debug: - print response.__class__.__name__ - print response + print(response.__class__.__name__) + print(response) self.assertEquals(status_code, response.status_code) - - return response, json.loads(response.content) + if isinstance(response.content, six.text_type): + return response, json.loads(response.content) + else: + return response, json.loads(response.content.decode('utf-8')) class EncodeTests(BaseTest): @@ -53,7 +66,7 @@ def test_encode(self): widget = Widget.objects.get(pk=encoded['pk']) for k in ('title','active','description'): self.assertEquals(encoded[k],getattr(widget,k)) - + class EndpointTests(BaseTest): def test_echo(self): @@ -83,30 +96,100 @@ def test_logged_out_user_fails(self): resp, content = self.post('/ajax/example/echo.json', {}, status_code=403) + def test_has_changes_does_save(self): + """Test that updating to a new value calls save.""" + with mock.patch.object(ModelEndpoint, '_save') as mock_save: + resp, content = self.post('/ajax/example/widget/6/update.json', + {'active': False}) + self.assertTrue(mock_save.called) + + def test_no_changes_doesnt_save(self): + """Test that updating to an existing value doesnt call save.""" + with mock.patch.object(ModelEndpoint, '_save') as mock_save: + resp, content = self.post('/ajax/example/widget/6/update.json', + {'active': True}) + self.assertFalse(mock_save.called) + class MockRequest(object): def __init__(self, **kwargs): self.POST = kwargs + self.user = None class ModelEndpointTests(BaseTest): def setUp(self): self.list_endpoint = WidgetEndpoint('example', Widget, 'list') + self.category_endpoint = CategoryEndpoint('example', Category, 'list') def test_list_returns_all_items(self): results = self.list_endpoint.list(MockRequest()) - self.assertEqual(len(results), Widget.objects.count()) + self.assertEqual(len(results.data), Widget.objects.count()) def test_list_obeys_endpoint_pagination_amount(self): self.list_endpoint.max_per_page = 1 results = self.list_endpoint.list(MockRequest()) - self.assertEqual(len(results), 1) + self.assertEqual(len(results.data), 1) + + def test_list__ajaxerror_if_can_list_isnt_set(self): + self.assertRaises(AJAXError, self.category_endpoint.list, MockRequest()) def test_out_of_range_returns_empty_list(self): results = self.list_endpoint.list(MockRequest(current_page=99)) - self.assertEqual(len(results), 0) + self.assertEqual(len(results.data), 0) def test_request_doesnt_override_max_per_page(self): self.list_endpoint.max_per_page = 1 results = self.list_endpoint.list(MockRequest(items_per_page=2)) - self.assertEqual(len(results), 1) + self.assertEqual(len(results.data), 1) + + def test_list_has_permission__default_empty(self): + Category.objects.create(title='test') + + self.category_endpoint.can_list = lambda *args, **kwargs: True + + results = self.category_endpoint.list(MockRequest()) + self.assertEqual(0, len(results.data)) + + def test_list_has_total(self): + self.category_endpoint.can_list = lambda *args, **kwargs: True + + results = self.list_endpoint.list(MockRequest()) + self.assertEqual(6, results.metadata['total']) + + +class ModelEndpointPostTests(TestCase): + """ + Integration test for full urls->views->endpoint->encoder (and back) cycle. + """ + def setUp(self): + for title in ['first', 'second', 'third']: + Widget.objects.create(title=title, active=True) + u = User(email='test@example.org', username='test') + u.set_password('password') + u.save() + + def test_can_request_list_with_total(self): + self.client.login(username='test', password='password') + + resp = self.client.post('/ajax/example/widget/list.json') + if isinstance(resp.content, six.text_type): + content = json.loads(resp.content) + else: + content = json.loads(resp.content.decode('utf-8')) + self.assertTrue('total' in list(content.keys())) + self.assertEquals(content['total'], 3) + + def test_delete(self): + widget = Widget.objects.all()[0] + + # spec attr required for mocking signal in Django 1.4. + mocked_ajax_delete_signal = mock.Mock(spec=lambda: None) + ajax_deleted.connect(mocked_ajax_delete_signal) + + self.client.login(username='test', password='password') + + self.client.post('/ajax/example/widget/%s/delete.json' % widget.pk) + + self.assertTrue(mocked_ajax_delete_signal.called) + self.assertEqual(widget.pk, mocked_ajax_delete_signal.call_args[1]['payload']['pk']) diff --git a/tests/manage.py b/tests/manage.py index 3e4eedc..1379e50 100755 --- a/tests/manage.py +++ b/tests/manage.py @@ -1,14 +1,10 @@ #!/usr/bin/env python -from django.core.management import execute_manager -import imp -try: - imp.find_module('settings') # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) - sys.exit(1) - -import settings +from __future__ import absolute_import +import os, sys if __name__ == "__main__": - execute_manager(settings) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/tests/settings.py b/tests/settings.py index f67de08..8d1a702 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import sys sys.path.insert(0, '../') @@ -141,6 +142,7 @@ 'handlers': { 'mail_admins': { 'level': 'ERROR', + 'filters': [], 'class': 'django.utils.log.AdminEmailHandler' } }, diff --git a/tests/urls.py b/tests/urls.py index fd6de3b..312f929 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,6 +1,15 @@ -from django.conf.urls.defaults import patterns, include, url +from __future__ import absolute_import +import django +try: + from django.conf.urls import patterns, include, url +except ImportError: + from django.conf.urls import include, url - -urlpatterns = patterns('', - url(http://github.com/r'%5Eajax/',%20include('ajax.urls')), -) +if django.VERSION < (1, 8): + urlpatterns = patterns('', + url(http://github.com/r'%5Eajax/',%20include('ajax.urls')), + ) +else: + urlpatterns = [ + url(http://github.com/r'%5Eajax/',%20include('ajax.urls')) + ]