Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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.
  • Loading branch information...
commit f5ae8bf995f8777a0f917c08723c2e8ef049dd76 1 parent c1eaaf0
@issackelly issackelly authored
View
70 docs/content_types.rst
@@ -0,0 +1,70 @@
+.. _ref-content_types:
+
+==================================
+ContentTypes and GenricForeignKeys
+==================================
+
+`Content Types`_ and GenericForeignKeys are for relationships where the model on
+one end is not defined by the model's schema.
+
+.. _Content Types: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
+
+If you're using GenericForeignKeys in django, you can use a
+GenericForeignKeyField in Tastypie.
+
+Usage
+=====
+
+Here's an example model with a GenericForeignKey taken from the Django docs::
+
+ from django.db import models
+ from django.contrib.contenttypes.models import ContentType
+ from django.contrib.contenttypes import generic
+
+ class TaggedItem(models.Model):
+ tag = models.SlugField()
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+
+ def __unicode__(self):
+ return self.tag
+
+A simple ModelResource for this model might look like this::
+
+ from tastypie.contrib.contenttypes.fields import GenericForeignKeyField
+ from tastypie.resources import ModelResource
+
+ from .models import Note, Quote, TaggedItem
+
+
+ class QuoteResource(ModelResource):
+
+ class Meta:
+ resource_name = 'quotes'
+ queryset = Quote.objects.all()
+
+
+ class NoteResource(ModelResource):
+
+ class Meta:
+ resource_name = 'notes'
+ queryset = Note.objects.all()
+
+
+ class TaggedItemResource(ModelResource):
+ content_object = GenericForeignKeyField({
+ Note: NoteResource,
+ Quote: QuoteResource
+ }, 'content_object')
+
+ class Meta:
+ resource_name = 'tagged_items'
+ queryset = TaggedItem.objects.all()
+
+Like ToOneField, you must add your GenericForeignKey attribute to your
+ModelResource definition. It will not be added automatically like most other
+field or attribute types. When you define it, you must also define the other
+models and match them to their resources in a dictionary, and pass that as the
+first argument, the second argument is the name of the attribute on the model
+that holds the GenericForeignKey.
View
1  docs/index.rst
@@ -26,6 +26,7 @@ interfaces.
throttling
paginator
geodjango
+ content_types
cookbook
debugging
View
0  tastypie/contrib/contenttypes/__init__.py
No changes.
View
53 tastypie/contrib/contenttypes/fields.py
@@ -0,0 +1,53 @@
+from tastypie import fields
+from tastypie.resources import Resource
+from tastypie.exceptions import ApiFieldError
+from django.db import models
+from django.core.exceptions import ObjectDoesNotExist
+from .resources import GenericResource
+
+
+class GenericForeignKeyField(fields.ToOneField):
+ """
+ Provides access to GenericForeignKey objects from the django content_types
+ framework.
+ """
+
+ 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.get(type(related_instance), None)
+
+ 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 and not issubclass(GenericResource, 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):
+ self._to_class = None
+ return super(GenericForeignKeyField, self).build_related_resource(*args, **kwargs)
View
34 tastypie/contrib/contenttypes/resources.py
@@ -0,0 +1,34 @@
+from tastypie.resources import ModelResource
+from tastypie.exceptions import NotFound
+from django.core.urlresolvers import resolve, Resolver404, get_script_prefix
+
+
+class GenericResource(ModelResource):
+ """
+ Provides a stand-in resource for GFK relations.
+ """
+
+ def get_via_uri(self, uri, request=None):
+ """
+ This pulls apart the salient bits of the URI and populates the
+ resource via a ``obj_get``.
+
+ Optionally accepts a ``request``.
+
+ If you need custom behavior based on other portions of the URI,
+ simply override this method.
+ """
+ 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)
+
+ parent_resource = view.func_closure[0].cell_contents.func_closure[0].cell_contents
+ return parent_resource.obj_get(**self.remove_api_resource_names(kwargs))
+
View
0  tests/content_gfk/__init__.py
No changes.
View
0  tests/content_gfk/api/__init__.py
No changes.
View
33 tests/content_gfk/api/resources.py
@@ -0,0 +1,33 @@
+from tastypie.contrib.contenttypes.fields import GenericForeignKeyField
+from tastypie.resources import ModelResource
+from content_gfk.models import Note, Quote, Definition, Rating
+
+
+class DefinitionResource(ModelResource):
+
+ class Meta:
+ resource_name = 'definitions'
+ queryset = Definition.objects.all()
+
+class NoteResource(ModelResource):
+
+ class Meta:
+ resource_name = 'notes'
+ queryset = Note.objects.all()
+
+
+class QuoteResource(ModelResource):
+
+ class Meta:
+ resource_name = 'quotes'
+ queryset = Quote.objects.all()
+
+class RatingResource(ModelResource):
+ content_object = GenericForeignKeyField({
+ Note: NoteResource,
+ Quote: QuoteResource
+ }, 'content_object')
+
+ class Meta:
+ resource_name = 'ratings'
+ queryset = Rating.objects.all()
View
13 tests/content_gfk/api/urls.py
@@ -0,0 +1,13 @@
+from django.conf.urls.defaults import *
+from tastypie.api import Api
+from content_gfk.api.resources import NoteResource, QuoteResource, \
+ RatingResource, DefinitionResource
+
+
+api = Api(api_name='v1')
+api.register(NoteResource())
+api.register(QuoteResource())
+api.register(RatingResource())
+api.register(DefinitionResource())
+
+urlpatterns = api.urls
View
26 tests/content_gfk/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+
+
+class Definition(models.Model):
+ word = models.CharField(max_length=255)
+ content = models.TextField()
+
+class Note(models.Model):
+ title = models.CharField(max_length=255)
+ content = models.TextField()
+
+
+class Quote(models.Model):
+ byline = models.CharField(max_length=255)
+ content = models.TextField()
+
+
+class Rating(models.Model):
+ RATINGS = [ (x, x) for x in range(1, 6) ]
+
+ rating = models.PositiveIntegerField(choices=RATINGS, default=3)
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
View
4 tests/content_gfk/tests/__init__.py
@@ -0,0 +1,4 @@
+import warnings
+warnings.simplefilter('ignore', Warning)
+
+from content_gfk.tests.fields import *
View
83 tests/content_gfk/tests/fields.py
@@ -0,0 +1,83 @@
+from django.test import TestCase
+from tastypie.contrib.contenttypes.fields import GenericForeignKeyField
+from tastypie.bundle import Bundle
+from content_gfk.models import Note, Quote, Rating, Definition
+from content_gfk.api.resources import NoteResource, DefinitionResource, \
+ QuoteResource, RatingResource
+
+
+class ContentTypeFieldTestCase(TestCase):
+
+ def test_init(self):
+ # Test that you have to use a dict some other resources
+ with self.assertRaises(ValueError):
+ GenericForeignKeyField(((Note, NoteResource)), 'nofield')
+
+ # Test that you must register some other resources
+ with self.assertRaises(ValueError):
+ GenericForeignKeyField({}, 'nofield')
+
+ # Test that the resources you raise must be models
+ with self.assertRaises(ValueError):
+ GenericForeignKeyField({NoteResource: Note}, 'nofield')
+
+ def test_get_related_resource(self):
+ gfk_field = GenericForeignKeyField({
+ Note: NoteResource,
+ Quote: QuoteResource
+ }, 'nofield')
+
+ definition_1 = Definition.objects.create(
+ word='toast',
+ content="Cook or brown (food, esp. bread or cheese)"
+ )
+
+ # Test that you can not link to a model that does not have a resource
+ with self.assertRaises(TypeError):
+ gfk_field.get_related_resource(definition_1)
+
+ note_1 = Note.objects.create(
+ title='All aboard the rest train',
+ content='Sometimes it is just better to lorem ipsum'
+ )
+
+ self.assertTrue(isinstance(gfk_field.get_related_resource(note_1), NoteResource))
+
+ def test_resource_from_uri(self):
+ note_2 = Note.objects.create(
+ title='Generic and such',
+ content='Sometimes it is to lorem ipsum'
+ )
+
+ gfk_field = GenericForeignKeyField({
+ Note: NoteResource,
+ Quote: QuoteResource
+ }, 'nofield')
+
+ self.assertEqual(
+ gfk_field.resource_from_uri(
+ gfk_field.to_class(),
+ '/api/v1/notes/%s/' % note_2.pk
+ ).obj,
+ note_2
+ )
+
+ def test_build_related_resource(self):
+ gfk_field = GenericForeignKeyField({
+ Note: NoteResource,
+ Quote: QuoteResource
+ }, 'nofield')
+
+ quote_1 = Quote.objects.create(
+ byline='Issac Kelly',
+ content='To ipsum or not to ipsum, that is the cliche'
+ )
+ qr = QuoteResource()
+ qr.build_bundle(obj=quote_1)
+
+ bundle = gfk_field.build_related_resource(
+ '/api/v1/quotes/%s/' % quote_1.pk
+ )
+
+ # Test that the GFK field builds the same as the QuoteResource
+ self.assertEqual(bundle.obj, quote_1)
View
5 tests/content_gfk/urls.py
@@ -0,0 +1,5 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+ (r'^api/', include('content_gfk.api.urls')),
+)
View
18 tests/manage_contenttypes.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+
+import os
+import sys
+
+from os.path import abspath, dirname, join
+from django.core.management import execute_manager
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+try:
+ import settings_contenttypes as settings
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings_core.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
+
View
2  tests/run_all_tests.sh
@@ -4,7 +4,7 @@ PYTHONPATH=$PWD:$PWD/..${PYTHONPATH:+:$PYTHONPATH}
export PYTHONPATH
# complex
-ALL="core basic alphanumeric slashless namespaced related validation gis"
+ALL="core basic alphanumeric slashless namespaced related validation gis content_gfk"
if [ $# -eq 0 ]; then
TYPES=$ALL
View
7 tests/settings_content_gfk.py
@@ -0,0 +1,7 @@
+from settings import *
+
+INSTALLED_APPS += [
+ 'content_gfk',
+]
+
+ROOT_URLCONF = 'content_gfk.urls'
Please sign in to comment.
Something went wrong with that request. Please try again.