Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Option to get related resource_uri with polymorphic resources #38

Merged
merged 4 commits into from

2 participants

leo-naeka Mitar
leo-naeka

Currently, when using polymorphic resources, we have resource_uri's mapped according to the polymorphic resource's resource_name.
This is perfect and the good behaviour when we want to group multiple specific resources into a common "entity".
But in some cases (and especially when you use js frameworks like emberjs/data -our case- or backbone) it is useful to get the targetted object with its original (related) resource_uri. Finally, the polymorphic resource acts like a "transparent proxy".

Please, look at the tests and how I have implemented it.

If you're satisfied, I'll add some doc and more tests for this option.

Signed-off-by: Léo S. leo@naeka.fr

tastypie_mongoengine/fields.py
@@ -23,6 +23,14 @@ def get_api_name(self):
return self._resource._meta.api_name
return None
+ def get_related_resource(self, related_instance):
+ related_resource = super(ApiNameMixin, self).get_related_resource(related_instance)
+ type_map = getattr(related_resource._meta, 'polymorphic', {})
+ if type_map and getattr(related_resource._meta, 'prefer_related_resource_name', False):
+ resource = related_resource._get_resource_from_class(type_map, related_instance.__class__)
+ related_resource._meta.resource_name = resource._meta.resource_name
Mitar Owner
mitar added a note

Hmm. Can related_resource object/class be shared among different instances of resources? In this case you are overriding here some value which could break something else?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mitar
Owner

I am not sure if I understand the problem correctly. If I understand correctly, you want that ContactResource be polymorphic class, while IndividualResource and CompanyResource be specific classes, and that resource_uri when accessing through ContactResource, point to IndividualResource and CompanyResource?

Why do you want IndividualResource and CompanyResource resources to be available in the first place? Why not just use ContactResource and polymorphic feature of django-tastypie-mongoengine?

Cosmetic comment: I am not sure if ApiNameMixin is correct place for this code.

leo-naeka

You've understood correctly.

In my case, I don't care of the ContactResource, I do not expose it through the API. But I use it to dehydrate correctly references to either IndividualResource or CompanyResource in other resources.

My client should not care about the resource_type and get the correct resource (full or not) directly. It should only care of Individual and Company and doesn't know Contact, this is backend use of inheritance.

Mitar
Owner

But why you need then Contact in the first place? Why not just have Contact be abstract and have two documents and two resources? Or even not having it abstract, but simple having only IndividualResource and CompanyResource as it is defined now? Because you want to make a reference in ContactGroupResource to any of those?

But you could use GenericReferenceField on MongoEngine to get this, no?

Mitar
Owner

(Just brainstorming here, trying to understand the landscape of possible ways to address this.)

leo-naeka

On the mongoengine layer, using a ReferenceField on the base model instead of a GenericReferenceField ensures that the reference is one of the sub-models.

But even if consider two separated resources : IndividualResource and CompanyResource, when I want to transparently refer to it in another resource, how can I do that ?

(Don't worry, also in brainstorming mode)

Mitar
Owner

You could always create your own GenericReferenceField subclass which would limit allowed documents (there is nothing at MongoDB level to prevent arbitrary links, this are all just MongoEngine checks anyway, I believe).

I never used GenericReferenceField field in resource. But it should probably be possible to do. And then you would just make a reference to any resource_uri you want.

Mitar
Owner

BTW, one general issue I have with this is: is it really callee who should define where does resource_uri point? Or it should be caller?

Mitar
Owner

One other thing; why do you need two resource endpoints, why is not enough to have only contacts and use polymorphic features of this package to get wanted resources? Your JS libraries do not support that?

leo-naeka

Yeah, those libraries may support that using adapters.
But that's not the expected behavior in my case: I need multiple endpoints for segmentation, my resources are quite different and should be acceeded through their own endpoint, never through a common one.

There is two ways to refers differenciated resources:

  • use the caller resource_uri, allowing grouping within a common endpoint
  • use the callee resource_uri, to refers to the original (related) endpoint
Mitar mitar commented on the diff
tests/test_project/urls.py
@@ -8,6 +8,10 @@
v1_api.register(resources.PersonResource())
v1_api.register(resources.PersonObjectClassResource())
v1_api.register(resources.OnlySubtypePersonResource())
+v1_api.register(resources.IndividualResource())
+v1_api.register(resources.CompanyResource())
+v1_api.register(resources.ContactResource())
Mitar Owner
mitar added a note

Do you have to register it?

If you don't want to expose it through the API, no.
In this case, I register it because of the test https://github.com/mitar/django-tastypie-mongoengine/pull/38/files#L4R1103

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mitar
Owner

OK. Sorry for being late in getting back to this. :-( This life is just too busy.

OK, I understand your need but I believe the approach is not nice. You are overriding resource_name when you just want resource uri to point differently. I would be more for approach of overriding resource uri method.

I still believe this should be simply done with custom field class. It would much less hackish and just reusing existing code.

Mitar
Owner

OK. After some looking around, it seems this is really the most reasonable approach. :-) So please add more tests, documentation and I have just some cosmetic requests:

  • use additional mixin, don't reuse ApiNameMixin (you can make new mixin and create another intermediate mixin combinig those two, but currently name ApiNameMixin does not have anything with what get_related_resource is doing)
  • prefer_related_resource_name in fact works only for polymorphic resources, when they are referenced somewhere else, name should somehow relate this
  • name also uses something quite programmatic, "related_resource_name", I am not sure people will understand this, what will they understand is that resource URIs will point to those polymorphic resources directly, not to the parent, maybe name should be polymorphic_resource_uri(s) or prefer_polymorphic_resource_uri or something like this?
  • currently, polymorphic option do not require polymorphic resources listed in polymorphic dict to be registered into API, with this flag this is required (otherwise reverse fails when constructing URI); as you name field "prefer" I think there should be a check which falls back to normal resource_name if resource is not registered (I hope this can be checked easily/cheaply)
  • tests for all such options, with and without flag and so on

I checked code and it seems there will be no problems with overriding that resource name.

Thanks for all you effort!

leo-naeka leo-naeka referenced this pull request from a commit in Naeka/django-tastypie-mongoengine
leo-naeka leo-naeka According to wlanslovenija/django-tastypie-mongoengine#38:
Use an additional mixin for get_related_resource
Changed meta property name to prefer_polymorphic_resource_uri
dd82774
leo-naeka leo-naeka Use an additional mixin for get_related_resource
Changed meta property name to prefer_polymorphic_resource_uri
b0a82de
leo-naeka

Regarding the registered check, I'm agree with you.
Unfortunately, the only way to check that is by checking in the Api registry. Neither the Api or the registry can be acceeded through the resource.
Anyway, we could use an inherited Api class which could store a is_registered boolean flag on the resource on register/unregister - cf. tastypie/api.py#L29-L62

Just tell me what's your opinion about that, for now I'll add some doc.

Mitar
Owner

I see two other options:

  • catch resolve exception and try not to use the polymorphic URI
  • instead of preferring, require polymorphic to be set for all resources

I like the first option a bit more. Because it is all about URIs, so if it cannot be found, the preferred one, we fall back to backup.

Mitar
Owner

Just to know: I do not get any notification when somebody just updates the pull request without any comment here. Sorry that I missed that you updated the pull request a while ago. :-( Please comment something here, like "updated", in the future.

Mitar
Owner

Sorry for delay.

Mitar mitar merged commit 15ecfb0 into from
Mitar
Owner

And thanks for the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 15, 2012
  1. leo-naeka
Commits on Jan 11, 2013
  1. leo-naeka

    Use an additional mixin for get_related_resource

    leo-naeka authored
    Changed meta property name to prefer_polymorphic_resource_uri
  2. leo-naeka

    Added a few documentation

    leo-naeka authored
  3. leo-naeka

    Check if resource is registered or don't override resource_name.

    leo-naeka authored
    Added some tests for this purpose
This page is out of date. Refresh to see the latest.
63 docs/usage.rst
View
@@ -161,3 +161,66 @@ an additional parameter ``type`` to ``Content-Type`` in your payload request::
Alternatively, you can pass a query string parameter.
All this works also for embedded documents in list.
+
+Polymorphic resource_uri
+------------------------
+
+By default, polymorphic resources are exposed through the API with a common
+``resource_uri``.
+
+In the previous case, ``PersonResource`` and ``StrangePersonResource`` are both
+exposed through the ``/<api_version>/person/`` resource URI.
+
+But in some cases, you may want to expose your resources through the polymorphic
+resource uri.
+To use this behaviour, you should set the ``prefer_polymorphic_resource_uri``
+meta variable to ``True``.
+
+You might define your resources as::
+
+ class IndividualResource(resources.MongoEngineResource):
+ class Meta:
+ queryset = documents.Individual.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+ paginator_class = paginator.Paginator
+
+ class CompanyResource(resources.MongoEngineResource):
+ class Meta:
+ queryset = documents.Company.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+ paginator_class = paginator.Paginator
+
+ class ContactResource(resources.MongoEngineResource):
+ class Meta:
+ queryset = documents.Contact.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+
+ prefer_polymorphic_resource_uri = True
+ polymorphic = {
+ 'individual': IndividualResource,
+ 'company': CompanyResource,
+ }
+
+You might now reference both resources::
+
+ class ContactGroupResource(resources.MongoEngineResource):
+ contacts = fields.ReferencedListField(of='test_project.test_app.api.resources.ContactResource', attribute='contacts', null=True)
+
+ class Meta:
+ queryset = documents.ContactGroup.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+
+And for each contact listed, the:
+
+* ``IndividualResource`` would be dehydrated to ``/<api_version>/individual/<id>/``
+* ``CompanyResource`` to ``/<api_version>/company/<id>/``
+
+.. warning::
+
+ The ``ContactResource`` could not be registered but be careful to register
+ all the resources present in the ``polymorphic`` *dict* otherwise the
+ dehydrated ``resource_uri`` will point to the parent resource.
19 tastypie_mongoengine/fields.py
View
@@ -23,7 +23,20 @@ def get_api_name(self):
return self._resource._meta.api_name
return None
-class BuildRelatedMixin(ApiNameMixin):
+class GetRelatedMixin(object):
+ def get_related_resource(self, related_instance):
+ related_resource = super(GetRelatedMixin, self).get_related_resource(related_instance)
+ type_map = getattr(related_resource._meta, 'polymorphic', {})
+ if type_map and getattr(related_resource._meta, 'prefer_polymorphic_resource_uri', False):
+ resource = related_resource._get_resource_from_class(type_map, related_instance.__class__)
+ if related_resource.get_resource_list_uri():
+ related_resource._meta.resource_name = resource._meta.resource_name
+ return related_resource
+
+class TastypieMongoengineMixin(ApiNameMixin, GetRelatedMixin):
+ pass
+
+class BuildRelatedMixin(TastypieMongoengineMixin):
def build_related_resource(self, value, **kwargs):
# A version of build_related_resource which allows only dictionary-like data
if hasattr(value, 'items'):
@@ -38,7 +51,7 @@ def build_related_resource(self, value, **kwargs):
else:
raise exceptions.ApiFieldError("The '%s' field was not given a dictionary-alike data: %s." % (self.instance_name, value))
-class ReferenceField(ApiNameMixin, fields.ToOneField):
+class ReferenceField(TastypieMongoengineMixin, fields.ToOneField):
"""
References another MongoEngine document.
"""
@@ -204,7 +217,7 @@ def to_class(self):
return self._to_class_with_listresource
-class ReferencedListField(ApiNameMixin, fields.ToManyField):
+class ReferencedListField(TastypieMongoengineMixin, fields.ToManyField):
"""
Represents a list of referenced objects. It must be used in conjunction
with ReferenceField.
3  tastypie_mongoengine/resources.py
View
@@ -310,6 +310,9 @@ def _wrap_polymorphic(self, resource, fun):
self._meta.queryset = resource._meta.queryset
self.base_fields = resource.base_fields.copy()
self.fields = resource.fields.copy()
+ if getattr(self._meta, 'prefer_polymorphic_resource_uri', False):
+ if resource.get_resource_list_uri():
+ self._meta.resource_name = resource._meta.resource_name
if getattr(self._meta, 'include_resource_type', True):
self.base_fields['resource_type'] = base_fields['resource_type']
self.fields['resource_type'] = fields['resource_type']
42 tests/test_project/test_app/api/resources.py
View
@@ -64,6 +64,48 @@ class Meta:
'strangeperson': EmbeddedStrangePersonResource,
}
+class IndividualResource(resources.MongoEngineResource):
+ class Meta:
+ queryset = documents.Individual.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+ paginator_class = paginator.Paginator
+
+class CompanyResource(resources.MongoEngineResource):
+ class Meta:
+ queryset = documents.Company.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+ paginator_class = paginator.Paginator
+
+class UnregisteredCompanyResource(resources.MongoEngineResource):
+ class Meta:
+ queryset = documents.UnregisteredCompany.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+ paginator_class = paginator.Paginator
+
+class ContactResource(resources.MongoEngineResource):
+ class Meta:
+ queryset = documents.Contact.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+
+ prefer_polymorphic_resource_uri = True
+ polymorphic = {
+ 'individual': IndividualResource,
+ 'company': CompanyResource,
+ 'unregisteredcompany': UnregisteredCompanyResource,
+ }
+
+class ContactGroupResource(resources.MongoEngineResource):
+ contacts = fields.ReferencedListField(of='test_project.test_app.api.resources.ContactResource', attribute='contacts', null=True)
+
+ class Meta:
+ queryset = documents.ContactGroup.objects.all()
+ allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
+ authorization = tastypie_authorization.Authorization()
+
class CustomerResource(resources.MongoEngineResource):
person = fields.ReferenceField(to='test_project.test_app.api.resources.PersonResource', attribute='person', full=True)
20 tests/test_project/test_app/documents.py
View
@@ -13,6 +13,26 @@ class Person(mongoengine.Document):
class StrangePerson(Person):
strange = mongoengine.StringField(max_length=100, required=True)
+
+class Contact(mongoengine.Document):
+ meta = {
+ 'allow_inheritance': True,
+ }
+
+ phone = mongoengine.StringField(max_length=16, required=True)
+
+class Individual(Contact):
+ name = mongoengine.StringField(max_length=200, required=True)
+
+class Company(Contact):
+ corporate_name = mongoengine.StringField(max_length=200, required=True)
+
+class UnregisteredCompany(Company):
+ pass
+
+class ContactGroup(mongoengine.Document):
+ contacts = mongoengine.ListField(mongoengine.ReferenceField(Contact, required=True))
+
class EmbeddedPerson(mongoengine.EmbeddedDocument):
name = mongoengine.StringField(max_length=200, required=True)
optional = mongoengine.StringField(max_length=200, required=False)
47 tests/test_project/test_app/tests/test_basic.py
View
@@ -416,7 +416,7 @@ def test_basic(self):
response = self.c.get(customer2_uri)
self.assertEqual(response.status_code, 200)
response = json.loads(response.content)
-
+
self.assertEqual(response['person']['name'], 'Person 1 PATCHED')
self.assertEqual(response['person']['optional'], 'Optional PATCHED')
@@ -1091,6 +1091,51 @@ def test_limited_polymorphic(self):
response = self.c.post(self.resourceListURI('onlysubtypeperson'), '{"name": "Person 1"}', content_type='application/json')
self.assertContains(response, 'Invalid object type', status_code=400)
+ def test_polymorphic_with_related_resource_names(self):
+ response = self.c.post(self.resourceListURI('individual'), '{"name": "Individual 1", "phone": "000-000000"}', content_type='application/json')
+ self.assertEqual(response.status_code, 201)
+ individual_uri = self.fullURItoAbsoluteURI(response['location'])
+
+ response = self.c.post(self.resourceListURI('company'), '{"corporate_name": "Company 1", "phone": "000-000000"}', content_type='application/json')
+ self.assertEqual(response.status_code, 201)
+ company_uri = self.fullURItoAbsoluteURI(response['location'])
+
+ response = self.c.get(self.resourceListURI('contact'), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ response = json.loads(response.content)
+
+ self.assertEqual(response['meta']['total_count'], 2)
+ for obj in response['objects']:
+ if obj['resource_type'] == 'individual':
+ self.assertEqual(obj['resource_uri'], individual_uri)
+ else:
+ self.assertEqual(obj['resource_uri'], company_uri)
+
+ response = self.c.post(self.resourceListURI('contactgroup'), '{"contacts": ["%s", "%s"]}' % (individual_uri, company_uri), content_type='application/json')
+ self.assertEqual(response.status_code, 201)
+
+ response = self.c.get(self.resourceListURI('contactgroup'), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ response = json.loads(response.content)
+
+ self.assertEqual(response['meta']['total_count'], 1)
+ self.assertIn(individual_uri, response['objects'][0]['contacts'])
+ self.assertIn(company_uri, response['objects'][0]['contacts'])
+
+ # Test fallback
+ # Because the resource is not registered, it should be added on the mongoengine layer
+ unreg_company = resources.UnregisteredCompanyResource()._meta.object_class(corporate_name='Unreg company', phone='000-000000')
+ unreg_company.save()
+
+ response = self.c.get(self.resourceListURI('contact'), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ response = json.loads(response.content)
+
+ self.assertEqual(response['meta']['total_count'], 3)
+ self.assertEqual(response['objects'][2]['resource_uri'], self.resourceDetailURI('company', unreg_company.id))
+ self.assertEqual(response['objects'][2]['resource_type'], 'unregisteredcompany')
+ self.assertEqual(response['objects'][2]['corporate_name'], 'Unreg company')
+
def test_polymorphic_duplicate_class(self):
with self.assertRaises(exceptions.ImproperlyConfigured):
class DuplicateSubtypePersonResource(tastypie_mongoengine_resources.MongoEngineResource):
4 tests/test_project/urls.py
View
@@ -8,6 +8,10 @@
v1_api.register(resources.PersonResource())
v1_api.register(resources.PersonObjectClassResource())
v1_api.register(resources.OnlySubtypePersonResource())
+v1_api.register(resources.IndividualResource())
+v1_api.register(resources.CompanyResource())
+v1_api.register(resources.ContactResource())
Mitar Owner
mitar added a note

Do you have to register it?

If you don't want to expose it through the API, no.
In this case, I register it because of the test https://github.com/mitar/django-tastypie-mongoengine/pull/38/files#L4R1103

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+v1_api.register(resources.ContactGroupResource())
v1_api.register(resources.CustomerResource())
v1_api.register(resources.BoardResource())
v1_api.register(resources.DocumentWithIDResource())
Something went wrong with that request. Please try again.