Django Model Inheritance - get child - django

Is there a way to access the actual child of the base model, means: Staying with the example from the django Docs, let's assume I am modeling different delivery restaurants, that just have in common
name
all have a deliver method
as of this:
class Place(models.Model):
name = models.CharField(max_length=10)
class Pizzeria(Place):
topping = models.CharField(max_length=10)
tip = models.IntegerField()
def deliver(self):
deliver_with_topping(self.topping)
ask_for_tip(self.tip)
class Shoarma(Place):
sauce = models.CharField(max_length=10)
meat = models.CharField(max_lenght=10)
def deliver(self):
prepare_sauce_with_meat(self.sauce, self.meat)
I would now like to execute:
Place.objects.get(name="my_place").<GENERIC_CHILD>.deliver()
i.e. I don't need to know what the place is actually, just the common deliver method. The model then 'knows' what to call.
Is there something like <GENERIC_CHILD>?

I always use Inheritance Manager from django-model-utils for this kind of operations. On your models:
class Place(models.Model):
objects = InheritanceManager() #<- add inheritance manager
name = models.CharField(max_length=10)
def deliver(self):
pass #not needed
Your query:
Place.objects.get_subclass(name="my_place").deliver()
For me it is a clean and elegant solution. Don't forget to star-up django-model-util repo if you like it.

I did it in a messy way.
I do have parent class Activity, with childs - Action, Deal, Order classes.
I want to list them all in 1 place, 1) with a field specifieing it's class, 2) link them to same page, where i will render page based on Activity class
So in my model Activity i add:
def get_type(self):
children = ['action', 'deal', 'order']
for c in children:
try:
_ = self.__getattribute__(c) # returns child model
except ObjectDoesNotExist:
pass
else:
return c
else:
return 'Not specified'

Related

Django: Access certain attribute of multiple subclasses with inheritance from one superclass

My aim is to access an attribute of a subclass without knowing beforehand which of the two subclasses was choosen (multiple choice classes)
Ideally there is an attribute in the SuperClass that changes depending upon which SubClass was choosen.
The reason is that I have created Forms directly from the SubClasses and use the SuperClass as the entry point for accessing values.
I am aware I can use true or false with hasattr(horse), but ideally I am asking if there is a bit neater solution, such as the SubClass can signal to SuperClass which SubClass was used.
e.g. for product 8 on my list
subclass = getattr(Product(8), 'subclass', 0)
print(subclass)
>> Horse
or
place = Product.location
Print(place)
>> Stable
The whole "problem" stem from the fact that I create Products via SubClass Forms, meanwhile much of the later logic goes top-down, starting with Product
class Product(models.Model):
product_name = models.Charfield(max_length=20)
class Car(Product):
engine = models.Charfield(max_length=20)
location = models.Charfield(default="Garage", max_length=20, editable=False)
product = models.OneToOneField(Product, parent_link=True, on_delete=models.CASCADE)
class Horse(Product):
saddle_model = models.Charfield(max_length=20)
location = models.Charfield(default="Stable", max_length=20, editable=False)
product = models.OneToOneField(Product, parent_link=True, on_delete=models.CASCADE)
If you want to access the other models properties from the Product model you could implement a property method on Product that inspects the reverse relation between it and its related models and then returns the appropriate location (https://docs.djangoproject.com/en/2.1/topics/db/examples/one_to_one/).
class Product(models.Model):
product_name = models.CharField(max_length=20)
#property
def location(self):
"""Return the location of the related subclass"""
if self.car:
return self.car.location
elif self.horse:
return self.horse.location
else:
return None
#property
def product_subclass(self):
"""Return the location of the related subclass"""
if self.car:
return self.car
elif self.horse:
return self.horse
else:
return None
This should allow you to use it like so:
car_product = Product.objects.create(product_name="Car Product")
car = Car.objects.create(engine="engine", location="123 Fake Street", product=car_product)
print(car_product.location) # Prints "123 Fake Street"
horse_product = Product.objects.create(product_name="Horse Product")
horse = Horse.objects.create(saddle_model="Buckingham", location="1982 Old Street", product=horse_product)
print(horse_product.location) # Prints "1982 Old Street"
If you'd like to do something similar to return the subclass:
print(car_product.product_subclass) # Prints <Car object>
print(horse_product.product_subclass) # Prints <Horse object>
These property methods require a database query to check with the Car and Horse table's, since the relation is stored on those tables as the product_id column. So to figure out if product.car is valid, the ORM does a query similar to Car.objects.get(product_id=product.pk)

Filter by custom QuerySet of a related model in Django

Let's say I have two models: Book and Author
class Author(models.Model):
name = models.CharField()
country = models.CharField()
approved = models.BooleanField()
class Book(models.Model):
title = models.CharField()
approved = models.BooleanField()
author = models.ForeignKey(Author)
Each of the two models has an approved attribute, which shows or hides the object from the website. If the Book is not approved, it is hidden. If the Author is not approved, all his books are hidden.
In order to define these criteria in a DRY manner, making a custom QuerySet looks like a perfect solution:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True)
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__approved=True)
After hooking up these QuerysSets to the corresponding models, they can be queried like this: Book.objects.for_site(), without the need to hardcode all the filtering every time.
Nevertheless, this solution is still not perfect. Later I can decide to add another filter to authors:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True).exclude(country='Problematic Country')
but this new filter will only work in Author.objects.for_site(), but not in Book.objects.for_site(), since there it is hardcoded.
So my questions is: is it possible to apply a custom queryset of a related model when filtering on a different model, so that it looks similar to this:
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__for_site=True)
where for_site is a custom QuerySet of the Author model.
I think, I've come up with a solution based on Q objects, which are described in the official documentation. This is definitely not the most elegant solution one can invent, but it works. See the code below.
from django.db import models
from django.db.models import Q
######## Custom querysets
class QuerySetRelated(models.query.QuerySet):
"""Queryset that can be applied in filters on related models"""
#classmethod
def _qq(cls, q, related_name):
"""Returns a Q object or a QuerySet filtered with the Q object, prepending fields with the related_name if specified"""
if not related_name:
# Returning Q object without changes
return q
# Recursively updating keywords in this and nested Q objects
for i_child in range(len(q.children)):
child = q.children[i_child]
if isinstance(child, Q):
q.children[i_child] = cls._qq(child, related_name)
else:
q.children[i_child] = ('__'.join([related_name, child[0]]), child[1])
return q
class AuthorQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True)
q = q & ~Q(country='Problematic Country')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
class BookQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True) & AuthorQuerySet.for_site_q('author')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
######## Models
class Author(models.Model):
name = models.CharField(max_length=255)
country = models.CharField(max_length=255)
approved = models.BooleanField()
objects = AuthorQuerySet.as_manager()
class Book(models.Model):
title = models.CharField(max_length=255)
approved = models.BooleanField()
author = models.ForeignKey(Author)
objects = BookQuerySet.as_manager()
This way, whenever the AuthorQuerySet.for_site_q() method is changed, it will be automatically reflected in the BookQuerySet.for_site() method.
Here the custom QuerySet classes perform selection at the class level by combining different Q objects, instead of using filter() or exclude() methods at the object level. Having a Q object allows 3 different ways of using it:
put it inside a filter() call, to filter a queryset in place;
combine it with other Q objects using & (AND) or | (OR) operators;
dynamically change names of keywords used in the Q objects by accessing its children attribute, which is defined in the superclass django.utils.tree.Node
The _qq() method defined in every custom QuerySet class takes care of prepending the specified related_name to all filter keys.
If we have a q = Q(approved=True) object, then we can have the following outputs:
self._qq(q) – is equivalent to self.filter(approved=True);
self._qq(q, 'author') – is equivalent to self.filter(author__approved=True)
This solution still has serious drawbacks:
one has to import and call custom QuerySet class of the related model explicitly;
for each filter method one has to define two methods filter_q (class method) and filter (instance method);
UPDATE: The drawback 2. can be partially reduced by creating filter methods dynamically:
# in class QuerySetRelated
#classmethod
def add_filters(cls, names):
for name in names:
method_q = getattr(cls, '{0:s}_q'.format(name))
def function(self, *args, **kwargs):
return self.filter(method_q(*args, **kwargs))
setattr(cls, name, function)
AuthorQuerySet.add_filters(['for_site'])
BookQuerySet.add_filters(['for_site'])
Therefore, if someone comes up with a more elegant solution, please suggest it. It would be very appreciated.

How can I write to the instance on the parent class from a subclass in Django models?

Following on from this question...
I have two primary models for my blog, Article and Link, and both are subclasses of Post. Simplifying a little, they look something like this:
class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()
post_date = models.DateField(db_index=True, auto_now_add=True)
class Article(Post):
feature_image = models.FileField(upload_to='feature_images')
class Link(Post):
link = models.URLField(verify_exists=True)
I want to collect over both Articles and Links, so in my view, I run Post.objects.order_by('post_date') and presto, I get the whole list--but only with the fields that are on Post. If I want to use the link in a Link instance, I can't.
I have the primary key, so I should be able to do something like Link.objects.get(pk=item.pk) and be set--but I'd have to know if this was a Link or an Article.
Can I create a post_type property on the parent model and write to it with the correct model name from the children?
I solved this in a totally different way in the end, by writing a custom manager for Post:
class PostManager(models.Manager):
def __get_final(self, pk):
for k in Post.__subclasses__():
if k.objects.filter(pk=pk).exists():
return k.objects.get(pk=pk)
return None
def __subclass_queryset(self, qs):
collection = []
for item in qs:
collection.append(self.__get_final(item.pk))
return collection
def all(self):
return self.__subclass_queryset(super(PostManager, self).all())
Now Post.objects.all() (or any other QuerySet operation I add to the manager, like order_by), and I'll get back a list of all of the objects, with their full set of specific fields. (I then reset the manager on the subclasses, so they're not saddled with these extra queries for routine operations.)

Geo Django Subclassing Queryset

I'm using GeoDjango to search for a bunch of locations of different types. For example, both House and Appartment models are subclasses of Location.
Using the Subclassing Queryset below, I'm able to do something like Location.objects.all() and have it return to me [<House: myhouse>, <House: yourhouse>, <Appartment: myappartment>], which is my desire.
However, I also want to determine the distance to each location. Normally, without the Subclassing Queryset, the code from Exhibit 2 returns for me the distances from the given point to each location.... [ (<Location: Location object>, Distance(m=866.092847284))]
However, if I try to find the distances using the Subclassing Querysets, I get an error such as:
AttributeError: 'House' object has no attribute 'distance'
Do you know how I can preserve the ability return a queryset of subclassed objects yet have the distance property available on the subclass objects? Any advice is much appreciated.
Exhibit 1:
class SubclassingQuerySet(models.query.GeoQuerySet):
def __getitem__(self, k):
result = super(SubclassingQuerySet, self).__getitem__(k)
if isinstance(result, models.Model) :
return result.as_leaf_class()
else :
return result
def __iter__(self):
for item in super(SubclassingQuerySet, self).__iter__():
yield item.as_leaf_class()
class LocationManager(models.GeoManager):
def get_query_set(self):
return SubclassingQuerySet(self.model)
class Location(models.Model):
content_type = models.ForeignKey(ContentType,editable=False,null=True)
objects = LocationManager()
class House(Location):
address = models.CharField(max_length=255, blank=True, null=True)
objects = LocationManager()
class Appartment(Location):
address = models.CharField(max_length=255, blank=True, null=True)
unit = models.CharField(max_length=255, blank=True, null=True)
objects = LocationManager()
Exhibit 2:
from django.contrib.gis.measure import D
from django.contrib.gis.geos import fromstr
ref_pnt = fromstr('POINT(-87.627778 41.881944)')
location_objs = Location.objects.filter(
point__distance_lte=(ref_pnt, D(m=1000)
)).distance(ref_pnt).order_by('distance')
[ (l, l.distance) for l in location_objs.distance(ref_pnt) ] # <--- errors out here
I'm busy trying to solve this one to. How about this:
class QuerySetManager(models.GeoManager):
'''
Generates a new QuerySet method and extends the original query object manager in the Model
'''
def get_query_set(self):
return super(QuerySetManager, self).get_query_set()
And the rest can follow from this DjangoSnippet.
You have to reassign the manager in all subclasses.
From Django documentation:
Managers defined on non-abstract base classes are not inherited by child classes. If you want to reuse a manager from a non-abstract base, redeclare it explicitly on the child class. These sorts of managers are likely to be fairly specific to the class they are defined on, so inheriting them can often lead to unexpected results (particularly as far as the default manager goes). Therefore, they aren't passed onto child classes.
https://docs.djangoproject.com/en/dev/topics/db/managers/#custom-managers-and-model-inheritance

Django Models: Subclassing approach?

ists,
I'm looking for some validation on a subclassing approach. I have the following:
class Person(models.Model):
"""
Basic person
"""
user = models.ForeignKey(User) # hide
first_name = models.CharField(max_length=200)
last_name = models.CharField(blank=True, max_length=200)
class Meta:
verbose_name_plural = "People"
def __unicode__(self):
return u"%s, (%s)" % (self.first_name, self.user)
class Contributor(Person):
"""
Contributor
A Core contributor of the site content workflow
"""
class Meta:
verbose_name = 'contributor'
verbose_name_plural = 'contributors'
def get_articles(self):
"""
Return the articles that the author has published.
"""
return Article.objects.filter(self_in=authors)
class Member(Person):
"""
Member
A Member of the website.
"""
# Member history, payments etc...
joined = models.DateTimeField()
So, each Member or Contributor is a Person within the system, but it is possible for a Person to be 'None', 1 or both Member & Contributor, depending on their context.
This subclassing approach makes it simple to do things like:
#...
contributors = models.ManyToManyField(Contributor, help_text="Contributors/Authors to this article")
or
print Member.objects.all()
... and of course the usual efficiencies of subclassing, i.e. common fields and methods.
However, I'm wondering about the pros & cons of doing something like
class Person(models.Model):
"""
Person
"""
user = models.ForeignKey(User) # hide
first_name = models.CharField(max_length=200)
last_name = models.CharField(blank=True, max_length=200)
is_contributor = models.BooleanField()
is_member = models.BooleanField()
but then needing to filter things like
# Assuming this is possible...
contributors = models.ManyToManyField(Person.objects.filter(is_contributor=True), help_text="Contributors/Authors to this article")
With the subclassing approach, I wonder about the challenges of being aware of users that are People (Person), Members or Contributors - and being able to discern between.
i.e. its really easy to do if person.is_contributor: but perhaps more challenging
try:
Contributor.objects.get(person__user_id=request.user.id)
except:
no_access()
else:
let_them_in()
Apologies for the open-endness of this question -- it may have been more an opportunity to think out aloud.
First, there are two oddities about your model to begin with:
1) Why is Person -=> User a ForeignKey and not a OneToOne? Might a user be more than one person?
2) User already has first and last names - why also assign them to person?
Next, to the extent that your ultimate goal is the authorization depicted at the end, why not just use permissions? Then you won't need the boolean fields or the try - except at the end.
Fundamentally, I see nothing wrong with subclassing the User model. Folks in #django often fight over this, but if done right, it is one of the most time-saving and powerful steps you can take when you first sit down with your new django project.
Adding different subclasses of User with different attributes and different methods can very quickly give you a robust user environment with enormous auth possibilities. Thus far, however, it doesn't look like you have done anything that requires you to subclass User.