Django Model conditional Validations - python

i have made an api on which a customer is sending his/her details for payment by making post request, so need to validate this data on model level,How can i validate a model field by comparing it to the other model fields For example:
models.py
Orders(models.Model):
amount = models.DecimalField(max_digits=19, decimal_places=4)
currency = models.CharField(max_length=3,choices=[('INR','INR')]) more choices are to be added
payee_pan = models.CharField(max_length=10)
Validation required : payee_pan must be present if currency is 'INR' and amount is greater than 50000.
in order to validate it i am using model.full_clean() while saving the the model object in views.py
views.py
try:
orders.full_clean()
except ValidationError:
return Response({"Error message":"invalid request body"})
else:
orders.save()
i would really appreciate if someone helps me in this as m stuck at this point for so long.enter code here

from django.db import models
from django.core.exceptions import ValidationError
class Orders(models.Model):
amount = models.DecimalField(max_digits=19, decimal_places=4)
currency = models.CharField(max_length=3, choices=[('INR','INR')]) # more choices are to be added
payee_pan = models.CharField(max_length=10)
class Meta:
verbose_name = "Order"
verbose_name_plural = "Orders"
def clean(self):
if self.currency == 'INR' and self.amount > 50000 and not self.payee_pan:
raise ValidationError(
{"payee_pan": "Payee Pan field is required"}
)
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)
You can remove the try-except block in your views. Calling the save() method will invoke the full_clean() method under the hood which will call any validation hooks.

You can perform such validation by overriding the clean method on the model (Reference Validating objects [Django docs]):
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class Orders(models.Model):
amount = models.DecimalField(max_digits=19, decimal_places=4)
currency = models.CharField(max_length=3,choices=[('INR','INR')])
payee_pan = models.CharField(max_length=10)
def clean(self):
if self.currency == 'INR' and self.amount > 50000 and not self.payee_pan:
raise ValidationError(_('Payee Pan is required'))
Note: Ideally model names are supposed to be singular (If you check the verbose name plural for your model it ends up as Orderss
with an extra s currently). Hence it should be Order
instead of Orders.

Related

Django choice field validation in admin

I have a model that has a choice field with choices loaded in the runtime.
from some_utils import get_currency_options
class Product(models.Model):
name = models.CharField(max_length=20)
currency = models.CharField(max_length=3, null=True, blank=True, default="USD", choices=[])
def clean(self):
# getting the currency options at execution time, options may vary at different times
currency_code_options = get_currency_options()
if self.currency and self.currency not in currency_code_options:
raise ValidationError({"currency": f"Invalid currency code {self.fb_check_currency}."})
super(Product, self).clean()
Please ignore the bad design here, it was defined like this since we need to integrate with a legacy system.
In the Django admin, I have a form like this
from some_utils import get_currency_options
class ProductAdminForm(ModelForm):
currency_choices = get_currency_options()
#staticmethod
def _values_list_to_options(values_list):
return [(val, val) for val in values_list]
def __init__(self, *args, **kwargs):
super(ProductAdminForm, self).__init__(*args, **kwargs)
self.fields["currency"] = ChoiceField(choices=self._values_list_to_options(self.currency_choices))
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
Now the problem is, when I go to Django admin and want to update the currency option, it fails to save with an error saying currency is not a valid option. I understand this is due to the choice list being empty, I tried to override the clean and clean_all method but it didn't work.
Which method does the admin update operation trigger? Is there a way I can use the get_currency_options method to load currency options to the validator so if my selection matches one of the value, it passes the validator?
I have the same error. I your case, you need overview the clean_field method in your model class. For example:
from some_utils import get_currency_options
class Product(models.Model):
name = models.CharField(max_length=20)
currency = models.CharField(max_length=3, null=True, blank=True, default="USD", choices=[])
def clean_fields(self, exclude=None):
exclude = ['currency']
super().clean_fiedls(exclude=exclude)
def clean(self):
self.validate_currency()
super().clean()
def validate_currency(self):
# getting the currency options at execution time, options may vary at different times
currency_code_options = get_currency_options()
if self.currency and self.currency not in currency_code_options:
raise ValidationError({"currency": f"Invalid currency code {self.fb_check_currency}."})

How to validate a django modelform if the form has some other fields

I have made a Django model form but the problem is in my logic I am using something else and now I want to figure out a way to validate it by either defining a Meta class and choosing the fields that I want to display to the user but of course this won't validate the form.
Now I want to know if there is a way to validate the form without touching the models and pass the data required for the logic and after take care of the information needed for the data of the model to be saved.
Here is the models:
from django.db import models
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class RoomCategory(models.Model):
name = models.CharField(max_length=59)
price = models.IntegerField()
beds = models.PositiveIntegerField()
capacity = models.PositiveIntegerField()
size = models.CharField(max_length=59)
def __str__(self):
return self.name
class Room(models.Model):
room_number = models.CharField(max_length=60)
room_category = models.ForeignKey(RoomCategory, on_delete=models.CASCADE)
def __str__(self):
return f"The room {self.room_number} {self.room_category} has a maximum of {self.room_category.capacity} person and cost {self.room_category.price}/night "
class Booking(models.Model):
customer = models.ForeignKey(User, on_delete=models.CASCADE)
room = models.ForeignKey(RoomCategory, on_delete=models.CASCADE)
check_in = models.DateField()
check_out = models.DateField()
adults = models.PositiveSmallIntegerField()
children = models.PositiveSmallIntegerField()
def __str__(self):
return f"{self.customer} has booked for {self.room} from {self.check_in} to {self.check_out}"
Here is the form:
class BookingForm(forms.ModelForm):
class Meta:
model = Booking
fields = ['room', 'check_in', 'check_out', 'adults', 'children']
here is the views.py
data = form.cleaned_data
roomlist = Room.objects.filter(room_category__name=data['room'])
available_rooms = []
for room in roomlist:
if data['adults'] + data['children'] > room.room_category.capacity:
return HttpResponse(f'Sorry !! But this category of room cannot handle more than {room.room_category.capacity}')
else:
if check_availability(room.room_category.name, data['check_in'], data['check_out'], data['adults'], data['children']):
available_rooms.append(room)
if len(available_rooms) > 0:
room = available_rooms[0]
new_booking = Booking.objects.create(
customer=self.request.user,
room=room,
check_in=data['check_in'],
check_out=data['check_out'],
adults=data['adults'],
children=data['children']
)
new_booking.save()
return HttpResponse(new_booking)
else:
return HttpResponse('All the rooms of this type are not available')
It is not printing the data means that the form is not valid and it fall down to the else statement.
You can validate any field in the form by writing a method in this way : def clean_(field_name) i.e def clean_room(self) read more:
https://docs.djangoproject.com/en/3.1/ref/forms/validation/#cleaning-a-specific-field-attribute

access attribute via serializer django

I got following models:
class OrderItem(models.Model):
ordered_amount = models.IntegerField(validators=[MinValueValidator(0)])
amount = models.IntegerField(default=0)
order = models.ForeignKey(
Order, on_delete=models.CASCADE, related_name="order_items"
)
class Order(models.Model):
reference = models.CharField(max_length=50)
purchase_order = models.CharField(max_length=15, blank=True, null=True)
I'm now writing a serializer for listing orders. In this OrderSerializer I need to access amount and ordered_amount in the OrderItem class. How do I do this?
This is What I have now:
class AdminOrderListSerializer(serializers.ModelSerializer):
amount = serializers.IntegerField()
ordered_amount = serializers.IntegerField()
class Meta:
model = Order
fields = [
"purchase_order",
"reference",
"amount",
"ordered_amount",
]
# noinspection PyMethodMayBeStatic
def validate_amount(self, order):
if order.order_items.amount:
return order.order_items.amount
return
# noinspection PyMethodMayBeStatic
def validate_ordered_amount(self, order):
if order.order_items.ordered_amount:
return order.order_items.ordered_amount
return
This gives me following error:
AttributeError: Got AttributeError when attempting to get a value for field amount on serializer AdminOrderItemListSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the Order instance.
Original exception text was: 'Order' object has no attribute 'amount'.
There are many ways to that, one of them is SerializerMethodField:
from django.db.models import Sum
class AdminOrderListSerializer(serializers.ModelSerializer):
amount = serializers.SerializerMethodField()
ordered_amount = serializers.SerializerMethodField()
def get_amount(self,obj):
return obj.order_items.aggregate(sum=Sum('amount'))['sum']
def get_ordered_amount(self,obj):
return obj.order_items.aggregate(sum=Sum('order_amount'))['sum']
Optimized solution
Another way of achieving this is to annotate the data to queryset, and access them in serializer. For that, you need to change in view:
class SomeView(ListAPIView):
queryset = Order.objects.annotate(amount=Sum('order_items__amount'),order_amount=Sum('order_items__order_amount'))
This is a optimized solution because it reduces database hits(it only hits once).

Set value of a model field that is not part of model form

I have a model like this
class Income(models.Model):
value = models.DecimalField(max_digits=10, decimal_places=2)
remitted = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
I have a form like this
class EditIncomeForm(forms.ModelForm):
class Meta:
model = Income
fields = ("value", )
def clean_value(self):
value = self.cleaned_data["value"]
if self.value < self.remitted:
raise forms.ValidationError("Error message")
return value
Now in the modelform, how do I update the value of the remitted field?
I can't seem to be able to access the remitted field this way. I'm on Django 2.0
This doesn't have anything to do with whether the field is on the form or not.
You're doing two things wrong; firstly, you're trying to access self.remitted instead of self.cleaned_data['remitted']; and secondly, remitted won't have been cleaned at the point that clean_value is called.
To access data from multiple fields, override the main clean() method instead:
def clean(self):
value = self.cleaned_data["value"]
remitted = self.cleaned_data["remitted"]
if value < remitted:
self.add_error("value", "Error message")
I did some reading about Validation on a ModelForm from the docs and finally I was able to raise the error in the form. Full form code below.
This section in particular proved to be what I needed. It says:
A model form instance attached to a model object will contain an
instance attribute that gives its methods access to that specific
model instance.
class EditIncomeForm(forms.ModelForm):
class Meta:
model = Income
fields = ("value", )
def clean(self):
value = self.cleaned_data['value']
remitted = self.instance.remitted # access other model field.
if value <= remitted:
self.add_error("value", "Error message.")

Django Unique Together (with foreign keys)

I have a situation where I want to use the Meta options of unique_together to enforce a certain rule, here's the intermediary model:
class UserProfileExtension(models.Model):
extension = models.ForeignKey(Extension, unique=False)
userprofile = models.ForeignKey(UserProfile, unique=False)
user = models.ForeignKey(User, unique=False)
class Meta:
unique_together = (("userprofile", "extension"),
("user", "extension"),
# How can I enforce UserProfile's Client
# and Extension to be unique? This obviously
# doesn't work, but is this idea possible without
# creating another FK in my intermediary model
("userprofile__client", "extension"))
and here's UserProfile:
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
client = models.ForeignKey(Client)
Thanks.
You can't.
The unique_together clause is directly translated to the SQL unique index. And you can only set those on columns of a single table, not a combination of several tables.
You can add validation for it yourself though, simply overwrite the validate_unique method and add this validation to it.
Docs: http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.validate_unique
My 2 cents, complementing the accepted response from #Wolph
You can add validation for it yourself though, simply overwrite the validate_unique method and add this validation to it.
This is a working example code someone could find usefull.
from django.core.exceptions import ValidationError
class MyModel(models.Model):
fk = models.ForeignKey(AnotherModel, on_delete=models.CASCADE)
my_field = models.CharField(...) # whatever
def validate_unique(self, *args, **kwargs):
super().validate_unique(*args, **kwargs)
if self.__class__.objects.\
filter(fk=self.fk, my_field=self.my_field).\
exists():
raise ValidationError(
message='MyModel with this (fk, my_field) already exists.',
code='unique_together',
)
My solution was to use Django's get_or_create. By using get_or_create, a useless get will occur if the row already exists in the database, and the row will be created if it does not exist.
Example:
extension = Extension.objects.get(pk=someExtensionPK)
userProfile = UserProfile.objects.get(pk=someUserProfilePK)
UserProfileExtension.objects.get_or_create(extension=extension, userprofile=userProfile)
From django 2.2+ versions, it is suggested to use constraint & Index as model class meta option:
https://docs.djangoproject.com/en/3.2/ref/models/options/#django.db.models.Options.unique_together
https://docs.djangoproject.com/en/3.2/ref/models/options/#django.db.models.Options.constraints
class UniqueConstraintModel(models.Model):
race_name = models.CharField(max_length=100)
position = models.IntegerField()
global_id = models.IntegerField()
fancy_conditions = models.IntegerField(null=True)
class Meta:
constraints = [
models.UniqueConstraint(
name="unique_constraint_model_global_id_uniq",
fields=('global_id',),
),
models.UniqueConstraint(
name="unique_constraint_model_fancy_1_uniq",
fields=('fancy_conditions',),
condition=models.Q(global_id__lte=1)
),
models.UniqueConstraint(
name="unique_constraint_model_fancy_3_uniq",
fields=('fancy_conditions',),
condition=models.Q(global_id__gte=3)
),
models.UniqueConstraint(
name="unique_constraint_model_together_uniq",
fields=('race_name', 'position'),
condition=models.Q(race_name='example'),
)
]
You need to call Models.full_clean() method to call validate_unique for foreignKey. You can override save() to call this
class UserProfileExtension(models.Model):
extension = models.ForeignKey(Extension, unique=False)
userprofile = models.ForeignKey(UserProfile, unique=False)
user = models.ForeignKey(User, unique=False)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class Meta:
unique_together = (("userprofile", "extension"),
("user", "extension"),
# How can I enforce UserProfile's Client
# and Extension to be unique? This obviously
# doesn't work, but is this idea possible without
# creating another FK in my intermediary model
("userprofile__client", "extension"))
from django.core.exceptions import ValidationError
.....
class UserProfileExtension(models.Model):
extension = models.ForeignKey(Extension, unique=False)
userprofile = models.ForeignKey(UserProfile, unique=False)
user = models.ForeignKey(User, unique=False)
def validate_unique(self, *args, **kwargs):
super(UserProfileExtension, self).validate_unique(*args, **kwargs)
query = UserProfileExtension.objects.filter(extension=self.extension)
if query.filter(userprofile__client=self.userprofile.client).exists():
raise ValidationError({'extension':['Extension already exits for userprofile__client',]})
The first query is to filter all records in UserProfileExtension model which has the same extension we are putting in the current record.
Then we filter the query returned to find if it already contains userprofile__client which we are passing in the current record.
Another possible solution is to add this on your save method from your Model:
def save(self, *args, **kwargs):
unique = self.__class__.objects.filter( extension =self.extension, userprofile=self.userprofile )
if unique.exists():
self.id = unique[0].id
super(self.__class__, self).save(*args, **kwargs)

Categories