django - different save methods for user actions and admin action - django

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.

Related

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()

How does form_valid work in django? How does it compare to save()?

The describtion of this in docs seems very sparse and unclear to me I am asking here. So what is exactly doing the form_valid method here? From what I understand, it gets triggered with POST method and it is kinda calling save() in the last line.
form.instance.entry_author=self.request.user in this line I understand that we are setting the current user to be the author but I dont understand why form referances to instance and also where did the form get from? I suppose its in-built in the form-valid function?
class CreateEntryView(CreateView):
model = Entry
template_name='entries/create_entry.html'
fields = ['entry_title','entry_text']
def form_valid(self, form):
form.instance.entry_author=self.request.user
return super().form_valid(form)
CreateView is a generic view which does a few things for you automatically without you having to write the logic yourself. One of those things is creating a Form. You indicated the model to use, which is Entry. Based on the fields, CreateView creates a form and uses that form.
From the docs: form_valid saves the form instance, sets the current object for the view, and redirects to get_success_url().
You can override this if you have any special requirements. Notice that entry_author is not included in the fields so it will not be present in the form. The idea here is to set the entry_author automatically instead of the user choosing the author in the form.

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.

Upload and process a file in django admin

I have two models, Course and Student. I want to include a file upload field to the Course form in the admin, to accept a CSV file that I will process and store as one or more records in the Students table. My questions are:
How can I add this "transient" field to the Course model, without it being a column or something in the corresponding table?
Where should I do the file processing? I was thinking perhaps overriding the save_model method in the CourseAdmin class will do it, am I right?
1 - You can override a form class in admin. Create you own ModelForm and add necessary field there
2 - After that, override form_save or form's save() method and process your file there

Why is adding site to an object doesn't seem to work in a save() override in the Django admin?

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?