How To Access The Request Object in Django's GenericStackedInline Admin - python

Using GenericStackedInline in Django 1.9 (Python 3.4) I want to access the request object before saving my model in the Django Admin.
When using MediaItemAdmin I can intercept the save function before obj.save() is run, as in this example:
admin.py
class StuffAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# Do some stuff here like obj.user = request.user before saving.
obj.save()
However, the same behaviour or 'hook' isn't available using a GenericStackedInline. It appears to call the model save method directly:
admin.py
class StuffAdmin(GenericStackedInline):
model = StuffModel
def save_model(self, request, obj, form, change):
print("I'm never run :(")
obj.save()
As I understand GenericStackedInline inherits from a form so I have also tried using a form and overriding that as in this example:
admin.py
class StuffAdmin(GenericStackedInline):
model = StuffModel
form = StuffForm
class StuffForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(StuffForm, self).__init__(*args, **kwargs)
def save_model(self, request, obj, form, change):
print("Still not run!(")
obj.save()
def save_form(self, request, obj, form, change):
print("Work already!")
obj.save()
I have searched stackoverflow, but most are unanswered, as seen here accessing request object within a django admin inline model or say use init to do something like self.request = kwargs.pop('request') however, request is never passed here, right?
Anyhow, any idea how I can call the request object and update my instance before the model save() is called?

The method that saves the "inlines" is part of ModelAdmin, not InlineModelAdmin.
class BarInline(GenericStackedInline):
model = Bar
class FooModelAdmin(ModelAdmin):
model = Foo
inlines = [BarInline]
def save_formset(self, request, form, formset, change):
"""
`form` is the base Foo form
`formset` is the ("Bar") formset to save
`change` is True if you are editing an existing Foo,
False if you are creating a new Foo
"""
if formset_matches_your_inline_or_some_requirement(formset):
do_something_with(request)
super().save_formset(request, form, formset, change)
If you want to check whether the formset is the BarInline's formset, you can do something like this:
class BarInline(GenericStackedInline):
model = Bar
def get_formset(self, *args, **kwargs):
formset = super().get_formset(*args, **kwargs)
formset.i_come_from_bar_inline = True
return formset
class FooModelAdmin(ModelAdmin):
model = Foo
inlines = [BarInline]
def save_formset(self, request, form, formset, change):
if getattr(formset, 'i_come_from_bar_inline', False):
do_something_with(request)
super().save_formset(request, form, formset, change)
Or even better, make it generic:
class BarInline(GenericStackedInline):
model = Bar
def pre_save_formset(self, request, form, model_admin, change):
"""Do something here with `request`."""
class FooModelAdmin(ModelAdmin):
model = Foo
inlines = [BarInline]
def save_formset(self, request, form, formset, change):
if hasattr(formset, 'pre_save_formset'):
formset.pre_save_formset(request, form, self, change)
super().save_formset(request, form, formset, change)
if hasattr(formset, 'post_save_formset'):
formset.post_save_formset(request, form, self, change)
If you need to do something with the request before each form save rather than before each formset, you will have to use your own Form and FormSet propagate the request through the formset to the form:
from django.forms import ModelForm
from django.forms.models import BaseInlineFormSet
class BarForm(ModelForm):
model = Bar
def __init__(self, *args, **kwargs):
request = kwargs.pop('request', None)
super().__init__(*args, **kwargs)
self.request = request
def save(self, commit=True):
print(self.request)
print(self.instance)
obj = super().save(False) # Get object but don't save it
do_something_with(self.request, obj)
if commit:
obj.save()
self.save_m2m()
return obj
class BarFormSet(BaseInlineFormSet):
#property
def request(self):
return self._request
#request.setter
def request(self, request):
self._request = request
for form in self.forms:
form.request = request
class BarInline(GenericStackedInline):
codel = Bar
form = BarForm
formset = BarFormSet
class FooModelAdmin(ModelAdmin):
inlines = [BarInline]
def _create_formsets(self, request, obj, change):
formsets, inline_instances = super()._create_formsets(request, obj, change)
for formset in formsets:
formset.request = request
return formsets, inline_instances
According to you usecase, the save method might also simply look like something like this:
class BarForm(ModelForm):
model = Bar
def save(self, commit=True):
do_something_with(self.request, self.instance)
return super().save(commit) # Get object but don't save it

Admin classes don't inherit from forms; they include forms. And ModelForms don't have either save_model or save_form methods, they just have a save method. It's perfectly possible to override that method, but it doesn't accept request; you'd need to also override __init__ to accept that argument and pass it in from the modeladmin's get_form_kwargs method.

Related

Does Django's get_queryset() in admin prevent malicious object saving?

I am developing a multi-tenant app in Django. In the Django admin, some querysets are filtered based on the user, using get_queryset().
Up till now, when a user updated an object from the Django change form, I would validate the data by creating a ModelAdmin form using a factory function to capture the HttpRequest object, then ensure that the Guest object's user was the current user:
EXAMPLE
models.py
class Guest(models.Model):
guest_name = models.CharField(max_length=64)
user = models.ForeignKey(User, on_delete=models.CASCADE)
admin.py
#admin.register(Guest)
class GuestAdmin(admin.ModelAdmin):
def get_queryset(self, request)
qs = super().get_queryset(request)
return qs.filter(user=request.user)
def get_form(self, request, obj=None, **kwargs):
self.form = _guest_admin_form_factory(request)
return super().get_form(request, obj, **kwargs)
forms.py
def _guest_admin_form_factory(request):
class GuestAdminForm(forms.ModelForm):
class Meta:
model = Guest
exclude = []
def clean_user(self):
user = self.cleaned_data.get('user', None)
if not user:
return user
if user != request.user:
raise forms.ValidationError('Invalid request.')
return user
return GuestAdminForm
It occurred to me that Django might use the get_queryset() method to validate this for me, since some simple logging showed that the method is called twice when an object gets updated from the change form.
Is this the case, or do I need to stick to validating through a ModelAdmin form?
The documented way to do this is to define has_change_permission():
#admin.register(Guest)
class GuestAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return super().get_queryset(request).filter(user=request.user)
def has_change_permission(self, request, obj=None):
return (obj is None or obj.user == request.user)
No need to muck about with the form.

Using the Django sites framework in the Django admin

I'm implementing a solution using the Django sites framework for the first time, and am not sure whether there is a better way of implementing it on the Django admin.
Currently I have it working on the frontend, but I want users to be restricted to only manage the content on the backend that belongs to 'their' site (each user is assigned to a site).
To do this currently, I'm splitting the fields available to a superuser (is_superuser) and anyone else by specifying the respective fields in the Admin class. I'm then overriding the following:
The get_form method to return a different form depending on the user. For instance, a superuser can create content for any site, whereas any other user can only create content for their own site.
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.fieldsets = self.user_fieldsets + self.superuser_fieldsets
else:
self.fieldsets = self.user_fieldsets
return super(FaqCategoryAdmin, self).get_form(request, obj, **kwargs)
The get_queryset method, to only show the relevant entries for the site the user has access to.
def get_queryset(self, request):
qs = super(FaqCategoryAdmin, self).get_queryset(request)
if request.user.is_superuser:
return qs
else:
return qs.filter(site=settings.SITE_ID)
The save_model to ensure if a non-superuser saves a new entry, that it defaults to their site:
def save_model(self, request, obj, form, change):
if not request.user.is_superuser:
obj.site = get_current_site(request)
obj.save()
This feels incredibly onerous, given how amazingly simple it is to use the sites framework to restrict frontend display of content (using a model manager). Is there a better way of going about this?
Thanks!
Yes, there is. Create your own custom admin base class. Derive all other admin classes from that one.
class MyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.fieldsets = self.user_fieldsets + self.superuser_fieldsets
else:
self.fieldsets = self.user_fieldsets
return super(MyAdmin, self).get_form(request, obj, **kwargs)
def get_queryset(self, request):
qs = super(MyAdmin, self).get_queryset(request)
if request.user.is_superuser:
return qs
else:
return qs.filter(site=settings.SITE_ID)
def save_model(self, request, obj, form, change):
if not request.user.is_superuser:
obj.site = get_current_site(request)
obj.save()
And then,
class FaqCategoryAdmin(MyAdmin):
# now this class is dry. Because repetitive code is in parent

Exclude fields but still provide a default value

I want to be able to hide a field from a form in the Django admin (I'm using Django 1.7), but still supply a default value (which is bound to the request as it is request.user).
Here are the contents of my admin.py:
from django.contrib import admin
from .models import News
class NewsAdmin(admin.ModelAdmin):
list_display = ('title', 'category', 'pub_date', 'visible',)
list_filter = ('visible', 'enable_comments', 'category__topic', 'category__site', 'category',)
search_fields = ['title']
def get_form(self, request, obj=None, **kwargs):
if not request.user.is_superuser:
self.exclude = ('author',)
return super(NewsAdmin, self).get_form(request, obj, **kwargs)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'author':
kwargs['initial'] = request.user.id
return super(NewsAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
admin.site.register(News, NewsAdmin)
And here is what it does:
As a superuser the author field correctly displays, preselecting the current user
As any other staff member allowed to create a News, the author field is hidden, but when the form is submitted an exception is raised:
IntegrityError at /admin/news/news/add/
Column 'author_id' cannot be null
How can I hide the author field and still provide an author_id?
Instead of excluding the field, you can set it to read only so that it will still show to the user, but they cannot change it:
self.readonly_fields = ('author',)
I finally found how to achieve what I wanted to do with a combination of two methods I have overriden.
Here the get_form method, now showing the author field as read-only instead of excluding it (thanks to this answer). This is a change that is being made for editing purposes only (see obj is not None) to prevent an unwanted exception while editing someone else's news.
def get_form(self, request, obj=None, **kwargs):
if not request.user.is_superuser and obj is not None:
self.readonly_fields = ('author',)
return super(NewsAdmin, self).get_form(request, obj, **kwargs)
And here is the formfield_for_foreignkey method. It simply filters the QuerySet to allow only one user.
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'author':
kwargs['initial'] = request.user.id
kwargs['queryset'] = User.objects.filter(pk=request.user.id)
return super(NewsAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

How to populate formview with model data when fetched via GET

I have the form display info via GET request, currently I assign a new form in get_context_data, is there a better way to do it? And http decorator doesn't work too.
#require_http_methods(['GET',])
class UniversityDetail(SingleObjectMixin, FormView):
model = KBUniversity
form_class = KBUniversityForm
template_name = 'universities/form.html'
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super(UniversityDetail, self).get(self, request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(UniversityDetail, self).get_context_data(**kwargs)
context['form'] = KBUniversityForm(instance=self.object)
context['university_id'] = self.object.pk
return context
Use UpdateView instead of FormView with SingleObjectMixin. In 99% of the cases you should not override the get method.

'User' object has no attribute 'get'

I am trying to construct a ModelForm from this solution here, however i am getting this error:
'User' object has no attribute 'get'
The idea is to get ModelForm to construct a form that when submitted the user logged in updates the entry.
The models.py is:
class UserDetailsForm(ModelForm):
class Meta:
model = UserProfile
fields = ['mobile_phone']
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
return super(UserDetailsForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
kwargs['commit']=False
obj = super(UserDetailsForm, self)
if self.request:
obj.user = UserProfile.objects.get(user=self.request.user)
obj.save()
And my model in models.py is
class UserProfile(models.Model):
user = models.OneToOneField(User)
mobile_phone = models.CharField(max_length=30,help_text='Max 30 characters.',blank=True)
#have shortened this for simplicity
def __unicode__(self):
return self.mobile_phone
At the request here is a traceback of the issue from views.py:
userprofile = UserProfile.objects.get(user=request.user)
if request.method == 'POST':
form = UserDetailsForm(request.user, request.POST, request.FILES)
if form.is_valid(): # it dies here
form.save()
return HttpResponseRedirect('/members-contact/')
Writing this answer because I was bitten twice in a single week by this error.
Came to this question and it was no use in helping me figure out the problem.
The problem with this code is that you have passed request.user an object into the init function of the UserDetailsForm. And your definition for init does not handle what happens with request.user.
userprofile = UserProfile.objects.get(user=request.user)
if request.method == 'POST':
====>form = UserDetailsForm(request.user, request.POST, request.FILES)
if form.is_valid(): # it dies here
form.save()
return HttpResponseRedirect('/members-contact/')
See arrow. If you compare that with your definition for the __init__of user details form. You can see init is not expecting that request.user
class UserDetailsForm(ModelForm):
class Meta:
model = UserProfile
fields = ['mobile_phone']
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
return super(UserDetailsForm, self).__init__(*args, **kwargs)
Note there are legitimate reasons why one would write the init to pass in an object.
def __init__(self, some_object, *args, **kwargs):
super(SomeFormClass, self).__init__(self, *args, **kwargs)
self.fields['some_field'].queryset = SomeModel.objects.filter(some_field = some_object)
Also Note that default def of __init__ for modelform has __init__(self, *args, **kwargs)
The dynamic form initialisation here above is a good example.
It appears that django is treating the passed in variable in this case request.user as some_field and is trying to call a method called get which the 'UserModel' does not have. If you check the stack trace you will notice. The below stack trace is an example simulated.
Traceback (most recent call last):
File "/home/user/.local/lib/python3.5/site-packages/django/core/handlers/exception.py", line 39, in inner
response = get_response(request)
return render(request, self.template_name, context)
File "/home/user/.local/lib/python3.5/site- packages/django/shortcuts.py", line 30, in render
content = loader.render_to_string(template_name, context, request, using=using)
---
---
---
packages/django/forms/forms.py", line 297, in non_field_errors
return self.errors.get(NON_FIELD_ERRORS, self.error_class(error_class='nonfield'))
File "/home/user/.local/lib/python3.5/site-packages/django/forms/forms.py", line 161, in errors
self.full_clean()
---
---
---
self._clean_fields()
File "/home/user/.local/lib/python3.5/site-packages/django/forms/forms.py", line 382, in _clean_fields
===>value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))<====
File "/home/sodara/.local/lib/python3.5/site-packages/django/forms/widgets.py", line 238, in value_from_datadict
====> return data.get(name) <====
AttributeError: 'SomeObject' object has no attribute 'get'
data.get is the return value the result of the method call field.widget.value_from_data_dict ...
if you notice, the SomeObject is being treated as the data here whoes get method is being called.
To answer the question, either define init to handle the request.user
def __init__(self, user, *args, **kwargs):
super(YourFormClass, self).__init__(*args, **kwargs):
self.fields["some_field"].some_attr = user
Or call the form without the request.user
`form = YourFormClass(request.POST, request.FILES)`
If you decide to go with option one. You have to remember to call super before calling self.fields. Because self.fields is created by the super method. If you dont you will run into another attributeerror no field named fields.
Edit
Django provides a convenient method get_form_kwargs for adding attributes to the init of form views that inherit from django.views.generic.edit.ModelFormMixin such as FormView.
class MyFormView(FormView):
form_class = MyModelFormClass
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
class MyModelFormClass(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user') # Important to do this
# If you dont, calling super will fail because the init does
# not expect, user among the fields.
super().__init__(*args, **kwargs)
self.fields['some_field'].queryset = SomeModel.objects.filter(user=user)
You need something a bit simpler. Have this as your model form:
class UserDetailsForm(ModelForm):
class Meta:
model = UserProfile
fields = ['mobile_phone']
In your view:
from django.core.urlresolvers import reverse_lazy
from django.views.generic import UpdateView
from .models import UserDetailsForm, UserProfile
class UpdateProfile(UpdateView):
template_name = 'users/update_profile.html'
form_class = UserDetailsForm
model = UserProfile
success_url = reverse_lazy('home')
def get_object(self, queryset=None):
'''This loads the profile of the currently logged in user'''
return UserProfile.objects.get(user=self.request.user)
def form_valid(self, form):
'''Here is where you set the user for the new profile'''
instance = form.instance # This is the new object being saved
instance.user = self.request.user
instance.save()
return super(UpdateProfile, self).form_valid(form)
In your urls.py, you need to make sure that the view is called with a logged in user:
from django.contrib.auth.decorators import login_required
from django.views.generic TemplateView
from .views import UpdateProfile
urlpatterns = patterns('',
(r'^profile/update/$', login_required(UpdateProfile.as_view())),
(r'^$', TemplateView.as_view(template='index.html'), name='home'),
)
form = UserDetailsForm(request.user, request.POST, request.FILES)
The problem is that you pass the user object as a positional argument, while your form expects the first positional argument to be the form data. Meanwhile, your form expects a keyword argument request that contains the request object, but you're not passing such an argument. Change the above line to:
form = UserDetailsForm(request.POST, request.FILES, request=request)
I can't see your full view function, but for simple form handling you might want to consider using a class-based view, based on Django's UpdateView, like Burhan suggested.
at __init__
instead of :
return super(UserDetailsForm, self).__init__(*args, **kwargs)
try:
forms.ModelForm.__init__(self, *args, **kwargs)
it works for me..
for visitors using get method here:
from django.contrib.auth.models import User
u = User.objects.all()[0]
print(u.get('username'))
# switch the line above to below
print(getattr(u, 'username'))

Categories