django: how do I query based on GenericForeignKey's fields?
Asked Answered
L

2

55

I'm new in using GenericForeignKey, and I couldn't make it to work in a query statement. The tables are roughly like the following:

class Ticket(models.Model):
    issue_ct = models.ForeignKey(ContentType, related_name='issue_content_type')
    issue_id = models.PositiveIntegerField(null=True, blank=True)
    issue = generic.GenericForeignKey('issue_ct', 'issue_id')

class Issue(models.Model):
    scan = models.ForeignKey(Scan)

A scan creates one issue, an issue generates some tickets, and I made Issue as a foreign key to Ticket table. Now I have a Scan object, and I want to query for all the tickets that related to this scan. I tried this first:

tickets = Tickets.objects.filter(issue__scan=scan_obj)

which doesn't work. Then I tried this:

issue = Issue.objects.get(scan=scan_obj)
content_type = ContentType.objects.get_for_model(Issue)
tickets = Tickets.objects.filter(content_type=content_type, issue=issue)

Still doesn't work. I need to know how to do these kind of queries in django? Thanks.

Listel answered 14/8, 2012 at 20:42 Comment(0)
C
73

The Ticket.issue field you've defined will help you go from a Ticket instance to the Issue it's attached to, but it won't let you go backwards. You're close with your second example, but you need to use the issue_id field - you can't query on the GenericForeignKey (it just helps you retrieve the object when you have a Ticket instance). Try this:

from django.contrib.contenttypes.models import ContentType

issue = Issue.objects.get(scan=scan_obj)
tickets = Ticket.objects.filter(
    issue_id=issue.id,
    issue_ct=ContentType.objects.get_for_model(issue).id
    )
Caulfield answered 14/8, 2012 at 20:48 Comment(1)
@Caulfield issue_id is very confusing as it can refer to the field issue_id in question or the id attribute of issue field in the question, can we make them distinguishable?Atomics
C
15

Filtering across a GenericForeignKey can by creating a second model that shares the db_table with Ticket. First split up Ticket into an abstract model and concrete model.

class TicketBase(models.Model):
    issue_ct = models.ForeignKey(ContentType, related_name='issue_content_type')
    issue_id = models.PositiveIntegerField(null=True, blank=True)

    class Meta:
        abstract = True

class Ticket(TicketBase):
    issue = generic.GenericForeignKey('issue_ct', 'issue_id')

Then create a model that also subclasses TicketBase. This subclass will have all the same fields except issue which is instead defined as a ForeignKey. Adding a custom Manager allows it to be filtered to just a single ContentType.

Since this subclass does not need to be synced or migrated it can be created dynamically using type().

def subclass_for_content_type(content_type):
    class Meta:
        db_table = Ticket._meta.db_table

    class Manager(models.Manager):
        """ constrain queries to a single content type """
        def get_query_set(self):
            return super(Manager, self).get_query_set().filter(issue_ct=content_type)

    attrs = {
        'related_to': models.ForeignKey(content_type.model_class()),
        '__module__': 'myapp.models',
        'Meta': Meta,
        'objects': Manager()
    }
   return type("Ticket_%s" % content_type.name, (TicketBase,), attrs)
Cabbageworm answered 10/1, 2014 at 16:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.