ForeignKey switching by user option in Django - python

Lets look at the first app (is quite simple)
app name = family; models:
class Parent(models.Model):
name = models.CharFiled(max_length="30")
title = models.CharField(max_length="30")
work_title = models.CharField(max_length="30")
class Child(models.Model):
name = models.CharField(max_length="30")
school_year = models.IntegerField(max_length="30")
Then I have another app that needs to have a foregin key to the 2 classes in family app depending on the user choice, lets look at the next example:
app name=persons, models:
class Info(models.Model):
code = models.CharField(max_length="30")
family_member = models.CharField(max_length="1", choices=(('1', 'Parent'),('2', 'Child'),))
person = models.ForeignKey('family.???') #here is where I have the issue
how can I build the ForeignKey depending on the user choice in family_member? so if the user selects Parent (1) then the ForeignKey should point to Parrent class but if the user selects Child(2) then the foreignKey should point to Child
I'd like to search for solutions in models.py and not fix the issue in views - if possible.

You might want to use GenericForeignKey for that task. See the docs.

You can inherit Parent and Child from one class and make foreign key point to it:
class Person(models.Model):
name = models.CharField(max_length="30")
class Parent(Person):
title = models.CharField(max_length="30")
work_title = models.CharField(max_length="30")
class Child(Person):
school_year = models.IntegerField(max_length="30")
and in persons:
class Info(models.Model):
code = models.CharField(max_length="30")
family_member = models.CharField(max_length="1", choices=(('1', 'Parent'),('2', 'Child'),))
person = models.ForeignKey('family.Person')
Although in this case user will be able to select child or parent independently from family_member choice, unless you filter choices for user by, for example, giving him only Child model objects to choice if he choose family_member 'Child' and vice versa.

Related

Setting relationship with chosen model class in Django Admin interface

Problem:
How to add relationship from chosen model instance to any other Django model dynamically via Django Admin interface?
Description:
I want to create Categories via Django Admin interface. Each Category has multiple Choices assigned to it. Choice(s) from given Category may be assigned only to objects of another specific Django class (model). Let's present a pseudocode example:
class Category(models.Model):
category_name = models.CharField()
class Choice(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="choices")
choice_name = models.CharField()
class ModelWithChoosableFields(models.Model):
possible_categories = ... # objects of class Category
selected_choices = ... # objects of class Choice
class Meta:
abstract = True
class Book(ModelWithChoosableFields):
...
class Animal(ModelWithChoosableFields):
...
Category with category_name = 'Genre' has three possible
Choices: choice_name = 'biography', choice_name = 'thriller'
and choice_name = 'poetry'.
Category with category_name = 'Animal type' has two possible
Choices: choice_name = 'mammal' and choice_name = 'reptile'.
Class Book may have one of the Choices from Category
category_name = 'Genre' assigned. However, Choices related to
category_name = 'Animal type' cannot be assigned to class Book.
Analogically, class Animal can only have Choices related to
category_name = 'Animal type' assigned to it.
In other words, in Admin panel for instance of class Book I want to have a way of selecting Choice objects from Categories appropriate for Book class.
The reason I want to do this is so that user using Django Admin interface can add dynamically possible Categories to chosen models (e.g. add Category category_name = "Conservation status" choosable for class Animal), add more Choices to Categories if needed (e.g. add another choice_name = 'fish' to category_name = 'Animal type'. This way it is very flexible for end admin user, no need to change anything in code.
I tried achieving it with Generic Relations - however, it wasn't successful, because AFAIK generic relation ties given object (e.g. Category) to instance of object of any other class, not generally to any other class (so, when using Generic Relations, for Category I would have to specify relationship with given Book object instance, not with Book class in general).
I wonder if such action is even feasible - I searched a lot and couldn't find anything. Maybe there is a simpler way? Thank you in advance!
With ContentTypes you can relate model instances to entire model classes, no overwriting necessary to achieve your goal.
Heres how to do it:
In your Category model define a many-to-many relationship to ContentType. This way, in your Category model-forms you will be able to choose which models this category applies to and you will be able to filter Choices based on whether their category contains a particular model. Use the limit_choices_to parameter of the ManyToManyField to restrict the ContentType choices to those with the correct app_label and of course exclude the Choice and Category models.
From the Book/Animal/Etc. models add many-to-many relationships to the Choice model and use the limit_choices_to parameter to limit the Choices to only those with a category which is related to the respective model.
Your models should then look somewhat like this:
from django.db import models
def get_ContentTypes():
appQ = models.Q(app_label='YourAppName') #change value of app_label to match your app's name
modelIsCatQ = models.Q(model='category')
modelIsChoice = models.Q(model='choice')
return appQ & ~modelIsCatQ & ~modelIsChoice
class Category(models.Model):
category_name = models.CharField(max_length=128)
asigned_models = models.ManyToManyField(ContentType,limit_choices_to=get_ContentTypes)
class Choice(models.Model):
choice_name = models.CharField(max_length=128)
category = models.ForeignKey(Category,on_delete=models.Model)
class Animal(models.Model):
choices = models.ManyToManyField(Choice,limit_choices_to=models.Q(category_assigned_models__model__startswith='animal'))
class Book(models.Model):
choices = models.ManyToManyField(Choice,limit_choices_to=models.Q(category_assigned_models__model__startswith='book'))
Aaand Voila. There you have it:
When creating/editing a category, you choose which models it should apply to
When creating/editing a Book/Animal/etc. you can only see the relevant choices.

Filter by child class type

Lets say we have a class Place with a class Restaurant inheriting from it :
from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
If I have a class Tag related to places :
class Tag(models.Model):
name = models.CharField(max_length=50)
tagged = models.ManyToManyField(Place, related_name="tags")
For a given tag, how do I get a queryset selecting all Restaurants that have this tag, but not other kind of places ?
The easiest way to doing this is calling filter from Restaurant.objects with something like :
Restaurant.objects.filter(tags=tag)
But if you want call filter from the Place.objects, you must use one of Django polymorphism apps such as Django-Polymorphic in your parent model because Django not supports models polymorphism by default.
Note: Read This article about OOP Polymorphism & This article for some extra information about Django model inheritance.

How to keep DRY while creating common models in Django?

For example I have 2 main models in my django app:
class Employee(Model):
name = CharField(max_length=50)
class Client(Model):
title = CharField(max_length=50)
Abstract base class for phones:
class Phone(Model):
number = CharField(max_length=10)
class Meta:
abstract = True
Inherited separate classes for Employee and for Client:
class EmployeePhone(Phone):
employee = ForeignKey(Employee, on_delete=CASCADE, related_name='employee_phones')
class ClientPhone(Phone):
client = ForeignKey(Client, on_delete=CASCADE, related_name='client_phones')
It works but I don't like it, I would prefer to keep just one Phone model instead of 3. I know I could use Generic-Models but unfortunately that's not longer an option, because my app is actually REST-API and it seems to be impossible to create Generic-Object while creating Parent-Object. So is there any solution to keep things clean and DRY ?
Other answers present good ideas how you can normalise the database, but if you'd like to keep the schema the same and just avoid repeating the same thing in the code, maybe a custom field subclass is what you're after?
Example:
# fields.py
class PhoneField(models.CharField):
def __init__(self, **kwargs):
kwargs.setdefault('max_length', 50)
...
super().__init__(**kwargs)
# models.py
class Employee(models.Model):
phone = PhoneField()
How about keeping 1 Phone class and linking Employee and Customer to Phone via a Foreign Key and removing the abstract :).
How about moving EmployeePhone (ClientPhone) as a ManyToManyField in Employee (Client) model related to Phone model?
class Employee(Model):
name = CharField(max_length=50)
phones = ManyToManyField(Phone, ...)

Django model inheritance: ForeignKey on parent, no related_name access to child model

Example situation as follows:
# models.py
class Form(models.Model):
name = models.CharField()
class A(models.Model):
form = models.ForeignKey(Form)
class B(A):
name = models.CharField()
# view.py
form = Form.objects.get(id=1)
form.a_set.all() # works
form.b_set.all() # doesn't work
I would like to access all the related B Objects via the parent class A foreign key but I can't seem to do this. And if I access them via A then I just get the generic parent class query set. Thank you.
When you inherit from a concrete model, there will be two tables (unlike inheriting from an abstract model) for Parent and Child models.
Django will implicitly create a OneToOneField from Child to Parent model named parent_ptr, thus:
B.objects.filter(a_ptr__form=form)
# B.objects.filter(a_ptr__form_id=1)
will give you the desired QuerySet.

django admin. Create parent with child in same form

I have two classes in my model.py
class User(models.Model):
name = models.CharField()
phone = models.CharField()
# Other common fields
class Customer(User):
payment = models.CharField()
user__id = models.OneToOneField('User', db_column='id', primary_key=True)
class Company(User):
address = models.CharField()
user_id = models.OneToOneField('User', db_column='id', primary_key=True)
When I use the admin of Customer/Company it includes all User fields, that is perfect for me. But this form of Customer/Company also includes a dropdown list with the foreign key of an User, and I don't want to create the User first and then the Customer/Company object. I want that when I fill the Customer/Company form (with User fields) I should create the User object too.
Is there a way to solve this without create a User instance prior the Customer/Company instance?
Thanks
You can do this by adding an inline in your model registration in admin.py.
class AInline(admin.TabularInline):
model = A
class BAdmin(admin.ModelAdmin):
inlines = [AInline]
admin.register(A)
admin.register(B, BAdmin)
Django inline model admin documentation

Categories