I used django-guardian library, I want to create Manager to filter objects according to user permission.
So for-example:
from guardian.shortcuts import get_objects_for_user
class WithUser(models.Manager):
user_obj = None
def assign_user(self,user_obj):
self.user_obj = user_obj
qs = super().get_queryset()
#########################
# how I know if the current qs have related field and how to get related field managers.
#???????????????
def get_queryset(self):
qs = super().get_queryset()
if self.user_obj:
qs = get_objects_for_user(self.user_obj,"view_%s"%self.model.__name__.lower(),qs,accept_global_perms=False)
return qs
class A(models.Model):
# the model fields
objects = WithUser()
class B(models.Model):
# the model fields
a = models.ForeignKey(A,on_delete=Model.CASCADE)
objects = WithUser()
class C(models.Model):
# the model fields
b = models.ForeignKey(B,on_delete=Model.CASCADE)
objects = WithUser()
How to filter C objects according to it's permission and permission of A,B.
I want general rule to filter any model objects according to its permission and permission of its related objects.
I would create a special ForeinKey field to mark the fields.
class PermissionForeignKey(models.ForeignKey):
# we only use this class to mark the field
pass
# you can add lru_cache here to speed up
# you could also move this to the model as #classproperty
def perm_fk_fields(model):
fk_fields = []
for field in model._meta.get_fields():
if isinstance(field, PermissionForeignKey):
fk_fields.append(field)
fk_model_fields = perm_fk_fields(field.remote_field.model)
if fk_model_fields:
fk_fields.append([field, fk_model_fields])
return fk_fields
# in your model
class B(models.Model):
# the model fields
a = PermissionForeignKey(A,on_delete=Model.CASCADE)
Having trouble with an UpdateView. I've tried over writing the get_object but I am getting
AttributeError at /companydata/update/
'User' object has no attribute 'get_companydata'
The CompanyData Model has a OneToOne relationship with User.
Here's my code:
urls.py
### Omitted ###
url(r'^update/$', CompanyDataUpdateView.as_view(),
name='companydataupdate')
### Omitted ###
views.py
class CompanyDataUpdateView(UpdateView):
model = CompanyData
fields = ['arr', 'num_cust']
template_name = 'company_data/companydata_form.html'
def get_object(self):
return self.request.user.get_companydata()
models.py
class CompanyData(models.Model):
user = models.OneToOneField(User)
arr = models.DecimalField(max_digits=20, decimal_places=2, validators=[MinValueValidator(1)])
num_cust = models.IntegerField(validators=[MinValueValidator(1)])
def get_absolute_url(self):
return reverse('companyrevenue')
Any help would be greatly apprecaited!
The User object has no method called get_companydata, hence your error. You need to access the reverse one-to-one relationship like so:
def get_object(self):
return self.request.user.companydata
Where companydata is a property, not a method (i.e., don't call it with brackets). This is the default reverse name for the one-to-one relationship:
If you do not specify the related_name argument for the OneToOneField, Django will use the lower-case name of the current model as default value.
If you want to be more explicit or use another name, then set the related_name on your OneToOneField.
I am using Django REST Framework to serialize a Django model. I have a ListCreateAPIView view to list the objects and a RetrieveUpdateDestroyAPIView view to retrieve/update/delete individual objects. The model stores information that the users submit themselves. The information they submit contains some private information and some public information. I want all users to be able to list and retrieve the public information but I want only the owner to list/retrieve/update/delete the private information. Therefore, I need per-field permissions and not object permissions.
The closest suggestion I found was https://groups.google.com/forum/#!topic/django-rest-framework/FUd27n_k3U0 which changes the serializer based on the request type. This won't work for my situation because I don't have the queryset or object at that point to determine if it is owned by the user or not.
Of course, I have my frontend hiding the private information but smart people can still snoop the API requests to get the full objects. If code is needed, I can provide it but my request applies to vanilla Django REST Framework designs.
How about switching serializer class based on user?
In documentation:
http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself
def get_serializer_class(self):
if self.request.user.is_staff:
return FullAccountSerializer
return BasicAccountSerializer
I had a similar problem the other day. Here is my approach:
This is a DRF 2.4 solution.
class PrivateField(serializers.Field):
def field_to_native(self, obj, field_name):
"""
Return null value if request has no access to that field
"""
if obj.created_by == self.context.get('request').user:
return super(PrivateField, self).field_to_native(obj, field_name)
return None
#Usage
class UserInfoSerializer(serializers.ModelSerializer):
private_field1 = PrivateField()
private_field2 = PrivateField()
class Meta:
model = UserInfo
And a DRF 3.x solution:
class PrivateField(serializers.ReadOnlyField):
def get_attribute(self, instance):
"""
Given the *outgoing* object instance, return the primitive value
that should be used for this field.
"""
if instance.created_by == self.context['request'].user:
return super(PrivateField, self).get_attribute(instance)
return None
This time we extend ReadOnlyField only because to_representation is not implemented in the serializers.Field class.
I figured out a way to do it. In the serializer, I have access to both the object and the user making the API request. I can therefore check if the requestor is the owner of the object and return the private information. If they are not, the serializer will return an empty string.
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = serializers.SerializerMethodField('get_private_field1')
class Meta:
model = UserInfo
fields = (
'id',
'public_field1',
'public_field2',
'private_field1',
)
read_only_fields = ('id')
def get_private_field1(self, obj):
# obj.created_by is the foreign key to the user model
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
Here:
-- models.py:
class Article(models.Model):
name = models.CharField(max_length=50, blank=False)
author = models.CharField(max_length=50, blank=True)
def __str__(self):
return u"%s" % self.name
class Meta:
permissions = (
# name
('read_name_article', "Read article's name"),
('change_name_article', "Change article's name"),
# author
('read_author_article', "Read article's author"),
('change_author_article', "Change article's author"),
)
-- serializers.py:
class ArticleSerializer(serializers.ModelSerializer):
class Meta(object):
model = Article
fields = "__all__"
def to_representation(self, request_data):
# get the original representation
ret = super(ArticleSerializer, self).to_representation(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if not current_user.has_perm(
'app_name.read_{}_article'.format(field_name)
):
ret.pop(field_name) # remove field if it's not permitted
return ret
def to_internal_value(self, request_data):
errors = {}
# get the original representation
ret = super(ArticleSerializer, self).to_internal_value(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if field_value and not current_user.has_perm(
'app_name.change_{}_article'.format(field_name)
):
errors[field_name] = ["Field not allowed to change"] # throw error if it's not permitted
if errors:
raise ValidationError(errors)
return ret
For a solution that allows both reading and writing, do this:
class PrivateField(serializers.Field):
def get_attribute(self, obj):
# We pass the object instance onto `to_representation`,
# not just the field attribute.
return obj
def to_representation(self, obj):
# for read functionality
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
def to_internal_value(self, data):
# for write functionality
# check if data is valid and if not raise ValidationError
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = PrivateField()
...
See the docs for an example.
This is an old question, but the topic is still relevant.
DRF recommends to create different serializers for different permission. But this approach only works, if you have only a few permissions or groups.
restframework-serializer-permissions is a drop in replacement for drf serializers.
Instead of importing the serializers and fields from drf, you are importing them from serializer_permissions.
Installation:
$ pip install restframework-serializer-permissions
Example Serializers:
# import permissions from rest_framework
from rest_framework.permissions import AllowAny, IsAuthenticated
# import serializers from serializer_permissions instead of rest_framework
from serializer_permissions import serializers
# import you models
from myproject.models import ShoppingItem, ShoppingList
class ShoppingItemSerializer(serializers.ModelSerializer):
item_name = serializers.CharField()
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('item_name', )
class ShoppingListSerializer(serializers.ModelSerializer):
# Allow all users to list name
list_name = serializers.CharField(permission_classes=(AllowAny, ))
# Only allow authenticated users to retrieve the comment
list_comment = serializers.CharField(permissions=(IsAuthenticated, ))
# show owner only, when the current user has 'auth.view_user' permission
owner = serializers.CharField(permissions=('auth.view_user', ), hide=True)
# serializer which is only available, when the user is authenticated
items = ShoppingItemSerializer(many=True, permissions=(IsAuthenticated, ), hide=True)
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('list_name', 'list_comment', 'owner', 'items', )
Disclosure: I'm the author of this extension
In case you are performing only READ operations, you can just pop the fields in to_representation method of the serializer.
def to_representation(self,instance):
ret = super(YourSerializer,self).to_representation(instance)
fields_to_pop = ['field1','field2','field3']
if instance.created_by != self.context['request'].user.id:
[ret.pop(field,'') for field in fields_to_pop]
return ret
This should be enough to hide sensitive fields.
Just share another possible solution
For example, to make email only show for oneself.
On UserSerializer, add:
email = serializers.SerializerMethodField('get_user_email')
Then implement get_user_email like this:
def get_user_email(self, obj):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
return obj.email if user.id == obj.pk else 'HIDDEN'
I solved it using a serializer Mixin:
class FieldPermissionModelSerializerMixin(serializers.ModelSerializer):
"""
A mixin that allows you to specify what fields will be returned based on field level permissions
"""
permission_fields = []
def get_field_names(self, declared_fields, info) -> List:
"""Determine the fields to apply."""
fields = getattr(self.Meta, "fields", [])
for permission_field in self.permission_fields:
app_name = getattr(self.Meta, "model", None)._meta.app_label
permission_name = f"can_view_field_{permission_field}"
full_permission_name = f"{app_name}.{permission_name}"
if self.context["request"].user.has_perm(full_permission_name):
fields.append(permission_field)
return fields
Then you can use this serializer with base fields and permissionable fields.
POSITION_BASE_FIELDS = [
"id",
"name",
"level",
"role",
"sort",
]
POSITION_PERMISSION_FIELDS = ["market_salary", "recommended_rate_per_hour"]
class PositionListSerializer(FieldPermissionModelSerializerMixin):
permission_fields = POSITION_PERMISSION_FIELDS
class Meta:
model = Position
fields = POSITION_BASE_FIELDS + []
This is then based on field level permissions defined on the model.
class Position(models.Model):
name = models.CharField(max_length=255, db_index=True)
level = models.CharField(max_length=255, null=True, blank=True)
sort = models.IntegerField(blank=True, default=0)
market_salary = models.DecimalField(max_digits=19, decimal_places=2, default=0.00)
recommended_rate_per_hour = models.DecimalField(
max_digits=7, decimal_places=2, null=True, blank=True
)
class Meta:
ordering = ["name", "sort"]
unique_together = ("name", "level")
permissions = (
("can_view_field_market_salary", "Can view field: market_salary"),
(
"can_view_field_recommended_rate_per_hour",
"Can view field: recommended_rate_per_hour",
),
)
I have a model with SlugField. Value of that field is created when the model instance is saved for the first time:
from django.db import models
from django.template.defaultfilters import slugify as default_slugify
class SlugModel(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100)
def save(self, *args, **kwargs):
if not self.pk:
self.slug = self.slugify(self.name)
return super(SlugModel, self).save(*args, **kwargs)
def slugify(self, tag):
slug = default_slugify(tag)
return slug
If i use that model in ModelForm the slug field is by default displayed.
from django.forms import ModelForm
class SlugModelForm(ModelForm):
class Meta:
model = SlugModel
How to automatically prevent all ModelForms from displaying of all of it's SlugFields without manually specifying ModelForm.exclude or SlugField(editable=False) on each form/field?.
I think you could also extend Lychas response, create a base class and inherit from that one:
class MyModelForm(ModelForm):
class Meta:
exclude = ('slug',)
abstract = True
class AnyForm(MyModelForm):
#more here
This is untested though.
You can exclude fields in the Meta class by assigning field names to exclude:
class Meta:
model = SlugModel
exclude = ('slug',)
# admin.py
class CustomerAdmin(admin.ModelAdmin):
list_display = ('foo', 'number_of_orders')
# models.py
class Order(models.Model):
bar = models.CharField[...]
customer = models.ForeignKey(Customer)
class Customer(models.Model):
foo = models.CharField[...]
def number_of_orders(self):
return u'%s' % Order.objects.filter(customer=self).count()
How could I sort Customers, depending on number_of_orders they have?
admin_order_field property can't be used here, as it requires a database field to sort on. Is it possible at all, as Django relies on the underlying DB to perform sorting? Creating an aggregate field to contain the number of orders seems like an overkill here.
The fun thing: if you change url by hand in the browser to sort on this column - it works as expected!
I loved Greg's solution to this problem, but I'd like to point that you can do the same thing directly in the admin:
from django.db import models
class CustomerAdmin(admin.ModelAdmin):
list_display = ('number_of_orders',)
def get_queryset(self, request):
# def queryset(self, request): # For Django <1.6
qs = super(CustomerAdmin, self).get_queryset(request)
# qs = super(CustomerAdmin, self).queryset(request) # For Django <1.6
qs = qs.annotate(models.Count('order'))
return qs
def number_of_orders(self, obj):
return obj.order__count
number_of_orders.admin_order_field = 'order__count'
This way you only annotate inside the admin interface. Not with every query that you do.
I haven't tested this out (I'd be interested to know if it works) but what about defining a custom manager for Customer which includes the number of orders aggregated, and then setting admin_order_field to that aggregate, ie
from django.db import models
class CustomerManager(models.Manager):
def get_query_set(self):
return super(CustomerManager, self).get_query_set().annotate(models.Count('order'))
class Customer(models.Model):
foo = models.CharField[...]
objects = CustomerManager()
def number_of_orders(self):
return u'%s' % Order.objects.filter(customer=self).count()
number_of_orders.admin_order_field = 'order__count'
EDIT: I've just tested this idea and it works perfectly - no django admin subclassing required!
The only way I can think of is to denormalize the field. That is - create a real field that get's updated to stay in sync with the fields it is derived from. I usually do this by overriding save on eith the model with the denormalized fields or the model it derives from:
# models.py
class Order(models.Model):
bar = models.CharField[...]
customer = models.ForeignKey(Customer)
def save(self):
super(Order, self).save()
self.customer.number_of_orders = Order.objects.filter(customer=self.customer).count()
self.customer.save()
class Customer(models.Model):
foo = models.CharField[...]
number_of_orders = models.IntegerField[...]