How to validate uniqueness constraint across foreign key (django) - python

I have the following (simplified) data structure:
Site
-> Zone
-> Room
-> name
I want the name of each Room to be unique for each Site.
I know that if I just wanted uniqueness for each Zone, I could do:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
class Meta:
unique_together = ('name', 'zone')
But I can't do what I really want, which is:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
class Meta:
unique_together = ('name', 'zone__site')
I tried adding a validate_unique method, as suggested by this question:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique(self, exclude=None):
qs = Room.objects.filter(name=self.name)
if qs.filter(zone__site=self.zone__site).exists():
raise ValidationError('Name must be unique per site')
models.Model.validate_unique(self, exclude=exclude)
but I must be misunderstanding the point/implementation of validate_unique, because it is not being called when I save a Room object.
What would be the correct way to implement this check?

Methods are not called on their own when saving the model.
One way to do this is to have a custom save method that calls the validate_unique method when a model is saved:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique(self, exclude=None):
qs = Room.objects.filter(name=self.name)
if qs.filter(zone__site=self.zone__site).exists():
raise ValidationError('Name must be unique per site')
def save(self, *args, **kwargs):
self.validate_unique()
super(Room, self).save(*args, **kwargs)

class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique(self, *args, **kwargs):
super(Room, self).validate_unique(*args, **kwargs)
qs = Room.objects.filter(name=self.name)
if qs.filter(zone__site=self.zone__site).exists():
raise ValidationError({'name':['Name must be unique per site',]})
I needed to make similar program. It worked.

The Django Validation objects documentation explains the steps involved in validation including this snippet
Note that full_clean() will not be called automatically when you call your model's save() method
If the model instance is being created as a result of using a ModelForm, then validation will occur when the form is validated.
There are a some options in how you handle validation.
Call the model instance's full_clean() manually before saving.
Override the save() method of the model to perform validation on every save. You can choose how much validation should occur here, whether you want full validation or only uniqueness checks.
class Room(models.Model):
def save(self, *args, **kwargs):
self.full_clean()
super(Room, self).save(*args, **kwargs)
Use a Django pre_save signal handler which will automatically perform validation before a save. This provides a very simple way to add validation on exisiting models without any additional model code.
# In your models.py
from django.db.models.signals import pre_save
def validate_model_signal_handler(sender, **kwargs):
"""
Signal handler to validate a model before it is saved to database.
"""
# Ignore raw saves.
if not kwargs.get('raw', False):
kwargs['instance'].full_clean()
pre_save.connect(validate_model_signal_handler,
sender=Room,
dispatch_uid='validate_model_room')

Related

Populating a model field based on another model field from a different model

I'm trying to populate "balance" in Transaction model based on "beginning_balance" in the Accounts model, but I'm getting the following error message:
save() missing 2 required positional arguments: 'request' and 'pk'
What am I doing wrong. Thank you.
models.py
from django.db import models
class Accounts(models.Model):
account_nickname = models.CharField(max_length=15, unique=True)
beginning_balance = models.DecimalField(max_digits=12, decimal_places=2)
def __str__(self):
return self.account_nickname
class Transaction(models.Model):
transaction_date = models.DateField()
account_nickname = models.ForeignKey(Accounts, on_delete=models.CASCADE)
amount = models.FloatField()
balance = models.FloatField(null=True, blank=True, editable=False)
def save(self, request, pk):
get_beginning_balance = Accounts.objects.get(id=pk)
self.balance = get_beginning_balance
super().save(request, pk, *args, **kwargs)
def __str__(self):
return self.account_nickname
This the correct query. I realized I wasn't pulling the right data.
def save(self, *args, **kwargs):
get_beginning_balance =
Accounts.objects.filter(account_nickname=self.account_nickname).values("beginning_balance")
self.balance = get_beginning_balance
super().save(*args, **kwargs)
I believe that what you are trying to do in your save function in the Transaction model would best be done in your views.py file. You could get the Accounts model based on the pk id that was passed in your url as an int:pk with something like this:
def view_name(request,pk):
You could then get your desired Accounts object with a get_object_or_404. I would then create your account balance with Transactions.objects.create(your arguments). It should end up looking something like this:
def view_name(request,pk):
acc=get_object_or_404(Accounts, account_nickname=sample_account_nickname)
Transaction.objects.create(transaction_date=sample_date,
account_nickname=acc,
amount=sample_amount)
Good luck! :D
You are overriding Django Model save function with wrong number of arguments.
If you want to set something in Django Admin you should override proper method, in ModelAdmin in this case save_model() would be appropriate
The save_model method is given the HttpRequest, a model instance, a
ModelForm instance, and a boolean value based on whether it is adding
or changing the object. Overriding this method allows doing pre- or
post-save operations. Call super().save_model() to save the object
using Model.save().

Access related ManyToManyField data pre-save in the Django Model.save method

We would like to access related ManyToManyField data pre-save within the Model.save method, however the data isn't available yet via the Django ORM because it's related ManyToManyField data and doesn't get set until post-save of the primary record.
Here's some example code of the relationship and where the related ManyToMany records are accessed in Model.save
class Friend(models.Model):
name = models.CharField(max_length=50)
class Person(models.Model):
name = models.CharField(max_length=50)
friends = models.ManyToManyField(Friend)
def save(self, *args, **kwargs):
friends = self.friends.all()
# 'friends' is an empty QuerySet at this point
# I'd like to do something with friends here,
# but it gets set after save
super(Friend, self).save(*args, **kwargs)
Example use case where friends are passed in on save:
friend = Friend.objects.all()[0]
friend2 = Friend.objects.all()[1]
friends = [friend, friend2]
Person.objects.create(friends=friends)
m2m relations establish after instance saved and get it's own id,so you can't access it within override save method,two way to archieve:
one: after django 1.9,transaction tools provide new method to listen db communication,doc is here.demo code is:
from django.db import transaction
class Person(models.Model):
name = models.CharField(max_length=50)
friends = models.ManyToManyField(Friend)
def save(self, *args, **kwargs):
instance = super(Person, self).save(*args, **kwargs)
transaction.on_commit(self.update_friend)
return instance
def update_friend(self):
for friend in self.friends.all():
print(friend.__str__())
second way is use signal,here is demo:
from django.db.models.signals import m2m_changed
#receiver(m2m_changed, sender=Person.friends.through)
def friends_change(sender, action, pk_set, instance=None, **kwargs):
if action in ['post_add', 'post_remove']:
queryset = instance.friends.all()
for friend in queryset:
print(friend.__str__())

How can I force 2 fields in a Django model to share the same default value?

I have a Django model MyModel as shown below.
It has two fields of type DateTimeField: my_field1, my_field2
from django.db import models
from datetime import datetime
class MyModel(models.Model):
my_field1 = models.DateTimeField(default=datetime.utcnow, editable=False)
my_field2 = models.DateTimeField(
# WHAT DO I PUT HERE?
)
I want both fields to default to the value of datetime.utcnow(). But I want to save the same value for both. It seems wasteful to call utcnow() twice.
How can I set the default value of my_field2 so that it simply copies the default value of my_field1?
The proper way to do this is by over riding the save method rather than the __init__ method. In fact it's not recommended to over ride the init method, the better way is to over ride from_db if you wish to control how the objects are read or save method if you want to control how they are saved.
class MyModel(models.Model):
my_field1 = models.DateTimeField(default=datetime.utcnow, editable=False)
my_field2 = models.DateTimeField()
def save(self, *arges, **kwargs):
if self.my_field1 is None:
self.my_field1 = datetime.utcnow()
if self.my_field2 is None:
self.my_field2 = self.my_field1
super(MyModel, self).save(*args, **kwargs)
Update: Reference for my claim: https://docs.djangoproject.com/en/1.9/ref/models/instances/
You may be tempted to customize the model by overriding the init
method. If you do so, however, take care not to change the calling
signature as any change may prevent the model instance from being
saved. Rather than overriding init, try using one of these
approaches:
As stated in the docs:
The default value is used when new model instances are created and a value isn’t provided for the field.
So to solve your task, I would fill the default values manually in the __init__. Something like:
def __init__(self, *args, **kwargs):
now = datetime.datetime.utcnow()
kwargs.setdefault('my_field1', now)
kwargs.setdefault('my_field2', now)
super(MyModel, self).__init__(*args, **kwargs)
Alternatively you can handle the values in save method.
If you want my_field2 to have any value that is in my_field1, I would go with this solution:
class MyModel(models.Model):
my_field1 = models.DateTimeField(default=datetime.utcnow, editable=False)
my_field2 = models.DateTimeField()
def __init__(self, **kwargs):
super(MyModel, self).__init__(**kwargs)
if self.my_field2 is None:
self.my_field2 = self.my_field1

Limit choices and validate django's foreign key to related objects (also in REST)

I have my models.py like this:
class Category(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=256, db_index=True)
class Todo(models.Model):
user = models.ForeignKey(User)
category = models.ForeignKey(Category)
...
And I want to limit choices of Category for Todo to only those ones where Todo.user = Category.user
Every solutuion that I've found was to set queryset for a ModelForm or implement method inside a form. (As with limit_choices_to it is not possible(?))
The problem is that I have not only one model with such limiting problem (e.g Tag, etc.)
Also, I'm using django REST framework, so I have to check Category when Todo is added or edited.
So, I also need functions validate in serializers to limit models right (as it does not call model's clean, full_clean methods and does not check limit_choices_to)
So, I'm looking for a simple solution, which will work for both django Admin and REST framework.
Or, if it is not possible to implement it the simple way, I'm looking for an advice of how to code it the most painless way.
Here what I've found so far:
To get Foreignkey showed right in admin, you have to specify a form in ModelAdmin
class TodoAdminForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = Category.objects.filter(user__pk=self.instance.user.pk)
#admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
form = TodoAdminForm
...
To get ManyToManyField showed right in InlineModelAdmin (e.g. TabularInline) here comes more dirty hack (can it be done better?)
You have to save your quiring field value from object and then manually set queryset in the field. My through model has two members todo and tag
And I'd like to filter tag field (pointing to model Tag):
class MembershipInline(admin.TabularInline):
model = Todo.tags.through
def get_formset(self, request, obj=None, **kwargs):
request.saved_user_pk = obj.user.pk # Not sure if it can be None
return super().get_formset(request, obj, **kwargs)
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
if db_field.name == 'tag':
kwargs['queryset'] = Tag.objects.filter(user__pk=request.saved_user_pk)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
And finally, to restrict elements only to related in Django REST framework, I have to implement custom Field
class PrimaryKeyRelatedByUser(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return super().get_queryset().filter(user=self.context['request'].user)
And use it in my serializer like
class TodoSerializer(serializers.ModelSerializer):
category = PrimaryKeyRelatedByUser(required=False, allow_null=True, queryset=Category.objects.all())
tags = PrimaryKeyRelatedByUser(required=False, many=True, queryset=Tag.objects.all())
class Meta:
model = Todo
fields = ('id', 'category', 'tags', ...)
Not sure if it actually working in all cases as planned. I'll continue this small investigation.
Question still remains. Could it be done simplier?

Restricting ForeignKey choices on sub-classes

I have a set of models, regarding restaurants and the chefs that run them*:
class Chef(models.Model):
name = models.TextField()
class Restaurant(models.Model):
name = models.TextField()
chef = models.ForeignKey(Chef)
class FrenchChef(Chef):
angryness = models.PositiveIntegerField()
class FrenchRestaurant(Restaurant):
region = models.TextField()
Unfortunately, this current model means a non-FrenchChef can run a FrenchRestaurant.
Is there away that I can restrict the queryset for the ForeignKey of a subbclassed model to be a subset of those available on the parent class?
* My modelling isn't actually chefs and restaurants, but this is easier to explain. It might not seem obvious, but Chefs and FrenchChefs do need to be modelled differently.
You could try defining clean method if you're concerned about that
class FrenchRestaurant(models.Model):
# ...
def clean(self):
if not isinstance(self.chief, FrenchChief):
raise ValidationError()
By doing this:
class FrenchChef(Chef):
angryness = models.PositiveIntegerField()
you are creating one more table in database besides Chef. Read about types of model inheritance here: https://docs.djangoproject.com/en/1.7/topics/db/models/#model-inheritance
I think you should create one table for chefs and one table for restaurants, no inheritance needed here:
class Chef(models.Model):
name = models.TextField()
# all chefs with not null angryness is frenchchefs...
# but you can add some field to explicitly save chef type
angryness = models.PositiveIntegerField(null=True)
class Restaurant(models.Model):
name = models.TextField()
chef = models.ForeignKey(Chef)
region = models.TextField()
# rtype added here but it is not necessarily
rtype = models.IntegerField(choices=restaurans_types)
And restriction (filtering) of choices should be in forms:
class FrenchRestaurantForm(forms.ModelForm):
def __init__(self, *args,**kwargs):
super (FrenchRestaurantForm, self ).__init__(*args,**kwargs)
self.fields['chef'].queryset = Chef.objects.filter(
angryness__gte=MIN_ANGRYNESS_LVL)
def save(commit=True):
model = super(FrenchRestaurantForm, self).save(commit=False)
model.rtype = SomeFrenchRestTypeConst
if commit:
model.save()
return model
class Meta:
model = Restaurant
To check user input you can add clean method to form field https://docs.djangoproject.com/en/1.7/ref/forms/validation/#cleaning-a-specific-field-attribute
If FrenchChef was created intentionally (it is a different table in database), then you should add it to FrenchRestaurant (another table -> another fk id):
class FrenchRestaurant(Restaurant):
region = models.TextField()
frenchchef = models.ForeignKey(FrenchChef)
Like I was mentioning in the comment, You can look at django model data validation methods. Just found another note at this post.
Adding Custom Django Model Validation
Below is a common pattern followed to do validation. code snippet is extract from one of the answers in the abouve mentioned post . answered by https://stackoverflow.com/users/247542/cerin
class BaseModel(models.Model):
def clean(self, *args, **kwargs):
# add custom validation here
super(BaseModel, self).clean(*args, **kwargs)
def save(self, *args, **kwargs):
self.full_clean()
super(BaseModel, self).save(*args, **kwargs)
You can go ahead and read more about validation in django documentation.
If you are looking for any type/inheretance based solutions that might exist. I am not sure if they might exist. I would still like to see if someone comes up with such provision in django.

Categories