How can I check if two models equal each other in Django? - python

models.py:
class office_list(models.Model):
name = models.CharField(max_length= 100)
num_of_pax = models.IntegerField()
class tg_list(models.Model):
name = models.CharField(max_length= 100)
num_of_pax = models.IntegerField()
How can I check that the office_list name equals to the tg_list name?
I want to check if any of the office_list.name == any of the tg_list.name

if you want
any of the office_list.name == any of the tg_list.name
you can do simple query with exists:
names = tg_list.objects.values_list('name', flat=True)
office_list.objects.filter(name__in=names).exists()

From the Django doc :
To compare two model instances, just use the standard Python comparison operator, the double equals sign: ==. Behind the scenes, that compares the primary key values of two models.
or :
Youu can do with __eq__ in python too:
See python docs too.

The accepted answer requires an DB call, and specifically checks 1 field. In a more realistic scenario, you would be checking some subset of fields to be equal - likely ignoring PK's, creation timestamps, amongst other things.
A more robust solution would be something like:
class MyModel(Model):
some_field_that_is_irrelevant_to_equivalence = models.CharField()
some_char_field = models.CharField()
some_integer_field = models.IntegerField()
_equivalent_if_fields_equal = (
'some_char_field',
'some_integer_field',
)
def is_equivalent(self, other: 'MyModel') -> bool:
"""Returns True if the provided `other` instance of MyModel
is effectively equivalent to self.
Keyword Arguments:
-- other: The other MyModel to compare this self to
"""
for field in self._equivalent_if_fields_equal:
try:
if getattr(self, field) != getattr(other, field):
return False
except AttributeError:
raise AttributeError(f"All fields should be present on both instances. `{field}` is missing.")
return True

Related

Annotating a Subquery with queryset filtering methods from another model through Many to Many fields

I'm not sure how to make this possible, but I'm hoping to understand the intended method to do the following:
I have a simple model:
class Author(models.Model):
id = models.TextField(primary_key=True, default=uuid4)
name = models.TextField()
main_titles = models.ManyToManyField(
"Book",
through="BookMainAuthor",
related_name="main_authors",
)
objects = AuthorManager.from_queryset(AuthorQuerySet)()
class Book(models.Model):
id = models.TextField(primary_key=True, default=uuid4)
title = models.TextField()
genre = models.ForeignKey("Genre", on_delete=models.CASCADE)
objects = BookManager.from_queryset(BookQuerySet)()
class BookQuerySet(QuerySet):
def by_genre_scifi(self) -> QuerySet:
return self.filter(**self.LOOKUP_POPULAR_SCIFI)
I'd like to add a new QuerySet method for AuthorQuerySet to annotate Author objects using the method from BookQuerySet above. I've tried the following, which is incorrect:
class AuthorQuerySet(QuerySet):
def annotate_with_total_titles_by_genres(self) -> QuerySet:
main_titles_by_genre_sci_fi_query = Book.objects.filter(main_authors__in=[OuterRef('pk')]).all()
.by_genre_sci_fi()
.annotate(cnt=Count('*'))
.values('cnt')[:1]
return self.annotate(sci_fi_titles_total =
Subquery(main_titles_by_genre_sci_fi_query, output_field=IntegerField()))
Intended usage:
annotated_authors = Author.objects.filter(<some filter>).annotate_with_total_titles_by_genres()
There are additional fields in the lookup not shown in the model above, but the method here is working, and returns a BookQuerySet filtered by the lookup:
Book.objects.filter(main_authors__in=['some_author_id']).all().by_genre_sci_fi()
Similarly, I can run the subquery independently and get the count like so:
`Book.objects.filter(main_authors__in=['some_author_id']).all()
.by_genre_sci_fi()
.annotate(cnt=Count('*'))
.values('cnt')[:1]`
Out[1]: <BookQuerySet [{'cnt': 1}]>
However when I try to annotate using the AuthorQuerySet method above, I get None for every entry.
I wonder if there is an issue here with OuterRef and using in which will evaluate each character independently if it receives a string. If I try running it without the square parens:
ProgrammingError: syntax error at or near ""bookshop_author"" LINE 1: ...RE (U0."deleted_at" IS NULL AND U1."author_id" IN "bookshop_...

Python convert string to class

I want to do a query on the django User table like this:
u = User.objects.filter(member__in = member_list)
where:
class Member(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
dob = models.DateField('Date of Birth', blank=True, null=True)
and member_list is a list of eligible members.
The query works fine but the problem is I do not actually know the model member is called member. It could be called anything.
I store the name of the model I want in a model called Category. I have a link to the name of the model through content_type.Category is defined as:
class Category(models.Model):
name = models.CharField('Category', max_length=30)
content_type = models.ForeignKey(ContentType)
filter_condition = JSONField(default="{}", help_text=_(u"Django ORM compatible lookup kwargs which are used to get the list of objects."))
user_link = models.CharField(_(u"Link to User table"), max_length=64, help_text=_(u"Name of the model field which links to the User table. 'No-link' means this is the User table."), default="No-link")
def clean (self):
if self.user_link == "No-link":
if self.content_type.app_label == "auth" and self.content_type.model == "user":
pass
else:
raise ValidationError(
_("Must specify the field that links to the user table.")
)
else:
if not hasattr(apps.get_model(self.content_type.app_label, self.content_type.model), self.user_link):
raise ValidationError(
_("Must specify the field that links to the user table.")
)
def __unicode__(self):
return self.name
def _get_user_filter (self):
return str(self.content_type.app_label)+'.'+str(self.content_type.model)+'.'+str(self.user_link)+'__in'
def _get_filter(self):
# simplejson likes to put unicode objects as dictionary keys
# but keyword arguments must be str type
fc = {}
for k,v in self.filter_condition.iteritems():
fc.update({str(k): v})
return fc
def object_list(self):
return self.content_type.model_class()._default_manager.filter(**self._get_filter())
def object_count(self):
return self.object_list().count()
class Meta:
verbose_name = _("Category")
verbose_name_plural = _("Categories")
ordering = ('name',)
So I can retrieve the name of the model that links to User but I then need to convert it into a class which I can include in a query.
I can create an object x = category.content_type.model_class() which gives me <class 'cltc.models.Member'> but when I them perform a query s = User.objects.filter(x = c.category.object_list()) I get the error Cannot resolve keyword 'x' into field.
Any thoughts most welcome.
The left hand side of the filter argument is a keyword, not a python object, so x is treated as 'x', and Django expects a field called x.
To get around this, you can ensure that x is a string, and then use the python **kwarg syntax:
s = User.objects.filter(**{x: c.category.object_list()})
Thanks to https://stackoverflow.com/a/4720109/823020 for this.

Specialized django query

As you can guess from the title, I'm not exactly sure how to describe what I want. Please take a look at the following classes:
from django.db import models
from django.contrib.auth.models import User as Auth_User
class User(Auth_User):
Portfolio = models.ManyToManyField('PortfolioItem', through='SkillTag')
Age = models.IntegerField(blank=False)
#property
def full_name(self):
return self.first_name + ' ' + self.last_name
def __unicode__(self):
return self.full_name
class PortfolioItem(models.Model):
Title = models.CharField(max_length=200, blank=False)
class SkillTag(models.Model):
User = models.ForeignKey('User')
PortfolioItem = models.ForeignKey('PortfolioItem')
Tag_Name = models.CharField(max_length=200, blank=False)
What I need to do, is for every user, get all the Tag_Name values of it's SkillTags, how do I do this?
You can do something like this
class User(Auth_User):
#other attributes
def tag_names(self):
return self.skilltag_set.values_list('Tag_Name', flat=True)
So, here, we are doing a couple of things:
Querying in reverse ForeignKey relationship.
Since you are not using a related_name in the ForeignKey attribute, by default django would assign the model name (lowercase) followed by _set attribute, which makes it .skilltag_set.all()
values_list
Returns a ValuesQuerySet — a QuerySet subclass that returns tuples when used as an iterable, rather than model-instance objects.
Example: [('a'), ('b'), ('c')]
Basically, you are retriving an iterable of ValuesQuerySet (think of it as a list or any other iterables) consisting of tuples.
flat=True
This basically flattens the on-tuples into single values.
Example: ['a', 'b', 'c']
most obvious: using the reverse relationship of ForeignKey fields:
def skill_names_1(user):
return [t.name for t in user.skilltag_set.all()]
The same thing, but explicitly selecting for the user. also, it fetches only the required field from the database.
def skill_names_2(user):
return SkillTag.objects.filter(User=user).values_list('Tag_Name',flat=True)
Either of these can also work as a method of User. Of course, typically the argument would be called self instead of user.
All the skills for a group of users:
def skill_names_3(users):
return SkillTag.objects.filter(User__in=users).values_list('Tag_Name',flat=True)

Do properties work on Django model fields?

I think the best way to ask this question is with some code... can I do this:
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
def get_foo(self):
if self.bar:
return self.bar
else:
return self.foo
def set_foo(self, input):
self.foo = input
foo = property(get_foo, set_foo)
or do I have to do it like this:
class MyModel(models.Model):
_foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
def get_foo(self):
if self.bar:
return self.bar
else:
return self._foo
def set_foo(self, input):
self._foo = input
foo = property(get_foo, set_foo)
note: you can keep the column name as 'foo' in the database by passing a db_column to the model field. This is very helpful when you are working on an existing system and you don't want to have to do db migrations for no reason
A model field is already property, so I would say you have to do it the second way to avoid a name clash.
When you define foo = property(..) it actually overrides the foo = models.. line, so that field will no longer be accessible.
You will need to use a different name for the property and the field. In fact, if you do it the way you have it in example #1 you will get an infinite loop when you try and access the property as it now tries to return itself.
EDIT: Perhaps you should also consider not using _foo as a field name, but rather foo, and then define another name for your property because properties cannot be used in QuerySet, so you'll need to use the actual field names when you do a filter for example.
As mentioned, a correct alternative to implementing your own django.db.models.Field class, one should use the db_column argument and a custom (or hidden) class attribute. I am just rewriting the code in the edit by #Jiaaro following more strict conventions for OOP in python (e.g. if _foo should be actually hidden):
class MyModel(models.Model):
__foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
#property
def foo(self):
if self.bar:
return self.bar
else:
return self.__foo
#foo.setter
def foo(self, value):
self.__foo = value
__foo will be resolved into _MyModel__foo (as seen by dir(..)) thus hidden (private). Note that this form also permits using of #property decorator which would be ultimately a nicer way to write readable code.
Again, django will create _MyModel table with two fields foo and bar.
The previous solutions suffer because #property causes problems in admin, and .filter(_foo).
A better solution would be to override setattr except that this can cause problems initializing the ORM object from the DB. However, there is a trick to get around this, and it's universal.
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
def __setattr__(self, attrname, val):
setter_func = 'setter_' + attrname
if attrname in self.__dict__ and callable(getattr(self, setter_func, None)):
super(MyModel, self).__setattr__(attrname, getattr(self, setter_func)(val))
else:
super(MyModel, self).__setattr__(attrname, val)
def setter_foo(self, val):
return val.upper()
The secret is 'attrname in self.__dict__'. When the model initializes either from new or hydrated from the __dict__!
It depends whether your property is a means-to-an-end or an end in itself.
If you want this kind of "override" (or "fallback") behavior when filtering querysets (without first having to evaluate them), I don't think properties can do the trick. As far as I know, Python properties do not work at the database level, so they cannot be used in queryset filters. Note that you can use _foo in the filter (instead of foo), as it represents an actual table column, but then the override logic from your get_foo() won't apply.
However, if your use-case allows it, the Coalesce() class from django.db.models.functions (docs) might help.
Coalesce() ... Accepts a list of at least two field names or
expressions and returns the first non-null value (note that an empty
string is not considered a null value). ...
This implies that you can specify bar as an override for foo using Coalesce('bar','foo'). This returns bar, unless bar is null, in which case it returns foo. Same as your get_foo() (except it doesn't work for empty strings), but on the database level.
The question that remains is how to implement this.
If you don't use it in a lot of places, simply annotating the queryset may be easiest. Using your example, without the property stuff:
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
Then make your query like this:
from django.db.models.functions import Coalesce
queryset = MyModel.objects.annotate(bar_otherwise_foo=Coalesce('bar', 'foo'))
Now the items in your queryset have the magic attribute bar_otherwise_foo, which can be filtered on, e.g. queryset.filter(bar_otherwise_foo='what I want'), or it can be used directly on an instance, e.g. print(queryset.all()[0].bar_otherwise_foo)
The resulting SQL query from queryset.query shows that Coalesce() indeed works at the database level:
SELECT "myapp_mymodel"."id", "myapp_mymodel"."foo", "myapp_mymodel"."bar",
COALESCE("myapp_mymodel"."bar", "myapp_mymodel"."foo") AS "bar_otherwise_foo"
FROM "myapp_mymodel"
Note: you could also call your model field _foo then foo=Coalesce('bar', '_foo'), etc. It would be tempting to use foo=Coalesce('bar', 'foo'), but that raises a ValueError: The annotation 'foo' conflicts with a field on the model.
There must be several ways to create a DRY implementation, for example writing a custom lookup, or a custom(ized) Manager.
A custom manager is easily implemented as follows (see example in docs):
class MyModelManager(models.Manager):
""" standard manager with customized initial queryset """
def get_queryset(self):
return super(MyModelManager, self).get_queryset().annotate(
bar_otherwise_foo=Coalesce('bar', 'foo'))
class MyModel(models.Model):
objects = MyModelManager()
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
Now every queryset for MyModel will automatically have the bar_otherwise_foo annotation, which can be used as described above.
Note, however, that e.g. updating bar on an instance will not update the annotation, because that was made on the queryset. The queryset will need to be re-evaluated first, e.g. by getting the updated instance from the queryset.
Perhaps a combination of a custom manager with annotation and a Python property could be used to get the best of both worlds (example at CodeReview).

How do you order lists in the same way QuerySets are ordered in Django?

I have a model that has an ordering field under its Meta class. When I perform a query and get back a QuerySet for the model it is in the order specified. However if I have instances of this model that are in a list and execute the sort method on the list the order is different from the one I want. Is there a way to sort a list of instances of a model such that the order is equal to that specified in the model definition?
Not automatically, but with a bit of work, yes. You need to define a comparator function (or cmp method on the model class) that can compare two model instances according to the relevant attribute. For instance:
class Dated(models.Model):
...
created = models.DateTimeField(default=datetime.now)
class Meta:
ordering = ('created',)
def __cmp__(self, other):
try:
return cmp(self.created, other.created)
except AttributeError:
return cmp(self.created, other)
The answer to your question is varying degrees of yes, with some manual requirements. If by list you mean a queryset that has been formed by some complicated query, then, sure:
queryset.order_by(ClassName.Meta.ordering)
or
queryset.order_by(instance._meta.ordering)
or
queryset.order_by("fieldname") #If you like being manual
If you're not working with a queryset, then of course you can still sort, the same way anyone sorts complex objects in python:
Comparators
Specifying keys
Decorate/Sort/Undecorate
See the python wiki for a detailed explanation of all three.
Building on Carl's answer, you could easily add the ability to use all the ordering fields and even detect the ones that are in reverse order.
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
birthday = date = models.DateField()
class Meta:
ordering = ['last_name', 'first_name']
def __cmp__(self, other):
for order in self._meta.ordering:
if order.startswith('-'):
order = order[1:]
mode = -1
else:
mode = 1
if hasattr(self, order) and hasattr(other, order):
result = mode * cmp(getattr(self, order), getattr(other, order))
if result: return result
return 0

Categories