Django, How to enforce model Foreinkeys to have same value - django

In Django I want to enforce that a model which has two foreinkeys of differents models which has the same type of field to be the same, in example:
class Model1(models.Model):
f1 = models.CharField(max_length=48)
class Model2(models.Model):
f1 = models.CharField(max_length=48)
class Model3(models.Model):
field1 = models.ForeignKey(Model1)
field2 = models.ForeignKey(Model2)
I want that creation of objects of Model3 would be made only if f1 field of Model1 and Model2 are the same. (Edit: please note that each model has other fields that are not relavant to the question)

I'm fairly certain this isn't possible at a db level. However, you could enforce it at the code level fairly easily through a pre-save signal. pre-save signals in django allow you to perform an operation/check before a model is actually saved to the database.
See the below example for a potential solution.
from django.db.models.signals import pre_save
#receiver(pre_save)
def pre_save_handler(sender, instance, *args, **kwargs):
if instance.field1.f1 != instance.field2.f2:
raise Exception('Cannot save models, because their values do not match!')
Here we are checking if the fields have the same value, and if they don't an exception is thrown, keeping the record from saving.

Related

Can I override default CharField to ChoiceField in a ModelForm?

I have a (horrible) database table that will be imported from a huge spreadsheet. The data in the fields is for human consumption and is full of "special cases" so its all stored as text. Going forwards, I'd like to impose a bit of discipline on what users are allowed to put into some of the fields. It's easy enough with custom form validators in most cases.
However, there are a couple of fields for which the human interface ought to be a ChoiceField. Can I override the default form field type (CharField)? (To clarify, the model field is not and cannot be constrained by choices, because the historical data must be stored. I only want to constrain future additions to the table through the create view).
class HorribleTable( models.Model):
...
foo = models.CharField( max_length=16, blank=True, ... )
...
class AddHorribleTableEntryForm( models.Model)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
FOO_CHOICES = (('square', 'Square'), ('rect', 'Rectangular'), ('circle', 'Circular') )
...?
Perhaps you could render the forms manually, passing the options through the context and make the fields in html.
Take a look at here:https://docs.djangoproject.com/en/4.0/topics/forms/#rendering-fields-manually
I think you can easily set your custom form field as long it will match the data type with the one set in your model (e.g. do not set choices longer than max_length of CharField etc.). Do the following where foo is the same name of the field in your model:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
...
I think this is perfectly fine for a creation form. It's will not work for updates as the values in the DB will most probably not match your choices. For that, I suggest adding a second form handling data updates (maybe with custom permission to restrict it).
UPDATE
Another approach will be to override the forms init method. That way you can handle both actions (create and update) within the same form. Let the user select from a choice field when creating an object. And display as a normal model field for existing objects:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get("instance", None)
if instance is None:
self.fields["foo"].widget = forms.widgets.Select(choices=self.FOO_CHOICES)

Updating a model when an object it references through a ManyToMany relation is deleted

I have a model, for instance
class ModelA(models.Model):
name = models.CharField()
class ModelB(models.Model):
last_modified_time = models.DateTimeField()
as = models.ManyToManyField('ModelA')
when I delete an instance of ModelA I would like to also update the last_modified_time field on all ModelB instances that referred to ModelA. I can use a predelete signal for this, i.e.
def pre_delete_handler(sender=None, instance=None, *args, **kwargs):
# update all using instance.modelbs...
models.signals.pre_delete.connect(pre_delete_handler, sender=ModelA)
but then every time a new model references it via a ManyToMany this bit of code needs to be updated which isn't the best from a maintenance point of view. I am looking for a way to enumerate all models that ModelA is referenced by so I can have a single update that covers all cases but can't figure out the best way to do this. What is the proper way to enumerate all objects that ModelA is related to?
If all the models that refer to A as many-to-many have the field last_modified_time, then something like this can work:
from django.db.models.fields.reverse_related import ManyToManyRel
from django.utils import timezone
def pre_delete_handler(sender=None, instance=None, *args, **kwargs):
if not instance:
return
# Get all reverse m2m fields
reverse_m2m_fields = [m for m in instance._meta.model._meta.get_fields() if isinstance(m, ManyToManyRel)]
# or [m for m in ModelA._meta.get_fields() if isinstance(m, ManyToManyRel)]
for f in reverse_m2m_fields:
# Find the name of the reverse m2m field from this model
field_name = f.related_name or f.related_query_name or f'{f.name}_set'
getattr(instance, field_name).update(last_modified_time=timezone.now())
models.signals.pre_delete.connect(pre_delete_handler, sender=ModelA)

Django model validate a ManyToMany field before adding

I have a model that looks somewhat like this:
class Passenger(models.Model):
name = models.CharField(max_length=50)
surname = models.CharField(max_length=50)
class Flight(models.Model):
capacity = models.IntegerField()
passengers = models.ManyToManyField(Passenger)
Before adding a new passenger to the flight I would like to validate whether the number of passengers is not going to exceed the capacity. I was wondering what would be the best way to go about this.
Obviously I could manually check the number of passengers before adding a new one, but maybe there is some support in django? I tried writing a validator, but wasn't sure how to do it.
According to Django docs you can listen to the m2m_changed signal, which will trigger pre_add and post_add actions.
Using add() with a many-to-many relationship, however, will not call
any save() methods (the bulk argument doesn’t exist), but rather
create the relationships using QuerySet.bulk_create(). If you need to
execute some custom logic when a relationship is created, listen to
the m2m_changed signal, which will trigger pre_add and post_add
actions.
According to #M.Void answer – Code Example:
from django.db import models
from django.db.models.signals import m2m_changed
from django.core.exceptions import ValidationError
class MyModel(models.Model):
m2mField = models.ManyToManyField('self')
m2mFieldLimit = 2
def m2mField_changed(sender,**kwargs):
instance = kwargs['instance']
if len(instance.m2mField.all()) >= instance.m2mFieldLimit :
raise ValidationError(f'Max number of records is {instance.m2mFieldLimi}')
m2m_changed.connect(commonobjects_changed,sender=MyModel.m2mField.through)
Override the clean method on the model to do the check you want:
class Passenger(models.Model):
name = models.CharField(max_length=50)
surname = models.CharField(max_length=50)
def clean(self, *args, **kwargs):
# clean gets called automatically by other things, so we can't always
# expect flight_id to be provided
if 'flight_id' in kwargs:
flight = Flight.objects.get(pk=kwargs['flight_id'])
if flight.passengers.all().count() >= flight.capacity:
# flight is full!
raise ValidationError
super(Passenger, self).clean()
class Flight(models.Model):
capacity = models.IntegerField()
passengers = models.ManyToManyField(Passenger)
Note that to do this, you will have to pass in the flight ID when validating the passenger:
f = Flight.objects.get(...)
p = Passenger(name='First', surname='Last')
try:
p.clean(flight_id=f.id) # full_clean calls clean, among other validations
p.save()
except ValidationError as e:
# do something to handle the error
Note that it is possible in multi-threaded applications for something to get validated successfully, but still fail to save in a race condition. You would need to add additional code to handle that.
See here for details on model validation.

Dynamically adding many to many relationships in the save method in Django

My Content model has a many-to-many relationship to the Tag model. When I save a Content object, I want to add the relationships dynamically. Im doing this the following way.
# models.py
def tag_content(content_id):
obj = Content.objects.get(pk=content_id)
print obj # Checking
obj.tags = [1, 2, 3] # Adding the relationships using the Tag IDs
class Tag(models.Model):
name = models.CharField(max_length=255)
class Content(models.Model):
title = models.CharField(max_length=255)
is_tagged = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag, blank=True)
def save(self, *args, **kwargs):
super(Content, self).save(*args, **kwargs)
if not self.is_tagged:
tag_content(self.pk) # calling the tagging method
In other words, when a Content object is saved, its tags field is related to 3 different Tag object models. Just to let you know, I do have the Tags with pks = 1, 2, and 3 in database.
However, this simply doesn't work. The save method calls the tag_content method, since the print obj statement works. However, the many-to-many field is not set and remains empty. The funny thing is, If I run the following commands in shell, the tags field is set perfectly.
# python manage.py shell
from myapp.models import *
obj = Content.objects.get(pk=1)
tag_content(obj.pk)
So how come the shell version works, but the other one doesn't? Any help is appreciated.
You can't work on an m2m relationship in a custom save method because of the way Django writes those relationships to the database. When saving a model instance with an m2m relationship, Django first writes the object then goes in again and writes the appropriate m2m relationships. Since the m2m stuff comes "second," trying to work with the relationship in a custom save fails.
The solution is to use a post-save signal. Remove the custom save stuff and add this below your model definitions, making sure to import receiver and post_save:
#receiver(post_save, sender = Content)
def update_m2m_relationships_on_save(sender, **kwargs):
if not kwargs['instance'].is_tagged:
tag_content(kwargs['instance'].pk)
Your tag_content function should probably swap is_tagged to True and then save the instance; if that boolean is never flipped then this could just run in an endless loop. You can also just pass in the object instead of passing in the pk:
def tag_content(thing_to_tag):
thing_to_tag.tags.add([1,2,3])
thing_to_tag.is_tagged = True
thing_to_tag.save()
return thing_to_tag
Note the use of .add(), which is important when adding to an m2m relationship.

Django ORM access User table through multiple models

views.py
I'm creating a queryset that I want to serialize and return as JSON. The queryset looks like this:
all_objects = Program.objects.all()
test_data = serializers.serialize("json", all_objects, use_natural_keys=True)
This pulls back everything except for the 'User' model (which is linked across two models).
models.py
from django.db import models
from django.contrib.auth.models import User
class Time(models.Model):
user = models.ForeignKey(User)
...
class CostCode(models.Model):
program_name = models.TextField()
...
class Program(models.Model):
time = models.ForeignKey(Time)
program_select = models.ForeignKey(CostCode)
...
Question
My returned data has Time, Program, and CostCode information, but I'm unable to query back the 'User' table. How can I get back say the 'username' (from User Table) in the same queryset?
Note: I've changed my queryset to all_objects = Time.objects.all() and this gets User info, but then it doesn't pull in 'CostCode'. My models also have ModelManagers that return the get_by_natural_key so the relevant fields appear in my JSON.
Ultimately, I want data from all four models to appear in my serialized JSON fields, I'm just missing 'username'.
Here's a picture of how the JSON object currently appears in Firebug:
Thanks for any help!
It seems a bit heavyweight at first glance but you could look at using Django REST Framework:
http://www.django-rest-framework.org/api-guide/serializers#modelserializer
You can define and use the serializer classes without having to do anything else with the framework. The serializer returns a python dict which can then be easily dumped to JSON.
To get all fields from each related model as nested dicts you could do:
class ProgramSerializer(serializers.ModelSerializer):
class Meta:
model = Program
depth = 2
all_objects = Program.objects.all()
serializer = ProgramSerializer(all_objects, many=True)
json_str = json.dumps(serializer.data)
To customise which fields are included for each model you will need to define a ModelSerializer class for each of your models, for example to output only the username for the time.user:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', )
class TimeSerializer(serializers.ModelSerializer):
"""
specifying the field here rather than relying on `depth` to automatically
render nested relations allows us to specify a custom serializer class
"""
user = UserSerializer()
class Meta:
model = Time
class ProgramSerializer(serializers.ModelSerializer):
time = TimeSerializer()
class Meta:
model = Program
depth = 1 # render nested CostCode with default output
all_objects = Program.objects.all()
serializer = ProgramSerializer(all_objects, many=True)
json_str = json.dumps(serializer.data)
What you really want is a "deep" serialization of objects which Django does not natively support. This is a common problem, and it is discussed in detail here: Serializing Foreign Key objects in Django. See that question for some alternatives.
Normally Django expects you to serialize the Time, CostCode, Program, and User objects separately (i.e. a separate JSON array for each) and to refer to them by IDs. The IDs can either be the numeric primary keys (PKs) or a "natural" key defined with natural_key.
You could use natural_key to return any fields you want, including user.username. Alternatively, you could define a custom serializer output whatever you want there. Either of these approaches will probably make it impossible to load the data back into a Django database, which may not be a problem for you.