Django-rules replacement of guardian.mixins.PermissionListMixin - django

In my django based application I want to enable users to keep track of their locations. Each location has an owner, and the list view should only show the locations the current user owns.
With django-guardian I was able to achieve the same with specifying the following in my views.py:
from django.views import generic
from guardian.mixins import PermissionRequiredMixin, PermissionListMixin
# Create your views here.
from .models import Location
class LocationListView(PermissionListMixin, generic.ListView):
model = Location
permission_required = 'view_location'
paginate_by = 20
ordering = ['name']
How would I create something similar with django-rules?

You need to share Location model so we can advise you properly, in both cases you need to specify queryset either in listview variable or by override get queryset method..
I would suggest you following lines of code assuming location model has foreign key from Auth User model where each location assigned to its owner..
def get queryset(self):
qs = Location.objects.filter(user=self.request.user)
return qs

Related

Enforcing permissions through a related model in Django Rest Framework

I'm working on building out permissions for an API built with Django REST Framework. Let's say I have the following models:
from django.db import models
class Study(models.Model):
pass
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
I have basic serializers and views for both of these models. I'll be using per-object permissions to grant users access to one or more studies. I want users to only be able to view Results for a Study which they have permissions to. There are two ways I can think of to do this, and neither seem ideal:
Keep per-object permissions on Results in sync with Study. This is just a non-starter since we want Study to always be the source of truth.
Write a custom permissions class which checks permissions on the related Study when a user tries to access a Result. This actually isn't too bad, but I couldn't find examples of others doing it this way and it got me thinking that I may be thinking about this fundamentally wrong.
Are there existing solutions for this out there? Or is a custom permissions class the way to go? If so, do you have examples of others who've implemented this way?
As you stated, you can make custom permission as per the second way:
And include the permission in your view:
I am considering your study model with some parameter course, based on that i am writing the solution you can consider for any element in study model
models.py
from django.db import models
class Study(models.Model):
course = models.CharField(max_length=50)
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
In permission.py
from rest_framework import permissions
class ResultOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS and obj.study.course == request.GET.get('course') :
# read only requests
return True
else:
# other requests such as post, patch, put
return obj.study == request.GET.get('course')
And include ,
class ReviewDetail(viewsets.ViewSet):
permission_classes =[ResultOrReadOnly]
And in urls.py,
Modify it to accept the URL parameter course
I would create a new field called enrolled_users in the Study model to indicate which all user has access to the particular Study object.
from django.db import models
from django.conf import settings
class Study(models.Model):
enrolled_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="studies"
)
# other fields
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
Then, it will very easy in DRF to filter the queryset in the views
# views.py
from .models import Study, Result
from rest_framework.permissions import IsAuthenticated
class StudyModelViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Study.objects.filter(enrolled_users=self.request.user)
class ResultModelViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Result.objects.filter(study__enrolled_users=self.request.user)
Notes
This will handle the object permission (Detail View) request as well
This will not return a status code of HTTP 403, but HTTP 404
Here I used the ModelViewSet class, but, you can use any views, but the filter plays the role.

Using Django LogEntry (ContentType) with a GenericRelation field

THE DESCRIPTION
I'm writing an API on a Django project that will get all the LogEntry on a specific model and filter them by the specific model's field.
So my model looks like this:
from django.contrib.admin.models import ContentType, LogEntry
from django.contrib.auth.models import Group
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.db import models
class LogEntryProxy(LogEntry):
content_object = GenericForeignKey()
class Meta:
proxy = True
class HomeItem(models.Model):
# Some Fields
groups = models.ManyToManyField(Group, related_name="home_item_group")
log_entry_relation = GenericRelation(LogEntryProxy, related_query_name='log_homeitem')
In the view I need to be able to make queries that fetch LogEntryProxy items that refer to HomeItem and filter them by the groups that have access to it and serialize the results. so something like this:
log_entry_queryset = LogEntryProxy.objects.filter(log_homeitem__groups__in=user.groups.all()).distinct()
My serializer looks like this (using "djangorestframework"):
from rest_framework import serializers
class LogEntryHomeItemSerializer(serializers.ModelSerializer):
content_object = HomeItemSerializer(many=False)
object_id = serializers.IntegerField()
class Meta:
model = LogEntryProxy
fields = ('object_id', 'action_flag', 'content_object', )
And it works!
THE PROBLEM
So what's the problem you may ask!
The Problem is that LogEntry actions like create and edit work! and the API will give you the results, but when you delete a HomeItem object all the LogEntry objects pointing to it will be deleted as well, thus no delete action will be given by the api (plus all the create and edit ones pointing to that object will be deleted as well). and that's all because Django's GenericRelation doesn't support on_delete. If I delete the GenericRelatin field this doesn't happen but then I have to be able to filter the HomeItem by groups in the LogEntryProxy queryset and it can't be done without the GenericRelation.
I was wondering if anyone could tell me what to do in this situation?
Should I implement a custom logging system? or there's another way that I haven't seen it yet!
try:
homeids = HomeItem.objects.filter(groups__in=user.groups.all()).values_list('id',flat=True)
log_entry_queryset = LogEntryProxy.objects.filter(object_id__in=homeids,content_type_id=ContentType.objects.get_for_model(HomeItem).id).distinct()
If you query this way you don't need GenericRelation
Update:
The above query will not fetch logentries of delete action.
It can be done like:
from django.db.models import Q
from django.contrib.admin.models import DELETION
log_entry_queryset = LogEntryProxy.objects.filter(Q(object_id__in=home_ids,content_type_id=ContentType.objects.get_for_model(HomeItem).id) | Q(action_flag=DELETION,content_type_id=ContentType.objects.get_for_model(HomeItem).id)).distinct()

Django Resuable QuerySet

I have a QuerySet in a view like the following. I would like to be able to use the same query in other views, but I don't want to have to copy and paste the code. This also feels like I would be violating the DRY principal.
If I would like to modify the query at a later date, I would have to change it in all my views, which clearly isn't ideal. Is there a class I should create or a method in my model which would let me call this from many different views? Are there any best practices concerning this?
tasks = Task.objects.filter(user = request.user).order_by('-created_at', 'is_complete')
One of the solutions would be to create a classmethod on the model or extending model's manager.
from django.db import models
Adding classmethod
class MyModel(models.Model):
#classmethod
def get_user_tasks(cls, user):
return cls.objects.filter(...).order_by(...)
Overriding manager
class MyModelManager(models.Manager):
def get_user_tasks(self, user):
return self.filter(...).order_by(...)
class MyModel(models.Model):
objects = MyModelManager()
# and in the view...
queryset = MyModel.objects.get_user_tasks(request.user)

Dynamical choices in model's field

I want my models to have order field, which will contain order of an item among all items of its kind.
And I want to use choices within that IntegerField, which would contain all the numbers of currently existing items in that table.
So it would need to be dynamic choices.
How do I load all existing "order" values of all existing items in a table, and use this list for choices?
It sounds like you want to build a manager for your model:
models.py
from django.db import models
class OrderManager(models.Manager):
def order_choices(self):
return [(i, i) for i in OrderModel.objects.values_list('order', flat=True)]
class OrderModel(models.Model):
objects = OrderManager()
order = models.IntegerField()
class Meta:
ordering = ['order']
def __unicode__(self):
return '%i' % self.order
forms.py
from django import forms
from yourapp.models import OrderModel
class OrderModelForm(forms.ModelForm):
order = forms.ChoiceField(choices=OrderModel.objects.order_choices())
class Meta:
model = OrderModel
admin.py
from django.contrib import admin
from yourapp.forms import OrderModelForm
from yourapp.models import OrderModel
class OrderModelAdmin(admin.ModelAdmin):
form = OrderModelForm
admin.site.register(OrderModel, OrderModelAdmin)
Edit
Managers are use to make general model queries without having an instance of a model object. If you don't understand the concept of managers, you can still refactor the code out of the manager class, stick it somewhere else and import that function across your code. Managers allow you to abstract custom general queryset that you can reuse. See more details https://docs.djangoproject.com/en/dev/topics/db/managers/
The code without the manager will look like
views.py or some other file
from app.models import OrderModel
def order_choices():
return [(i, i) for i in OrderModel.objects.values_list('order', flat=True)]
From anywhere in your code, if you want to reuse the above multiple times:
from app.views import oder_choices
order_choices()
as opposed to:
from app.models import OderModel
OrderModel.objects.order_choices()
If you only want to use the above once, you can leave it in the forms.py as shown in the other answer. It's really up to you on how you want to refactor your code.
Dont add the choices directly to the model, add them to a form represnting the model later, by overriding the field with a set of choices.
than, do something like:
class MyForm(..):
myfield_order_field = IntegerField(choices = [(i,i) for range(MyModel.objects.count)])
class Meta():
model = MyModel
if you want to use it in the admin, add to your Admin Class:
class MyModelAdmin(admin.ModelAdmin):
...
form = MyForm
it will override this field in the admin too.

Reorder users in django auth

I have a model that has a ForeignKey to the built-in user model in django.contrib.auth and I'm frustrated by the fact the select box in the admin always sorts by the user's primary key.
I'd much rather have it sort by username alphabetically, and while it's my instinct not to want to fiddle with the innards of Django, I can't seem to find a simpler way to reorder the users.
The most straightforward way I can think of would be to dip into my Django install and add
ordering = ('username',)
to the Meta class of the User model.
Is there some kind of monkeypatching that I could do or any other less invasive way to modify the ordering of the User model?
Alternatively, can anyone thing of anything that could break by making this change?
There is a way using ModelAdmin objects to specify your own form. By specifying your own form, you have complete control over the form's composition and validation.
Say that the model which has an FK to User is Foo.
Your myapp/models.py might look like this:
from django.db import models
from django.contrib.auth.models import User
class Foo(models.Model):
user = models.ForeignKey(User)
some_val = models.IntegerField()
You would then create a myapp/admin.py file containing something like this:
from django.contrib.auth.models import User
from django import forms
from django.contrib import admin
class FooAdminForm(forms.ModelForm):
user = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
class Meta:
model = Foo
class FooAdmin(admin.ModelAdmin):
form = FooAdminForm
admin.site.register(Foo, FooAdmin)
Once you've done this, the <select> dropdown will order the user objects according to username. No need to worry about to other fields on Foo... you only need to specify the overrides in your FooAdminForm class. Unfortunately, you'll need to provide this custom form definition for every model having an FK to User that you wish to present in the admin site.
Jarret's answer above should actually read:
from django.contrib.auth.models import User
from django.contrib import admin
from django import forms
from yourapp.models import Foo
class FooAdminForm(forms.ModelForm):
class Meta:
model = Foo
def __init__(self, *args, **kwds):
super(FooAdminForm, self).__init__(*args, **kwds)
self.fields['user'].queryset = User.objects.order_by(...)
class FooAdmin(admin.ModelAdmin):
# other stuff here
form = FooAdminForm
admin.site.register(Foo, FooAdmin)
so the queryset gets re-evaluated each time you create the form, as opposed to once, when the module containing the form is imported.