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

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)

Related

Django Model Inheritance - get child

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'

Can a model manager access its models' Meta attribute (`Meta.unique_together`)?

Here's my attempt at a generalized natural key model manager. It's like the docs except it tries (unsuccessfully) to determine the natural key field names from the Meta.unique_together attribute.
class NaturalKeyModelManager(Manager):
def get_by_natural_key(self, *args):
field_dict = {}
for i, k in enumerate(self.model.Meta.unique_together[0]):
field_dict[k] = args[i]
return self.get(**field_dict)
If I insert a debug print just before the for loop like this:
print dir(self.model.Meta)
it doesn't list the unqiue_together attribute at all:
['__doc__', '__module__', 'abstract']
The 'abstract' bit worried me, but another debug print shows that the model I'm trying manage with natural keys is not abstract:
>>> print self.model.Meta.abstract
False
I am mixing in a lot of abstract base classes. Could that be the problem?
class MixedModel(NamedModel, TimeStampedModel, VersionedModel, Model):
objects = NaturalKeyModelManager()
class Meta:
unique_together = (('name', 'version',),)
For completeness here's one of the mixins:
class TimeStampedModel(Model):
created = DateTimeField(_("Created"), auto_now_add=True, null=True, editable=False)
updated = DateTimeField(_("Updated"), auto_now=True, null=True, editable=True)
class Meta:
abstract = True
The hard-coded model manager works just fine:
class MixedModelManager(Manager):
def get_by_natural_key(self, name, version):
return self.get(name=name, version=version)
In order to get the actual options passed to meta, you should use self.model._meta rather than self.model.Meta

Django: Adding property to User model after creating model based on abstract class

I have a normal model and an abstract model like so:
class TaggedSubject(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
category = models.CharField(max_length=200)
foo = models.CharField(max_length=50)
bar = models.CharField(max_length=50)
# etc
content_type = models.ForeignKey(ContentType)
content_object_pk = models.CharField(max_length=255)
content_object = generic.GenericForeignKey("content_type", "content_object_pk")
def __unicode__(self):
if self.user:
return "%s" % (self.user.get_full_name() or self.user.username)
else:
return self.label
class Taggable(models.Model):
tagged_subjects = generic.GenericRelation(TaggedSubject, content_type_field='content_type', object_id_field='content_object_pk')
#property
def tagged_users(self):
return User.objects.filter(pk__in=self.tagged_subjects.filter(user__isnull=False).values("user"))
class Meta:
abstract = True
The Taggable abstract model class then gets used like so:
class Photo(Taggable):
image = models.ImageField(upload_to="foo")
# ... etc
So if we have a photo object:
photo = Photo.objects.all()[0]
I can all the users tagged in the photo with photo.tagged_users.all()
I want to add the inverse relation to the user object, so that if I have a user:
user = User.objects.filter(pk__in=TaggedSubject.objects.exclude(user__isnull=True).values("user"))[0]
I can call something like user.tagged_photo_set.all() and have it return all the photo objects.
I suspect that since TaggedSubject connects to the Taggable model on a generic relation that it won't be possible to use it as a through model with a ManyToMany field.
Assuming this is true, this is the function I believe I'd need to add (somehow) to the User model:
def tagged_photo_set(self):
Photo.objects.filter(pk__in=TaggedSubject.objects.filter(user=self, content_type=ContentType.objects.get_for_model(Photo))
I'm wondering if it's possible to set it up so that each time a new model class is created based on Taggable, it creates a version of the function above and adds it (ideally as a function that behaves like a property!) to User.
Alternatively, if it is somehow possible to do ManyToMany field connections on a generic relation (which I highly doubt), that would work too.
Finally, if there is a third even cooler option that I am not seeing, I'm certainly open to it.
You could use add_to_class and the class_prepared signal to do some post processing when models subclassing your base class are set up:
def add_to_user(sender, **kwargs):
def tagged_FOO_set(self):
return sender.objects.filter(pk__in=TaggedSubject.objects.filter(
user=self,
content_type=ContentType.objects.get_for_model(sender)))
if issubclass(sender, MyAbstractClass):
method_name = 'tagged_{model}_set'.format(model=sender.__name__.lower())
User.add_to_class(method_name, property(tagged_FOO_set))
class_prepared.connect(add_to_user)

Setting default value for Foreign Key attribute

What is the best way to set a default value for a foreign key field in a model? Suppose I have two models, Student and Exam with student having exam_taken as foreign key. How would I ideally set a default value for it? Here's a log of my effort
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam", default=1)
Works, but have a hunch there's a better way.
def get_exam():
return Exam.objects.get(id=1)
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam", default=get_exam)
But this fails with tables does not exist error while syncing.
Any help would be appreciated.
I would modify #vault's answer above slightly (this may be a new feature). It is definitely desirable to refer to the field by a natural name. However instead of overriding the Manager I would simply use the to_field param of ForeignKey:
class Country(models.Model):
sigla = models.CharField(max_length=5, unique=True)
def __unicode__(self):
return u'%s' % self.sigla
class City(models.Model):
nome = models.CharField(max_length=64, unique=True)
nation = models.ForeignKey(Country, to_field='sigla', default='IT')
As already implied in #gareth's answer, hard-coding a default id value might not always be the best idea:
If the id value does not exist in the database, you're in trouble. Even if that specific id value does exist, the corresponding object may change. In any case, when using a hard-coded id value, you'd have to resort to things like data-migrations or manual editing of existing database content.
To prevent that, you could use get_or_create() in combination with a unique field (other than id).
Here's one way to do it:
from django.db import models
class Exam(models.Model):
title = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=255)
#classmethod
def get_default_pk(cls):
exam, created = cls.objects.get_or_create(
title='default exam',
defaults=dict(description='this is not an exam'),
)
return exam.pk
class Student(models.Model):
exam_taken = models.ForeignKey(
to=Exam, on_delete=models.CASCADE, default=Exam.get_default_pk
)
Here an Exam.title field is used to get a unique object, and an Exam.description field illustrates how we can use the defaults argument (for get_or_create) to fully specify the default Exam object.
Note that we return a pk, as suggested by the docs:
For fields like ForeignKey that map to model instances, defaults should be the value of the field they reference (pk unless to_field is set) instead of model instances.
Also note that default callables are evaluated in Model.__init__() (source). So, if your default value depends on another field of the same model, or on the request context, or on the state of the client-side form, you should probably look elsewhere.
I use natural keys to adopt a more natural approach:
<app>/models.py
from django.db import models
class CountryManager(models.Manager):
"""Enable fixtures using self.sigla instead of `id`"""
def get_by_natural_key(self, sigla):
return self.get(sigla=sigla)
class Country(models.Model):
objects = CountryManager()
sigla = models.CharField(max_length=5, unique=True)
def __unicode__(self):
return u'%s' % self.sigla
class City(models.Model):
nome = models.CharField(max_length=64, unique=True)
nation = models.ForeignKey(Country, default='IT')
In my case, I wanted to set the default to any existing instance of the related model. Because it's possible that the Exam with id 1 has been deleted, I've done the following:
class Student(models.Model):
exam_taken = models.ForeignKey("Exam", blank=True)
def save(self, *args, **kwargs):
try:
self.exam_taken
except:
self.exam_taken = Exam.objects.first()
super().save(*args, **kwargs)
If exam_taken doesn't exist, django.db.models.fields.related_descriptors.RelatedObjectDoesNotExist will be raised when a attempting to access it.
The issue with most of these approaches are that they use HARD CODED values or lambda methods inside the Model which are not supported anymore since Django Version 1.7.
In my opinion, the best approach here is to use a sentinel method which can also be used for the on_delete argument.
So, in your case, I would do
# Create or retrieve a placeholder
def get_sentinel_exam():
return Exam.objects.get_or_create(name="deleted",grade="N/A")[0]
# Create an additional method to return only the id - default expects an id and not a Model object
def get_sentinel_exam_id():
return get_sentinel_exam().id
class Exam(models.Model):
....
# Making some madeup values
name=models.CharField(max_length=200) # "English", "Chemistry",...
year=models.CharField(max_length=200) # "2012", "2022",...
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam",
on_delete=models.SET(get_sentinel_exam),
default=get_sentinel_exam_id
)
Now, when you just added the exam_taken field uses a guaranteed existing value while also, when deleting the exam, the Student themself are not deleted and have a foreign key to a deleted value.
You could use this pattern:
class Other(models.Model):
DEFAULT_PK=1
name=models.CharField(max_length=1024)
class FooModel(models.Model):
other=models.ForeignKey(Other, default=Other.DEFAULT_PK)
Of course you need to be sure that there is a row in the table of Other. You should use a datamigration to be sure it exists.
I'm looking for the solution in Django Admin, then I found this:
class YourAdmin(admin.ModelAdmin)
def get_changeform_initial_data(self, request):
return {'owner': request.user}
this also allows me to use the current user.
see django docs
the best way I know is to use lambdas
class TblSearchCase(models.Model):
weights = models.ForeignKey('TblSearchWeights', models.DO_NOTHING, default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want'))
so you can specify the default row..
default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want')

Django inline for model inheritance target

I am trying to solve problem related to model inheritance in Django. I have four relevant models: Order, OrderItem which has ForeignKey to Order and then there is Orderable model which is model inheritance superclass to children models like Fee, RentedProduct etc. In python, it goes like this (posting only relevant parts):
class Orderable(models.Model):
real_content_type = models.ForeignKey(ContentType, editable=False)
objects = OrderableManager()
available_types = []
def save(self, *args, **kwargs):
"""
Saves instance and stores information about concrete class.
"""
self.real_content_type = ContentType.objects.get_for_model(type(self))
super(Orderable, self).save(*args, **kwargs)
def cast(self):
"""
Casts instance to the most concrete class in inheritance hierarchy possible.
"""
return self.real_content_type.get_object_for_this_type(pk=self.pk)
#staticmethod
def register_type(type):
Orderable.available_types.append(type)
#staticmethod
def get_types():
return Orderable.available_types
class RentedProduct(Orderable):
"""
Represent a product which is rented to be part of an order
"""
start_at = models.ForeignKey(Storage, related_name='starting_products',
verbose_name=_('Start at'))
real_start_at = models.ForeignKey(Storage, null=True,
related_name='real_starting_products', verbose_name=_('Real start at'))
finish_at = models.ForeignKey(Storage, related_name='finishing_products',
verbose_name=_('Finish at'))
real_finish_at = models.ForeignKey(Storage, null=True,
related_name='real_finishing_products', verbose_name=_('Real finish at'))
target = models.ForeignKey(Product, verbose_name=_('Product'))
Orderable.register_type(RentedProduct)
class OrderItem(BaseItem):
unit_price = models.DecimalField(max_digits=8, decimal_places=2,
verbose_name=_('Unit price'))
count = models.PositiveIntegerField(default=0, verbose_name=_('Count'))
order = models.ForeignKey('Order', related_name='items',
verbose_name=_('Order'))
discounts = models.ManyToManyField(DiscountDescription,
related_name='order_items', through=OrderItemDiscounts, blank=True,
verbose_name=_('Discounts'))
target = models.ForeignKey(Orderable, related_name='ordered_items',
verbose_name=_('Target'))
class Meta:
unique_together = ('order', 'target')
I would like to have an inline tied to Order model to enable editing OrderItems. Problem is, that the target field in OrderItem points to Orderable (not the concrete class which one can get by calling Orderable's cast method) and the form in inline is therefore not complete.
Does anyone have an idea, how to create at least a bit user-friendly interface for this? Can it be solved by Django admin inlines only, or you would suggest creating special user interface?
Thanks in advance for any tips.
Try inherit OrderItemInlineAdmin's Form a define your own Form there. But fingers crossed for that.
I'm looking for a solid answer to this very thing, but you should check out FeinCMS. They are doing this quite well.
See, for example, the FeinCMS inline editor. I need to figure out how to adapt this to my code.