Exclude fields but still provide a default value - python

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)

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

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

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.

django admin: show list of items filtered

Am working on a django admin interface and I have a model which has a foreign key. In that field, am getting a drop down menu when the admin pages are viewed. Is there a way to filter the drop down result only where is_active=1 for example?
Regards,
limit_choices_to is what you are after.
If you only want the limited selection in your ModelAdmin you should tweak your ModelForm accordingly.
Something like this should do it:
class YourAdminForm(forms.ModelForm):
class Meta:
model = YourModel
def __init__(self, *args, **kwargs):
super(YourAdminForm, self).__init__(*args, **kwargs)
qs = self.fields['your_fk_field'].queryset
self.fields['your_fk_field'].queryset = qs.filter(is_active=1)
According to the docs
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(is_active=1)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
If you need access to current object check this How do I get the actual object id in a Django admin page (inside formfield_for_foreignkey)?

Dynamic forms in django-admin

I want to make admin add-form dynamic. I want to add few formfields depending on setting in related object.
I have something like this:
class ClassifiedsAdminForm(forms.ModelForm):
def __init__(self,*args, **kwargs):
super(ClassifiedsAdminForm, self).__init__(*args, **kwargs)
self.fields['testujemy'] = forms.CharField(label = "test")
And in admin.py:
class ClassifiedAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
return ClassifiedsAdminForm
As you can see, I want to add "testujemy" CharField to admin add-form and change-form. However, this way doesnt work. Is there any way to add field in init? It is working in normal view.
I've managed to do it using type().
class ClassifiedAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
adminform = ClassifiedsAdminForm()
fields = adminform.getNewFields()
form = type('ClassifiedsAdminForm', (forms.ModelForm,), fields)
return form
Hope it will help someone.

Categories