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