How to access inline data in django ModelAdmin - django

I need to process a file uploaded in a django admin form. I've added a file upload field to the form:
class ExampleInline(admin.TabularInline):
model = OtherExample
extra = 1
class ExampleForm(forms.ModelForm):
filedata = forms.FileField()
class Meta:
model = ExampleModel
class ExampleModelAdmin(admin.ModelAdmin):
form = ExampleForm
inlines = [ExampleInline,]
This renders the form exactly like I want it to render. The data returned in Request is exactly what I expect.
The issue is that I want to access the contents of the inline.
class ExampleAdmin(admin.ModelAdmin):
...
def save_model(self, Request, obj, form, change):
the_file = form.cleaned_data['filedata']
# do amazing things to contents of file
At this point I want to reference the results of what the user selected in the inline. Whatever they picked for OtherExample.
How do I access that through the form? I would prefer not to go through the Request but am willing to do that. I'm also willing to examine save_related(self,request, form, formset, change)

save_related can do this, although it's called after the form is saved so you'll end up saving the object twice. You can access the object as form.instance or formset.instance.
def save_related(self, request, form, formsets, change):
obj = form.instance
# whatever your formset dependent logic is to change obj.filedata
obj.save()
super(ExampleAdmin, self).save_related(request, form, formsets, change)

Related

'save_model' method + 'save_as = True' // how to change "old" object

I have the following model and i am using 'save_as' in admin.py to enable a “save as new” feature on admin change forms.
The goal is that if i use 'save_as' the "old" object should have set the boolean field 'hide' to True.
I tried to implement the ModelAdmin method 'save_model' to admin.py but this only changes the value for the "new" object.
Is it possible to change the "old" object also?
models.py
class Person(models.Model):
name = models.CharField(max_length=64)
hide = models.BooleanField(default=0)
admin.py
class personAdmin(admin.ModelAdmin):
save_as = True
def save_model(self, request, obj, form, change):
obj.hide = True
super(personAdmin, self).save_model(request, obj, form, change)
Try this:
def save_model(self, request, obj, form, change):
# Get old object's id from the url
old_obj_id = resolve(request.path).args[0]
# Now that we know the id, we can easily fetch it from database
old_obj = Person.objects.get(id=old_obj_id)
# Update it's hide field
old_obj.hide = True
# Don't forget to save it!
old_obj.save()
# Now we can save the new object as a new one (save_as)
super(personAdmin, self).save_model(request, obj, form, change)

How to display a custom field only in change view (admin)

I'm totally new in Python and Django :) and i need some help.
What i want to do:
I have a model Page and i need to add a custom field "message" when someone try to update one object.
Why? Because i'm building a revision system. This field, it's just an explanation about the change. So this field is not linked to the Page (but to another model PageRevision)
After some research i managed to add this field to my form in the admin.py file, like this:
class PageAdminForm(forms.ModelForm):
# custom field not backed by database
message = forms.CharField(required=False)
class Meta:
model = Page
it's working, my field is now displayed...But i don't want this field everywhere. Just when someone try to update a Page object.
i have found this answer different-fields-for-add-and-change-pages-in-admin but it's not working for me because it's a custom field (i think).
The rest of my code in admin.py:
class PageAdmin(admin.ModelAdmin):
form = PageAdminForm
fields = ["title", "weight", "description", "message"]
list_display = ["title", "weight", "description"]
list_filter = ["updated_at"]
def get_form(self, request, obj=None, **kwargs):
if obj is None:
# not working ?
kwargs['exclude'] = ['message']
# else:
# kwargs['exclude'] = ['message']
return super(PageAdmin, self).get_form(request, obj, **kwargs)
def save_model(self, request, obj, form, change):
if not obj.id:
obj.author = request.user
obj.modified_by = request.user
wiki_page = obj.save()
# save page in revision table
revision = PageRevision(change=change, obj=wiki_page,
request=request)
# retrieve value in the custom field
revision.message = form.cleaned_data['message']
revision.save()
def get_form doesn't exclude my custom message field because i think it doesn't know is existence. If i put another field like title, it's works.
So how to exclude the custom field from add view ?
Thanks :)
You're right, it won't work this way, because 'message' is not a field found on the Page model and the ModelAdmin class will ignore the exclusion. You can achieve this in many ways, but I think the best way to do it is this:
class PageAdmin(admin.ModelAmin):
change_form = PageAdminForm
...
def get_form(self, request, obj=None, **kwargs):
if obj is not None:
kwargs['form'] = self.change_form
return super(UserAdmin, self).get_form(request, obj, **defaults)
Basicaly here django will use an auto-generated ModelForm when adding a Page and your custom form when editing the Page. Django itself uses a similar technique to display different forms when adding and changing a User:
https://github.com/django/django/blob/stable/1.6.x/django/contrib/auth/admin.py (the interesting part is in line 68)
I just stumbled upon this same question and, since it comes in the first search results I would like to add this other solution for people that do not want to use a custom form.
You can override the get_fields method of your admin class to remove a custom field from the Add page.
def get_fields(self, request, obj=None):
fields = list(super().get_fields(request, obj=obj))
if obj is None:
fields.remove("message")
return fields

Save the related objects before the actual object being edited on django admin

Is it possible to save the related objects before the actual object being edited on a django admin form?
For example:
in models.py
class Parent(model.Model):
pass
class Child(model.Model):
parent = models.ForeignKey(Parent)
#receiver(post_save,sender = Parent)
def notify_parent_save(sender, instance=None, **kwargs):
print "Parent save"
#receiver(post_save,sender = Child)
def notify_child_save(sender, instance=None, **kwargs):
print "Child saved"
in admin.py
class ChildInline(admin.TabularInline):
model = Child
extra = 1
class ParentsAdmin(admin.ModelAdmin):
inlines = [ChildInline]
admin.site.register(Parent,ParentsAdmin)
Now, in django admin if I save a parent object, it will output on the console.
Parent save
Child save
I need this to happen in revese order:
Child save
Parent save
The following will save the children first:
class ParentAdmin(admin.ModelAdmin):
inlines = [ChildInline]
def save_model(self, request, obj, form, change):
pass # don't actually save the parent instance
def save_formset(self, request, form, formset, change):
formset.save() # this will save the children
form.instance.save() # form.instance is the parent
I was having issues with the answers in this post, so I figured out a more concise answer. I was having an issue because using django-fsm, the other answers here would try to save the model multiple times (once for every formset) rather than once at the end.
def save_model(self, request, obj, form, change):
if not obj.pk: # call super method if object has no primary key
super(YourAdmin, self).save_model(request, obj, form, change)
else:
pass # don't actually save the parent instance
def save_related(self, request, form, formsets, change):
form.save_m2m()
for formset in formsets:
self.save_formset(request, form, formset, change=change)
super(YourAdmin, self).save_model(request, form.instance, form, change)
This essential just flips the order of save_model and save_related as called in Django ModelAdmin source
ccrisan's answer brought me on the right track, but I think there is a flaw regarding save behavior of instances that do not yet exist in the database. In this case it's not possible to save the related objects first, because there is no foreign key that they can point to. For me the following extension did the trick:
class ParentAdmin(admin.ModelAdmin):
inlines = [ChildInline]
def save_model(self, request, obj, form, change):
if not obj.pk: # call super method if object has no primary key
super(ParentAdmin, self).save_model(request, obj, form, change)
else:
pass # don't actually save the parent instance
def save_formset(self, request, form, formset, change):
formset.save() # this will save the children
form.instance.save() # form.instance is the parent
Depending on what you exactly want to do in your signals, can you just change the post_save to pre_save for the Child model ?

Django admin change to_python output based on request

I'm wondering how to change the behavior of a form field based on data in the request... especially as it relates to the Django Admin. For example, I'd like to decrypt a field in the admin based on request data (such as POST or session variables).
My thoughts are to start looking at overriding the change_view method in django/contrib/admin/options.py, since that has access to the request. However, I'm not sure how to affect how the field value displays the field depending on some value in the request. If the request has the correct value, the field value would be displayed; otherwise, the field value would return something like "NA".
My thought is that if I could somehow get that request value into the to_python() method, I could directly impact how the field is displayed. Should I try passing the request value into the form init and then somehow into the field init? Any suggestions how I might approach this?
Thanks for reading.
In models.py
class MyModel(models.Model):
hidden_data = models.CharField()
In admin.py
class MyModelAdmin(models.ModelAdmin):
class Meta:
model = MyModel
def change_view(self, request, object_id, extra_context=None):
.... # Perhaps this is where I'd do a lot of overriding?
....
return self.render_change_form(request, context, change=True, obj=obj)
I haven't tested this, but you could just overwrite the render_change_form method of the ModelAdmin to sneak in your code to change the field value between when the change_view is processed and the actual template rendered
class MyModelAdmin(admin.ModelAdmin):
...
def render_change_form(self, request, context, **kwargs):
# Here we have access to the request, the object being displayed and the context which contains the form
form = content['adminform'].form
field = form.fields['field_name']
...
if 'obj' in kwargs:
# Existing obj is being saved
else:
# New object is being created (an empty form)
return super(MyModelAdmin).render_change_form(request, context, **kwargs)

Override save on Django InlineModelAdmin

That question might look similar to this one, but it's not...
I have a model structure like :
class Customer(models.Model):
....
class CustomerCompany(models.Model):
customer = models.ForeignKey(Customer)
type = models.SmallIntegerField(....)
I am using InlineModels, and have two types of CustomerCompany.type. So I define two different inlines for the CustomerCompany and override InlineModelAdmin.queryset
class CustomerAdmin(admin.ModelAdmin):
inlines=[CustomerCompanyType1Inline, CustomerCompanyType2Inline]
class CustomerCompanyType1Inline(admin.TabularInline):
model = CustomerCompany
def queryset(self, request):
return super(CustomerCompanyType1Inline, self).queryset(request).filter(type=1)
class CustomerCompanyType2Inline(admin.TabularInline):
model = CustomerCompany
def queryset(self, request):
return super(CustomerCompanyType2Inline, self).queryset(request).filter(type=2)
All is nice and good up to here, but for adding new records for InlineModelAdmin, I still need to display type field of CustomerCompany on the AdminForm, since I can not override save method of an InlineModelAdmin like:
class CustomerCompanyType2Inline(admin.TabularInline):
model = CustomerCompany
def queryset(self, request):
return super(CustomerCompanyType2Inline, self).queryset(request).filter(type=2)
#Following override do not work
def save_model(self, request, obj, form, change):
obj.type=2
obj.save()
Using a signal is also not a solution since my signal sender will be the same Model, so I can not detect which InlineModelAdmin send it and what the type must be...
Is there a way that will let me set type field before save?
Alasdair's answer isn't wrong, but it has a few sore points that could cause problems. First, by looping through the formset using form as the variable name, you actually override the value passed into the method for form. It's not a huge deal, but since you can do the save without commit right from the formset, it's better to do it that way. Second, the all important formset.save_m2m() was left out of the answer. The actual Django docs recommend the following:
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
# Do something with `instance`
instance.save()
formset.save_m2m()
The problem you're going to run into is that the save_formset method must go on the parent ModelAdmin rather than the inlines, and from there, there's no way to know which inline is actually being utilized. If you have an obj with two "types" and all the fields are the same, then you should be using proxy models and you can actually override the save method of each to set the appropriate type automatically.
class CustomerCompanyType1(CustomerCompany):
class Meta:
proxy = True
def save(self, *args, **kwargs):
self.type = 1
super(CustomerCompanyType1, self).save(*args, **kwargs)
class CustomerCompanyType2(CustomerCompany):
class Meta:
proxy = True
def save(self, *args, **kwargs):
self.type = 2
super(CustomerCompanyType2, self).save(*args, **kwargs)
Then, you don't need to do anything special at all with your inlines. Just change your existing inline admin classes to use their appropriate proxy model, and everything will sort itself out.
There's a save_formset method which you could override. You'd have to work out which inline the formset represents somehow.
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
# Do something with `instance`
instance.save()
formset.save_m2m()
Other answers are right when it comes to using save_formset. They are missing a way to check what model is currently saved though. To do that, you can just:
if formset.model == CustomerCompany:
# actions for specific model
Which would make the save_formset function look like: (assuming you just want to override save for the specific model(s))
def save_formset(self, request, form, formset, change):
for obj in formset.deleted_objects:
obj.delete()
# if it's not the model we want to change
# just call the default function
if formset.model != CustomerCompany:
return super(CustomerAdmin, self).save_formset(request, form, formset, change)
# if it is, do our custom stuff
instances = formset.save(commit=False)
for instance in instances:
instance.type = 2
instance.save()
formset.save_m2m()
For the cases where you need to take an action if the registry is new, you need to do it before saving the formset.
def save_formset(self, request, form, formset, change):
for form in formset:
model = type(form.instance)
if change and hasattr(model, "created_by"):
# craeted_by will not appear in the form dictionary because
# is read_only, but we can anyway set it directly at the yet-
# to-be-saved instance.
form.instance.created_by = request.user
super().save_formset(request, form, formset, change)
In this case I'm also applying it when the model contains a "created_by" field (because this is for a mixin that I'm using in many places, not for a specific model).
Just remove the and hasattr(model, "created_by") part if you don't need it.
Some other properties that might be interesting for you when messing with formsets:
"""
The interesting fields to play with are:
for form in formset:
print("Instance str representation:", form.instance)
print("Instance dict:", form.instance.__dict__)
print("Initial for ID field:", form["id"].initial)
print("Has changed:", form.has_changed())
form["id"].initial will be None if it's a new entry.
"""
I hope my digging helps someone else! ^^