Jeppe Larsen
Jeppe Larsen

Reputation: 13

Dynamically create Resources for multiple django models in tastypie

I need to expose multiple existing django models to tastypie. I got the basics covered with creating af ModelResource and registering it in urls.py. However, I would like to avoid writing Resource classes for every django model and since they all need to work the same way, I would like to have it generalised in some way.

So basically what I hope to archive is with a set of regular django models:

class ModelA:
  field1 = ...
  field2 = ...

class ModelB:
  field3 = ...
  field4 = ...

class ModelC:
  field8 = ...
  field9 = ...

And then automatically have them exposed to tastypie API as '/api/v1/modela/', '/api/v1/modelb/' and '/api/v1/modelc/ and so forth.

Not looking for a complete solution, just suggestions for a good approach to this.

Upvotes: 1

Views: 1166

Answers (2)

tstoev
tstoev

Reputation: 1435

create a custom base model derived from models.Model. In that model add an empty Resource class that has no object and follow the instructions in the Tastypie manuals:

https://django-tastypie.readthedocs.io/en/latest/non_orm_data_sources.html

Derive all models in your application from the newly created custom model and you have it.

In order for it to work you will need some magic. I am using a separate class that acts as a middleware between the ORM and the business layer(the model adapter), but I would say it should be possible to go without it.

What got me struggling was the way to pass the correct class in the init of the apiResource.

I ended overriding the init and registering the resource like that:

v1_api.register(BusinessClientContact.apiResource(BusinessClientContact))

Unfortunately there is no way to inject content into the meta class of the resource. You can get it to work, but as you start adding resources you will find out that the interpreter does not create separate instances of the class.Meta and the self._meta unless you do no make explicit changes in the code. The latter leaves all attempts to modify the _meta structure dynamically broken, since you work on the same _meta instance regardless of the fact that each of your resources is a different instance. The workaround I use is to inherit the apiResourceBase class in each of the model classes which I want to expose through the api and manually configure the Meta class. The code in the model class ends looking like that:

class apiResource(SystemModelBase.apiResourceBase):
    class Meta:
        resource_name = 'client_contacts'
        #more stuff here

Depending on the design you go for, you may expand the localized apiResource to enable custom views and filters.

A sample implementation of the resource class itself looks like this:

class apiResourceBase(Resource):        
    def __init__(self,f_modelClass):
        self.m_objMetaModel = f_modelClass.create_model_adapter()
        super(SystemModelBase.apiResource,self).__init__()

        #obj_create
        #obj_update
        #obj_delete_list
        #obj_delete
        #rollback

    def detail_uri_kwargs(self, bundle_or_obj):
        kwargs = {}
        if isinstance(bundle_or_obj, Bundle):
            kwargs['pk'] = bundle_or_obj.obj.id
        else:
            kwargs['pk'] = bundle_or_obj.id
        return kwargs
    def obj_get_list(self, bundle, **kwargs): 
        return self.get_object_list(bundle.request) 
    def get_object_list(self,request): 
        return self.m_objMetaModel.REST_IMPL.get_object_list(request)   
    def obj_get(self, bundle, **kwargs): 
        self.m_objMetaModel.load(kwargs['pk'],True)
        return self.m_objMetaModel.instance
    def dehydrate(self, bundle):
        return self.m_objMetaModel.REST_IMPL.dehydrate(bundle)

the actual implementation here (get_object_list and dehydrate) is out of scope but, it may help someone so I will add it:

    def get_object_list(self,request):
        l_clientID =  request.GET['client']
        l_filterDict = {}
        l_filterDict['client_account__id']=l_clientID
        return self.query_filter(l_filterDict)
    def dehydrate(self,bundle):
        l_objInstance = bundle.obj
        l_apiDict = {}
        l_apiDict['resource_uri'] = bundle.data['resource_uri']
        l_apiDict['id']           = l_objInstance.id
        l_apiDict['name']         = l_objInstance.user_account.name
        l_apiDict['email']        = l_objInstance.user_account.email
        l_apiDict['phone']        = l_objInstance.user_account.phone
        l_apiDict['additional_contacts'] = l_objInstance.user_account.additional_contacts
        l_apiDict['is_active']           = l_objInstance.user_account.user.is_active
        bundle.data = l_apiDict
        return bundl

this is a proof of concept code, in a production you can pretty much replicate the logic Tastypie uses to load models and expose those.

Upvotes: 1

Hedde van der Heide
Hedde van der Heide

Reputation: 22449

A Resource is 'just' a python class so you could simply create a base resource and inherit it with only the queryset and resource_name Meta attributes defined.

You could probably automate naming too by fiddling with the Resource class's __new__ method or create a custom classmethod, but I'm not sure the effort will gain you much.

Registering the classes to the api can be automated in numerous ways, of which one could be:

for name, obj in inspect.getmembers(sys.modules['resources']):
    if inspect.isclass(obj):  # might want to add a few exclusions
        v1_api.register(obj())

where 'resources' is the name of the module containing resources, but this is kind of implicit..

Upvotes: 1

Related Questions