From defd01b6932a4c1b79e5513db4fe0713501ba696 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Fri, 8 Feb 2013 09:47:29 -0800 Subject: [PATCH 1/4] Django 1.5 custom user model compatability --- tastypie/authentication.py | 4 ++-- tastypie/compat.py | 9 +++++++++ tastypie/management/commands/backfill_api_keys.py | 2 +- tastypie/models.py | 4 ++-- 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 tastypie/compat.py diff --git a/tastypie/authentication.py b/tastypie/authentication.py index 562f67d..9779230 100644 --- a/tastypie/authentication.py +++ b/tastypie/authentication.py @@ -178,7 +178,7 @@ def is_authenticated(self, request, **kwargs): Should return either ``True`` if allowed, ``False`` if not or an ``HttpResponse`` if you need something custom. """ - from django.contrib.auth.models import User + from tastypie.compat import User try: username, api_key = self.extract_credentials(request) @@ -357,7 +357,7 @@ def is_authenticated(self, request, **kwargs): return True def get_user(self, username): - from django.contrib.auth.models import User + from tastypie.compat import User try: user = User.objects.get(username=username) diff --git a/tastypie/compat.py b/tastypie/compat.py new file mode 100644 index 0000000..20fae94 --- /dev/null +++ b/tastypie/compat.py @@ -0,0 +1,9 @@ +import django +__all__ = ['User'] + +# Django 1.5+ compatibility +if django.VERSION >= (1, 5): + from django.contrib.auth import get_user_model + User = get_user_model() +else: + from django.contrib.auth.models import User \ No newline at end of file diff --git a/tastypie/management/commands/backfill_api_keys.py b/tastypie/management/commands/backfill_api_keys.py index a3c9f60..27f30a4 100644 --- a/tastypie/management/commands/backfill_api_keys.py +++ b/tastypie/management/commands/backfill_api_keys.py @@ -1,5 +1,5 @@ -from django.contrib.auth.models import User from django.core.management.base import NoArgsCommand +from tastypie.compat import User from tastypie.models import ApiKey diff --git a/tastypie/models.py b/tastypie/models.py index ce980ca..cbb9cb9 100644 --- a/tastypie/models.py +++ b/tastypie/models.py @@ -28,10 +28,10 @@ def save(self, *args, **kwargs): if 'django.contrib.auth' in settings.INSTALLED_APPS: import uuid - from django.contrib.auth.models import User + AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') class ApiKey(models.Model): - user = models.OneToOneField(User, related_name='api_key') + user = models.OneToOneField(AUTH_USER_MODEL, related_name='api_key') key = models.CharField(max_length=256, blank=True, default='', db_index=True) created = models.DateTimeField(default=now) -- 1.8.1.5 From 8390f1e965cb64d6dc6c569c6aa66416d090655b Mon Sep 17 00:00:00 2001 From: marblar Date: Fri, 15 Feb 2013 19:00:00 -0500 Subject: [PATCH 2/4] Added unit tests for Django 1.5 custom AUTH_USER_MODEL. --- tests/customuser/__init__.py | 0 tests/customuser/models.py | 1 + tests/customuser/tests/__init__.py | 1 + tests/customuser/tests/custom_user.py | 47 +++++++++++++++++++++++++++++++++++ tests/manage_customuser.py | 18 ++++++++++++++ tests/settings_customuser.py | 26 +++++++++++++++++++ 6 files changed, 93 insertions(+) create mode 100644 tests/customuser/__init__.py create mode 100644 tests/customuser/models.py create mode 100644 tests/customuser/tests/__init__.py create mode 100644 tests/customuser/tests/custom_user.py create mode 100755 tests/manage_customuser.py create mode 100644 tests/settings_customuser.py diff --git a/tests/customuser/__init__.py b/tests/customuser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/customuser/models.py b/tests/customuser/models.py new file mode 100644 index 0000000..71ace1d --- /dev/null +++ b/tests/customuser/models.py @@ -0,0 +1 @@ +from django.contrib.auth.tests.custom_user import CustomUser diff --git a/tests/customuser/tests/__init__.py b/tests/customuser/tests/__init__.py new file mode 100644 index 0000000..bc76405 --- /dev/null +++ b/tests/customuser/tests/__init__.py @@ -0,0 +1 @@ +from customuser.tests import * diff --git a/tests/customuser/tests/custom_user.py b/tests/customuser/tests/custom_user.py new file mode 100644 index 0000000..5789856 --- /dev/null +++ b/tests/customuser/tests/custom_user.py @@ -0,0 +1,47 @@ +from django.conf import settings +from django.http import HttpRequest +from django.test import TestCase +from tastypie.models import ApiKey, create_api_key +from django import get_version as django_version +from django.test import TestCase +from django.contrib.auth.tests.custom_user import CustomUser + +class CustomUserTestCase(TestCase): + fixtures = ['custom_user.json'] + def setUp(self): + if django_version() < '1.5': + self.skipTest('This test requires Django 1.5 or higher') + else: + super(CustomUserTestCase, self).setUp() + ApiKey.objects.all().delete() + + def test_is_authenticated_get_params(self): + auth = ApiKeyAuthentication() + request = HttpRequest() + + # Simulate sending the signal. + john_doe = CustomUser.objects.get(pk=1) + create_api_key(CustomUser, instance=john_doe, created=True) + + # No username/api_key details should fail. + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Wrong username details. + request.GET['username'] = 'foo' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # No api_key. + request.GET['username'] = 'daniel' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Wrong user/api_key. + request.GET['username'] = 'daniel' + request.GET['api_key'] = 'foo' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Correct user/api_key. + create_api_key(CustomUser, instance=john_doe, created=True) + request.GET['username'] = 'johndoe' + request.GET['api_key'] = john_doe.api_key.key + self.assertEqual(auth.is_authenticated(request), True) + self.assertEqual(auth.get_identifier(request), 'johndoe') diff --git a/tests/manage_customuser.py b/tests/manage_customuser.py new file mode 100755 index 0000000..895ecda --- /dev/null +++ b/tests/manage_customuser.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import os +import sys + +from os.path import abspath, dirname, join +from django.core.management import execute_manager +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +try: + import settings_core as settings +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings_core.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(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) + diff --git a/tests/settings_customuser.py b/tests/settings_customuser.py new file mode 100644 index 0000000..12d6b40 --- /dev/null +++ b/tests/settings_customuser.py @@ -0,0 +1,26 @@ +from settings import * +INSTALLED_APPS.append('customuser') +INSTALLED_APPS.append('django.contrib.auth') + +ROOT_URLCONF = 'core.tests.api_urls' +MEDIA_URL = 'http://localhost:8080/media/' + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'handlers': { + 'simple': { + 'level': 'ERROR', + 'class': 'core.utils.SimpleHandler', + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['simple'], + 'level': 'ERROR', + 'propagate': False, + }, + } +} + +AUTH_USER_MODEL = 'auth.CustomUser' -- 1.8.1.5 From 8b1c0fad039365c4a2a34cdba22b4ecbac611e4e Mon Sep 17 00:00:00 2001 From: Jharrod LaFon Date: Fri, 22 Mar 2013 16:09:30 -0600 Subject: [PATCH 3/4] Added new attribute to Bundle to contain related objects to be saved in ModelResource.save_related --- tastypie/bundle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tastypie/bundle.py b/tastypie/bundle.py index 3499881..5602279 100644 --- a/tastypie/bundle.py +++ b/tastypie/bundle.py @@ -16,7 +16,9 @@ def __init__(self, request=None, related_obj=None, related_name=None, - objects_saved=None): + objects_saved=None, + related_objects_to_save=None, + ): self.obj = obj self.data = data or {} self.request = request or HttpRequest() @@ -24,6 +26,7 @@ def __init__(self, self.related_name = related_name self.errors = {} self.objects_saved = objects_saved or set() + self.related_objects_to_save = related_objects_to_save or {} def __repr__(self): return "" % (self.obj, self.data) -- 1.8.1.5 From 431aaa9a17498a475df3ab26529cf0a94175a7af Mon Sep 17 00:00:00 2001 From: Jharrod LaFon Date: Fri, 22 Mar 2013 16:13:47 -0600 Subject: [PATCH 4/4] Due to a bug in Django (ticket https://code.djangoproject.com/ticket/18153) and the corresponding patch (https://github.com/django/django/commit/3190abcd75b1fcd660353da4001885ef82cbc596), tests were failing with Django 1.5 (tests/validation/). This commit modifies ModelResource so that related resources no longer rely on this incorrect Django behavior by storing the objects to be saved in save_related (which is called after authentication/authorization checks). --- tastypie/resources.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tastypie/resources.py b/tastypie/resources.py index d8e9c08..4ea5284 100644 --- a/tastypie/resources.py +++ b/tastypie/resources.py @@ -905,7 +905,13 @@ def full_hydrate(self, bundle): setattr(bundle.obj, field_object.attribute, value) elif not getattr(field_object, 'is_m2m', False): if value is not None: - setattr(bundle.obj, field_object.attribute, value.obj) + # NOTE: A bug fix in Django (ticket #18153) fixes incorrect behavior + # which Tastypie was relying on. To fix this, we store value.obj to + # be saved later in save_related. + try: + setattr(bundle.obj, field_object.attribute, value.obj) + except (ValueError, ObjectDoesNotExist): + bundle.related_objects_to_save[field_object.attribute] = value.obj elif field_object.blank: continue elif field_object.null: @@ -2294,7 +2300,7 @@ def save_related(self, bundle): try: related_obj = getattr(bundle.obj, field_object.attribute) except ObjectDoesNotExist: - related_obj = None + related_obj = bundle.related_objects_to_save.get(field_object.attribute, None) # Because sometimes it's ``None`` & that's OK. if related_obj: -- 1.8.1.5