From defd01b6932a4c1b79e5513db4fe0713501ba696 Mon Sep 17 00:00:00 2001
From: Nathaniel Tucker <me@ntucker.me>
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 <pub@mblar.us>
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 <jlafon@eyesopen.com>
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 "<Bundle for obj: '%s' and with data: '%s'>" % (self.obj, self.data)
--
1.8.1.5
From 431aaa9a17498a475df3ab26529cf0a94175a7af Mon Sep 17 00:00:00 2001
From: Jharrod LaFon <jlafon@eyesopen.com>
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