I'm using the Django admin and trying to make some changes to a related object that is mapped as an InlineModelAdmin object. I'm trying to do this using the save_related(self, request, form, formsets, change) method that Django provides. When I try to save something, I get an error:
AttributeError: 'AlumniResponseFormFormSet' object has no attribute 'new_objects'
Other Info:
1) I have two InlineModelAdmins
2) I'm not saving the AlumniResponseInline when this error occurs. I'm saving another InlineModelAdmin associated with the same parent model
3) Until I added the save_related() method, I wasn't having problems saving either InlineModelAdmin
4) This error is happening after all my code is executed in save_related(), so I don't have control over catching that exception
From the documentation on save_related():
The save_related method is given the HttpRequest, the parent ModelForm instance, the list of inline formsets and a boolean value based on whether the parent is being added or changed. Here you can do any pre- or post-save operations for objects related to the parent. Note that at this point the parent object and its form have already been saved.
I use save_formset instead of save_related and I was having the same problem until I realized that I missed two important lines inside the method:
instances = formset.save(commit=False)
at the beginning, and then, after loop instances to do something with each instance:
instance.save() #commit instance changes
formset.save_m2m() #commit the whole formset changes
at the end.
If you don't call the save_m2m() method before return, the formset object won't have the 'new_objects' attribute, needed in the construct_change_message(self, request, form, formsets) method in contrib/admin/options.py
So, this should be done for every inline you have in the main model, no matter whether you want to make something with it or not.
Related
I have some items connected to users.
When every item is added, timestamp is created through inheritance of BaseModel on auto_now field.
By mistake when i added new field and populated i updated timestamps.
I resolved timestamps with some custom migrations and copy data methods in django.
What i wonder - is there possibility to override save method on admin to do only update_fields (so in other words with that i would not update update_at timestamp),
while on user actions i want to retain original django save method which would update timestamp.
So basically is it possible to have two different save methods?
I know that i can override save method - but i don't know if i can have two save methods at the same time.
ModelAdmin.save_model() might deliver that for you. Check out the docs. Within your admin file, override the save_model function
class ObjectAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save(update_fields = ['fields', 'to', 'save'])
From looking at the django github, the super of save_model doesn't do much more than call obj.save(), so I don't think you need or want to call super() in this case.
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()
So I have a function in the Django admin that allows me to create a duplicate MyModel in the database:
def save_model(self, request, obj, form, change):
if '_saveasnew' in request.POST:
old_obj_id = resolve(request.path).args[0]
old_obj = MyModel.objects.get(id=old_obj_id)
obj.other_id = old_obj.other_id
obj.status = old_obj.status
obj.project_id = old_obj.project_id
obj.test_url = old_obj.test_url
obj.save()
super(MyModelAdmin, self).save_model(request, obj, form, change)
This creation works fine, but I have another system interacting with this database that is seeing insert failures every time this function has been called. For example, if I create 2 duplicate entries in the Django admin this way, then the other system will see two errors like
IntegrityError duplicate key value violates unique constraint "my_model_pkey" DETAIL: Key (id)=(1234) already exists.
I'm using Django 1.11.15 & PostgreSQL 9.5.15.
My best guess is that somewhere your code is telling your database to create a new object row and explicitly set the ID of that row to be X. When really the code should be telling your database to create a new object row and implicitly set the ID of that row to be whatever the next available integer is.
Your code is confusing because you're doing it in a very complicated way. Why does this function take in both an object and a request? And then it finds the old object from the request? Where was the new object created?
A simpler way would be to first check if you want to save as new. If not, have a function that updates an object and give it the existing object. If it is a Save As New Request, create a new object row that has similar the values as the existing object (except ID). And then update that new object with the changes. Or however your logic should work. In any case, there is a much more straightforward way of accomplishing the steps that you want to happen if you think about how the steps are ordered.
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.
I have overrided the save() method of one of my model so it can inherit from the sites object and tags from its parent.
def save(self, *args, **kwargs):
ret = models.Model.save(self, *args, **kwargs)
if self.id:
for site in self.parent.sites.all():
self.sites.add(site.id)
for tag in self.parent.tags_set.all():
Tag.objects.add_tag(self, tag)
Using ipdb, I can see that self.sites.all() DOES return 4 sites at the end of the method, but strangely, once the request is finish, the same self.sites.all() does not return anything anymore.
I don't use transactions (at least explicitly), and I'm using Django 1.3 and Ubuntu 11.04
EDIT: found out that it works anywhere but in the admin. Doesn't the admin call save? If not, how can I hook to the object creation / update?
EDIT2: tested, and does call save. I have print statements to prove it. But it doesn't add the sites. It's a mystery.
In fact, this is a problem about adding programatically many to many relationships when saving a model if you use the Django admin.
Django save m2m relationships in the admin by calling 'clear' to wipe them out, then setting them again. It means that the form destroy any attached data (including your programatically attached) to the object then add the ones you entered in the admin.
It works outside the admin because we don't use the admin form that clear the m2m relationship.
The reason it works for tags in the admin is that the tagging application doesn't use m2m but emulate it by placing a TaggedItem object with a foreign key to a tag and to your model with a generic relation. Plus it's an inline field inclusion.
I tried a lot of things and finally had to look at the Django source code to realize that Django does not process admin forms in the usual way. What it does it:
call ModelAdmin.save_form: it calls form.save with commit = False, returning an unsaved instance and adding a save_m2m method to the form.
call ModelAdmin.save_model that actually calls the instance save method.
call form.save_m2m
Therefor:
you can't override your save method since save_m2m is called after and clear the m2m relations.
you can't override save_model for the same reason.
you can't override save_m2m because it is added by monkey patch to the form model in form.save, erasing you own method definition.
I didn't find a clean solution, but something that works is:
Provide a form for the ModelAdmin class with a method to override save_m2m with your own method:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def set_m2m_method(self, update_tags_and_sites=True):
alias = self.save_m2m
def new_save_m2m(): # this is your new method
alias() # we need to call the original method as well
self.instance.add_niche_sites_and_tags()
self.save_m2m = new_save_m2m # we erase Django erasing :-)
Call this method in a ModelAdmin.model_save override:
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
def save_model(self, request, obj, form, change):
obj.save()
form.set_m2m_method()
This cause the following:
Django calls save_model, replacing its monkey patch by yours
django calls our form.save_m2m that first call its old method that clears relations, then attach the m2m to the object.
I'm completely open to any better way to do this as this is twisted and plain ugly.
Since the problem seems to be reserved to admin, I tried to add some logic to do this in the ModelAdmin's save_model method, but it doesn't seem to help at all:
class SomeModelAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save()
for site in Site.objects.all():
obj.sites.add(site.id)
print obj.sites.all()
Oddly print obj.sites.all() does list all the sites, however, they don't stay saved. Some sort of M2M issue perhaps?