retrieving the 'many' end of a Generic Foreign Key relationship in Django
Asked Answered
L

2

19

In Django, when I request a resource that has a many-to-many relationship, I end up getting all the items in child part of the relationship, even those not directly related to the parent. It'll be easier if I show you with code (classes trimmed down to only show what's necessary):

Models

class Report(models.Model):
    name = models.CharField(max_length=255)
    slug = AutoSlugField(_('slug'), populate_from='name')
    wells = models.ManyToManyField(Well, null=True)
    uuid = UUIDField(editable=False, blank=True, version=4, unique=True)


class Well(models.Model):
    slug = AutoSlugField(_('slug'), populate_from='name')
    name = models.CharField(max_length=255)

class Node(models.Model):
    @property
    def well(self):
        raise NotImplementedError("The 'well' field must be implemented")
    //irrelevant GFK omitted
    page_content_type = models.ForeignKey(ContentType, null=True, blank=True, related_name='page')
    page_object_id = models.PositiveIntegerField(blank=True, null=True)
    page_content_object = generic.GenericForeignKey('page_content_type', 
     'page_object_id')

Resources

class ReportResource(ModelResource):
    wells = fields.ManyToManyField(WellResource, 'wells', full=True)
    stock = fields.ForeignKey(TickerResource, 'stock', full=True)

    class Meta:
        queryset = Report.objects.all()
        resource_name = 'ticker_reports'

class WellResource(ModelResource):
    nodes = fields.ToManyField('wells.api.NodeResource', 'nodes', full=True)
    type = fields.ForeignKey(WellTypeResource, 'type', full=True)

    class Meta:
        queryset = Well.objects.all()
        resource_name = 'wells'

class NodeResource(ModelResource):
    order = fields.IntegerField()
    content_object = GenericForeignKeyField({
                                                Content: UUIDOnlyContentResource
                                            }, 'content_object', full=True)

    class Meta:
        queryset = Node.objects.all()
        resource_name = 'nodes'
        filtering = {
            'ticker_report': ALL_WITH_RELATIONS
        }

A Ticker Report has many Wells, and these Wells are shared across all Ticker Reports. What is different is that you can tie Nodes to Wells; for a given ticker report, the only nodes that should display are the ones that are related to that Ticker Report.

So, for a given ticker report and a set of wells, only the nodes that share that GenericForeignKey to that Ticker report should be shown.

Relationships:

page_object_id,page_content_object, page_content_type is a GenericForeignKey relationship to Report

Currently, all Nodes are shown (this is a bug).

In TastyPie, how do I tell it to only show the related objects and not all objects?

Here's a short python console that shows the problem more succicintly:

>>> r = Report.objects.get(id=1)                                                                                                        
>>> for well in r.wells.all():                                                                                                          
...     for node in well.nodes.all():                                                                                                   
...         print 'Node in Well {0} is {1}'.format(well, node)                                                                          
...                                                                                                                                     
Node in Well The Areas You Must Watch (the-areas-you-must-watch - Fancy List) is Apple Content #1:Apple (0)                             
Node in Well The Areas You Must Watch (the-areas-you-must-watch - Fancy List) is First Solar Content #1:first solar (0)                 
Node in Well Risks (risks - Headline and Lead) is Apple Content #2:Apple (0)                                                            
Node in Well Risks (risks - Headline and Lead) is First Solar Content #2:first solar (0)                                                
>>> 

SQL Actual Output

SELECT node.id, node.uuid, node.order,node.content_type_id, node.object_id,
       node.page_content_type_id, node.page_object_id, node.well_id FROM node 
WHERE node.well_id = 1  
ORDER BY node.order ASC 

(Modified to make it easier to read)

Expected SQL Output:

SELECT node.id, node.uuid, node.order,node.content_type_id, node.object_id,
       node.page_content_type_id, node.page_object_id, node.well_id FROM node 
WHERE node.well_id = 1  AND node.page_content_type_id = 99 /*Report Content TypeID */ AND node.page_content_object_id = 1 /*ReportID*/
ORDER BY node.order ASC 

Expected output:

Node in Well The Areas You Must Watch is Apple Content #1
Node in Well Risks is Apple Content #2:Apple (0)

How can I filter out the child end of a many-to-many relationship with Django and TastyPie (though this problem is apparent without TastyPie as well, leaving me to believe it's a structural issue)

Loni answered 28/3, 2013 at 20:44 Comment(0)
R
5

When you execute the query

well.nodes.all()

this fetches all nodes that are related to well via the relationship you described in your model.

It sounds as though you want to limit the nodes returned to those that reference the Report object r via the page_content_object generic foreign key relation. Is that right? If it is, then you need to filter the nodes explicitly, like this:

r = Report.objects.get(id=1)
for well in r.wells.all():                                                                                                          
    for node in well.nodes.filter(page_object_id = r.id,
                                  page_content_type = ContentType.objects.get_for_model(r)):
        # ...

Update: I don't know anything about TastyPie and I have to say that I don't really understand what you are trying to do, but in Django, if there is a relation between wells and nodes, then you need to add it to your models. For example, if each Node belongs to exactly one Well, the natural thing to do would be to add a field to the Node model:

class Node(models.Model):
    # ...
    well = models.ForeignKey('Well')

and then if you want to find all nodes that belong to wells that belong to a particular Report object r, said nodes also referencing r via the generic foreign key relation, you'd issue the query:

 Node.objects.filter(well__report = r,
                     page_object_id = r.id,
                     page_content_type = ContentType.objects.get_for_model(r))

If you have to do this a lot, then a natural thing to do would be to add a method on the Report model.

Randell answered 1/4, 2013 at 13:16 Comment(0)
V
1

A Ticker Report has many Wells, and these Wells are shared across all Ticker Reports. What is different is that you can tie Nodes to Wells; for a given ticker report, the only nodes that should display are the ones that are related to that Ticker Report.

In Django, when I request a resource that has a many-to-many relationship, I end up getting all the items in child part of the relationship, even those not directly related to the parent. It'll be easier if I show you with code (classes trimmed down to only show what's necessary):

If I understand correctly, the node can be related to report or wells (because you mentioned related to that Ticke Report). And you are looking for the node related to the report, not the nodes related to the wells of the report. (because you are looking for node directly related to the parent(report?) )

If I am correctly, it is quite simple:

https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#reverse-generic-relations

class Report(models.Model):
    name = models.CharField(max_length=255)
    slug = AutoSlugField(_('slug'), populate_from='name')
    wells = models.ManyToManyField(Well, null=True)
    uuid = UUIDField(editable=False, blank=True, version=4, unique=True)

    nodes = generic.GenericRelation(Node)


# usage
r = Report.objects.get(id=1)
nodes = r.nodes.all()
Vanir answered 4/4, 2013 at 2:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.