I have a custom django admin page, and I would like to make the two ForeignKey fields optional in the admin interface. I do not want to change the underlying model.
This is the model:
class IncorporationTicket(models.Model, AdminURL):
ordered_by = models.ForeignKey('Organisation', # organisation which ordered this
null = True,
blank = False, # i.e. can only be null as a result of delete
on_delete = models.SET_NULL)
ordered_by_individual = models.ForeignKey('Individual', # individual at organisation which ordered this
null = True,
blank = False, # i.e. can only be null as a result of delete
on_delete = models.SET_NULL)
(AdminURL is a mixin which provides get_absolute_url)
This is the ModelAdmin:
class TicketAdmin(admin.ModelAdmin):
readonly_fields = ('ordered', 'charge', 'amount_paid', 'submitted_on')
formfield_overrides = {
models.ForeignKey: {'required': False},
}
def formfield_for_foreignkey(self, db_field, request, **kwargs):
pk = resolve(request.path).args[0] # the request url should only have one arg, the pk
instance = self.get_object(request, pk)
user = request.user
kwargs['required'] = False # will be passed to every field
if db_field.name == "ordered_by_individual":
# queryset should be a union of (a) individual already set on object (b) individual for current user
## None option is provided by admin interface - just need to let field be optional.
if instance.ordered_by_individual:
kwargs["queryset"] = (
Individual.objects.filter(pk = instance.ordered_by_individual.pk) |
user.individual_set.all())
else: kwargs["queryset"] = user.individual_set.all()
elif db_field.name == "ordered_by":
# queryset should be a union of (a) organisation already set (b) any organisations for which user is authorised
try:
individual = user.individual_set.all()[0]
all_orgs = Organisation.all_organisations_for_which_individual_authorised_to_incorporate(individual)
except:
all_orgs = Organisation.objects.none()
if instance.ordered_by:
kwargs["queryset"] = (
Organisation.objects.filter(pk = instance.ordered_by.pk) |
all_orgs)
else: kwargs["queryset"] = all_orgs
return super(type(self), self).formfield_for_foreignkey(db_field, request, **kwargs)
As you can see, I have tried to use both formfield_overrides, and formfield_for_foreignkey to set required = False on the FormField, but it is not having the required effect: when attempting to save through the admin interface without setting (that is, leaving the field in its original blank state), the admin interface shows the error 'This field is required.'
So, my question is: how does one prevent the underlying form from requiring certain fields, while still also setting the choices in formfield_for_foreignkey?
While I'm not sure why kwargs['required'] wouldn't work, you could always override the admin form with your own form. It hasn't failed me with magical django admin behavior so it's a pretty good bet.
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['my_fk_field'].required = False
class Meta:
model = MyModel
class MyAdmin(admin.ModelAdmin):
form = MyForm
This would still allow you to modify the QuerySet via the formfield_for_foo methods.
... almost 9 years later, in Django v3.1.2 ...
blank=True works fine for me:
from django.contrib.auth.models import User
owner = models.ForeignKey(User,
related_name="notes",
on_delete=models.CASCADE,
null=True,
blank=True)
(the solution has been taken from here)
Related
I need to manually add an entry to the database via the admin panel, but the admin should not be able to set all the values:
#models.py
class Product(models.Model):
price = models.DecimalField("price")
status = models.PositiveIntegerField("status")
name = models.CharField("name", max_length=31, unique=True)
## tried:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.initial['status'] = 2
#admin.py
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ["price", "name",]
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
form = ProductForm
## tried:
def get_changeform_initial_data(self, request):
return {'status': 99}
Admin should be able to create a Product and give it a name and price, but the status value should be set to 0. I already tried this approach and this, but no success.
I tried:
Using an __init__ method to add initial values
get_changeform_initial_data() method to add a status.
I always end up with sqlite3.IntegrityError: NOT NULL constraint failed: _TEST_Product.status.
edit: I know this can be done by setting a default value in the model, but this is not what I am looking for - I want to set this in the admin.
Edit
Since you don't want to use the default field, you should try this approach from this answer.
class ProductAdmin(admin.ModelAdmin):
form = ProductForm
def save_model(self, request, obj, form, change):
obj.status = 0
super().save_model(request, obj, form, change)
Why don't you just use the default field?
status = models.PositiveIntegerField("status", default=0)
I'm trying to limit the fields list in serializers based on user permissions. I have a generic routine that does it for all serializers. It's working on the parent serializer, but not on a nested serializer.
I have a client model, and a client profile (referred to as "contacts") as shown below. The client profile model is an extension of the user model (one-to-one relationship).
class Client(AddressPhoneModelMixin, DateFieldsModelMixin, models.Model):
name = models.CharField(max_length=100)
status = models.CharField(max_length=25)
class Meta:
permissions = (
# Object-level
('view_all_clients', 'Can view all clients'),
('change_all_clients', 'Can change all clients'),
# Field-level
('view_client_id', 'Can view client ID'),
('view_client_name', 'Can view client name'),
...others omitted...
)
class ClientProfile(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
blank=True,
null=True,
)
client = models.ForeignKey(
Client,
on_delete=models.PROTECT,
related_name='contacts',
)
receive_invoices = models.BooleanField(default=False)
My object-level permission logic is in the list view:
class ClientList(ListAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = ClientSerializer
def get_queryset(self):
user = self.request.user
queryset = None
if user.has_perm('view_client') or user.has_perm('clients.view_all_clients'):
queryset = Client.objects.all().exclude(status__in=['deleted', 'archived'])
if user.has_perm('view_client'): # View only "assigned" clients
if user.type == 'client':
# See if user is a "contact".
queryset = queryset.distinct().filter(contacts__user=self.request.user)
else:
# See if user is assigned to projects for the client(s).
queryset = queryset.distinct().filter(projects__project_users__user=self.request.user)
if queryset is None:
raise PermissionDenied('You do not have permission to view clients.')
return self.get_serializer_class().setup_eager_loading(queryset)
Removing fields from the serializer "fields" property is done in the serializer __init__ method (from examples I found here in SO):
class ClientContactsSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:clientprofile-detail')
user = UserSerializer()
class Meta:
model = ClientProfile
fields = (
'url',
'receive_invoices',
'user',
)
def __init__(self, *args, **kwargs):
super(ClientContactsSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
class ClientSerializer(AddressPhoneSerializerMixin, serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:client-detail')
contacts = ClientContactsSerializer(many=True, read_only=True)
projects = ClientProjectsSerializer(many=True, read_only=True)
class Meta:
model = Client
fields = (
'url',
'id',
'name',
...omitted for brevity...
'contacts',
'projects',
)
def __init__(self, *args, **kwargs):
super(ClientSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('country')
return queryset.prefetch_related('contacts', 'contacts__user', 'contacts__user__country', 'projects')
And, finally, here's the check_field_permissions function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
for field_name in fields:
if hasattr(serializer.fields[field_name], 'child'):
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
Stepping through in debug on a page-load of the Client list, I can see that the above function is invoked first for clientprofile, and request is None. The second time, it is invoked for client, and request is a valid request object.
First question, is __init__ the correct place for limiting the list of fields to be serialized?
Second, how to get the request object in the nested serializer (clientprofile)?
After reading a post by Tom Christie, who is an undisputed authority on DRF, I was able to solve my issue. He pointed out that each nested serializer does, in fact, have the context object (and the request, and the user). You just have to deal with nested serializers in the parent __init__ - not their own __init__.
Here's my revised check_field_permissions() function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
extra_fields = []
for field_name in fields:
if field_name == 'url':
extra_fields.append(field_name)
continue
if hasattr(serializer.fields[field_name], 'child'):
check_field_permissions(serializer.fields[field_name].child, action)
extra_fields.append(field_name)
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
# If only "url" and child fields remain, remove all fields.
if len(serializer.fields) == len(extra_fields):
for field_name in extra_fields:
serializer.fields.pop(field_name)
It is now recursive. If it hits a field with a "child" attribute, it knows that's a nested serializer field. It calls itself with that child serializer passed as an arg.
The other change is that __init__ was removed from ClientContactsSerializer, because it doesn't need to call check_field_permissions().
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'm using Django-Profiles with Django 1.4, and I need a way to unsubscribe a user, so they can stop getting emails.
One of the fields in my UserProfile model is user_type, and I have a USER_TYPES list of choices. To keep users in the system, even if they unsubscribe, I decided to have one of the USER_TYPES be InactiveClient, and I'd include a checkbox like so:
Models.py:
USER_TYPES = (
('Editor', 'Editor'),
('Reporter', 'Reporter'),
('Client', 'Client'),
('InactiveClient', 'InactiveClient'),
('InactiveReporter', 'InactiveReporter'),
)
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
user_type = models.CharField(max_length=25, choices=USER_TYPES, default='Client')
... etc.
forms.py
class UnsubscribeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(UnsubscribeForm, self).__init__(*args, **kwargs)
try:
self.initial['email'] = self.instance.user.email
self.initial['first_name'] = self.instance.user.first_name
self.initial['last_name'] = self.instance.user.last_name
except User.DoesNotExist:
pass
email = forms.EmailField(label='Primary Email')
first_name = forms.CharField(label='Editor first name')
last_name = forms.CharField(label='Editor last name')
unsubscribe = forms.BooleanField(label='Unsubscribe from NNS Emails')
class Meta:
model = UserProfile
fields = ['first_name','last_name','email','unsubscribe']
def save(self, *args, **kwargs):
u = self.instance.user
u.email = self.cleaned_data['email']
u.first_name = self.cleaned_data['first_name']
u.last_name = self.cleaned_data['last_name']
if self.unsubscribe:
u.get_profile().user_type = 'InactiveClient'
u.save()
client = super(UnsubscribeForm, self).save(*args,**kwargs)
return client
Edit: I've added additional code context. if self.unsubscribe: is in save() override. Should that be somewhere else? Thank you.
Edit2: I've tried changing UnsubscribeForm in several ways. Now I get a 404, No User matches the given query. But the view function being called works for other forms, so I'm not sure why?
urls.py
urlpatterns = patterns('',
url('^client/edit', 'profiles.views.edit_profile',
{
'form_class': ClientForm,
'success_url': '/profiles/client/edit/',
},
name='edit_client_profile'),
url('^unsubscribe', 'profiles.views.edit_profile',
{
'form_class': UnsubscribeForm,
'success_url': '/profiles/client/edit/',
},
name='unsubscribe'),
)
These two urls are calling the same view, just using a different form_class.
Edit3: So I don't know why, but when I removed the trailing slash from the unsubscribe url, the form finally loads. But when I submit the form, I still get an error: 'UnsubscribeForm' object has no attribute 'unsubscribe' If anyone could help me understand why a trailing slash would cause the 404 error (No User matches the given query) I wouldn't mind knowing. But as of now, the form loads, but doesn't submit, and the trace ends on this line of my form:
if self.unsubscribe:
Answering my own question again. On ModelForms, you can add form elements that don't exist in the model, and access the value of those fields by accessing self.cleaned_data['form_element_name'] in the save method.
This is what my save method looks like:
def save(self, *args, **kwargs):
u = self.instance.user
p = self.instance.user.get_profile()
u.email = self.cleaned_data['email']
u.first_name = self.cleaned_data['first_name']
u.last_name = self.cleaned_data['last_name']
if self.cleaned_data['unsubscribe']:
p.user_type = 'InactiveClient'
u.save()
p.save()
client = super(UnsubscribeForm, self).save(*args,**kwargs)
return client
I have a read-only field in a django form that I sometimes want to edit.
I only want the right user with the right permissions to edit the field. In most cases the field is locked, but an admin could edit this.
Using the init function, I am able to make the field read-only or not, but not optionally read-only. I also tried passing an optional argument to StudentForm.init but that turned much more difficult that I expected.
Is there a proper way to do accomplish this?
models.py
class Student():
# is already assigned, but needs to be unique
# only privelidged user should change.
student_id = models.CharField(max_length=20, primary_key=True)
last_name = models.CharField(max_length=30)
first_name = models.CharField(max_length=30)
# ... other fields ...
forms.py
class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = ('student_id', 'last_name', 'first_name',
# ... other fields ...
def __init__(self, *args, **kwargs):
super(StudentForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance:
self.fields['student_id'].widget.attrs['readonly'] = True
views.py
def new_student_view(request):
form = StudentForm()
# Test for user privelige, and disable
form.fields['student_id'].widget.attrs['readonly'] = False
c = {'form':form}
return render_to_response('app/edit_student.html', c, context_instance=RequestContext(request))
Is that what you are looking for? By modifying your code a little bit:
forms.py
class StudentForm(forms.ModelForm):
READONLY_FIELDS = ('student_id', 'last_name')
class Meta:
model = Student
fields = ('student_id', 'last_name', 'first_name')
def __init__(self, readonly_form=False, *args, **kwargs):
super(StudentForm, self).__init__(*args, **kwargs)
if readonly_form:
for field in self.READONLY_FIELDS:
self.fields[field].widget.attrs['readonly'] = True
views.py
def new_student_view(request):
if request.user.is_staff:
form = StudentForm()
else:
form = StudentForm(readonly_form=True)
extra_context = {'form': form}
return render_to_response('forms_cases/edit_student.html', extra_context, context_instance=RequestContext(request))
So the thing is to check permissions on the views level, and then to pass argument to your form when it is initialized. Now if staff/admin is logged in, fields will be writeable. If not, only fields from class constant will be changed to read only.
It would be pretty easy to use the admin for any field editing and just render the student id in the page template.
I'm not sure if this answers your questions though.