How to add Foreign Keys to Django Field History? - django

I'm trying to track Foreign Keys using django-field-history, but when I add it, it does additional queries on every page using the Model
For example
from field_history.tracker import FieldHistoryTracker
class Author(models.Model):
user = models.ForeignKey('auth.user)
field_history = FieldHistoryTracker(['user'])
will always give more queries on pages using Author, like so
SELECT ••• FROM "auth_user" WHERE "auth_user"."id" = '2'
1239 similar queries. Duplicated 1235 times.
I've tried using user_id instead of user in Field History Tracker, but it will always return None. Using user.id or anything like it just returns an error.
I really need to keep that history data, but not at the cost of thousands of additional queries.
Also, would really enjoy keeping django-field-history as my whole DB is using it, but I'm aware I might have to switch package, and if so, which one would you advise ?

As far as my understanding goes, you are trying to log which user has updated, for this you should use _field_history_user as described in the documentation.
For example:
class Pizza(models.Model):
name = models.CharField(max_length=255)
updated_by = models.ForeignKey('auth.User')
field_history = FieldHistoryTracker(['name'])
#property
def _field_history_user(self):
return self.updated_by
It would always update which user has updated the row for this table.

Related

Constrain Django model by field value

Consider the following Django model:
class Account(models.Model):
ACCOUNT_CHOICES = [
('m', 'Main',),
('s','Secondary'),
('o', 'Other')
]
user = models.ForeignKey(User)
level = models.CharField(max_length=1, choices=ACCOUNT_CHOICES)
How can I enforce a database constraint of a maximum of one 'Main' account per user, while still allowing users any number of 'Secondary' or 'Other' accounts? In a sense, I want unique_together for user and level, but only when the value of level is m.
I know that I can manually check on saving, but I would prefer the database to check automatically and raise an IntegrityError when appropriate.
I don't think you can do that with your current model, but if those are the only two choices for the level field, consider changing it to a nullable BooleanField, for example
is_main = models.BooleanField(null=True)
and set it to None for secondary accounts. Then a unique_together will work because every null value is unique as far as SQL is concerned (see this answer).
Since there are more choices for the level field as you later clarified, you may add a third field and possibly override the .save() method to have it automatically set to None if level is not "m" for extra convenience.
Edit: If you are not concerned about portability, #Trent has suggested that PostgreSQL supports partial unique indexes, for example:
create unique index u_i on accounts(user_id, level_id) WHERE level_id = 'm';
Here is an SQL Fiddle.
Edit 2: Actually it looks like it is finally possible to create partial indexes in Django ORM starting from Django 2.2. See this question for details.

Joining three or more tables using django ORM

I have designed my models such way that all the models will have one to one relation on auth_user table which is User. For your quick reference I am pasting the Picture below
Now I want to select all the data related to username which are in tables BasicDetails, Department and Project. The below query is not fetching the results.
User.objects.select_related().get(username='user1')
Can someone help me on this?
-Vikram
You should use prefetch_related for efficiency, since your relation is reverse (you want to access the other records from User and not the other way around):
u = User.objects.prefetch_related('project_set', 'department_set', 'basicdetails_set').get(username='user1')
This will not generate a single query, but Django will use caching techniques to effectively produce less possible db overhead. If I recall correctly it will produce 4 queries (a single join in 4 tables might be slower anyway, depending on number of records, indexes etc). The benefit is that on subsequent requests no queres will be generated. For example to get a user's projects:
u.project_set.all() #hits the db
u.project_set.all() #cached version, no db hit
For more info see here https://docs.djangoproject.com/en/dev/topics/db/queries/#one-to-one-relationships.
EDIT: what is project_set?
If your Project model is defined like this
class Project(models.Model):
...
user = models.ForeignKey(User)
then you can do Project.objects.get(pk=1).user to access the user associated to a project instance, but how would you do the opposite (get all projects of a certain user)? Django will automatically include a '_set' property to the other model for convenience. Therefore we can get the projects of a certain user like this:
u = User.objects.get(pk=1)
user_objects = u.project_set.all()
However if you want to explicitly set a name for this reverse relation, django allows you define the ForeignKey with a related_name keyword argument like this:
class Project(models.Model):
...
user = models.ForeignKey(User, related_name='projects')
Now instead of .project_set you could use .projects to access a user's projects:
u = User.objects.get(pk=1)
user_objects = u.projects.all()

Adding ManyToMany extra field to QuerySet in django

Suppose I have following models:
class Thing(models.Model):
name = models.CharField(max_length=100)
ratings = models.ManyToManyField('auth.User', through='Rating')
class Rating(models.Model):
user = models.ForeignKey('auth.User')
thing = models.ForeignKey('Thing')
rating = models.IntegerField()
So I have a lot of things, and every user can rate every thing. I also have a view showing a list of all things (and they are huge in numbers) with a rating that user assigned to each of them. I need a way to retreive all the data from database: Thing objects with additional field user_rating taken from at most one (because we have a fixed User) related Rating object.
Trivial solution looks like that:
things = Thing.objects.all()
for thing in things:
try:
thing.user_rating = thing.ratings.objects.get(user=request.user).rating
except Rating.DoesNotExist:
thing.user_rating = None
But the flaw of this approach is obvious: if we have 500 things, we'll do 501 requests to database. Per one page. Per user. And this is the most viewed page of the site. This task is easily solvable with SQL JOINs but in practice I have more complicated schema and I will certainly benefit from Django model framework. So the question is: is it possible to do this Django-way? It would be really strange if it isn't, considering that such tasks are very common.
As I understood, neither annotate(), nor select_related() will help me here.
I guess you should try this:
https://docs.djangoproject.com/en/1.3/ref/models/querysets/#extra
Example
result = Thing.objects.all().extra(select={'rating': 'select rating from ratings where thing_id = id'})
Your result set gets a new field 'rating' for each 'thing' object.
I use this approach in one of my recent projects. It produces one complex query instead of n+1 queries.
Hope this helps :)
Since you are planning to display everything in one page. I can think of this approach. You can give this a try:
Get all the ratings given by the current user and Get all the Things.
Now try to create a dictionary like this:
thing_dict = {}
for thing in Thing.objects.all():
thing_dict[thing] = None
for rating in Rating.objects.filter(user = request.user):
thing_dict[rating.thing] = rating
Now thing_dict contains all the entries of model Thing as keys and has its rating as its value.
May not be the best way. I am keen on seeing what others answer.

Finding multiple instances of a tag with Django "through" field

I run a lab annotation website where users can annotate samples with tags relating to disease, tissue type, etc. Here is a simple example from models.py:
from django.contrib.auth.models import User
from django.db import models
class Sample(models.Model):
name = models.CharField(max_length = 255)
tags=models.ManyToManyField('Tag', through = 'Annot')
class Tag(models.Model):
name = models.CharField(max_length = 255)
class Annot(models.Model):
tag = models.ForeignKey('Tag')
sample = models.ForeignKey('Sample')
user = models.ForeignKey(User, null = True)
date = models.DateField(auto_now_add = True)
I'm looking for a query in django's ORM which will return the tags in which two users agree on the annotation of same tag. It would be helpful if I could supply a list of users to limit my query (if someone only believes User1 and User2 and wants to find the sample/tag pairs that only they agree on.)
I think I understood what you need. This one made me think, thanks! :-)
I believe the equivalent SQL query would be something like:
select t.name, s.name, count(user_id) count_of_users
from yourapp_annot a, yourapp_tag t, yourapp_sample s
where a.tag_id = t.id
and s.id = a.sample_id
group by t.name, s.name
having count_of_users > 1
While I try hard not to think in SQL when I'm coming up with django model navigation (it tends to get in the way); when it comes to aggregation queries it always helps me to visualize what the SQL would be.
In django we now have aggregations.
Here is what I came up with:
models.Annot.objects.select_related().values(
'tag__name','sample__name').annotate(
count_of_users=Count('user__id')).filter(count_of_users__gt=1)
The result set will contain the tag, the sample, and the count of users that tagged said sample with said tag.
Breaking it apart for the folks that are not used to django aggregation:
models.Annot.objects.select_related()
select_related() is forcing all tables related to Annot to be retrieved in the same query
This is what will allow me to specify tag__name and sample__name in the values() call
values('tag__name','sample__name')
values() is limiting the fields to retrieve to tag.name and sample.name
This makes sure that my aggregation on count of clients will group by just these fields
annotate(count_of_users=Count('user__id'))
annotate() adds an aggregation as an extra field to a query
filter(count_of_users__gt=1)
And finally I filter on the aggregate count.
If you want to add an additional filter on what users should be taken into account, you need to do this:
models.Annot.objects.filter(user=[... list of users...]).select_related().values(
'tag__name','sample__name').annotate(
count_of_users=Count('user__id')).filter(count_of_users__gt=1)
I think that is it.
One thing... Notice that I used tag__name and sample__name in the query above. But your models do not specify that tag names and sample names are unique.
Should they be unique? Add a unique=True to the field definitions in the models.
Shouldn't they be unique? You need to replace tag__name and sample__name with tag__id and sample__id in the query above.

How to set up Django admin.TabularInline on a table with a compount key

I've essentially got two tables: Page(PK=url) and PageProperty(PK=url+name).
Here is how I have my Models set up:
class Page(model.Model):
url = model.CharField(primary_key=True, max_length=255, db_column='url')
#.....
class PageProperty(model.Model):
# table with compound key (url + name)
url = model.ForeignKey('Page', to_field='url', db_column='url', primary_key=True)
name = model.CharField(primary_key=True, max_length=20)
value = model.TextField()
I have a ModelAdmin set up so I can Inline edit PageProperty(s) from Page. Its a legacy database and I know there's a lot of data in there. But the Admin is only showing ONE of the PagePropertys, not all.
I think you might need to apply the extra option to your TabularInline. Example:
class PagePropertyInline(admin.TabularInline):
model = PageProperty
extra = 3
You could probably do some magic to make the amount of extra items dynamic (such as the number of PageProperty objects for a given Page, but I'll leave that up to you.
I would suggest further reading on InlineModelAdmin options and Formsets.
Because it felt as thought a non-integer primary key was too much against the grain, I ended up buckling down and migrating the schema to use an auto generated integer pk for both tables. After that everything was smooth sailing again and the Inlines worked perfectly.