django model: check relation before saving the object - django

I have django model consists of two class annualReport and annualReportAttachment
The relation between the two models is oneToMany. In the admin form I need to validation that the user has uploaded at least one file so I use the following clean method in the annualReport class
def clean(self):
attachments = annualReportAttachment.objects.filter(annualReport=self)
if len(attachments) == 0:
raise ValidationError("You should upload at least one file")
The problem is that the attached files is not saved yet so the attachments variable is empty and the form always raise that error.
How could I check that the user has uploaded at least one file?

You'll need to make sure that at least one form in your inline model gets saved. To do this, I would recommend leveraging the RequireOneFormSet class from https://code.google.com/p/wadofstuff/wiki/WadOfStuffDjangoForms

Related

Django video field contains url or file

How to make only one field of these two fields?
is it possible?
class MyModel(models.Model):
video_file = models.FileField(blank=True)
video = models.URLField(blank=True)
def clean(self, *args, **kwargs):
if not self.video_file and not self.video: # This will check for None or Empty
raise ValidationError({'video_file': 'Even one of field1 or field2 should have a value.'})
elif self.video_file and self.video: # This will check for None or Empty
raise ValidationError({'video_file': 'Even one of field1 or field2 should have a value.'})
if self.video == '':
self.video = self.video_file.url
super(MyModel, self).save(*args, **kwargs)```
**UPDATED**
I think this is the solution, my bad.
Having only one option
The easiest solution might be to not accept 2 different types, and only support either Image upload or Image URL. I'd suggest Image upload only if you're going to implement this solution.
However, if having those 2 options is a requirement you can take a look at the solutions I've listed below.
Checking at a controller level (Simple solution)
One solution is to check if both fields are populated at the controller level, or View in django jargon. If both are populated you can throw some error and handle it from there.
Changing the model and handling at service level (Recommended)
The above solution might work, but that wouldn't be the ideal solution for the long run.
I'd recommend you to change your model to only have a FileField, then in service layer you can directly upload if user uploads a file, however if user passes a URL you can download the image and save it.
You can also make the DB field a UrlField, and if user uploads a file, you can upload it to some external storage bucket like s3 or cloudinary and save the URL in your database.
As for the constraint, you can apply the constraint as mentioned above in solution 2 of adding the constraint in controller or some other way using django magic.

How to check for duplicate records in a Django admin inline?

How do you do perform validation checks involving multi-inline forms in Django admin inlines?
For example, I have a simple Parent/Child model, with an admin interface showing the children in an inline table on the parent's admin change page.
Each child has a "name" field which must be unique.
On child model, I've implemented a clean() method to enforce this rule, raising a forms.ValidationError so the error is displayed in a user-friendly fashion in the admin UI. This method is called from the model's full_clean() method, which is called by Django admin during the validation step for each inline form. So, individually, if the user attempts to create a child record, that check caches the error.
However, since Django runs the validation for each inline table separately before saving the records, it doesn't cache duplicates in the new data. So if the user creates two new inline rows and enters duplicate names in each of those rows, they pass the validation check, but then when Django goes to actually save the records, it encounters the exception, which is now handled like a very user-unfriendly 500 error.
Is there an easy way to fix this? Looking through Django's code, I'm not seeing anything obvious in the _changeform_view() that houses most of the admin form validation logic.
Presumably, I'd override something on the inline's ModelForm, but even the clean method on that only validates the fields for a single record, not across multiple records.
I had a similar problem myself and spent quite a while on a solution.
I'm not sure if you found an answer (since it has been 5 months since you have asked) but either way I think that sharing my solution might be beneficial so here you go:
I tried to override the clean() method of various classes also but to no avail. Then I found this page (Customize Save In Django Admin Inline Form) which suggested overriding the save_new_objects and save_existing_objects methods on a CustomInLineFormSet class.
So in admin.py I added the following method to a CustomInLineFormSet class (or in your case this would be intended for the Child model):
class ChildInLineFormSet(BaseInLineFormSet):
def save_new_objects(self, commit=True):
saved_instances = super(ChildInLineFormSet, self).save_new_objects(commit)
if commit:
for instance in saved_instances:
instance.delete()
try:
ChildModel.objects.get(name=instance.name)
except ChildModel.DoesNotExist:
instance.save()
else:
saved_instances.remove(instance)
return saved_instances
Also, wherever you have declared your InLine class you must also add the definition for the formset field:
class ChildInLine(admin.StackedInline):
formset = ChildInLineFormSet #add this to whatever you already have
I hope this helps!
EDIT: I did a bit more digging on this: Using a custom formset is NOT necessary after all.
You can override the save_formset() method in the admin class and and should obtain the same result without having to save the models to the database:
class ParentAdmin(admin.ModelAdmin):
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
unique_names = []
for obj in formset.deleted_objects:
obj.delete()
for instance in instances:
if (instance.name) in unique_names:
instance.delete()
continue
unique_names.append(instance.name)
instance.save()
formset.save_m2m()

Django form field validation - How to tell if operation is insert or update?

I'm trying to do this in Django:
When saving an object in the Admin I want to save also another object of a different type based on one of the fields in my fist object.
In order to do this I must check if that second object already exists and return an validation error only for the particular field in the first object if it does.
My problem is that I want the validation error to appear in the field only if the operation is insert.
How do I display a validation error for a particular admin form field based on knowing if the operation is update or insert?
P.S. I know that for a model validation this is impossible since the validator only takes the value parameter, but I think it should be possible for form validation.
This ca be done by writing a clean_[name_of_field] method in a Django Admin Form. The insert or update operation can be checked by testing self.instance.pk.
class EntityAdminForm(forms.ModelForm):
def clean_field(self):
field = self.cleaned_data['field']
insert = self.instance.pk == None
if insert:
raise forms.ValidationError('Some error message!')
else:
pass
return field
class EntityAdmin(admin.ModelAdmin):
form = EntityAdminForm
You have to use then the EntityAdmin class when registering the Entity model with the Django admin:
admin.site.register(Entity, EntityAdmin)
You can write your custom validation at the model level:
#inside your class model ...
def clean(self):
is_insert = self.pk is None
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
#do your business rules
if is_insert:
...
if __some_condition__ :
raise ValidationError('Dups.')
Create a model form for your model. In the clean method, you can set errors for specific fields.
See the docs for cleaning and validating fields that depend on each other for more information.
That is (probably) not an exact answer, but i guess it might help.
Django Admin offers you to override save method with ModelAdmin.save_model method (doc is here)
Also Django api have a get_or_create method (Doc is here). It returns two values, first is the object and second one is a boolean value that represents whether object is created or not (updated an existing record).
Let me say you have FirstObject and SecondObject
In your related admin.py file:
class FirstObjectAdmin(admin.ModelAdmin):
...
...
def save_model(self, request, obj, form, change):
s_obj, s_created = SecondObject.objects.get_or_create(..., defaults={...})
if not s_created:
# second object already exists... We will raise validation error for our first object
...
For the rest, I do not have a clear idea about how to handle it. Since you have the form object at hand, you can call form.fields{'somefield'].validate(value) and write a custom validation for admin. You will probably override clean method and try to trigger a raise ValidationError from ModelAdmin.save_model method. you can call validate and pass a value from there...
You may dig django source to see how django handles this, and try to define some custom validaton steps.

does django models offer something similar to forms' clean_<fieldname>()?

I am trying to move all business-logic-related validations to models, instead of leaving them in forms. But here I have a tricky situation, for which I like to consult with the SO community.
In my SignupForm (a model form), I have the following field-specific validation to make sure the input email does not exist already.
def clean_email(self):
email = self.cleaned_data['email']
if ExtendedUser.objects.filter(email=email).exists():
raise ValidationError('This email address already exists.')
return email
If I were to move this validation to the models, according to the official doc, I would put it in clean() of the corresponding model, ExtendedUser. But the doc also mentions the following:
Any ValidationError exceptions raised by Model.clean() will be stored
in a special key error dictionary key, NON_FIELD_ERRORS, that is used
for errors that are tied to the entire model instead of to a specific
field
That means, with clean(), I cannot associate the errors raised from it with specific fields. I was wondering if models offer something similar to forms' clean_<fieldname>(). If not, where would you put this validation logic and why?
You could convert your clean method into a validator and include it when you declare the field.
Another option is to subclass the model field and override its clean method.
However there is no direct equivalent of defining clean_<field name> methods as you can do for forms. You can't even assign errors to individual fields, as you can do for forms
As stated in the comment I believe you should handle this validation at the modelform level. If you still feel like it would be better to do it closer to the model, and since they can't be changed, I would advise a change directly at the db level:
ALTER TABLE auth_user ADD UNIQUE (email)
Which is the poor way to add the unique=True constraint to the User model without monkey patching auth.
As requested, I think that a good way to go about customizing different forms should be done by inheriting from a base modelform. A good example of this is found in django-registration. The only difference is that instead of the parent form inheriting from forms.Form you would make it a modelForm:
class MyBaseModelForm(ModelForm):
class Meta:
model = MyModel
You could then inherit from it and make different forms from this base model:
class OtherFormWithCustomClean(MyBaseModelForm):
def clean_email(self):
email = self.cleaned_data['email']
if ExtendedUser.objects.filter(email=email).exists():
raise ValidationError('This email address already exists.')
return email

Django Admin - Validating inlines together with main models

I have quite a complex validation requirement, and I cannot get Django admin to satisfy it.
I have a main model (django.contrib.auth.models.User) and several models which look like
class SomeProfile(models.Model):
user = models.OneToOneField(User)
# more fields
I want to check that, if the user belongs to some group, then it has the corresponding profile. So if user is in group Foo he should have a non empty FooProfile.
Where do I put this validation rule? I cannot put it in the model. Indeed, the user is not created yet when the form is validated, hence I cannot access his groups. So I need to resort to form validation. This is what I put:
class UserAdminForm(forms.ModelForm):
"""
A custom form to add validation rules which cannot live in the
model. We check that users belonging to various groups actually
have the corresponding profiles.
"""
class Meta:
model = User
def clean(self):
# Here is where I would like to put the validation
class FooInline(admin.TabularInline):
model = FooProfile
max_num = 1
class UserAdmin(admin.ModelAdmin):
model = User
form = UserAdminForm
inlines = [FooInline]
admin.site.register(User, UserAdmin)
My problem is that inside UserAdminForm.clean() I do not have access to the data posted inside the inlines. So I can tell whether the user is in group Foo by inspecting self.cleaned_data['groups'], but I have no way to tell whether a FooProfile was transmitted.
How do I check this validation requirement?
Edit:
I try to explain the issue better, because there has been a misunderstading in an answer.
I have an issue when I create a new user. The fact is that the profiles are mandatory (according to the groups). Say an admin creates a new user; then I have to add inlines in the admin form for the various GroupProfiles.
How do I check that the right profiles are not null? I cannot use the clean() method of the User model, because in there I cannot check what groups the user belongs to: it has not been created yet.
I can only access the information about the groups in the clean() method of the form - but there I do not have the information about the profiles, since this information is submitted trhough inlines.
1
well i have been looking around, how all this stuff works, and i found one question very similar here.
2
There are one way to get all the data at the same time maybe with this you can find the answer to your problem
class UserAdminForm(forms.ModelForm):
"""
A custom form to add validation rules which cannot live in the
model. We check that users belonging to various groups actually
have the corresponding profiles.
"""
class Meta:
model = User
def clean(self):
self.data # <--here is all the data of the request
self.data['groups']
self.data['profile_set-0-comments'] # some field
# some validations
return self.cleaned_data