Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: glencoates/django-tastypie
...
head fork: glencoates/django-tastypie
  • 14 commits
  • 30 files changed
  • 0 commit comments
  • 5 contributors
Commits on Nov 29, 2011
@toastdriven toastdriven Fixed ``setup.py`` to include migrations. Thanks to zbyte64 for the p…
…atch!
75f8a02
Commits on Dec 09, 2011
@toastdriven toastdriven Bumped to v0.9.11! 76e33c9
@toastdriven toastdriven I hate Python packaging. 8a94dc5
Commits on Dec 13, 2011
@jezdez jezdez Added cookbook entry for determining the format by looking at the URL. f51b940
Commits on Jan 09, 2012
@issackelly issackelly Added 'related_type' of 'to_one'|'to_many' to schema definition for r…
…elated resources.
ea480e5
Commits on Jan 17, 2012
@toastdriven toastdriven Better admin availability for ``ApiKey``s. 97b11c4
@toastdriven toastdriven Nevermind. That worked well on the ``User`` admin & blew everything e…
…lse up.
498afa6
@jezdez jezdez Added compatibility for Django 1.4's time zone awareness. This also u…
…pdates the tox configuration quite a bit to make it a little bit more flexible. Also drop 1.2.X since the tests already weren't passing there and 1.2.X is about to get dropped from Django core support.
4c1dfb0
Commits on Jan 19, 2012
@toastdriven toastdriven Changed the ``ApiKeyAuthentication`` class to accept a header as well. 6053743
Commits on Jan 23, 2012
@toastdriven toastdriven Fixed authentication to be case-insensitive, per the RFCs. Thanks to …
…rbarlow for the report!
9ff0ddd
@jezdez jezdez Reverted parts of 4c1dfb0 now that old migrations are handled more gr… 14e0123
Commits on Jan 26, 2012
@passy passy Added closing ticks in resources documentation. f64ad42
Commits on Jan 30, 2012
@toastdriven toastdriven Fixed ``method_check`` to include the ``Allow`` header. Thanks to rya…
…nisnan for the report!
5c7ef3c
Commits on Feb 02, 2012
@glencoates Merge remote-tracking branch 'upstream/master' d57fa74
View
5 MANIFEST.in
@@ -0,0 +1,5 @@
+recursive-include docs *
+recursive-include tastypie/templates/tastypie *.html
+include AUTHORS
+include LICENSE
+include README.rst
View
13 docs/authentication_authorization.rst
@@ -68,6 +68,17 @@ As an alternative to requiring sensitive data like a password, the
machine-generated api key. Tastypie ships with a special ``Model`` just for
this purpose, so you'll need to ensure ``tastypie`` is in ``INSTALLED_APPS``.
+To use this mechanism, the end user can either specify an ``Authorization``
+header or pass the ``username/api_key`` combination as ``GET/POST`` parameters.
+Examples::
+
+ # As a header
+ # Format is ``Authorization: ApiKey <username>:<api_key>
+ Authorization: ApiKey daniel:204db7bcfafb2deb7506b89eb3b9b715b09905c8
+
+ # As GET params
+ http://127.0.0.1:8000/api/v1/entries/?username=daniel&api_key=204db7bcfafb2deb7506b89eb3b9b715b09905c8
+
Tastypie includes a signal function you can use to auto-create ``ApiKey``
objects. Hooking it up looks like::
@@ -115,6 +126,8 @@ consumption.
It merely checks that the credentials are valid. No requests are made
to remote services as part of this authentication class.
+.. _mechanize: http://pypi.python.org/pypi/mechanize/
+
Authorization Options
=====================
View
4 docs/conf.py
@@ -45,9 +45,9 @@
# built documents.
#
# The short X.Y version.
-version = '1.0.0'
+version = '0.9.11'
# The full version, including alpha/beta/rc tags.
-release = '1.0.0-beta'
+release = '0.9.11'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
View
45 docs/cookbook.rst
@@ -29,12 +29,14 @@ A common pattern is needing to limit a queryset by something that changes
per-request, for instance the date/time. You can accomplish this by lightly
modifying ``get_object_list``::
+ from tastypie.utils import now
+
class MyResource(ModelResource):
class Meta:
queryset = MyObject.objects.all()
def get_object_list(self, request):
- return super(MyResource, self).get_object_list(request).filter(start_date__gte=datetime.datetime.now)
+ return super(MyResource, self).get_object_list(request).filter(start_date__gte=now)
Using Your ``Resource`` In Regular Views
@@ -292,3 +294,44 @@ values in camelCase instead::
return underscored_data
+Determining format via URL
+--------------------------
+
+Sometimes it's required to allow selecting the response format by
+specifying it in the API URL, for example ``/api/v1/users.json`` instead
+of ``/api/v1/users/?format=json``. The following snippet allows that kind
+of syntax additional to the default URL scheme::
+
+ # myapp/api/resources.py
+ class UserResource(ModelResource):
+ class Meta:
+ queryset = User.objects.all()
+
+ def override_urls(self):
+ """
+ Returns a URL scheme based on the default scheme to specify
+ the response format as a file extension, e.g. /api/v1/users.json
+ """
+ return [
+ url(r"^(?P<resource_name>%s)\.(?P<format>\w+)$" % self._meta.resource_name, self.wrap_view('dispatch_list'), name="api_dispatch_list"),
+ url(r"^(?P<resource_name>%s)/schema\.(?P<format>\w+)$" % self._meta.resource_name, self.wrap_view('get_schema'), name="api_get_schema"),
+ url(r"^(?P<resource_name>%s)/set/(?P<pk_list>\w[\w/;-]*)\.(?P<format>\w+)$" % self._meta.resource_name, self.wrap_view('get_multiple'), name="api_get_multiple"),
+ url(r"^(?P<resource_name>%s)/(?P<pk>\w[\w/-]*)\.(?P<format>\w+)$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
+ ]
+
+ def determine_format(self, request):
+ """
+ Used to determine the desired format from the request.format
+ attribute.
+ """
+ if (hasattr(request, 'format') and
+ request.format in self._meta.serializer.formats):
+ return self._meta.serializer.get_mime_for_format(request.format)
+ return super(UserResource, self).determine_format(request)
+
+ def wrap_view(self, view):
+ def wrapper(request, *args, **kwargs):
+ request.format = kwargs.pop('format', None)
+ wrapped_view = super(UserResource, self).wrap_view(view)
+ return wrapped_view(request, *args, **kwargs)
+ return wrapper
View
5 docs/fields.rst
@@ -15,8 +15,7 @@ Quick Start
For the impatient::
- import datetime
- from tastypie import fields
+ from tastypie import fields, utils
from tastypie.resources import Resource
from myapp.api.resources import ProfileResource, NoteResource
@@ -24,7 +23,7 @@ For the impatient::
class PersonResource(Resource):
name = fields.CharField(attribute='name')
age = fields.IntegerField(attribute='years_old', null=True)
- created = fields.DateTimeField(readonly=True, help_text='When the person was created', default=datetime.datetime.now)
+ created = fields.DateTimeField(readonly=True, help_text='When the person was created', default=utils.now)
is_active = fields.BooleanField(default=True)
profile = fields.ToOneField(ProfileResource, 'profile')
notes = fields.ToManyField(NoteResource, 'notes', full=True)
View
2  docs/resources.rst
@@ -130,7 +130,7 @@ As an example, we'll walk through what a GET request to a list endpoint (say
Processing on other endpoints or using the other HTTP methods results in a
similar cycle, usually differing only in what "actual work" method gets called
-(which follows the format of "``<http_method>_<list_or_detail>"). In the case
+(which follows the format of "``<http_method>_<list_or_detail>``"). In the case
of POST/PUT, the ``hydrate`` cycle additionally takes place and is used to take
the user data & convert it to raw data for storage.
View
4 docs/tutorial.rst
@@ -25,7 +25,7 @@ of the code that are Tastypie-specific in any kind of depth.
For example purposes, we'll be adding an API to a simple blog application.
Here is ``myapp/models.py``::
- import datetime
+ from tastypie.utils import now
from django.contrib.auth.models import User
from django.db import models
from django.template.defaultfilters import slugify
@@ -33,7 +33,7 @@ Here is ``myapp/models.py``::
class Entry(models.Model):
user = models.ForeignKey(User)
- pub_date = models.DateTimeField(default=datetime.datetime.now)
+ pub_date = models.DateTimeField(default=now)
title = models.CharField(max_length=200)
slug = models.SlugField()
body = models.TextField()
View
2  requirements.txt
@@ -3,5 +3,5 @@ mimeparse>=0.1.3
python-dateutil==1.5
lxml
PyYAML
-python_digest
+python-digest
biplist
View
3  setup.py
@@ -9,7 +9,7 @@
setup(
name='django-tastypie',
- version='1.0.0-beta',
+ version='0.9.11',
description='A flexible & capable API layer for Django.',
author='Daniel Lindsley',
author_email='daniel@toastdriven.com',
@@ -20,6 +20,7 @@
'tastypie.utils',
'tastypie.management',
'tastypie.management.commands',
+ 'tastypie.migrations',
],
package_data={
'tastypie': ['templates/tastypie/*'],
View
2  tastypie/__init__.py
@@ -1,2 +1,2 @@
__author__ = 'Daniel Lindsley, Cody Soyland, Matt Croydon'
-__version__ = (1, 0, 0, 'beta')
+__version__ = (0, 9, 11)
View
5 tastypie/admin.py
@@ -4,7 +4,10 @@
if 'django.contrib.auth' in settings.INSTALLED_APPS:
from tastypie.models import ApiKey
-
+
class ApiKeyInline(admin.StackedInline):
model = ApiKey
extra = 0
+
+ # Also.
+ admin.site.register(ApiKey)
View
27 tastypie/authentication.py
@@ -94,7 +94,7 @@ def is_authenticated(self, request, **kwargs):
try:
(auth_type, data) = request.META['HTTP_AUTHORIZATION'].split()
- if auth_type != 'Basic':
+ if auth_type.lower() != 'basic':
return self._unauthorized()
user_pass = base64.b64decode(data)
except:
@@ -136,6 +136,20 @@ class ApiKeyAuthentication(Authentication):
def _unauthorized(self):
return HttpUnauthorized()
+ def extract_credentials(self, request):
+ if request.META.get('HTTP_AUTHORIZATION') and request.META['HTTP_AUTHORIZATION'].lower().startswith('apikey '):
+ (auth_type, data) = request.META['HTTP_AUTHORIZATION'].split()
+
+ if auth_type.lower() != 'apikey':
+ raise ValueError("Incorrect authorization header.")
+
+ username, api_key = data.split(':', 1)
+ else:
+ username = request.GET.get('username') or request.POST.get('username')
+ api_key = request.GET.get('api_key') or request.POST.get('api_key')
+
+ return username, api_key
+
def is_authenticated(self, request, **kwargs):
"""
Finds the user and checks their API key.
@@ -145,8 +159,10 @@ def is_authenticated(self, request, **kwargs):
"""
from django.contrib.auth.models import User
- username = request.GET.get('username') or request.POST.get('username')
- api_key = request.GET.get('api_key') or request.POST.get('api_key')
+ try:
+ username, api_key = self.extract_credentials(request)
+ except ValueError:
+ return self._unauthorized()
if not username or not api_key:
return self._unauthorized()
@@ -179,7 +195,8 @@ def get_identifier(self, request):
This implementation returns the user's username.
"""
- return request.REQUEST.get('username', 'nouser')
+ username, api_key = self.extract_credentials(request)
+ return username or 'nouser'
class DigestAuthentication(Authentication):
@@ -226,7 +243,7 @@ def is_authenticated(self, request, **kwargs):
try:
(auth_type, data) = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
- if auth_type != 'Digest':
+ if auth_type.lower() != 'digest':
return self._unauthorized()
except:
return self._unauthorized()
View
8 tastypie/fields.py
@@ -6,7 +6,7 @@
from django.utils import datetime_safe, importlib
from tastypie.bundle import Bundle
from tastypie.exceptions import ApiFieldError, NotFound
-from tastypie.utils import dict_strip_unicode_keys
+from tastypie.utils import dict_strip_unicode_keys, make_aware
class NOT_PROVIDED:
@@ -322,7 +322,7 @@ def hydrate(self, bundle):
if value and not hasattr(value, 'year'):
try:
# Try to rip a date/datetime out of it.
- value = parse(value)
+ value = make_aware(parse(value))
if hasattr(value, 'hour'):
value = value.date()
@@ -348,7 +348,7 @@ def convert(self, value):
if match:
data = match.groupdict()
- return datetime_safe.datetime(int(data['year']), int(data['month']), int(data['day']), int(data['hour']), int(data['minute']), int(data['second']))
+ return make_aware(datetime_safe.datetime(int(data['year']), int(data['month']), int(data['day']), int(data['hour']), int(data['minute']), int(data['second'])))
else:
raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value))
@@ -360,7 +360,7 @@ def hydrate(self, bundle):
if value and not hasattr(value, 'year'):
try:
# Try to rip a date/datetime out of it.
- value = parse(value)
+ value = make_aware(parse(value))
except ValueError:
pass
View
1  tastypie/migrations/0001_initial.py
@@ -4,6 +4,7 @@
from south.v2 import SchemaMigration
from django.db import models
+
class Migration(SchemaMigration):
def forwards(self, orm):
View
6 tastypie/models.py
@@ -3,6 +3,8 @@
import time
from django.conf import settings
from django.db import models
+from tastypie.utils import now
+
try:
from hashlib import sha1
except ImportError:
@@ -33,8 +35,8 @@ def save(self, *args, **kwargs):
class ApiKey(models.Model):
user = models.OneToOneField(User, related_name='api_key')
key = models.CharField(max_length=256, blank=True, default='')
- created = models.DateTimeField(default=datetime.datetime.now)
-
+ created = models.DateTimeField(default=now)
+
def __unicode__(self):
return u"%s for %s" % (self.key, self.user)
View
13 tastypie/resources.py
@@ -479,15 +479,17 @@ def method_check(self, request, allowed=None):
allowed = []
request_method = request.method.lower()
+ allows = ','.join(map(str.upper, allowed))
if request_method == "options":
- allows = ','.join(map(str.upper, allowed))
response = HttpResponse(allows)
response['Allow'] = allows
raise ImmediateHttpResponse(response=response)
if not request_method in allowed:
- raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed())
+ response = http.HttpMethodNotAllowed(allows)
+ response['Allow'] = allows
+ raise ImmediateHttpResponse(response=response)
return request_method
@@ -787,6 +789,12 @@ def build_schema(self):
'help_text': field_object.help_text,
'unique': field_object.unique,
}
+ if field_object.dehydrated_type == 'related':
+ if getattr(field_object, 'is_m2m', False):
+ related_type = 'to_many'
+ else:
+ related_type = 'to_one'
+ data['fields'][field_name]['related_type'] = related_type
return data
@@ -2005,7 +2013,6 @@ def convert_post_to_VERB(request, verb):
request.META['REQUEST_METHOD'] = 'POST'
request._load_post_and_files()
request.META['REQUEST_METHOD'] = verb
-
setattr(request, verb, request.POST)
return request
View
3  tastypie/serializers.py
@@ -7,7 +7,7 @@
from django.utils.encoding import force_unicode
from tastypie.bundle import Bundle
from tastypie.exceptions import UnsupportedFormat
-from tastypie.utils import format_datetime, format_date, format_time
+from tastypie.utils import format_datetime, format_date, format_time, make_naive
try:
import lxml
from lxml.etree import parse as parse_xml
@@ -120,6 +120,7 @@ def format_datetime(self, data):
Default is ``iso-8601``, which looks like "2010-12-16T03:02:14".
"""
+ data = make_naive(data)
if self.datetime_formatting == 'rfc-2822':
return format_datetime(data)
View
1  tastypie/utils/__init__.py
@@ -2,3 +2,4 @@
from tastypie.utils.formatting import mk_datetime, format_datetime, format_date, format_time
from tastypie.utils.urls import trailing_slash
from tastypie.utils.validate_jsonp import is_valid_jsonp_callback_value
+from tastypie.utils.timezone import now, make_aware, make_naive, aware_date, aware_datetime
View
9 tastypie/utils/formatting.py
@@ -1,6 +1,7 @@
import email
import datetime
from django.utils import dateformat
+from tastypie.utils.timezone import make_aware, make_naive, aware_datetime
# Try to use dateutil for maximum date-parsing niceness. Fall back to
# hard-coded RFC2822 parsing if that's not possible.
@@ -8,13 +9,13 @@
from dateutil.parser import parse as mk_datetime
except ImportError:
def mk_datetime(string):
- return datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate(string)))
+ return make_aware(datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate(string))))
def format_datetime(dt):
"""
RFC 2822 datetime formatter
"""
- return dateformat.format(dt, 'r')
+ return dateformat.format(make_naive(dt), 'r')
def format_date(d):
"""
@@ -22,7 +23,7 @@ def format_date(d):
"""
# workaround because Django's dateformat utility requires a datetime
# object (not just date)
- dt = datetime.datetime(d.year, d.month, d.day, 0, 0, 0)
+ dt = aware_datetime(d.year, d.month, d.day, 0, 0, 0)
return dateformat.format(dt, 'j M Y')
def format_time(t):
@@ -30,5 +31,5 @@ def format_time(t):
RFC 2822 time formatter
"""
# again, workaround dateformat input requirement
- dt = datetime.datetime(2000, 1, 1, t.hour, t.minute, t.second)
+ dt = aware_datetime(2000, 1, 1, t.hour, t.minute, t.second)
return dateformat.format(dt, 'H:i:s O')
View
30 tastypie/utils/timezone.py
@@ -0,0 +1,30 @@
+import datetime
+from django.conf import settings
+
+try:
+ from django.utils import timezone
+
+ def make_aware(value):
+ if getattr(settings, "USE_TZ", False):
+ default_tz = timezone.get_default_timezone()
+ value = timezone.make_aware(value, default_tz)
+ return value
+
+ def make_naive(value):
+ if getattr(settings, "USE_TZ", False) and timezone.is_aware(value):
+ default_tz = timezone.get_default_timezone()
+ value = timezone.make_naive(value, default_tz)
+ return value
+
+ def now():
+ return timezone.localtime(timezone.now())
+
+except ImportError:
+ now = datetime.datetime.now
+ make_aware = make_naive = lambda x: x
+
+def aware_date(*args, **kwargs):
+ return make_aware(datetime.date(*args, **kwargs))
+
+def aware_datetime(*args, **kwargs):
+ return make_aware(datetime.datetime(*args, **kwargs))
View
11 tests/alphanumeric/models.py
@@ -1,16 +1,17 @@
import datetime
from django.db import models
+from tastypie.utils import now
class Product(models.Model):
artnr = models.CharField(max_length=8, primary_key=True)
name = models.CharField(max_length=32, null=False, blank=True, default='')
- created = models.DateTimeField(default=datetime.datetime.now)
- updated = models.DateTimeField(default=datetime.datetime.now)
-
+ created = models.DateTimeField(default=now)
+ updated = models.DateTimeField(default=now)
+
def __unicode__(self):
return "%s - %s" % (self.artnr, self.name)
-
+
def save(self, *args, **kwargs):
- self.updated = datetime.datetime.now()
+ self.updated = now()
return super(Product, self).save(*args, **kwargs)
View
10 tests/basic/models.py
@@ -1,7 +1,7 @@
import datetime
from django.contrib.auth.models import User
from django.db import models
-
+from tastypie.utils import now
class Note(models.Model):
user = models.ForeignKey(User, related_name='notes')
@@ -9,14 +9,14 @@ class Note(models.Model):
slug = models.SlugField()
content = models.TextField()
is_active = models.BooleanField(default=True)
- created = models.DateTimeField(default=datetime.datetime.now)
- updated = models.DateTimeField(default=datetime.datetime.now)
-
+ created = models.DateTimeField(default=now)
+ updated = models.DateTimeField(default=now)
+
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
- self.updated = datetime.datetime.now()
+ self.updated = now()
return super(Note, self).save(*args, **kwargs)
class AnnotatedNote(models.Model):
View
8 tests/complex/models.py
@@ -9,6 +9,8 @@
from django.db.models import signals, get_models
from django.conf import settings
+from tastypie.utils import now
+
class Post(models.Model):
user = models.ForeignKey(User, related_name='notes')
@@ -16,15 +18,15 @@ class Post(models.Model):
slug = models.SlugField()
content = models.TextField()
is_active = models.BooleanField(default=True)
- created = models.DateTimeField(default=datetime.datetime.now)
- updated = models.DateTimeField(default=datetime.datetime.now)
+ created = models.DateTimeField(default=now)
+ updated = models.DateTimeField(default=now)
comments = generic.GenericRelation(Comment, content_type_field="content_type", object_id_field="object_pk")
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
- self.updated = datetime.datetime.now()
+ self.updated = now()
return super(Post, self).save(*args, **kwargs)
View
17 tests/core/models.py
@@ -1,6 +1,7 @@
import datetime
from django.contrib.auth.models import User
from django.db import models
+from tastypie.utils import now, aware_datetime
class Note(models.Model):
@@ -9,19 +10,19 @@ class Note(models.Model):
slug = models.SlugField()
content = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
- created = models.DateTimeField(default=datetime.datetime.now)
- updated = models.DateTimeField(default=datetime.datetime.now)
-
+ created = models.DateTimeField(default=now)
+ updated = models.DateTimeField(default=now)
+
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
- self.updated = datetime.datetime.now()
+ self.updated = now()
return super(Note, self).save(*args, **kwargs)
def what_time_is_it(self):
- return datetime.datetime(2010, 4, 1, 0, 48)
-
+ return aware_datetime(2010, 4, 1, 0, 48)
+
def get_absolute_url(self):
return '/some/fake/path/%s/' % self.pk
@@ -34,8 +35,8 @@ class Subject(models.Model):
notes = models.ManyToManyField(Note, related_name='subjects')
name = models.CharField(max_length=255)
url = models.URLField(verify_exists=False)
- created = models.DateTimeField(default=datetime.datetime.now)
-
+ created = models.DateTimeField(default=now)
+
def __unicode__(self):
return self.name
View
42 tests/core/tests/authentication.py
@@ -78,6 +78,13 @@ def test_is_authenticated(self):
request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('johndoe:pass:word')
self.assertEqual(auth.is_authenticated(request), True)
+ # Capitalization shouldn't matter.
+ john_doe = User.objects.get(username='johndoe')
+ john_doe.set_password('pass:word')
+ john_doe.save()
+ request.META['HTTP_AUTHORIZATION'] = 'bAsIc %s' % base64.b64encode('johndoe:pass:word')
+ self.assertEqual(auth.is_authenticated(request), True)
+
class ApiKeyAuthenticationTestCase(TestCase):
fixtures = ['note_testdata.json']
@@ -86,7 +93,7 @@ def setUp(self):
super(ApiKeyAuthenticationTestCase, self).setUp()
ApiKey.objects.all().delete()
- def test_is_authenticated(self):
+ def test_is_authenticated_get_params(self):
auth = ApiKeyAuthentication()
request = HttpRequest()
@@ -116,6 +123,39 @@ def test_is_authenticated(self):
request.GET['api_key'] = john_doe.api_key.key
self.assertEqual(auth.is_authenticated(request), True)
+ def test_is_authenticated_header(self):
+ auth = ApiKeyAuthentication()
+ request = HttpRequest()
+
+ # Simulate sending the signal.
+ john_doe = User.objects.get(username='johndoe')
+ create_api_key(User, 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.META['HTTP_AUTHORIZATION'] = 'foo'
+ self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True)
+
+ # No api_key.
+ request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel'
+ self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True)
+
+ # Wrong user/api_key.
+ request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel:pass'
+ self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True)
+
+ # Correct user/api_key.
+ john_doe = User.objects.get(username='johndoe')
+ request.META['HTTP_AUTHORIZATION'] = 'ApiKey johndoe:%s' % john_doe.api_key.key
+ self.assertEqual(auth.is_authenticated(request), True)
+
+ # Capitalization shouldn't matter.
+ john_doe = User.objects.get(username='johndoe')
+ request.META['HTTP_AUTHORIZATION'] = 'aPiKeY johndoe:%s' % john_doe.api_key.key
+ self.assertEqual(auth.is_authenticated(request), True)
+
class DigestAuthenticationTestCase(TestCase):
fixtures = ['note_testdata.json']
View
24 tests/core/tests/fields.py
@@ -9,6 +9,8 @@
from tastypie.resources import ModelResource
from core.models import Note, Subject, MediaBit
+from tastypie.utils import aware_datetime, aware_date
+
class ApiFieldTestCase(TestCase):
fixtures = ['note_testdata.json']
@@ -75,7 +77,7 @@ def test_dehydrate(self):
# Correct callable attribute.
field_6 = ApiField(attribute='what_time_is_it', default=True)
- self.assertEqual(field_6.dehydrate(bundle), datetime.datetime(2010, 4, 1, 0, 48))
+ self.assertEqual(field_6.dehydrate(bundle), aware_datetime(2010, 4, 1, 0, 48))
def test_convert(self):
field_1 = ApiField()
@@ -390,7 +392,7 @@ def test_dehydrate(self):
bundle = Bundle(obj=note)
field_1 = TimeField(attribute='created')
- self.assertEqual(field_1.dehydrate(bundle), datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(field_1.dehydrate(bundle), aware_datetime(2010, 3, 30, 20, 5))
field_2 = TimeField(default=datetime.time(23, 5, 58))
self.assertEqual(field_2.dehydrate(bundle), datetime.time(23, 5, 58))
@@ -452,7 +454,7 @@ def test_dehydrate(self):
bundle = Bundle(obj=note)
field_1 = DateField(attribute='created')
- self.assertEqual(field_1.dehydrate(bundle), datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(field_1.dehydrate(bundle), aware_datetime(2010, 3, 30, 20, 5))
field_2 = DateField(default=datetime.date(2010, 4, 1))
self.assertEqual(field_2.dehydrate(bundle), datetime.date(2010, 4, 1))
@@ -517,14 +519,14 @@ def test_dehydrate(self):
bundle = Bundle(obj=note)
field_1 = DateTimeField(attribute='created')
- self.assertEqual(field_1.dehydrate(bundle), datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(field_1.dehydrate(bundle), aware_datetime(2010, 3, 30, 20, 5))
- field_2 = DateTimeField(default=datetime.datetime(2010, 4, 1, 1, 7))
- self.assertEqual(field_2.dehydrate(bundle), datetime.datetime(2010, 4, 1, 1, 7))
+ field_2 = DateTimeField(default=aware_datetime(2010, 4, 1, 1, 7))
+ self.assertEqual(field_2.dehydrate(bundle), aware_datetime(2010, 4, 1, 1, 7))
note.created_string = '2010-04-02 01:11:00'
field_3 = DateTimeField(attribute='created_string')
- self.assertEqual(field_3.dehydrate(bundle), datetime.datetime(2010, 4, 2, 1, 11))
+ self.assertEqual(field_3.dehydrate(bundle), aware_datetime(2010, 4, 2, 1, 11))
def test_hydrate(self):
note = Note.objects.get(pk=1)
@@ -534,19 +536,19 @@ def test_hydrate(self):
})
field_1 = DateTimeField(attribute='created')
field_1.instance_name = 'datetime'
- self.assertEqual(field_1.hydrate(bundle_1), datetime.datetime(2010, 5, 12, 10, 36, 28))
+ self.assertEqual(field_1.hydrate(bundle_1), aware_datetime(2010, 5, 12, 10, 36, 28))
bundle_2 = Bundle()
- field_2 = DateTimeField(default=datetime.datetime(2010, 4, 1, 2, 0))
+ field_2 = DateTimeField(default=aware_datetime(2010, 4, 1, 2, 0))
field_2.instance_name = 'datetime'
- self.assertEqual(field_2.hydrate(bundle_2), datetime.datetime(2010, 4, 1, 2, 0))
+ self.assertEqual(field_2.hydrate(bundle_2), aware_datetime(2010, 4, 1, 2, 0))
bundle_3 = Bundle(data={
'datetime': 'Tue, 30 Mar 2010 20:05:00 -0500',
})
field_3 = DateTimeField(attribute='created_string')
field_3.instance_name = 'datetime'
- self.assertEqual(field_3.hydrate(bundle_3), datetime.datetime(2010, 3, 30, 20, 5, tzinfo=tzoffset(None, -18000)))
+ self.assertEqual(field_3.hydrate(bundle_3), aware_datetime(2010, 3, 30, 20, 5, tzinfo=tzoffset(None, -18000)))
bundle_4 = Bundle(data={
'datetime': None,
View
182 tests/core/tests/resources.py
@@ -22,6 +22,7 @@
from tastypie.resources import Resource, ModelResource, ALL, ALL_WITH_RELATIONS, convert_post_to_put, convert_post_to_patch
from tastypie.serializers import Serializer
from tastypie.throttle import CacheThrottle
+from tastypie.utils import aware_datetime, make_naive
from tastypie.validation import Validation, FormValidation
from core.models import Note, Subject, MediaBit
from core.tests.mocks import MockRequest
@@ -58,7 +59,7 @@ def dehydrate_date_joined(self, bundle):
if bundle.data.get('date_joined') is not None:
return bundle.data.get('date_joined')
- return datetime.datetime(2010, 3, 27, 22, 30, 0)
+ return aware_datetime(2010, 3, 27, 22, 30, 0)
def hydrate_date_joined(self, bundle):
bundle.obj.date_joined = bundle.data['date_joined']
@@ -167,7 +168,7 @@ def test_to_put(self):
}
# Make Django happy.
request._read_started = False
- request._raw_post_data = ''
+ request._raw_post_data = request._body = ''
modified = convert_post_to_put(request)
self.assertEqual(modified.method, 'PUT')
@@ -188,7 +189,7 @@ def test_to_patch(self):
}
# Make Django happy.
request._read_started = False
- request._raw_post_data = ''
+ request._raw_post_data = request._body = ''
modified = convert_post_to_patch(request)
self.assertEqual(modified.method, 'PATCH')
@@ -283,7 +284,7 @@ def test_full_dehydrate(self):
test_object_1 = TestObject()
test_object_1.name = 'Daniel'
test_object_1.view_count = 12
- test_object_1.date_joined = datetime.datetime(2010, 3, 30, 9, 0, 0)
+ test_object_1.date_joined = aware_datetime(2010, 3, 30, 9, 0, 0)
test_object_1.foo = "Hi, I'm ignored."
basic = BasicResource()
@@ -316,7 +317,7 @@ def test_full_dehydrate(self):
test_object_3 = TestObject()
test_object_3.name = 'Joe'
test_object_3.view_count = 5
- test_object_3.created = datetime.datetime(2010, 3, 29, 11, 0, 0)
+ test_object_3.created = aware_datetime(2010, 3, 29, 11, 0, 0)
test_object_3.is_active = False
test_object_3.bar = "But sometimes I'm not ignored!"
another_1 = AnotherBasicResource()
@@ -338,7 +339,7 @@ def test_full_hydrate(self):
basic_bundle_1 = Bundle(data={
'name': 'Daniel',
'view_count': 6,
- 'date_joined': datetime.datetime(2010, 2, 15, 12, 0, 0)
+ 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0)
})
# Now load up the data.
@@ -346,16 +347,16 @@ def test_full_hydrate(self):
self.assertEqual(hydrated.data['name'], 'Daniel')
self.assertEqual(hydrated.data['view_count'], 6)
- self.assertEqual(hydrated.data['date_joined'], datetime.datetime(2010, 2, 15, 12, 0, 0))
+ self.assertEqual(hydrated.data['date_joined'], aware_datetime(2010, 2, 15, 12, 0, 0))
self.assertEqual(hydrated.obj.name, 'Daniel')
self.assertEqual(hydrated.obj.view_count, 6)
- self.assertEqual(hydrated.obj.date_joined, datetime.datetime(2010, 2, 15, 12, 0, 0))
+ self.assertEqual(hydrated.obj.date_joined, aware_datetime(2010, 2, 15, 12, 0, 0))
another = AnotherBasicResource()
another_bundle_1 = Bundle(data={
'name': 'Daniel',
'view_count': 6,
- 'date_joined': datetime.datetime(2010, 2, 15, 12, 0, 0),
+ 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0),
'aliases': ['test', 'test1'],
'meta': {'foo': 'bar'},
'owed': '12.53',
@@ -366,19 +367,19 @@ def test_full_hydrate(self):
self.assertEqual(hydrated.data['name'], 'Daniel')
self.assertEqual(hydrated.data['view_count'], 6)
- self.assertEqual(hydrated.data['date_joined'], datetime.datetime(2010, 2, 15, 12, 0, 0))
+ self.assertEqual(hydrated.data['date_joined'], aware_datetime(2010, 2, 15, 12, 0, 0))
self.assertEqual(hydrated.data['aliases'], ['test', 'test1'])
self.assertEqual(hydrated.data['meta'], {'foo': 'bar'})
self.assertEqual(hydrated.data['owed'], '12.53')
self.assertEqual(hydrated.obj.name, 'Daniel')
self.assertEqual(hydrated.obj.view_count, 6)
- self.assertEqual(hydrated.obj.date_joined, datetime.datetime(2010, 2, 15, 12, 0, 0))
+ self.assertEqual(hydrated.obj.date_joined, aware_datetime(2010, 2, 15, 12, 0, 0))
self.assertEqual(hasattr(hydrated.obj, 'bar'), False)
another_bundle_2 = Bundle(data={
'name': 'Daniel',
'view_count': 6,
- 'date_joined': datetime.datetime(2010, 2, 15, 12, 0, 0),
+ 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0),
'bar': True,
})
@@ -387,10 +388,10 @@ def test_full_hydrate(self):
self.assertEqual(hydrated.data['name'], 'Daniel')
self.assertEqual(hydrated.data['view_count'], 6)
- self.assertEqual(hydrated.data['date_joined'], datetime.datetime(2010, 2, 15, 12, 0, 0))
+ self.assertEqual(hydrated.data['date_joined'], aware_datetime(2010, 2, 15, 12, 0, 0))
self.assertEqual(hydrated.obj.name, 'Daniel')
self.assertEqual(hydrated.obj.view_count, 6)
- self.assertEqual(hydrated.obj.date_joined, datetime.datetime(2010, 2, 15, 12, 0, 0))
+ self.assertEqual(hydrated.obj.date_joined, aware_datetime(2010, 2, 15, 12, 0, 0))
self.assertEqual(hydrated.obj.bar, 'O HAI BAR!')
# Test that a nullable value with a previous non-null value
@@ -585,9 +586,21 @@ def test_method_check(self):
# No allowed methods. Kaboom.
self.assertRaises(ImmediateHttpResponse, basic.method_check, request)
+ try:
+ basic.method_check(request)
+ self.fail("Should have thrown an exception.")
+ except ImmediateHttpResponse, e:
+ self.assertEqual(e.response['Allow'], '')
+
# Not an allowed request.
self.assertRaises(ImmediateHttpResponse, basic.method_check, request, allowed=['post'])
+ try:
+ basic.method_check(request, allowed=['post'])
+ self.fail("Should have thrown an exception.")
+ except ImmediateHttpResponse, e:
+ self.assertEqual(e.response['Allow'], 'POST')
+
# Allowed (single).
request_method = basic.method_check(request, allowed=['get'])
self.assertEqual(request_method, 'get')
@@ -603,6 +616,12 @@ def test_method_check(self):
# Not an allowed request.
self.assertRaises(ImmediateHttpResponse, basic.method_check, request, allowed=['get'])
+ try:
+ basic.method_check(request, allowed=['get', 'put', 'delete', 'patch'])
+ self.fail("Should have thrown an exception.")
+ except ImmediateHttpResponse, e:
+ self.assertEqual(e.response['Allow'], 'GET,PUT,DELETE,PATCH')
+
# Allowed (multiple).
request_method = basic.method_check(request, allowed=['post', 'get', 'put'])
self.assertEqual(request_method, 'post')
@@ -1159,6 +1178,105 @@ def test_determine_format(self):
request.META = {'HTTP_ACCEPT': 'text/plain,application/xml,application/json;q=0.9,*/*;q=0.8'}
self.assertEqual(resource.determine_format(request), 'application/xml')
+ def adjust_schema(self, schema_dict):
+ for field, field_info in schema_dict['fields'].items():
+ if isinstance(field_info['default'], fields.NOT_PROVIDED):
+ schema_dict['fields'][field]['default'] = 'No default provided.'
+ if isinstance(field_info['default'], (datetime.datetime, datetime.date)):
+ schema_dict['fields'][field]['default'] = 'The current date.'
+
+ return schema_dict
+
+ def test_build_schema(self):
+ related = RelatedNoteResource()
+ schema = self.adjust_schema(related.build_schema())
+ self.assertEqual(schema, {
+ 'filtering': {
+ 'subjects': 2,
+ 'author': 1
+ },
+ 'allowed_detail_http_methods': ['get', 'post', 'put', 'delete', 'patch'],
+ 'fields': {
+ 'author': {
+ 'related_type': 'to_one',
+ 'nullable': False,
+ 'default': 'No default provided.',
+ 'readonly': False,
+ 'blank': False,
+ 'help_text': 'A single related resource. Can be either a URI or set of nested resource data.',
+ 'unique': False,
+ 'type': 'related'
+ },
+ 'title': {
+ 'nullable': False,
+ 'default': 'No default provided.',
+ 'readonly': False,
+ 'blank': False,
+ 'help_text': 'Unicode string data. Ex: "Hello World"',
+ 'unique': False,
+ 'type': 'string'
+ },
+ 'created': {
+ 'nullable': False,
+ 'default': 'The current date.',
+ 'readonly': False,
+ 'blank': False,
+ 'help_text': 'A date & time as a string. Ex: "2010-11-10T03:07:43"',
+ 'unique': False,
+ 'type': 'datetime'
+ },
+ 'is_active': {
+ 'nullable': False,
+ 'default': True,
+ 'readonly': False,
+ 'blank': False,
+ 'help_text': 'Boolean data. Ex: True',
+ 'unique': False,
+ 'type': 'boolean'
+ },
+ 'content': {
+ 'nullable': False,
+ 'default': '',
+ 'readonly': False,
+ 'blank': False,
+ 'help_text': 'Unicode string data. Ex: "Hello World"',
+ 'unique': False,
+ 'type': 'string'
+ },
+ 'subjects': {
+ 'related_type': 'to_many',
+ 'nullable': False,
+ 'default': 'No default provided.',
+ 'readonly': False,
+ 'blank': False,
+ 'help_text': 'Many related resources. Can be either a list of URIs or list of individually nested resource data.',
+ 'unique': False,
+ 'type': 'related'
+ },
+ 'slug': {
+ 'nullable': False,
+ 'default': 'No default provided.',
+ 'readonly': False,
+ 'blank': False,
+ 'help_text': 'Unicode string data. Ex: "Hello World"',
+ 'unique': False,
+ 'type': 'string'
+ },
+ 'resource_uri': {
+ 'nullable': False,
+ 'default': 'No default provided.',
+ 'readonly': True,
+ 'blank': False,
+ 'help_text': 'Unicode string data. Ex: "Hello World"',
+ 'unique': False,
+ 'type': 'string'
+ }
+ },
+ 'default_format': 'application/json',
+ 'default_limit': 20,
+ 'allowed_list_http_methods': ['get', 'post', 'put', 'delete', 'patch']
+ })
+
def test_build_filters(self):
resource = NoteResource()
@@ -1415,12 +1533,12 @@ def test_get_list(self):
title='Another fresh note.',
slug='another-fresh-note',
content='Whee!',
- created=datetime.datetime(2010, 7, 21, 11, 23),
- updated=datetime.datetime(2010, 7, 21, 11, 23),
+ created=aware_datetime(2010, 7, 21, 11, 23),
+ updated=aware_datetime(2010, 7, 21, 11, 23),
)
resp = resource.get_list(request)
self.assertEqual(resp.status_code, 200)
- self.assertEqual(resp.content, '{"meta": {"limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 5}, "objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": "1", "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": "2", "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}, {"content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", "created": "2010-04-01T20:05:00", "id": "4", "is_active": true, "resource_uri": "/api/v1/notes/4/", "slug": "recent-volcanic-activity", "title": "Recent Volcanic Activity.", "updated": "2010-04-01T20:05:00"}, {"content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", "created": "2010-04-02T10:05:00", "id": "6", "is_active": true, "resource_uri": "/api/v1/notes/6/", "slug": "grannys-gone", "title": "Granny\'s Gone", "updated": "2010-04-02T10:05:00"}, {"content": "Whee!", "created": "2010-07-21T11:23:00", "id": "7", "is_active": true, "resource_uri": "/api/v1/notes/7/", "slug": "another-fresh-note", "title": "Another fresh note.", "updated": "%s"}]}' % new_note.updated.isoformat())
+ self.assertEqual(resp.content, '{"meta": {"limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 5}, "objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": "1", "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": "2", "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}, {"content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", "created": "2010-04-01T20:05:00", "id": "4", "is_active": true, "resource_uri": "/api/v1/notes/4/", "slug": "recent-volcanic-activity", "title": "Recent Volcanic Activity.", "updated": "2010-04-01T20:05:00"}, {"content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", "created": "2010-04-02T10:05:00", "id": "6", "is_active": true, "resource_uri": "/api/v1/notes/6/", "slug": "grannys-gone", "title": "Granny\'s Gone", "updated": "2010-04-02T10:05:00"}, {"content": "Whee!", "created": "2010-07-21T11:23:00", "id": "7", "is_active": true, "resource_uri": "/api/v1/notes/7/", "slug": "another-fresh-note", "title": "Another fresh note.", "updated": "%s"}]}' % make_naive(new_note.updated).isoformat())
# Regression - Ensure that the limit on the Resource gets used if
# no other limit is requested.
@@ -1570,7 +1688,7 @@ def test_patch_list(self):
request._read_started = False
self.assertEqual(Note.objects.count(), 6)
- request._raw_post_data = '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}], "deleted_objects": ["/api/v1/notes/1/"]}'
+ request._raw_post_data = request._body = '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}], "deleted_objects": ["/api/v1/notes/1/"]}'
resp = resource.patch_list(request)
self.assertEqual(resp.status_code, 202)
@@ -1587,7 +1705,7 @@ def test_patch_detail(self):
request.GET = {'format': 'json'}
request.method = 'PATCH'
request._read_started = False
- request._raw_post_data = '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00"}'
+ request._raw_post_data = request._body = '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00"}'
resp = resource.patch_detail(request, pk=10)
self.assertEqual(resp.status_code, 404)
@@ -1597,9 +1715,9 @@ def test_patch_detail(self):
self.assertEqual(Note.objects.count(), 6)
note = Note.objects.get(pk=1)
self.assertEqual(note.content, "The cat is back. The dog coughed him up out back.")
- self.assertEqual(note.created, datetime.datetime(2010, 4, 3, 20, 5))
+ self.assertEqual(note.created, aware_datetime(2010, 4, 3, 20, 5))
- request._raw_post_data = '{"content": "The cat is gone again. I think it was the rabbits that ate him this time."}'
+ request._raw_post_data = request._body = '{"content": "The cat is gone again. I think it was the rabbits that ate him this time."}'
resp = resource.patch_detail(request, pk=1)
self.assertEqual(resp.status_code, 202)
@@ -1751,16 +1869,16 @@ def test_obj_get(self):
note = NoteResource()
note_obj = note.obj_get(pk=1)
self.assertEqual(note_obj.content, u'This is my very first post using my shiny new API. Pretty sweet, huh?')
- self.assertEqual(note_obj.created, datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(note_obj.created, aware_datetime(2010, 3, 30, 20, 5))
self.assertEqual(note_obj.is_active, True)
self.assertEqual(note_obj.slug, u'first-post')
self.assertEqual(note_obj.title, u'First Post!')
- self.assertEqual(note_obj.updated, datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(note_obj.updated, aware_datetime(2010, 3, 30, 20, 5))
custom = VeryCustomNoteResource()
custom_obj = custom.obj_get(pk=1)
self.assertEqual(custom_obj.content, u'This is my very first post using my shiny new API. Pretty sweet, huh?')
- self.assertEqual(custom_obj.created, datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(custom_obj.created, aware_datetime(2010, 3, 30, 20, 5))
self.assertEqual(custom_obj.is_active, True)
self.assertEqual(custom_obj.author.username, u'johndoe')
self.assertEqual(custom_obj.title, u'First Post!')
@@ -1768,7 +1886,7 @@ def test_obj_get(self):
related = RelatedNoteResource()
related_obj = related.obj_get(pk=1)
self.assertEqual(related_obj.content, u'This is my very first post using my shiny new API. Pretty sweet, huh?')
- self.assertEqual(related_obj.created, datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(related_obj.created, aware_datetime(2010, 3, 30, 20, 5))
self.assertEqual(related_obj.is_active, True)
self.assertEqual(related_obj.author.username, u'johndoe')
self.assertEqual(related_obj.title, u'First Post!')
@@ -1816,8 +1934,8 @@ def test_get_schema(self):
# Patch the ``created/updated`` defaults for testability.
old_created = resource.fields['created']._default
old_updated = resource.fields['updated']._default
- resource.fields['created']._default = datetime.datetime(2011, 9, 24, 0, 2)
- resource.fields['updated']._default = datetime.datetime(2011, 9, 24, 0, 2)
+ resource.fields['created']._default = aware_datetime(2011, 9, 24, 0, 2)
+ resource.fields['updated']._default = aware_datetime(2011, 9, 24, 0, 2)
resp = resource.get_schema(request)
self.assertEqual(resp.status_code, 200)
@@ -2198,18 +2316,18 @@ def test_obj_update(self):
note = NoteResource()
note_obj = note.obj_get(pk=1)
self.assertEqual(note_obj.title, u'Yet another another new post!')
- self.assertEqual(note_obj.created, datetime.datetime(2010, 3, 30, 20, 5))
+ self.assertEqual(note_obj.created, aware_datetime(2010, 3, 30, 20, 5))
note_bundle = note.build_bundle(obj=note_obj)
note_bundle = note.full_dehydrate(note_bundle)
note_bundle.data['title'] = 'OMGOMGOMGOMG!'
- note_bundle.data['created'] = datetime.datetime(2011, 11, 23, 1, 0, 0)
+ note_bundle.data['created'] = aware_datetime(2011, 11, 23, 1, 0, 0)
note.obj_update(note_bundle, pk=1, created='2010-03-30T20:05:00')
self.assertEqual(Note.objects.all().count(), 6)
numero_uno = Note.objects.get(pk=1)
self.assertEqual(numero_uno.title, u'OMGOMGOMGOMG!')
self.assertEqual(numero_uno.slug, u'yet-another-another-new-post')
self.assertEqual(numero_uno.content, u'WHEEEEEE!')
- self.assertEqual(numero_uno.created, datetime.datetime(2011, 11, 23, 1, 0))
+ self.assertEqual(numero_uno.created, aware_datetime(2011, 11, 23, 1, 0))
# Now try a lookup that should fail.
note = NoteResource()
@@ -2596,7 +2714,7 @@ def test_readonly_full_hydrate(self):
hbundle = Bundle(obj=note, data={
'name': 'Daniel',
'view_count': 6,
- 'date_joined': datetime.datetime(2010, 2, 15, 12, 0, 0),
+ 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0),
})
hydrated = rornr.full_hydrate(hbundle)
self.assertEqual(hydrated.obj.author.username, 'johndoe')
@@ -2605,7 +2723,7 @@ def test_readonly_full_hydrate(self):
hbundle_2 = Bundle(obj=note, data={
'name': 'Daniel',
'view_count': 6,
- 'date_joined': datetime.datetime(2010, 2, 15, 12, 0, 0),
+ 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0),
'author': '/api/v1/users/2/',
})
hydrated_2 = rornr.full_hydrate(hbundle_2)
View
8 tests/requirements.txt
@@ -0,0 +1,8 @@
+django-oauth-plus
+oauth2
+lxml
+python-digest
+biplist
+pyyaml
+mimeparse>=0.1.3
+python-dateutil==1.5
View
13 tests/settings.py
@@ -12,6 +12,16 @@
DATABASE_NAME = 'tastypie.db'
TEST_DATABASE_NAME = 'tastypie-test.db'
+# for forwards compatibility
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.%s' % DATABASE_ENGINE,
+ 'NAME': DATABASE_NAME,
+ 'TEST_NAME': TEST_DATABASE_NAME,
+ }
+}
+
+
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -21,3 +31,6 @@
DEBUG = True
TEMPLATE_DEBUG = DEBUG
CACHE_BACKEND = 'locmem://'
+
+# to make sure timezones are handled correctly in Django>=1.4
+USE_TZ = True
View
31 tox.ini
@@ -1,29 +1,40 @@
[tox]
-envlist = py25-1.2.X,py26-1.2.X,py27-1.2.X,py25,py26,py27,docs
-downloadcache = .tox/_download/
+envlist = py25-1.4.X,py26-1.4.X,py27-1.4.X,py25,py26,py27,docs
[testenv]
setenv =
- PYTHONPATH = {toxinidir}/tests
+ PYTHONPATH = {toxinidir}:{toxinidir}/tests
commands =
- {toxinidir}/tests/run_all_tests.sh
+ {envbindir}/django-admin.py test core --settings=settings_core
+ {envbindir}/django-admin.py test basic --settings=settings_basic
+ #{envbindir}/django-admin.py test complex --settings=settings_complex
+ {envbindir}/django-admin.py test alphanumeric --settings=settings_alphanumeric
+ {envbindir}/django-admin.py test slashless --settings=settings_slashless
+ {envbindir}/django-admin.py test namespaced --settings=settings_namespaced
+ {envbindir}/django-admin.py test related_resource --settings=settings_related
+ {envbindir}/django-admin.py test validation --settings=settings_validation
+
deps =
+ -r{toxinidir}/tests/requirements.txt
django==1.3
-[testenv:py25-1.2.X]
+[testenv:py25-1.4.X]
basepython = python2.5
deps =
- http://www.djangoproject.com/download/1.2.5/tarball/
+ -r{toxinidir}/tests/requirements.txt
+ https://github.com/django/django/zipball/master
-[testenv:py26-1.2.X]
+[testenv:py26-1.4.X]
basepython = python2.6
deps =
- http://www.djangoproject.com/download/1.2.5/tarball/
+ -r{toxinidir}/tests/requirements.txt
+ https://github.com/django/django/zipball/master
-[testenv:py27-1.2.X]
+[testenv:py27-1.4.X]
basepython = python2.7
deps =
- http://www.djangoproject.com/download/1.2.5/tarball/
+ -r{toxinidir}/tests/requirements.txt
+ https://github.com/django/django/zipball/master
[testenv:py25]
basepython = python2.5

No commit comments for this range

Something went wrong with that request. Please try again.