Exposing "virtual" field in a tastypie view?
Asked Answered
S

4

4

I want to create a view using tastypie to expose certain objects of the same type, but with the following two three twists:

  1. I need to get the objects using three separate queries;
  2. I need to add a field which doesn't exist in the underlying model, and the value of that field depends on which of the queries it came from; and
  3. The data will be per-user (so I need to hook in to one of the methods that gets a request).

I'm not clear on how to hook into the tastypie lifecycle to accomplish this. The recommended way for adding a "virtual" field is in the dehydrate method, which only knows about the bundle it's operating on.

Even worse, there's no official way to join querysets.

My problem would go away if I could get tastypie to accept something other than a queryset. In that case I could pass it a list of subclasses of my object, with the additional field added.

I'm open to any other sensible solution.

Edit: Added twist 3 - per-user data.

Suttle answered 11/6, 2011 at 18:30 Comment(0)
S
1

OK, so this is my solution. Code is below.

Points to note:

  1. The work is basically all done in obj_get_list. That's where I run my queries, having access to the request.
  2. I can return a list from obj_get_list.
  3. I would probably have to override all of the other obj_* methods corresponding to the other operations (like obj_get, obj_create, etc) if I wanted them to be available.
  4. Because I don't have a queryset in Meta, I need to provide an object_class to tell tastypie's introspection what fields to offer.
  5. To expose my "virtual" attribute (which I create in obj_get_list), I need to add a field declaration for it.
  6. I've commented out the filters and authorisation limits because I don't need them right now. I'd need to implement them myself if I needed them.

Code:

from tastypie.resources import ModelResource
from tastypie import fields
from models import *
import logging

logger = logging.getLogger(__name__)


class CompanyResource(ModelResource):
    role = fields.CharField(attribute='role')


    class Meta:
        allowed_methods = ['get']
        resource_name = 'companies'
        object_class = CompanyUK
        # should probably have some sort of authentication here quite soon


    #filters does nothing. If it matters, hook them up
    def obj_get_list(self, request=None, **kwargs):
#         filters = {}

#         if hasattr(request, 'GET'):
#             # Grab a mutable copy.
#             filters = request.GET.copy()

#         # Update with the provided kwargs.
#         filters.update(kwargs)
#         applicable_filters = self.build_filters(filters=filters)

        try:
            #base_object_list = self.get_object_list(request).filter(**applicable_filters)
            def add_role(role):
                def add_role_company(link):
                    company = link.company
                    company.role = role
                    return company
                return add_role_company

            director_of = map(add_role('director'), DirectorsIndividual.objects.filter(individual__user=request.user))
            member_of   = map(add_role('member'),   MembersIndividual.objects.filter(individual__user=request.user))
            manager_of  = map(add_role('manager'),  CompanyManager.objects.filter(user=request.user))

            base_object_list = director_of + member_of + manager_of
            return base_object_list #self.apply_authorization_limits(request, base_object_list)
        except ValueError, e:
            raise BadRequest("Invalid resource lookup data provided (mismatched type).")
Suttle answered 12/6, 2011 at 12:21 Comment(1)
Stumbled over similar problem here. Just want to note that you can also override alter_detail_data_to_serialize() and alter_list_data_to_serialize() to add "virtual" fields.Patency
K
8

In the last version you should override the dehydrate method, e.g.

def dehydrate(self, bundle):
    bundle.data['full_name'] = bundle.obj.get_full_name()
    return bundle
Korwun answered 29/6, 2011 at 8:3 Comment(2)
The actual problem is that it's not always possible to access request in dehydrate(). The request should be stored in bundle.request, but it actually weren't there earlier, is this fixed now?Patency
using bundle.request works on the development version as of todayRoue
P
7

Stumbled over similar problem here. In my case, items in the list could be "checked" by user.

  • When an item is retrieved by AJAX, its checked status is returned with the resource as a normal field.
  • When an item is saved to the server, "checked" field from the resource is stored in user's session.

First I thought hydrate() and dehydrate() methods to be the best match for this job, but turned out there are problems with accessing request object in these. So I went with alter_data_to_serialize() and obj_update(). I think there's no need to override obj_create(), since item can't be checked when it's first created, I think.

Here is the code, but note that it hasn't been properly tested yet.

class ItemResource(ModelResource):
    def get_object_checked_status(self, obj, request):
        if hasattr(request, 'session'):
            session = request.session
            session_data = session.get(get_item_session_key(obj), dict())
            return session_data.get('checked', False)
        return False

    def save_object_checked_status(self, obj, data, request):
        if hasattr(request, 'session'):
            session_key = get_item_session_key(obj)
            session_data = request.session.get(session_key, dict())
            session_data['checked'] = data.pop('checked', False)
            request.session[session_key] = session_data

    # Overridden methods
    def alter_detail_data_to_serialize(self, request, bundle):
        # object > resource
        bundle.data['checked'] = \
            self.get_object_checked_status(bundle.obj, request)
        return bundle

    def alter_list_data_to_serialize(self, request, to_be_serialized):
        # objects > resource
        for bundle in to_be_serialized['objects']:
            bundle.data['checked'] = \
                self.get_object_checked_status(bundle.obj, request)
        return to_be_serialized

    def obj_update(self, bundle, request=None, **kwargs):
        # resource > object
        save_object_checked_status(bundle.obj, bundle.data, request)
        return super(ItemResource, self)\
            .obj_update(bundle, request, **kwargs)

def get_item_session_key(obj): return 'item-%s' % obj.id
Patency answered 25/6, 2011 at 16:49 Comment(0)
S
1

OK, so this is my solution. Code is below.

Points to note:

  1. The work is basically all done in obj_get_list. That's where I run my queries, having access to the request.
  2. I can return a list from obj_get_list.
  3. I would probably have to override all of the other obj_* methods corresponding to the other operations (like obj_get, obj_create, etc) if I wanted them to be available.
  4. Because I don't have a queryset in Meta, I need to provide an object_class to tell tastypie's introspection what fields to offer.
  5. To expose my "virtual" attribute (which I create in obj_get_list), I need to add a field declaration for it.
  6. I've commented out the filters and authorisation limits because I don't need them right now. I'd need to implement them myself if I needed them.

Code:

from tastypie.resources import ModelResource
from tastypie import fields
from models import *
import logging

logger = logging.getLogger(__name__)


class CompanyResource(ModelResource):
    role = fields.CharField(attribute='role')


    class Meta:
        allowed_methods = ['get']
        resource_name = 'companies'
        object_class = CompanyUK
        # should probably have some sort of authentication here quite soon


    #filters does nothing. If it matters, hook them up
    def obj_get_list(self, request=None, **kwargs):
#         filters = {}

#         if hasattr(request, 'GET'):
#             # Grab a mutable copy.
#             filters = request.GET.copy()

#         # Update with the provided kwargs.
#         filters.update(kwargs)
#         applicable_filters = self.build_filters(filters=filters)

        try:
            #base_object_list = self.get_object_list(request).filter(**applicable_filters)
            def add_role(role):
                def add_role_company(link):
                    company = link.company
                    company.role = role
                    return company
                return add_role_company

            director_of = map(add_role('director'), DirectorsIndividual.objects.filter(individual__user=request.user))
            member_of   = map(add_role('member'),   MembersIndividual.objects.filter(individual__user=request.user))
            manager_of  = map(add_role('manager'),  CompanyManager.objects.filter(user=request.user))

            base_object_list = director_of + member_of + manager_of
            return base_object_list #self.apply_authorization_limits(request, base_object_list)
        except ValueError, e:
            raise BadRequest("Invalid resource lookup data provided (mismatched type).")
Suttle answered 12/6, 2011 at 12:21 Comment(1)
Stumbled over similar problem here. Just want to note that you can also override alter_detail_data_to_serialize() and alter_list_data_to_serialize() to add "virtual" fields.Patency
B
0

You can do something like this (not tested):

def alter_list_data_to_serialize(self, request, data):

    for index, row in enumerate(data['objects']):

        foo = Foo.objects.filter(baz=row.data['foo']).values()
        bar = Bar.objects.all().values()

        data['objects'][index].data['virtual_field'] = bar

    return data
Beka answered 7/7, 2013 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.