Django choice field validation in admin - python

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}."})

Related

Django Model conditional Validations

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.

create value to a model from another model's method

I'm trying to create a new table (Payslip model) that will contain the computed salary on an employee in a cutoff.
I want Payslip.salary to get value from Employee.compute_pay()
Given the example in the url link above, what should my views.py look like?
Is this the best approach to this kind of process? or Is there any library that can help with what I want to do here?
https://imgur.com/a/wVG5qrd
model.py
class Employee(models.Model):
name = models.CharField(max_length=20)
rate = models.IntegerField()
absent = models.IntegerField()
def __str__(self):
return self.name
def compute_pay(self):
daily = rate / 20
return rate - (daily*absent)
class Payslip(models.Model):
name = models.CharField(max_length=20)
salary = models.IntegerField()
def __str__(self):
return self.name
views.py
def compute(request):
if request.method == "POST":
return render(request,'payroll/compute/compute.html')
I do not think there is a need for another model Payslip, also you have no ForeignKey connections between the two models for it to work.
Considering your requirement, property decorator should work. Read up on how #property works. Basically, it acts as a pseudo model field, keep in mind the value of this field is not stored anywhere in the database, but it is tailor-made for situations like this where the field changes based on other fields and is needed for read-only.
Try this
class Employee(models.Model):
name = models.CharField(max_length=20)
rate = models.IntegerField()
absent = models.IntegerField()
def __str__(self):
return self.name
#property
def compute_pay(self):
daily = self.rate / 20
return (self.rate - (daily*self.absent))
you can get the employee's salary by Employee.compute_pay just like any other model field

"... matching query does not exist." error, but object clearly does exist

I have a lesson model:
class Lesson(models.Model):
list_name = models.CharField(max_length=50, primary_key=True)
def __str__(self):
return self.list_name
A sound model:
class Sound(models.Model):
sound_hash = models.CharField(max_length=40, primary_key=True, editable=False)
lessons = models.ManyToManyField(Lesson, verbose_name="associated lessons", related_name="words")
And a test_pair model:
class TestPair(models.Model):
master_sound = models.ForeignKey(Sound, related_name="used_as_master")
user_sound = models.ForeignKey(Sound, related_name="used_as_user")
My form looks something like this:
class SoundTestPairForm(ModelForm):
user_sounds = forms.ModelMultipleChoiceField(Sound.objects.all())
class Meta:
model = TestPair
fields = ['master_sound']
def __init__(self, *args, **kwargs):
super(SoundTestPairForm, self).__init__(*args, **kwargs)
self.fields['master_sound'] = forms.CharField()
self.fields['master_sound'].widget.attrs['readonly'] = 'readonly'
def clean(self):
cleaned_data = super(SoundTestPairForm, self).clean()
if 'user_sounds' not in cleaned_data.keys():
raise forms.ValidationError('You must select at least one sound to be compared')
cleaned_data['master_sound'] = Sound.objects.get(pk=self.fields['master_sound'])
return cleaned_data
This is throwing a DoesNotExist error. The traceback points to this line: cleaned_data['master_sound'] = Sound.objects.get(pk=self.fields['master_sound'])
The local vars are as follows:
self
<SoundTestPairForm bound=True, valid=True, fields=(master_sound;associated_test;associated_lesson;user_sounds)>
cleaned_data
{'associated_lesson': u'pooooooooooooooooooooooop',
'associated_test': u'cats a',
'master_sound': u'ad27ec5e0d048ddbb17d0cef0c7b9d4406a2c33',
'user_sounds': [<Sound: Pants>]}
But when I go to python manage.py shell, and import my model:
Sound.objects.get(pk=u'ad27ec5e0d048ddbb17d0cef0c7b9d4406a2c33')
<Sound: oombah>
It hits the appropriate object.
This is something I've been dealing with for a long time, and it's super frustrating. I try to switch my logic around and ultimately it ends up throwing a DoesNotExist error, regardless of what I try.
Does anyone have any ideas?
I think you should replace:
Sound.objects.get(pk=self.fields['master_sound'])
with:
Sound.objects.get(pk=cleaned_data['master_sound'])
When the Form is valid, cleaned_data will include a key and value for all its fields and you can see that in your question as well (The local vars paragraph).
In general speaking you should do validation in the individual clean_<fieldname> methods. So:
def clean_master_sound(self):
master_sound_key = self.cleaned_data.get('master_sound')
try:
master_sound=Sound.objects.get(pk=master_sound_key)
except Sound.DoesNotExist:
raise forms.ValidationError("Sound with id: %s does not exist", master_sound_key)
return master_sound

Correct way to save nested formsets in Django

I have a 3-level Test model I want to present as nested formsets. Each Test has multiple Results, and each Result can have multiple Lines. I am following Yergler's method for creating nested formsets, along with this SO question that updates Yergler's code for more recent Django version (I'm on 1.4)
I am running into trouble because I want to use FormSet's "extra" parameter to include an extra Line in the formset. The ForeignKey for each Line must point to the Result that the Line belongs to, but cannot be changed by the user, so I use a HiddenInput field to contain the Result in each of the FormSet's Lines.
This leads to "missing required field" validation errors because the result field is always filled out (in add_fields), but the text and severity may not (if the user chose not to enter another line). I do not know the correct way to handle this situation. I think that I don't need to include the initial result value in add_fields, and that there must be a better way that actually works.
Update below towards bottom of this question
I will gladly add more detail if necessary.
The code of my custom formset:
LineFormSet = modelformset_factory(
Line,
form=LineForm,
formset=BaseLineFormSet,
extra=1)
class BaseResultFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(BaseResultFormSet, self).__init__(*args, **kwargs)
def is_valid(self):
result = super(BaseResultFormSet, self).is_valid()
for form in self.forms:
if hasattr(form, 'nested'):
for n in form.nested:
n.data = form.data
if form.is_bound:
n.is_bound = True
for nform in n:
nform.data = form.data
if form.is_bound:
nform.is_bound = True
# make sure each nested formset is valid as well
result = result and n.is_valid()
return result
def save_all(self, commit=True):
objects = self.save(commit=False)
if commit:
for o in objects:
o.save()
if not commit:
self.save_m2m()
for form in set(self.initial_forms + self.saved_forms):
for nested in form.nested:
nested.save(commit=commit)
def add_fields(self, form, index):
# Call super's first
super(BaseResultFormSet, self).add_fields(form, index)
try:
instance = self.get_queryset()[index]
pk_value = instance.pk
except IndexError:
instance=None
pk_value = hash(form.prefix)
q = Line.objects.filter(result=pk_value)
form.nested = [
LineFormSet(
queryset = q, #data=self.data, instance = instance, prefix = 'LINES_%s' % pk_value)]
prefix = 'lines-%s' % pk_value,
initial = [
{'result': instance,}
]
)]
Test Model
class Test(models.Model):
id = models.AutoField(primary_key=True, blank=False, null=False)
attempt = models.ForeignKey(Attempt, blank=False, null=False)
alarm = models.ForeignKey(Alarm, blank=False, null=False)
trigger = models.CharField(max_length=64)
tested = models.BooleanField(blank=False, default=True)
Result Model
class Result(models.Model):
id = models.AutoField(primary_key=True)
test = models.ForeignKey(Test)
location = models.CharField(max_length=16, choices=locations)
was_audible = models.CharField('Audible?', max_length=8, choices=audible, default=None, blank=True)
Line Model
class Line(models.Model):
id = models.AutoField(primary_key=True)
result = models.ForeignKey(Result, blank=False, null=False)
text = models.CharField(max_length=64)
severity = models.CharField(max_length=4, choices=severities, default=None)
Update
Last night I added this to my LineForm(ModelForm) class:
def save(self, commit=True):
saved_instance = None
if not(len(self.changed_data) == 1 and 'result' in self.changed_data):
saved_instance = super(LineForm, self).save(commit=commit)
return saved_instance
It ignores the requests to save if only the result (a HiddenInput) is filled out. I haven't run into any problems with this approach yet, but I haven't tried adding new forms.
When I used extra on formsets in similar situation I ended up having to include all the required fields from the model in the form, as HiddenInputs. A bit ugly but it worked, curious if anyone has a hack-around.
edit
I was confused when I wrote above, I'd just been working on formsets using extra with initial to pre-fill the extra forms and also I hadn't fully got all the details of your questions.
If I understand correctly, where you instantiate the LineFormSets in add_fields each of those will point to the same Result instance?
In this case you don't really want to supply result in initial due to the problems you're having. Instead you could remove that field from the LineForm model-form altogether and customise the LineFormSet class something like:
class LineFormSet(forms.BaseModelFormSet):
# whatever other code you have in it already
# ...
# ...
def __init__(self, result, *args, **kwargs):
super(LineFormSet, self).__init__(*args, **kwargs)
self.result = result
def save_new(self, form, commit=True):
instance = form.save(commit=False)
instance.result = self.result
if commit:
instance.save()
return instance
def save_existing(self, form, instance, commit=True):
return self.save_new(form, commit)
(this should be ok in Django 1.3 and 1.4, not sure other versions)
so the relevant part of your add_fields method would look like:
form.nested = [
LineFormSet(
result = instance,
queryset = q, #data=self.data, instance = instance, prefix = 'LINES_%s' % pk_value)]
prefix = 'lines-%s' % pk_value,
)]

Simple multiplication in model's attribute django?

I am trying to set a default value for attribute threshold in this code, the threshold should be the current level*50 and this is the model
class Level (models.Model):
name = models.CharField(max_length=20,null=True, blank=True)
description = models.CharField(max_length=20, null=True, blank=True)
number = models.IntegerField(null=True, blank=True)
threshold = models.IntegerField(null=True, blank=True,default=50*number,editable=False)
i get an error unsupported operand types for * : 'int' and 'IntegerField'
You best best is to do such calculation while saving the object. So override Model.save
or a better generic way would be to write a custom field and override pre_save
class DependentIntegerField(models.IntegerField):
def pre_save(self, model_instance, add):
if not add: # set the default only while adding model
return super(self, DependentIntegerField).pre_save(model_instance, add)
return model_instance.number*50
You can further enhance it and make DependentIntegerField generic so that you can pass callable to it and do any calculation, and you can do further enhancements like checking if user has set the value or not before using default value, and to make it more generic so that you can make any Field as dependent field by passing the field class to a factory function. e.g.
from django.db import models
class_map = {}
def depends_field_pre_save(self, model_instance, add):
"""
if default is not callable or it is not a model add, lets skip our hook
"""
if not add or not callable(self.default):
super(self.__class__, self).__init__(self,*args, **kwargs)
value = self.default(model_instance)
setattr(model_instance, self.attname, value)
return value
def FieldDepends(field_class):
"""
return a dervied class from field_class which supports dependent default
"""
if field_class in class_map:
# we already created this class so return that
return class_map[field_class]
new_class = type('Depends'+field_class.__name__, (field_class,), {'pre_save':depends_field_pre_save })
class_map[field_class] = new_class
return new_class
and use it like this
class DependentModel(models.Model):
def threshold_default(model_instance=None):
if model_instance is None:
return 10
return model_instance.number*10
number = models.IntegerField(null=True, blank=True, default=10)
threshold = FieldDepends(models.IntegerField)(null=True, blank=True, default=threshold_default,editable=False)
I have created a small django project djangodepends on bitbucket with test cases
You can override save method to calculation.
https://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods
class Level (models.Model):
name = models.CharField(max_length=20,null=True, blank=True)
description = models.CharField(max_length=20, null=True, blank=True)
number = models.IntegerField(null=True, blank=True)
threshold = models.IntegerField(null=True, blank=True ,editable=False)
def save(self, *args, **kwargs):
try:
self.threshold = self.number * 50
except TypeError:
pass
super(Level, self).save(*args, **kwargs) # Call the "real" save() method.

Categories