Django customize AdminModel view - python

In my Django application I have Guest user accounts that are created for all unregistered users (they all have email='guest#mysite.com'). At the same time I create some demo objects related to the Guest account.
These objects live in the same table (have the same model) as objects for registered users. And I have more that one type (model) of these objects like:
class Object1(models.Model):
user = ForeignKey(...)
...
class Object2(models.Model):
user = ForeignKey(...)
...
And what I would like to achieve is to filter out all objects related to guest accounts when I view them in django admin.
Right now I subclass django.contrib.admin.views.main.ChangeList and override get_query_set method to do the required exclude, and I redefine get_changelist method of django's ModelAdmin class at runtime:
class FilteredChangeList(ChangeList):
def get_query_set(self):
qs = super(FilteredChangeList, self).get_query_set()
if is_related_to(self.model, Profile):
qs = qs.exclude(user__email='guest#mysite.com')
return qs
def my_getchangelist(self, request, **kwargs):
return FilteredChangeList
ModelAdmin.get_changelist = my_getchangelist
I suppose redefining django's methods at runtime is a bad practice, so is there any correct solution for the problem?

Guess you are doing a lot more work than necessary. You could also create your own ModelAdmin class and overwrite its queryset method, no need to construct your own ChangeList class:
class MyFilteredAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyFilteredAdmin, self).queryset(request)
if is_related_to(self.model, Profile):
qs = qs.exclude(user__email='guest#mysite.com')
return qs
You could then either register your models directly with this new admin class - admin.site.register(Model, MyFilteredAdmin) - or create subclasses that inherit from MyFilteredAdmin instead from django's ModelAdmin.

You can also have proxy models. You could have one that has 'real' users, and one that has 'guest' users.

You can subclass ModelAdmin and override the get_changelist method instead of redefining at runtime.
class FilteredModelAdmin(ModelAdmin):
def get_changelist(self, request, **kwargs):
return FilteredChangeList
Then register your models with FilteredModelAdmin instead of ModelAdmin.
admin.site.register(Object1, FilteredModelAdmin)
admin.site.register(Object2, FilteredModelAdmin)
If you need to customize any other settings for your models subclass FilteredModelAdmin instead of ModelAdmin.
class Object1ModelAdmin(FilteredModelAdmin)
# other customizations here
admin.site.register(Object1, Object1ModelAdmin)

Related

Django Rest Framework Permissions and Ownership

I have two simple models
class User(AbstractUser):
pass
class Vacation(Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
I am not really sure what is the scalable way of doing user permissions for Django Rest Framework. In particular:
Users should only be able to see their own vacations
On the /vacation endpoint, user would see a filtered list
On the /vacation/$id endpoint, user would get a 403 if not owner
Users should only be able to Create/Update vacations as long as they are the owners of that object (through Foreign Key)
What is the best way to achieve this in a future-proof fashion. Say if further down the line:
I add a different user type, which can view all vacations, but can only create/update/delete their own
I add another model, where users can read, but cannot write
Thank you!
From the docs:
Permissions in REST framework are always defined as a list of permission classes. Before running the main body of the view each permission in the list is checked. If any permission check fails an exceptions.PermissionDenied or exceptions.NotAuthenticated exception will be raised, and the main body of the view will not run.
REST framework permissions also support object-level permissioning. Object level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.
For your current need you can define your own Permission class:
class IsVacationOwner(permissions.BasePermission):
# for view permission
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
# for object level permissions
def has_object_permission(self, request, view, vacation_obj):
return vacation_obj.owner.id == request.user.id
And add this permission to your view. For example on a viewset:
class VacationViewSet(viewsets.ModelViewSet):
permission_classes = (IsVacationOwner,)
One thing is important to notice here, since you will respond with a filtered list for '/vacations', make sure you filter them using the request.user. Because object level permission will not be applicable for lists.
For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.
For your future requirement, you can always set the permissions conditionally with the help of get_permissions method.
class VacationViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action == 'list':
# vacations can be seen by anyone
# remember to remove the filter for list though
permission_classes = [IsAuthenticated]
# or maybe that special type of user you mentioned
# write a `IsSpecialUser` permission class first btw
permission_classes = [IsSpecialUser]
else:
permission_classes = [IsVacationOwner]
return [permission() for permission in permission_classes]
DRF has great documentation. I hope this helps you to get started and helps you to approach different use cases according to your future needs.
I would suggest you to use drf-viewsets link. We are going to use vacation viewset to do this work.
our urls.py
from your_app.views import VacationViewSet
router.register('api/vacations/', VacationViewSet)
our serializers.py
from rest_framework import serializers
from your_app.models import Vacation
class VacationSerializer(serializers.ModelSerializer):
class Meta:
model = Vacation
fields = ('id', 'owner',)
read_only_fields = ('id',)
our views.py
Here we are going to overwrite viewset's retrive and list method. There are other possible way to do that but i like this most as i can able to see what is happening in code. Django model viewset inherited link of drf-mixins retrive and list method.
from rest_framework import viewsets, permissions, exceptions, status
from your_app.models import Vacation, User
from your_app.serializers import VacationSerializer
class VacationViewSet(viewsets.ModelViewSet):
queryset = Vacation.objects.all()
permission_classes = [IsAuthenticated]
serializer = VacationSerializer
# we are going to overwrite list and retrive
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# now we are going to filter on user
queryset = queryset.filter(owner=self.request.user)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# not permitted check
if instance.owner is not self.request.user:
raise exceptions.PermissionDenied()
serializer = self.get_serializer(instance)
return Response(serializer.data)
Django rest framework provides in-build settings for this
Just import the required permission and add it to you class variable permission_classes
in my_name.api.views
from rest_framework.permissions import ( AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly,)
class Vacation(ListAPIView):
serializer_class = VacationListSerializer
permission_classes = [IsAuthenticated]
You can add multiple permission classes as a list
Furthur, in case this is not helpful, you can always filter the model objects as
Mymodel.objects.filter(owner = self.request.user)

Django rest framework: override create() in ModelSerializer passing an extra parameter

I am looking for a way to properly ovverride the default .create() method of a ModelSerializer serializer in Django Rest Framework for dealing with an extra parameter.
In my original Django model I have just overridden the default.save() method for managing an extra param. Now .save() can be called also in this way: .save(extra = 'foo').
I have to create a ModelSerializer mapping on that original Django model:
from OriginalModels.models import OriginalModel
from rest_framework import serializers
class OriginalModelSerializer(serializers.ModelSerializer):
# model fields
class Meta:
model = OriginalModel
But in this way I can't pass the extra param to the model .save() method.
How can I properly override the .create() method of my OriginalModelSerializer class to take (eventually) this extra param into account?
Hmm. this might not be the perfect answer given I don't know how you want to pass this "extra" in (ie. is it an extra field in a form normally, etc)
What you'd probably want to do is just represent foo as a field on the serializer. Then it will be present in validated_data in create, then you can make create do something like the following
def create(self, validated_data):
obj = OriginalModel.objects.create(**validated_data)
obj.save(foo=validated_data['foo'])
return obj
You'd probably want to look at the default implementation of create for some of the other things it does though (like remove many-to-many relationships, etc.).
You can now do this in the view set (threw in user as a bonus ;) ):
class OriginalModelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows OriginalModel classes to be viewed or edited.
"""
serializer_class = OriginalModelSerializer
queryset = OriginalModel.objects.all()
def perform_create(self, serializer):
user = None
if self.request and hasattr(self.request, "user"):
user = self.request.user
serializer.save(user=user, foo='foo')
That way the Serializer can stay generic, i.e.:
class OriginalModelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = OriginalModel
fields = '__all__'

Django rest framework populate fields of nested objects programmatically

When fields need to be filled programmatically in Django Rest Framework, the pre_save method may be overridden in the APIView, and the needed fields can be populated there, like:
def pre_save(self, obj):
obj.owner = self.request.user
This works great for flat objects, but in case of nested situations, the nested object cannot be accessed in the pre_save method. The only solution I found so far is to override the save_object method, and check if the object is an instance of the nested class, and if so, populate that field there. Although this works, I don't like the solution, and would like to know if anyone found a better way?
Demonstrating the situation:
class Notebook(models.Model):
owner = models.ForeignKey(User)
class Note(models.Model):
owner = models.ForeignKey(User)
notebook = models.ForeignKey(Notebook)
note = models.TextField()
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
class Meta:
model = Note
fields = ('note', 'owner')
class NotebookSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True)
owner = serializers.Field(source='owner.username')
class Meta:
model = Notebook
fields = ('notes', 'owner')
def save_object(self, obj, **kwargs):
if isinstance(obj, Note):
obj.owner = obj.notebook.owner
return super(NotebookSerializer, self).save_object(obj, **kwargs)
class NotebookCreateAPIView(CreateAPIView):
model = Notebook
permission_classes = (IsAuthenticated,)
serializer_class = NotebookSerializer
def pre_save(self, obj):
obj.owner = self.request.user
Before asking why don't I use different endpoints for creating notebooks and notes separately, let me say that I do that, but I also need a functionality to provide initial notes on creation of the notebook, so that's why I need this kind of endpoint as well.
Also, before I figured out this hackish solution, I actually expected that I will have to override the save_object method of the NoteSerializer class itself, but it turned out in case of nested objects, it won't even be called, only the root object's save_objects method, for all the nested objects, but I guess it was a design decision.
So once again, is this solvable in a more idiomatic way?
You can access the request in your serializer context.
So my approach to this would be:
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
def restore_object(self, attrs, instance=None):
instance = super(NoteSerializer, self).restore_object(attrs, instance)
instance.owner = self.context['request'].user
return instance
class Meta:
model = Note
fields = ('note', 'owner')
And the same on the NotebookSerializer.
The Serializer context will be made available to all used serializers in the ViewSet.

Why can't I register multiple Django ModelAdmin with same Model?

I have the following ModelAdmin:
class EventAdmin(admin.ModelAdmin):
# ModelAdmin config
def queryset(self, request):
queryset = super(EventAdmin, self).queryset(request)
return queryset.exclude(date_end__lt=date.today())
admin.site.register(Event, EventAdmin)
Now I want to add a model to manage archived (older than today) events.
class EventArchiveAdmin(admin.ModelAdmin):
# ModelAdmin config
def queryset(self, request):
queryset = super(EventArchiveAdmin, self).queryset(request)
return queryset.filter(date_end__lt=date.today())
admin.site.register(Event, EventArchiveAdmin)
But if I try to do so I get AlreadyRegistered exception.
Why can't I implement another ModelAdmin with same Model and how can I get different admin views of the same model?
I know I can implement a custom list_filter in my class but I'd like to keep things separated in different pages.
Use proxy models:
class Event(db.Model):
...
class ActiveEventManager(models.Manager):
def get_queryset(self):
return super(ActiveEventManager, self).get_queryset().filter(active=True)
class ActiveEvent(Event):
class Meta:
proxy = True
objects = ActiveEventManager()
class ArchiveEventManager(models.Manager):
def get_queryset(self):
return super(ArchiveEventManager, self).get_queryset().filter(active=False)
class ArchiveEvent(Event):
class Meta:
proxy = True
objects = ArchiveEventManager()
Now, you can register 2 models without override ModelAdmin.queryset method:
class EventAdmin(admin.ModelAdmin):
# ModelAdmin config
admin.site.register(ActiveEvent, EventAdmin)
admin.site.register(ArchiveEvent, EventAdmin)
You can read mode about proxy models and managers in the doc.
Also, use this:
queryset = super(EventArchiveAdmin, self).queryset(request)
As first argument super() take current class. See doc
Note: django has renamed Manager.get_query_set to Manager.get_queryset in django==1.7.

Django ModelForm with User data in Generic View

I have a model with a foreign key to group (the other fields don't matter):
class Project(models.Model) :
group = models.ForeignKey(Group)
...
I have a model form for this model:
class AddProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ["group","another"]
In my urls, I am using this in a generic view:
(r'^$', create_object, {'form_class':AddProjectForm, 'template_name':"form.html", 'login_required':True, 'extra_context':{'title':'Add a Project'}}),
That all works, but I want to have the group field display only the groups that the current user belongs to, not all of the groups available. I'd normally do this by passing in the user to the model form and overriding init if I wasn't in a generic view. Is there any way to do this with the generic view or do I need to go with a regular view to pass in that value?
This is gonna look dirty, since the generic view instantiates the form_class with no parameters. If you really want to use the generic_view you're gonna have to generate the class dynamically :S
def FormForUser(user):
class TmpClass(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TmpClass, self).__init__(*args, **kwargs)
self.fields['group'].queryset = user.group_set.all()
class Meta:
model = Project
fields = ['group', 'another']
Then wrap the create object view
#login_required # Only logged users right?
def create_project(request):
user = request.user
form_class = FormForUser(user)
return create_object(request, form_class=form_class, ..... )
My recommendation is to write your own view, it will give you more control on the long term and it's a trivial view.
No, you'll need to make a regular view. As can be seen by looking at the source code for create_object(), there's no functionality to pass in extra parameters to the modelform (in django 1.2):
http://code.djangoproject.com/svn/django/branches/releases/1.2.X/django/views/generic/create_update.py

Categories