Using the Django sites framework in the Django admin - python

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

Related

Django. DetailView simple validation. Check weather user has a subscription

When user requests object, when user wants to enter detail view's page, I want to check weather user have subscription and redirect him. But I don't know how to request user and validate him in DetailView. This is what at least I could did.
class PropertyDetailView(LoginRequiredMixin, DetailView):
login_url = '/login/'
model = Property
template_name = 'project/property/property_detail.html'
def post(self, *args, **kwargs):
if self.request.user.sale_tariff is None:
return redirect('/')
Are there any ways how to validate DetailView?
Overriding the get request something like this
def get(self, *args, **kwargs):
if self.request.user.sale_tariff is None:
return redirect('/')
else:
return render(template_name)

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.

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.

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)

Return the current user with Django Rest Framework

I am currently developing an API using Django.
However, I would like to create a view that returns the current User with the following endpoint: /users/current/.
To do so, I created a list view and filtered the queryset on the user that made the request. That works, but the result is a list, not a single object. Combined with pagination, the result looks way too complicated and inconsistent compared to other endpoints.
I also tried to create a detail view and filtering the queryset, but DRF complains that I provided no pk or slug.
With something like this you're probably best off breaking out of the generic views and writing the view yourself.
#api_view(['GET'])
def current_user(request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
You could also do the same thing using a class based view like so...
class CurrentUserView(APIView):
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
Of course, there's also no requirement that you use a serializer, you could equally well just pull out the fields you need from the user instance.
#api_view(['GET'])
def current_user(request):
user = request.user
return Response({
'username': user.username,
'email': user.email,
...
})
The best way is to use the power of viewsets.ModelViewSet like so:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_object(self):
pk = self.kwargs.get('pk')
if pk == "current":
return self.request.user
return super().get_object()
viewsets.ModelViewSet is a combination of mixins.CreateModelMixin + mixins.RetrieveModelMixin + mixins.UpdateModelMixin + mixins.DestroyModelMixin + mixins.ListModelMixin + viewsets.GenericViewSet. If you need just list all or get particular user including currently authenticated you need just replace it like this
class UserViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
# ...
Instead of using full power of ModelViewSet you can use mixins. There is RetrieveModelMixin used to retrieve single object just like it is mentioned here - http://www.django-rest-framework.org/api-guide/viewsets/#example_3
class UserViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = UserSerializer
def get_object(self):
return self.request.user
If you need also update your model, just add UpdateModelMixin.
If you must use the generic view set for some reason, you could do something like this,
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
def get_object(self):
return self.request.user
def list(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
retrieve method is called when the client requests a single instance using an identifier like a primary key /users/10 would trigger the retrieve method normally. Retrieve itself calls get_object. If you want the view to always return the current used then you could modify get_object and force list method to return a single item instead of a list by calling and returning self.retrieve inside it.
I used a ModelViewSet like this:
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
def dispatch(self, request, *args, **kwargs):
if kwargs.get('pk') == 'current' and request.user:
kwargs['pk'] = request.user.pk
return super().dispatch(request, *args, **kwargs)
Use this way to get logged in user data in django rest framework
class LoggedInUserView(APIView):
def get(self, request):
serializer = UserSerializer(self.request.user)
return Response(serializer.data)
Add the api in urls.py file.
path('logged_in_user', LoggedInUserView.as_view())

Categories