Generic relations #191

Closed
wants to merge 14 commits into
from
View
110 tastypie/fields.py
@@ -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.'
View
26 tastypie/resources.py
@@ -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.
View
13 tests/related_resource/api/resources.py
@@ -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()
View
6 tests/related_resource/api/urls.py
@@ -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
View
15 tests/related_resource/models.py
@@ -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)
View
273 tests/related_resource/tests.py
@@ -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)