django: how do I query based on GenericForeignKey's fields? - django

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.

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
)

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)

Related

N duplicated queries nested model

I've got an Area model allowing sub areas (you might think of it as categories with subcategories). I reached this by nesting one field to self as foreign key.
class Area(models.Model):
area = models.CharField(max_length=120)
parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, related_name='subarea')
def __str__(self):
return self.area
With the django rest framwork I've manages to get the correct output. The problem is that when I analyze the request with django-toolbar multiple duplicated requests are made (N*Area(parent=None)). I've solved similar issues by using prefetch_related or select_related. But never done it with a nested model. Is there any way to solve this? Or is this design of the model bad?
I manage to serialize the correct output with the following view and
class ListArea(generics.ListCreateAPIView):
serializer_class = AreaSerializer
queryset = Area.objects.prefetch_related('parent').filter(parent=None)
and serializers
class SubAreaSerializer(serializers.ModelSerializer):
class Meta:
model = Area
fields = ('area','id')
class AreaSerializer(serializers.ModelSerializer):
subarea=SubAreaSerializer(many=True)
class Meta:
model = Area
fields = ('area','id','subarea')
Or might those extra calls be due to the browsable API?
Solution
I solved this with help of the following thread Django: Does prefetch_related() follow reverse relationship lookup?
Instead of
queryset = Area.objects.prefetch_related('parent').filter(parent=None)
I should use
queryset = Area.objects.prefetch_related('parent').prefetch_related('subarea')

How do I use a ModelManager on a Django ManyToMany through field?

Let's say I have the following models:
class Poll(model):
title = models.CharField()
class Option(model):
title = models.CharField()
polls = models.ManyToManyField(
Poll,
through='PollOption',
null=True,
blank=True,
related_name='options'
)
class PollOptionManager(models.Manager):
use_for_related_fields = True
def get_queryset(self):
return super(PollOptionManager, self).get_queryset().filter(
is_active=True
)
class PollOption(model):
poll = ForeignKey(Poll)
option = ForeignKey(Option)
is_active = BooleanField(default=True)
objects = PollOptionManager()
When I try to query Poll.options.all() I'm still getting Option instances for which PollOption.is_active is False. How can I get my model manager to appropriately filter my ManyToMany relationship based on a flag on the through field?
The problem is that the through model's (related) manager is never actually used in your scenario. In order to utilize the custom manager, you have to explicitly use it, e.g.:
class Poll(models.Model):
#property
def active_options(self):
return Option.objects.filter(id__in=self.polloption_set.values_list('option'))
Here, polloption_set filters out inactive options as intended. This, however, makes the manager kind of pointless because you can just as well put the extra filter in the custom property.

Django REST Framework Serializers: Display the latest object of a reverse relationship

The default behavior of the ListAPIView (code below) is to serialize all Report objects and the nested Log objects per Report object. What if I only want the latest Log object to be displayed per Report? How do I go about doing that?
# models.py
class Log(models.Model):
# ...
report = models.ForeignKey(Report)
timestamp = models.DateTimeField(default=datetime.datetime.now)
class Report(models.Model):
code = models.CharField(max_length=32, unique=True)
description = models.TextField()
# serializers.py
class LogSerializer(serializers.ModelSerializer):
class Meta:
model = Log
class ReportSerializer(serializers.ModelSerializer):
log_set = LogSerializer(many=True, read_only=True)
class Meta:
model = Report
fields = ('code', 'description', 'log_set')
# views.py
class ReportListView(generics.ListAPIView):
queryset = Report.objects.all()
serializer_class = ReportSerializer
I know I can do this by using a SerializerMethodField, but this can be a potentially expensive operation, since there will be an extra SQL query to retrieve the appropriate Log object for each Report object.
class ReportSerializer(serializers.ModelSerializer):
latest_log = serializers.SerializerMethodField()
class Meta:
model = Report
def get_latest_log(self, obj):
try:
latest_log = Log.objects.filter(report_id=obj.id).latest('timestamp')
except Log.DoesNotExist:
latest_log = None
return latest_log
If I have 1000 report objects, there will be 1000 extra queries if I want to render them all. How do I avoid those extra queries besides using pagination? Can anyone point me to the right direction? Thanks!
EDIT: Regarding the possible duplicate tag, the link alone provided by Mark did not completely clear up the picture for me. Todor's answer was more clear.
You need to somehow annotate the latest_log in the ReportQuerySet, so it can be used by the serializer without making any extra queries.
The simplest way to achieve this is by prefetching all the logs per report. The drawback of this approach is that you load in memory all the logs per report per page. Which is not so bad if one report got something like 5-10-15 logs. This will mean that for a page with 50 reports, you are gonna load 50*10=500 logs which is not a big deal. If there are more logs per report (lets say 100) then you need to make additional filtering of the queryset.
Here is some example code:
Prefetch the logs.
# views.py
class ReportListView(generics.ListAPIView):
queryset = Report.objects.all()\
.prefetch_related(Prefetch('log_set',
queryset=Log.objects.all().order_by('-timestamp'),
to_attr='latest_logs'
))
serializer_class = ReportSerializer
Create a helper method for easy access the latest_log
class Report(models.Model):
#...
#property
def latest_log(self):
if hasattr(self, 'latest_logs') and len(self.latest_logs) > 0:
return self.latest_logs[0]
#you can eventually implement some fallback logic here
#to get the latest log with a query if there is no cached latest_logs
return None
Finally the serializer just use the property
class ReportSerializer(serializers.ModelSerializer):
latest_log = serializers.LogSerializer()
class Meta:
model = Report
An example of a more advanced filtering of the logs can be something like this:
Report.objects.all().prefetch_related(Prefetch('log_set', queryset=Log.objects.all().extra(where=[
"`myapp_log`.`timestamp` = (\
SELECT max(timestamp) \
FROM `myapp_log` l2 \
WHERE l2.report == `myapp_log`.`report`\
)"]
), to_attr='latest_logs'
))
You can use select related argument. It will hit the database only once using JOIN.
class ReportListView(generics.ListAPIView):
queryset = Report.objects.select_related('log');
serializer_class = ReportSerializer

Django filter on generic relationship

I have a model below which points to a generic relationship. This can either be a contact object or a customer object.
class Unsubscribe(models.Model):
"""
Notes:
See: http://www.screamingatmyscreen.com/2012/6/django-and-generic-relations/
"""
content_type = models.ForeignKey(ContentType, help_text="Represents the name of the model")
object_id = models.PositiveIntegerField(help_text="stores the object id")
content_object = generic.GenericForeignKey('content_type', 'object_id')
reason = models.CharField(max_length=60)
request_made = models.DateTimeField(auto_now_add=True,
help_text="Shows when object was created.")
class Meta:
ordering = ['-request_made']
I would like to list out all unsubscribed both unsubscribe customers and contacts only for the user.
queryset = Unsubscribe.objects.filter()
Above gives me all unsubscribe customers and contacts for any users normally I would solve this by doing....
queryset = Unsubscribe.objects.filter(user=request.user)
However, Unsubscribe object does not have a user, but both customers and contacts do.
So how can I filter on the generic relationship?
You could try this
Unsubscribe.objects.filter(content_type__name='user', user=request.user)
For content_type__name='user' specify name of your model class for user or whatever you have associated it with.
Or this also
Unsubscribe.objects.filter(content_type__name='user', object_id=request.user.id)
I assume your model is like:
class Contact(models.Model):
...
user = models.ForeignKey(User)
You can get all the contacts related to the current user using:
contact_ids = [each.id for each in request.user.contact_set.all()]
You can get all the unsubscribed contacts for that user:
unsubscribed_contacts = Unsubscribe.objects.filter(content_type__name='contact', object_id__in=contact_ids)
Remember that a generic foreign key is just two fields, one that is a ForeignKey to a ContentType model, and the other which is the primary key of whichever model you're pointing to. So, to be more generic, you could do something like this:
content_type = ContentType.objects.get_for_model(User)
Unsubscribe.objects.filter(content_type=content_type, object_id=user.id)
for newcommers to this question that have django newer versions , content_type__name is just a property and you can't query with that. instead use content_type__model in your filter methods just like that:
Unsubscribe.objects.filter(content_type__model='user', user=request.user)

Error using a base class field in subclass unique_together meta option

Using the following code:
class Organization(models.Model):
name = models.CharField(max_length="100",)
alias = models.SlugField()
...
class Division(Organization):
parent_org = models.ForeignKey(Organization)
class Meta:
unique_together=['parent_org', 'alias']
...
Trying to syncdb give me this error:
Error: One or more models did not validate:
organizations.division: "unique_together" refers to alias. This is not in the
same model as the unique_together statement.
Any help is appreciated,
Thanks,
Eric
This is by design. Reading the documentation for the unique_together option, it states that:
It's used in the Django admin and is enforced at the database level.
If you look at the table that a subclass creates, you'll see that it doesn't actually have the fields that its parent has. Instead, it gets a soft Foreign Key to the parent table with a field name called [field]_ptr_id, where [field] is the name of the table you're inheriting from excluding the app name. So your division table has a Primary Foreign Key called organization_ptr_id.
Now because unique_together is enforced at the database level using the UNIQUE constraint, there's no way that I know of for the database to actually apply that to a field not in the table.
Your best bet is probably through using Validators at your business-logic level, or re-thinking your database schema to support the constraint.
Edit: As Manoj pointed out, you could also try using Model Validators such as validate_unique.
[Model] Validators would work for you. Perhaps simplest, though, would be to use:
class BaseOrganization(models.Model):
name = models.CharField(max_length="100",)
alias = models.SlugField()
class Meta:
abstract = True
class Organization(BaseOrganization):
pass
class Division(BaseOrganization):
parent_org = models.ForeignKey(Organization)
class Meta:
unique_together=['parent_org', 'alias']
Note: as with your current code, you could not have subdivisions of divisions.
This is a solution I recently used in Django 1.6 (thanks to Manoj Govindan for the idea):
class Organization(models.Model):
name = models.CharField(max_length="100",)
alias = models.SlugField()
...
class Division(Organization):
parent_org = models.ForeignKey(Organization)
# override Model.validate_unique
def validate_unique(self, exclude=None):
# these next 5 lines are directly from the Model.validate_unique source code
unique_checks, date_checks = self._get_unique_checks(exclude=exclude)
errors = self._perform_unique_checks(unique_checks)
date_errors = self._perform_date_checks(date_checks)
for k, v in date_errors.items():
errors.setdefault(k, []).extend(v)
# here I get a list of all pairs of parent_org, alias from the database (returned
# as a list of tuples) & check for a match, in which case you add a non-field
# error to the error list
pairs = Division.objects.exclude(pk=self.pk).values_list('parent_org', 'alias')
if (self.parent_org, self.alias) in pairs:
errors.setdefault(NON_FIELD_ERRORS, []).append('parent_org and alias must be unique')
# finally you raise the ValidationError that includes all validation errors,
# including your new unique constraint
if errors:
raise ValidationError(errors)
This does not strictly apply the question but is very closely related; unique_together will work if the base class is abstract. You can mark abstract model classes as such using:
class Meta():
abstract = True
This will prevent django from creating a table for the class, and its fields will be directly included in any subclasses. In this situation, unique_together is possible because all fields are in the same table.
https://docs.djangoproject.com/en/1.5/topics/db/models/#abstract-base-classes
I had similar challenge when trying to apply unique_together to a permission group created by a given client.
class ClientGroup(Group):
client = models.ForeignKey(Client, on_delete=models.CASCADE)
is_active = models.BooleanField()
class Meta():
# looking at the db i found a field called group_ptr_id.
# adding group_ptr did solve the problem.
unique_together = ('group_ptr', 'client', 'is_active')