Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Generic relations #191

Open
wants to merge 14 commits into from

10 participants

David Hatch Bartosz Grzys David Harks Gabe Jackson Nathan Keilar Alex Churchill Yotam Ofek Daniel Lindsley Michael Curry Greg MacWilliam
David Hatch

I've added support for Generic Relations in the API by modifying the RelatedField to allow the to field to be a dictionary which maps django models to Resources. Basic usage is as follows:

Django Model:

class GenericTag(models.Model):
    name = models.CharField(max_length = 30)
    object_id = models.PositiveIntegerField()
    content_type = models.ForeignKey(generic.ContentType)
    content_object = generic.GenericForeignKey()

    def __unicode__(self):
        return u"%s" % (self.name)

Resource:

class GenericTagResource(ModelResource):
    content_type = fields.ContentTypeField()
    content_object = fields.ToOneField(
        {Category : CategoryResource, Taggable : TaggableResource}, attribute="content_object", contenttype_field=content_type)

    class Meta:
        resource_name = 'generictag'
        queryset = GenericTag.objects.all()
        authorization = Authorization()

The to field dictionary tells the RelatedField class what Resources to use when hydrating/dehydrating. A contenttype_field reference must also be passed if writing is allowed which allows for the proper type object to be written. Two new classes were added, ContentTypeField which provides a shortcut for this purpose and ContentTypeResource which users can subclass to customize. I've left it up to users to add ContentTypeResource to their urlconf by using Api.register or some other method. All functionality is supported including POST/PUT with nested resources. I've also written tests for the code in the GenericForeignKeyTest class inside tests/related_resource/tests.py The reverse GenericRelation is supported by using the ToManyField. All tests pass when run with tests/run_all_tests.sh

Bartosz Grzys

I have error when using PUT - "Cannot resolve keyword 'api_name' into field."

Bartosz Grzys

I've resolved this. In resources.py in line 1062 was
updated_bundle = self.obj_update(bundle, request=request, kwargs)
Shoud be:
updated_bundle = self.obj_update(bundle, request=request, pk=kwargs.get('pk'))
And the same in line 1065.

David Hatch

@bgrzys tastypie has moved forward a bit since this, curious if you had that issue with this merged into the current HEAD of toastdriven/tastypie/master or with the generic-relations branch.

At some point I need to update it to match current tastypie revision.

Appreciate your feedback, I think this would be great for a contrib package of some sort, maybe doesn't belong in tastypie base.

Bartosz Grzys

Thanks for reply!
I had that issue with generic-relations branch, didn't merged anything. And yes, a contrib package would be great. Can't wait for it :)

David Harks

I'm using this for a heavily-generic site I'm working on, and it applied to toastdriven:master with only fairly trivial merges.

Would love to see it included in tastypie as ContentTypes is basically stock Django nowadays, and it doesn't break standard easypie usage.

Gabe Jackson

what's the status on this? would love to see this in trunk

David Hatch

Haven't had a chance to look at it for some time. @dwink could you share the merge you did with the toastdriven/master?

Nathan Keilar

Just tried the patch, on trunk and it failed. Can anyone suggest how to merge this patch. Also can anyone provide an eta for this being included in trunk? It would be nice out of the box. Many thanks.

Nathan Keilar madteckhead referenced this pull request from a commit in madteckhead/django-tastypie
Nathan K #191 manually applying dhatch's patch to trunk
note: haven't updated tests
3373945
Nathan Keilar

Merged everything but the tests into https://github.com/madteckhead/django-tastypie/tree/issue191. Was able to get it all working and will give a write-up tonight to clear up the point where I was a bit lost.

Alex Churchill

Incidentally, anyone still trying to do this, I've managed it using subclassing Resources and Fields:

from tastypie import fields
from tastypie.resources import Resource, ModelResource
from tastypie.exceptions import ApiFieldError, NotFound, BadRequest
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import resolve, Resolver404, get_script_prefix

class GenericResource(ModelResource):

    def get_via_uri(self, uri, request=None):
        prefix = get_script_prefix()
        chomped_uri = uri

        if prefix and chomped_uri.startswith(prefix):
            chomped_uri = chomped_uri[len(prefix)-1:]

        try:
            view, args, kwargs = resolve(chomped_uri)
        except Resolver404:
            raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri)

        # Hack to climb the closure chain to get back to the original resource
        # since tastypie doesn't have a great way of getting from a uri to
        # a resource.
        parent_resource = view.func_closure[0].cell_contents.func_closure[0].cell_contents
        return parent_resource.obj_get(**self.remove_api_resource_names(kwargs))

class GenericForeignKeyField(fields.ToOneField):
    def __init__(self, to, attribute, **kwargs):
        if not isinstance(to, dict):
            raise ValueError('to field must be a dictionary in GenericForeignKeyField')
        if len(to) <= 0:
            raise ValueError('to field must have some values')
        for k, v in to.iteritems():
            if not issubclass(k, models.Model) or not issubclass(v, Resource):
                raise ValueError('to field must map django models to tastypie resources')
        super(GenericForeignKeyField, self).__init__(to, attribute, **kwargs)
    def get_related_resource(self, related_instance):
        self._to_class = self.to[type(related_instance)]
        if self._to_class is None:
            raise TypeError('no resource for model %s' % type(related_instance))
        return super(GenericForeignKeyField, self).get_related_resource(related_instance)
    @property
    def to_class(self):
        if self._to_class:
            return self._to_class
        return GenericResource
    def resource_from_uri(self, fk_resource, uri,
                         request=None, related_obj=None, related_name=None):
        try:
            obj = fk_resource.get_via_uri(uri, request=request)
            fk_resource = self.get_related_resource(obj)
            return super(GenericForeignKeyField, self).resource_from_uri(
                       fk_resource, uri, request, related_obj, related_name)
        except ObjectDoesNotExist:
            raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri)
    def build_related_resource(self, *args, **kwargs):
        # Reset self._to_class so we're guaranteed to re-figure out which class we want.
        self._to_class = None
        return super(GenericForeignKeyField, self).build_related_resource(*args, **kwargs)

I've hooked it up as follows:

from myapp.models import Rating, Note, Comment
from myapp.api.resources import NoteResource, CommentResource

class RatingResource(ModelResource):
    content_object = GenericForeignKeyField(
                   { Note: NoteResource, Comment: CommentResource }, 'content_object')

class Meta:
    # select_related() is completely optional, for optimization purposes, in django 1.4
    queryset = Rating.objects.all().select_related()
    allowed_methods = ['get', 'put', 'post']

Note that this will only work for POST/PUT setting content_object using a resource URI. For other use cases, check out the patch, though be warned you'll have to deal with ContentType (which, while standard django, is in my opinion somewhat annoying for use in a real API).

Yotam Ofek

archur, your solution works quite well!

Alex Churchill

OK, if you need to use generic foreign keys, I've put together a package (pip install tastypie-generic). The source code and documentation.

Daniel Lindsley
Owner

@achur Would you mind terribly if I integrate your above code into Tastypie itself? I think I like that approach & it's only waited because I needed time to write tests.

Alex Churchill

@toastdriven sure, works for me; when you integrate it, please include a real way to go from uri to resource: climbing out of the view function closure is a really bad hack. @dhatch has a decent approach (attaching parent_resource to the view wrapper), but it would obviously be ideal to have a function in Api that just takes a uri to a resource directly).

Note this won't work with nested resources (only when resource_from_uri is called), which will potentially impact #382.

Michael Curry

@achur :+1: :shipit:
Sadly, I'm unable to contribute unit tests ATM. :(

Issac Kelly issackelly referenced this pull request from a commit
Issac Kelly issackelly Added GenericForeignKey support.
Thanks for the original patch @achur (Alex Churchill)
Thanks for reporting @tiabas (Kevin Mutyaba), @dhatch (David Hatch)
Thanks @joshbodhe and @toastdriven for the code review and support.

Closes Issue #101
Closes Issue #191

This was resolved and added with issue #620.

Squashed commit of the following:

commit eb87af0
Author: Issac Kelly <issac.kelly@gmail.com>
Date:   Mon Aug 27 22:58:16 2012 -0700

    Remove unused file and empty tests file. Resources are tested via the fields.py file

commit 00453c1
Author: Issac Kelly <issac.kelly@gmail.com>
Date:   Mon Aug 27 22:53:24 2012 -0700

    Whitespace

commit 67298f4
Author: Issac Kelly <issac.kelly@gmail.com>
Date:   Mon Aug 27 22:39:44 2012 -0700

    Add Docs.

commit 179ab3f
Author: Issac Kelly <issac.kelly@gmail.com>
Date:   Mon Aug 27 21:17:10 2012 -0700

    Fix field init tests. Thanks Josh Bohde

commit 7b37078
Author: Issac Kelly <issac.kelly@gmail.com>
Date:   Mon Aug 27 21:00:29 2012 -0700

    Whitespace cleanup and comments.

commit 02988b0
Author: Issac Kelly <issac.kelly@gmail.com>
Date:   Mon Aug 13 23:28:01 2012 -0700

    WIP contenttypes/GFK integration.

    Needs:
    more thought about tests
    code review
    release notes
    documentation
    gracious thanking of the people who helped
    Python 2.5 solution for context managers in tests.
f5ae8bf
Matt Hughes hughes referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Matt Hughes hughes referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Greg MacWilliam

It would be really helpful if documentation was updated to include usage of filtering on a GenericForeignKeyField. I've found this question unanswered on StackOverflow numerous times, my latest question instance is here. Thanks a bunch, Tastypie is crazy good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 5, 2011
  1. David Hatch
  2. David Hatch

    fixed syntax error

    dhatch authored
  3. David Hatch

    fixed incorrect import

    dhatch authored
  4. David Hatch

    fixed circular import

    dhatch authored
  5. David Hatch

    forgot to include self!

    dhatch authored
  6. David Hatch

    fixed problems making get_via_uri() Resource type agnostic

    dhatch authored
    made the wrap_view function set a parent_resource attribute on the function returned to make it easy to find out what resource "owns" a view function
  7. David Hatch

    few todos for reminders, etc

    dhatch authored
  8. David Hatch
  9. David Hatch

    completed read tests

    dhatch authored
Commits on Jul 6, 2011
  1. David Hatch

    added proper support for ContentTypeResource and ContentTypeField whi…

    dhatch authored
    …ch enabled contenttype_field attribute of RelatedField to function
    
    tests pass
  2. David Hatch
  3. David Hatch
  4. David Hatch
Commits on Jul 31, 2011
  1. David Hatch

    fixed some comments

    dhatch authored
This page is out of date. Refresh to see the latest.
110 tastypie/fields.py
View
@@ -5,10 +5,9 @@
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.utils import datetime_safe, importlib
from tastypie.bundle import Bundle
-from tastypie.exceptions import ApiFieldError, NotFound
+from tastypie.exceptions import ApiFieldError, NotFound, BadRequest
from tastypie.utils import dict_strip_unicode_keys
-
class NOT_PROVIDED:
pass
@@ -381,12 +380,12 @@ class RelatedField(ApiField):
self_referential = False
help_text = 'A related resource. Can be either a URI or set of nested resource data.'
- def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None):
+ def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None, contenttype_field=None):
"""
Builds the field and prepares it to access to related data.
The ``to`` argument should point to a ``Resource`` class, NOT
- to a ``Model``. Required.
+ to a ``Model`` or be a dictionary matching ``Model`` classes to ``Resource`` classes. Required.
The ``attribute`` argument should specify what field/callable points to
the related data on the instance object. Required.
@@ -416,6 +415,11 @@ def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=
Optionally accepts ``help_text``, which lets you provide a
human-readable description of the field exposed at the schema level.
Defaults to the per-Field definition.
+
+ Optionally accepts ``contenttype_field`` which is the
+ field which points to the appropriate contenttype for the relation.
+ Dictionary must be provided for ``to`` to provide ``Resource`` mappings
+ for possible content types or a ``ValueError`` will be raised.
"""
self.instance_name = None
self._resource = None
@@ -431,7 +435,20 @@ def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=
self.resource_name = None
self.unique = unique
self._to_class = None
-
+ self.contenttype_field = contenttype_field
+
+ if self.contenttype_field and not isinstance(self.to, dict):
+ raise ValueError(
+ "to argument must be a dictionary " +
+ "when used with contenttype_field")
+
+ if self.contenttype_field and not issubclass(type(self.contenttype_field), ToOneField):
+ raise ValueError(
+ "contenttype_field must be a ToOneField which provides access"+
+ " to the Resource's content_type ForeignKey")
+ from tastypie.resources import ContentTypeResource
+ if not issubclass(type(self.contentype_field.to), ContentTypeResource):
+ raise ValueError("contenttype_field.to must be a ContentTypeResource or subclass")
if self.to == 'self':
self.self_referential = True
self._to_class = self.__class__
@@ -454,6 +471,14 @@ def get_related_resource(self, related_instance):
"""
related_resource = self.to_class()
+ if isinstance(self.to, dict):
+ # we're using a dict for self.to. we also finally know the
+ # actual type of the related object
+ self._to_class = self.to[type(related_instance)]
+ # TODO make so if key is not in dictionary we check if we have default or set to null
+ related_resource = self.to_class()
+
+
# Fix the ``api_name`` if it's not present.
if related_resource._meta.api_name is None:
if self._resource and not self._resource._meta.api_name is None:
@@ -472,7 +497,14 @@ def to_class(self):
return self._to_class
if not isinstance(self.to, basestring):
- self._to_class = self.to
+ if isinstance(self.to, dict):
+ # we're expected to return a functioning resource class
+ # import is here because at top it creates a circular import to
+ # resources
+ from tastypie.resources import ModelResource
+ self._to_class = ModelResource
+ else:
+ self._to_class = self.to
return self._to_class
# It's a string. Let's figure it out.
@@ -506,26 +538,40 @@ def dehydrate_related(self, bundle, related_resource):
bundle = related_resource.build_bundle(obj=related_resource.instance, request=bundle.request)
return related_resource.full_dehydrate(bundle)
- def build_related_resource(self, value, request=None):
+ def build_related_resource(self, value, request=None, resource_type=None):
"""
Used to ``hydrate`` the data provided. If just a URL is provided,
the related resource is attempted to be loaded. If a
dictionary-like structure is provided, a fresh resource is
created.
"""
- self.fk_resource = self.to_class()
+ if resource_type:
+ self._to_class = resource_type
+
+ self.fk_resource = self.to_class()
+
if isinstance(value, basestring):
# We got a URI. Load the object and assign it.
try:
obj = self.fk_resource.get_via_uri(value)
+ # at this point even though our obj is the right type,
+ # fk_resource may be the wrong type. set it to the right type
+ if isinstance(self.to, dict):
+ self.fk_resource = self.get_related_resource(obj)
bundle = self.fk_resource.build_bundle(obj=obj, request=request)
return self.fk_resource.full_dehydrate(bundle)
except ObjectDoesNotExist:
raise ApiFieldError("Could not find the provided object via resource URI '%s'." % value)
elif hasattr(value, 'items'):
+ # Make sure they included the contenttype_field which is required
+ # if the api is used in this way.
+ if not resource_type and isinstance(self.to, dict):
+ if self.contenttype_field:
+ raise BadRequest("You must set the %s field when setting a GenericForeignKey in this way" % (self.contenttype_field.instance_name))
# Try to hydrate the data provided.
value = dict_strip_unicode_keys(value)
+
self.fk_bundle = self.fk_resource.build_bundle(data=value, request=request)
# We need to check to see if updates are allowed on the FK
@@ -564,8 +610,11 @@ class ToOneField(RelatedField):
"""
help_text = 'A single related resource. Can be either a URI or set of nested resource data.'
- def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None):
- super(ToOneField, self).__init__(to, attribute, related_name=related_name, default=default, null=null, blank=blank, readonly=readonly, full=full, unique=unique, help_text=help_text)
+ def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None, contenttype_field=None):
+ if isinstance(to, dict):
+ if contenttype_field:
+ help_text = 'A single related resource. Can be either a URI or set of nested resource data. If nested resource data is provided, the resource\'s %s must be set.' % contenttype_field.instance_name
+ super(ToOneField, self).__init__(to, attribute, related_name=related_name, default=default, null=null, blank=blank, readonly=readonly, full=full, unique=unique, help_text=help_text, contenttype_field=contenttype_field)
self.fk_resource = None
def dehydrate(self, bundle):
@@ -590,7 +639,27 @@ def hydrate(self, bundle):
if value is None:
return value
- return self.build_related_resource(value, request=bundle.request)
+ resource_type = None
+ # see if we have a contenttype_field
+ if self.contenttype_field:
+ # find out the class of model we're looking at from this field
+ related_content_type = self.contenttype_field.hydrate(bundle)
+ if related_content_type:
+ resource_type = self.to[related_content_type.obj.model_class()]
+ else:
+ # check to see if the obj knows its content type
+ try:
+ if hasattr(bundle.obj, self.contenttype_field.attribute):
+ resource_type = getattr(bundle.obj, self.contenttype_field.attribute)
+ if resource_type:
+ resource_type = self.to[resource_type.model_class()]
+ except ObjectDoesNotExist:
+ resource_type = None
+ if 'content_type' in bundle.data and not 'content_object' in bundle.data:
+ raise BadRequest("You must supply a content_object when setting content_type")
+
+ return self.build_related_resource(value, request=bundle.request,
+ resource_type=resource_type)
class ForeignKey(ToOneField):
"""
@@ -605,7 +674,20 @@ class OneToOneField(ToOneField):
"""
pass
-
+class ContentTypeField(ToOneField):
+ """
+ A convenience subclass to easily create a ContentTypeField for a
+ ``ContentType`` ForeignKey. Assumes ``django.contrib.contentypes`` is an
+ installed app. Ensure you register ``ContentTypeResource`` with your
+ ``Api`` or include ContentTypeResource URLs in another manner.
+ """
+ def __init__(self, to=None, attribute="content_type", related_name=None, default=NOT_PROVIDED, null=False, blank=True, readonly=False, full=False, unique=False, help_text=None, contenttype_field=None):
+ from tastypie.resources import ContentTypeResource
+ if not to:
+ to = ContentTypeResource
+ super(ToOneField, self).__init__(to, attribute, related_name=related_name, default=default, null=null, blank=blank, readonly=readonly, full=full, unique=unique, help_text=help_text, contenttype_field=contenttype_field)
+ self.fk_resource = None
+
class ToManyField(RelatedField):
"""
Provides access to related data via a join table.
@@ -615,6 +697,10 @@ class ToManyField(RelatedField):
Note that the ``hydrate`` portions of this field are quite different than
any other field. ``hydrate_m2m`` actually handles the data and relations.
This is due to the way Django implements M2M relationships.
+
+ Can be used to represent a ``GenericRelation``
+ (reverse of ``GenericForeignKey``). Simply set ``to`` to the resource
+ which represents the other end of the relation.
"""
is_m2m = True
help_text = 'Many related resources. Can be either a list of URIs or list of individually nested resource data.'
26 tastypie/resources.py
View
@@ -210,6 +210,8 @@ def wrapper(request, *args, **kwargs):
# error message.
return self._handle_500(request, e)
+ # make it easier to find out what resource the view belongs to
+ wrapper.parent_resource = self
return wrapper
def _handle_500(self, request, exception):
@@ -625,7 +627,10 @@ def get_via_uri(self, uri):
except Resolver404:
raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri)
- return self.obj_get(**self.remove_api_resource_names(kwargs))
+ # view's parent_resource will always give us the correct resource for
+ # that view
+ # TODO maybe this should be a class method now
+ return view.parent_resource.obj_get(**self.remove_api_resource_names(kwargs))
# Data preparation.
@@ -1800,6 +1805,25 @@ def get_resource_uri(self, bundle_or_obj):
return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
+class ContentTypeResource(ModelResource):
+ """
+ Convenience model to represent ContentType model
+ """
+ # import here since otherwise importing TastyPie.resources will cause an
+ # error unless django.contrib.contenttypes is enabled
+ def __init__(self, *args, **kwargs):
+ from django.contrib.contenttypes.models import ContentType
+ self.Meta.queryset = ContentType.objects.all()
+ self.Meta.object_class = self.Meta.queryset.model
+ self._meta.queryset = ContentType.objects.all()
+ self._meta.object_class = self.Meta.queryset.model
+ super(ContentTypeResource,self).__init__(*args, **kwargs)
+
+ class Meta:
+ fields = ['model']
+ detail_allowed_methods = ['get',]
+ list_allowed_methods = ['get',]
+
class NamespacedModelResource(ModelResource):
"""
A ModelResource subclass that respects Django namespaces.
13 tests/related_resource/api/resources.py
View
@@ -3,7 +3,7 @@
from tastypie.resources import ModelResource
from tastypie.authorization import Authorization
from core.models import Note
-from related_resource.models import Category, Tag, ExtraData, Taggable, TaggableTag
+from related_resource.models import Category, Tag, ExtraData, Taggable, TaggableTag, GenericTag
class UserResource(ModelResource):
@@ -24,7 +24,7 @@ class Meta:
class CategoryResource(ModelResource):
parent = fields.ToOneField('self', 'parent', null=True)
-
+ tags = fields.ToManyField('related_resource.api.resources.GenericTagResource', attribute="tags", null=True, blank=True)
class Meta:
resource_name = 'category'
queryset = Category.objects.all()
@@ -81,3 +81,12 @@ class Meta:
queryset = ExtraData.objects.all()
authorization = Authorization()
+class GenericTagResource(ModelResource):
+ content_type = fields.ContentTypeField()
+ content_object = fields.ToOneField(
+ {Category : CategoryResource, Taggable : TaggableResource}, attribute="content_object", contenttype_field=content_type)
+
+ class Meta:
+ resource_name = 'generictag'
+ queryset = GenericTag.objects.all()
+ authorization = Authorization()
6 tests/related_resource/api/urls.py
View
@@ -2,7 +2,8 @@
from tastypie.api import Api
from related_resource.api.resources import NoteResource, UserResource, \
CategoryResource, TagResource, TaggableTagResource, TaggableResource, \
- ExtraDataResource
+ ExtraDataResource, GenericTagResource
+from tastypie.resources import ContentTypeResource
api = Api(api_name='v1')
api.register(NoteResource(), canonical=True)
@@ -12,5 +13,6 @@
api.register(TaggableResource(), canonical=True)
api.register(TaggableTagResource(), canonical=True)
api.register(ExtraDataResource(), canonical=True)
-
+api.register(GenericTagResource(), canonical=True)
+api.register(ContentTypeResource())
urlpatterns = api.urls
15 tests/related_resource/models.py
View
@@ -1,11 +1,11 @@
from django.db import models
-
+from django.contrib.contenttypes import generic
# A self-referrential model to test regressions.
class Category(models.Model):
parent = models.ForeignKey('self', null=True)
name = models.CharField(max_length=32)
-
+ tags = generic.GenericRelation('related_resource.GenericTag')
def __unicode__(self):
return u"%s (%s)" % (self.name, self.parent)
@@ -54,4 +54,13 @@ class ExtraData(models.Model):
def __unicode__(self):
return u"%s" % (self.name)
-
+# Tag which can be applied to any other model (will be tested on Categories and
+# Taggable)
+class GenericTag(models.Model):
+ name = models.CharField(max_length = 30)
+ object_id = models.PositiveIntegerField()
+ content_type = models.ForeignKey(generic.ContentType)
+ content_object = generic.GenericForeignKey()
+
+ def __unicode__(self):
+ return u"%s" % (self.name)
273 tests/related_resource/tests.py
View
@@ -5,9 +5,11 @@
from django.conf import settings
from related_resource.api.resources import UserResource, \
CategoryResource, TagResource, TaggableResource, TaggableTagResource, \
- ExtraDataResource
+ ExtraDataResource, GenericTagResource
from related_resource.api.urls import api
-from related_resource.models import Category, Tag, Taggable, TaggableTag, ExtraData
+from related_resource.models import Category, Tag, Taggable, TaggableTag, ExtraData, GenericTag
+from django.contrib.contenttypes.models import ContentType
+from tastypie.resources import ContentTypeResource
settings.DEBUG = True
@@ -162,3 +164,270 @@ def test_post_new_tag(self):
deserialized = json.loads(resp.content)
self.assertEqual(len(deserialized), 5)
self.assertEqual(deserialized['name'], 'school')
+
+class GenericForeignKeyTest(TestCase):
+ urls = 'related_resource.api.urls'
+
+ def setUp(self):
+ super(GenericForeignKeyTest, self).setUp()
+ # create some test objects to point generic relations to
+ self.category_1 = Category.objects.create(name="Programming")
+ self.taggable_1 = Taggable.objects.create(name="Programming Post")
+ # create a tag resources
+ self.tag_1 = GenericTag.objects.create(name='Python',
+ content_object=self.category_1)
+ self.tag_2 = GenericTag.objects.create(name='Django',
+ content_object=self.taggable_1)
+
+ def test_read_tag_content_object(self):
+
+ # access tag_1 through the database and assert that the content_object
+ # points to category_1
+ request = MockRequest()
+ request.GET = {'format': 'json'}
+ request.method = 'GET'
+
+ resource = api.canonical_resource_for('generictag')
+ # should get us self.tag_1
+ resp = resource.wrap_view('dispatch_detail')(request, pk=self.tag_1.pk)
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ # test that the uri for content_object pointed to self.category_1
+ self.assertEqual(data['content_object'],
+ CategoryResource().get_resource_uri(self.category_1))
+
+ # should get us self.tag_2
+ resp = resource.wrap_view('dispatch_detail')(request, pk=self.tag_2.pk)
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['content_object'],
+ TaggableResource().get_resource_uri(self.taggable_1))
+
+ def test_read_tag_content_object_full(self):
+ request = MockRequest()
+ request.GET = {'format': 'json'}
+ request.method = 'GET'
+
+ # set the content_object field to full mode
+ resource = api.canonical_resource_for('generictag')
+ resource.fields['content_object'].full = True
+
+ # check for self.tag_1 and self.category_1
+ resp = resource.wrap_view('dispatch_detail')(request, pk=self.tag_1.pk)
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['content_object'],
+ CategoryResource().full_dehydrate(
+ CategoryResource().build_bundle(obj=self.category_1,
+ request=request)).data)
+
+ # now for self.tag_2 and self.taggable_1
+ resp = resource.wrap_view('dispatch_detail')(request, pk=self.tag_2.pk)
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['content_object'],
+ TaggableResource().full_dehydrate(
+ TaggableResource().build_bundle(obj=self.taggable_1,
+ request=request)).data)
+
+ def test_post_by_uri(self):
+ """Create a new GenericTag item using POST request.
+ Point content_object to a category by it's uri"""
+ new_category = Category.objects.create(name="Design")
+ self.assertEqual(new_category.name, "Design")
+
+ request = MockRequest()
+ request.GET = {'format': 'json'}
+ request.method = 'POST'
+ request.raw_post_data = '{"name": "Photoshop", "content_object": "%s"}' % CategoryResource().get_resource_uri(new_category)
+
+ resource = api.canonical_resource_for('generictag')
+
+ resp = resource.wrap_view('dispatch_list')(request)
+ self.assertEqual(resp.status_code, 201)
+
+ # get newly created object via headers.locaion
+ self.assertTrue(resp.has_header('location'))
+ location = resp['location']
+
+ resp = self.client.get(location, data={"format": "json"})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['name'], 'Photoshop')
+ self.assertEqual(data['content_object'],
+ CategoryResource().get_resource_uri(new_category))
+
+ # now try doing this with a TaggableObject instead
+
+ new_taggable = Taggable.objects.create(name="Design Post")
+
+ request.raw_post_data = '{"name": "UX", "content_object": "%s"}' % TaggableResource().get_resource_uri(new_taggable)
+ resp = resource.wrap_view('dispatch_list')(request)
+ self.assertEqual(resp.status_code, 201)
+
+ self.assertTrue(resp.has_header('location'))
+ location = resp['location']
+
+ resp = self.client.get(location, data={"format" : "json"})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['name'], 'UX')
+ self.assertEqual(data['content_object'],
+ TaggableResource().get_resource_uri(new_taggable))
+
+ def test_post_by_data_requires_content_type(self):
+ """Make sure 400 (BadRequest) is the response if an attempt is made to post with data
+ for the GenericForeignKey without providing a content_type
+ """
+
+ request = MockRequest()
+ request.GET = {'format': 'json'}
+ request.method = 'POST'
+ request.raw_post_data = '{"name": "Photoshop", "content_object": %s}' % '{"name": "Design"}'
+
+ resource = api.canonical_resource_for('generictag')
+ resp = resource.wrap_view('dispatch_list')(request)
+ self.assertTrue(resp.status_code, 400)
+
+ def test_post_by_data(self):
+ """Create a new GenericTag item using a POST request.
+ content_type must be set on the new object and the serialized
+ data for the GenericForeignKey will be included in the POST
+ """
+
+ new_category = Category(name="Design")
+ self.assertEqual(new_category.name, "Design")
+
+ request = MockRequest()
+ request.GET = {'format': 'json'}
+ request.method = 'POST'
+ request.raw_post_data = (
+ '{"name": "Photoshop", "content_type": "%s", "content_object": {"name": "Design"}}'
+ % (ContentTypeResource().get_resource_uri(
+ ContentType.objects.get_for_model(Category))))
+
+ resource = api.canonical_resource_for('generictag')
+
+ resp = resource.wrap_view('dispatch_list')(request)
+ self.assertEqual(resp.status_code, 201)
+
+ # get newly created object via headers.locaion
+ self.assertTrue(resp.has_header('location'))
+ location = resp['location']
+
+ resp = self.client.get(location, data={"format": "json"})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['name'], 'Photoshop')
+ self.assertTrue(data['content_object'])
+ resp = self.client.get(data['content_object'], data={'format': 'json'})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['name'], new_category.name)
+ # make sure this represents a category
+ self.assertEqual(type(resource.get_via_uri(data['resource_uri'])),
+ Category)
+
+ # test posting taggable data instead of category this time
+ request.raw_post_data = (
+ '{"name": "Photoshop", "content_type": "%s", "content_object": {"name": "Design Post"}}'
+ % (ContentTypeResource().get_resource_uri(
+ ContentType.objects.get_for_model(Taggable))))
+
+ resp = resource.wrap_view('dispatch_list')(request)
+ self.assertEqual(resp.status_code, 201)
+
+ # get newly created object via headers.locaion
+ self.assertTrue(resp.has_header('location'))
+ location = resp['location']
+
+ resp = self.client.get(location, data={"format": "json"})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['name'], 'Photoshop')
+ self.assertTrue(data['content_object'])
+ resp = self.client.get(data['content_object'], data={'format': 'json'})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['name'], "Design Post")
+ # make sure this represents a category
+ self.assertEqual(type(resource.get_via_uri(data['resource_uri'])),
+ Taggable)
+
+ def test_put(self):
+ new_category = Category.objects.create(name="Design")
+ self.assertEqual(new_category.name, "Design")
+
+ request = MockRequest()
+ request.GET = {'format': 'json'}
+ request.method = 'POST'
+ request.raw_post_data = '{"name": "Photoshop", "content_object": "%s"}' % CategoryResource().get_resource_uri(new_category)
+
+ resource = api.canonical_resource_for('generictag')
+
+ resp = resource.wrap_view('dispatch_list')(request)
+ self.assertEqual(resp.status_code, 201)
+
+ # get newly created object via headers.locaion
+ self.assertTrue(resp.has_header('location'))
+ location = resp['location']
+
+ # put to this location and replace the name of content_object with "Web Design"
+ resp = self.client.get(location, data={"format": "json"})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['name'], 'Photoshop')
+ self.assertEqual(data['content_object'],
+ CategoryResource().get_resource_uri(new_category))
+ # now put the new data
+ request.raw_post_data = '{"content_object": {"name": "Web Design"}}'
+ request.method = 'PUT'
+ resp = resource.put_detail(request, pk=data['id'])
+ self.assertEqual(resp.status_code, 204)
+
+ # test putting a different content type
+ request.raw_post_data = ('{"content_type": "%s", "content_object": {"name": "Web Design"}}'
+ % (ContentTypeResource().get_resource_uri(
+ ContentType.objects.get_for_model(Taggable))))
+ resp = resource.put_detail(request, pk=data['id'])
+ self.assertEqual(resp.status_code, 204)
+ self.assertEqual(GenericTag.objects.get(pk=data['id']).content_type, ContentType.objects.get_for_model(Taggable))
+
+ def test_reverse(self):
+ tags = self.category_1.tags.all()
+
+ request = MockRequest()
+ request.GET = {'format': 'json'}
+ request.method = 'GET'
+
+ resource = api.canonical_resource_for('generictag')
+ resource.fields['content_object'].full = False
+ # should get us self.tag_1
+ resp = resource.wrap_view('dispatch_detail')(request, pk=self.tag_1.pk)
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ # test that the uri for content_object pointed to self.category_1
+ self.assertEqual(data['content_object'],
+ CategoryResource().get_resource_uri(self.category_1))
+
+ resp = self.client.get(data['content_object'], data={"format": "json"})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ tags_urls = list()
+ for tag in tags:
+ tags_urls.append(GenericTagResource().get_resource_uri(tag))
+ self.assertEqual(data['tags'], tags_urls)
+
+ # add some more tags to this category
+ GenericTag.objects.create(name="Object Orientated", content_object=self.category_1)
+ GenericTag.objects.create(name="Interpreted", content_object=self.category_1)
+
+ tags = self.category_1.tags.all()
+ tags_urls = list()
+ for tag in tags:
+ tags_urls.append(GenericTagResource().get_resource_uri(tag))
+
+ resp = self.client.get(data['resource_uri'], data={"format": "json"})
+ self.assertEqual(resp.status_code, 200)
+ data = json.loads(resp.content)
+ self.assertEqual(data['tags'], tags_urls)
Something went wrong with that request. Please try again.