Django admin save() doesn't get data from ManyToManyField - django

I'm trying to get chosen objects from affiliate_networks in Django admin when the user clicks the submit button.
When I choose one from affiliate_networks and submit it, the console prints an empty of affiliate_networks, and then I come back to the page and the chosen object is stored properly. So, I submit it again, then the console prints the chosen object. save() only receives objects that are already stored, not objects that I choose before saving.
Is there a way, that I can have save() to notice affiliate_networks to have any object chosen?
class Store(models.Model):
...
affiliate_networks = models.ManyToManyField(AffiliateNetwork, blank=True)
def save(self, *args, **kwargs):
print(self.affiliate_networks.all())

You can't do it in save() - as you have discovered, Django admin doesn't save ManyToMany objects until afterwards. You need to do it in the save_related method of ModelAdmin. See https://timonweb.com/posts/many-to-many-field-save-method-and-the-django-admin/
In admin.py:
...
class StoreAdmin(admin.ModelAdmin):
def save_related(self, request, form, formsets, change):
super(StoreAdmin, self).save_related(request, form, formsets, change)
print(form.instance.affiliate_networks.all())
...
admin.site.register(Store, StoreAdmin)

Related

Readonly View for 10 Django Class Based Views

I have 10 Django Class Based Views and I want to display them read-only to the user.
I want the whole form to be read-only, not only some values. Submitting the form should be disabled on the client (HTML) and a second time on the server (POST not allowed).
Is there a MixIn or an other simple solution?
Here's a mixin that does two simple things:
Sets html attributes for all fields in form for disabled andreadonly.
Overrides the form_valid method of your CBV so that no model saving ever happens; instead, the template is rendered (just as if there was no submitted data). The user, this way, does not cause any action if they submitted the form.
Form field errors may appear next to disabled fields if you are rendering the full form in your template; solve this by either erasing the form's error dictionary or by rendering each field individually without errors.
from django.views.generic.edit import FormMixin, ModelFormMixin
class ReadOnlyModelFormMixin(ModelFormMixin):
def get_form(self, form_class=None):
form = super(ReadOnlyModelFormMixin, self).get_form()
for field in form.fields:
# Set html attributes as needed for all fields
form.fields[field].widget.attrs['readonly'] = 'readonly'
form.fields[field].widget.attrs['disabled'] = 'disabled'
return form
def form_valid(self, form):
"""
Called when form is submitted and form.is_valid()
"""
return self.form_invalid(form)
Extending this concept for a non-model FormView is pretty simple; inherit from class FormMixin instead. :)
To disallow POST requests in general for class-based views you could use the following mixin:
class DisallowPostMixin(object):
def post(self, request, *args, **kwargs):
return self.http_method_not_allowed(self, request, *args, **kwargs)
If you also want to disable certain form fields etc. you could add the get_form method from Ian Price's answer.
You can hack it through middleware. On request - check view name and request method (if post - redirect), on response - add input attrs in response.content. But mixin - best solution.

Show different model admin list_display and fields set for different user groups

In django I have superuser and group of content editors.
When I edit model as superuser I want be able to edit all fields. And if someone logged in as editor I want to allow him to edit only specific fields.
I have done that with get_form method:
class VideoAdmin(admin.ModelAdmin):
editor_fields = ('description','description_rewrited')
def get_form(self, request, obj=None, **kwargs):
if not hasattr(request.user, 'perms_list'):
request.user.perms_list = request.user.groups.values_list('name',flat=True)
if 'video_description_rewriter' in request.user.perms_list:
print('rewrite fields to normal')
self.fields = self.normaluser_fields
return super(VideoAdmin, self).get_form(request, obj, **kwargs)
It works for me. But when I open video for editing as regular editor it changes superuser fields set to editors fields set.
Open admin model as superuser - http://joxi.ru/zAN5wWMIVjz429
Open admin model as editor - http://joxi.ru/p27LJPZiDNgeA7
Now superuser has the same fields set as editor - http://joxi.ru/L21jko5TW0ydAX
I assume that there is some kind of template caching?
You're setting self.fields to self.normaluser_fields when user is editor, but you aren't setting self.fields back to default value when user is admin. ModelAdmin objects are created on application load and they're shared between all users!
Changing fields back to default value when admin enters page won't solve problem in 100% percent. When admin and non-admin user will try to enter edit page in same time, race condition might occur. Both of them can get same fields.
Instead of rewritting get_form, you can do it simpler by rewritting get_fields method:
def get_fields(self, request):
if not hasattr(request.user, 'perms_list'):
request.user.perms_list = request.user.groups.values_list('name',flat=True)
if 'video_description_rewriter' in request.user.perms_list:
print('rewrite fields to normal')
return self.normaluser_fields
return self.fields
That method won't overwrite any values in ModelAdmin object, so change will be only visible for one user.

Mailto in Django Admin

I have a Django application where a user enters an e-mail address in a form, amongst the other fields that they have to fill in. In my Django admin, it displays the typed results from all these fields. What I'd like to add is a button beside the e-mail field in the admin view to send an e-mail to the entered address. How would I go about this? Would I need to edit the admin page template or model, perhaps?
You could add an boolean field "email_on_save" and a text field for the "message". When the record gets saved in the admin it calls the Model.save() method.
You extend the model.save() this way:
def save(self, *args, **kwargs):
if self.email_on_save:
send_email(self.email, self.message)
self.email_on_save = False
self.message = ""
# Call super save method
return super(MyModel, self).save(*args, **kwargs)

accessing request object within a django admin inline model

I have the following model / form / admin hierarchy. What I want to achieve is basically, add the currently logged in user to a field of the AttachmentInlines. So I thought I would need the request object which gets passed in at several methods, implemented below.
However, none of these methods gets called in that context, obviously because I am working with Inline models.
I COULD work in InvoiceAdmin#save_model, but that sucks because I would then have to that in a whole bunch of model admins all using the same Inlines. Which doesn't seem too DRY to me.
So is there any other way to get access to the request object from within my Inline models in the admin?
class AttachmentForm(forms.ModelForm):
class Meta:
model = Attachment
def save(self, request, obj, *args, **kwargs):
print 'I never get called :-('
class AttachmentInlines(generic.GenericStackedInline):
model = Attachment
form = AttachmentForm
def save_model(self, request, obj, form, change):
print 'I never get called either...'
class InvoiceAdmin(admin.ModelAdmin):
inlines = [ AttachmentInlines, ]
def save_model(self, request, obj, form, change):
print 'I DO get called, but I am not needed...'

Django: customizing the message after a successful form save

whenever I save a model in my Admin interface, it displays the usual "successfully saved message."
However, I want to know if it's possible to customize this message because I have a situation where I want to warn the user about what he just saved and the implications of these actions.
class PlanInlineFormset(forms.models.BaseInlineFormset):
def clean(self):
### How can I detect the changes?
### (self.changed_data doesn't work because it's an inline)
### and display what he/she just changed at the top AFTER the successful save?
class PlanInline(admin.TabularInline):
model = Plan
formset = PlanInlineFormset
Django (> version 1.2) uses the messages framework for admin messages. You can add additional messages using that interface. Here's an example:
from django.contrib import messages
class SomeModelAdmin(admin.ModelAdmin):
# your normal ModelAdmin stuff goes here
def save_model(self, request, obj, form, change):
# add an additional message
messages.info(request, "Extra message here.")
super(SomeModelAdmin, self).save_model(request, obj, form, change)
To detect changes to the object being saved, you should be to override the save_model method of ModelAdmin, and compare the object the method is passed to the version currently in the database. To do this in the case of inlines, you can override the save_formset method. A possible approach might look like (untested code):
class SomeModelAdmin(admin.ModelAdmin):
# your normal ModelAdmin stuff goes here
def save_formset(self, request, form, formset, change):
if not change:
formset.save()
else:
instances = formset.save(commit=False)
for instance in instances:
try:
# if you've got multiple types of inlines
# make sure your fetching from the
# appropriate model type here
old_object = SomeOtherModel.get(id=instance.id)
except SomeOtherModel.DoesNotExist:
continue
if instance.field_x != old_object.field_x:
messages.info(request, "Something Changed")
instance.save()
formset.save_m2m()
If you're using Django 1.2 or newer, the messages framework may hold the answer.
http://docs.djangoproject.com/en/dev/ref/contrib/messages/