Django admin get_readonly_fields inconsistent behavior - django

I have the same request as this other thread, I'm using Django 3.2.8
For my project some fields in the Admin pages will have to become readonly based on the value of one boolean field of that same model object, so I applied the solution that was advised in the other thread above.
class OrdineAdmin(admin.ModelAdmin):
inlines = [
FatturaAdmin
]
readonly_fields = ["data"]
def get_readonly_fields(self, request, obj=None):
if obj and obj.pagato == True:
self.readonly_fields = ['utente', 'abbonamento', 'importo', 'giorni_attivi_abbonamento', 'data']
return self.readonly_fields
as I was expecting, this solution would lead to some inconsistency in the admin panel because one instance of OrdineAdmin would be generated in memory but it wouldn't update on each request. So when I set the field "pagato" to True this method does set all the other fields to readonly as expected but won't reverse its effect if I set that field back to False, even if the saving process through the admin panel succeeds.
That is why I was thinking that something needed to be done inside the __init__() method but I read the answer to this other thread where in relation to inline admin elements it is said that it wouldn't make sense, as opposed to what I believed, because:
It also makes no sense to set values like that in init, since the Modeladmin instance is created once and may persist for a lot more than one request!
So now I'm completely confused. How can I grant an update per each request on the ModelAdmin pages for a consistent behavior of my readonly fields logic?
On my side, I tried to get hold of the object inside __init__() with the get_object() method but I need to provide the object id which I still haven't figured out how to access. But if what I'm reading above is true, it would still be useless to provide the logic to the constructor since instances will last for longer than a request anyway.
Maybe I should address the form with get_form() or in my case get_formsets_with_inlines() and customize those methods? But how would I make sure they get re-generated at each request?

When pagato is set to True, you override the readonly_fields property with this line:
self.readonly_fields = ['utente', 'abbonamento', 'importo', 'giorni_attivi_abbonamento', 'data']
So when pagato is set again to False, get_readonly_fields returns the updated version instead of the original.
Try this:
class OrdineAdmin(admin.ModelAdmin):
inlines = [
FatturaAdmin
]
readonly_fields = ["data"]
def get_readonly_fields(self, request, obj=None):
if obj and obj.pagato == True:
return ['utente', 'abbonamento', 'importo', 'giorni_attivi_abbonamento', 'data']
return self.readonly_fields
Or more concise:
class OrdineAdmin(admin.ModelAdmin):
inlines = [
FatturaAdmin
]
def get_readonly_fields(self, request, obj=None):
if obj and obj.pagato == True:
return ['utente', 'abbonamento', 'importo', 'giorni_attivi_abbonamento', 'data']
return ["data"]

Related

Custom permissions in Django REST API don't raise any error even when print() function returns False in the terminal

I wrote a permission:
class IsParticipant(permissions.BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
if isinstance(obj, Message):
return request.user in obj.chat.chat_participants.all()
elif ....
return False
I checked, conditions work correctly (now I need just the first if condition), the program enters the if condition, and returns False or True (I've written print() function, so it showed in terminal that program returns False of True depending on values). But it doesn't change anything, even if returns False, it doesn't forbid anything for user. It even doesn't raise any default error.
Here is the view class (although I don't think it is necessary here):
class WriteMessageCreateAPIView(generics.CreateAPIView):
permission_classes = [IsAuthenticated, IsParticipant]
serializer_class = WriteMessageSerializer #the model is named as 'Message'
def perform_create(self, serializer):
serializer.validated_data['author'] = self.request.user
serializer.validated_data['chat_id'] = self.kwargs['pk']
return super(WriteMessageCreateAPIView, self).perform_create(serializer)
So the questions are: Why the permission class doesn't do anything? How can I fix this issue?
Try by doing
permission_classes = [IsParticipant]
It will work I think, I had the same issue few days ago and I found out that the
permission_classes = [ ] is giving the 'or' of the classes and not the 'and', I mean if either of the permission class is true, then it is returning true. (or permitting the user)
You may directly add what you wanna do in views.py by overriding the post function. (or whichever is needed)
if you use many permissions need to indicate how they relate to each other
https://www.django-rest-framework.org/community/3.9-announcement/#composable-permission-classes
for example:
permission_classes = (IsAuthenticated | IsParticipant)
I solved the problem by adding
obj = get_object_or_404(Chat, id=self.kwargs['pk'])
self.check_object_permissions(self.request, obj)
to my perform_create function in WriteMessageCreateAPIView class.
Now it works: checks if the user is a participant of the chat group or not. If not, denies permission related with that chat group for the user.

Django REST Framework ModelSerializer get_or_create functionality

When I try to deserialize some data into an object, if I include a field that is unique and give it a value that is already assigned to an object in the database, I get a key constraint error. This makes sense, as it is trying to create an object with a unique value that is already in use.
Is there a way to have a get_or_create type of functionality for a ModelSerializer? I want to be able to give the Serializer some data, and if an object exists that has the given unique field, then just return that object.
In my experience nmgeek's solution won't work in DRF 3+ as serializer.is_valid() correctly honors the model's unique_together constraint. You can work around this by removing the UniqueTogetherValidator and overriding your serializer's create method.
class MyModelSerializer(serializers.ModelSerializer):
def run_validators(self, value):
for validator in self.validators:
if isinstance(validator, validators.UniqueTogetherValidator):
self.validators.remove(validator)
super(MyModelSerializer, self).run_validators(value)
def create(self, validated_data):
instance, _ = models.MyModel.objects.get_or_create(**validated_data)
return instance
class Meta:
model = models.MyModel
The Serializer restore_object method was removed starting with the 3.0 version of REST Framework.
A straightforward way to add get_or_create functionality is as follows:
class MyObjectSerializer(serializers.ModelSerializer):
class Meta:
model = MyObject
fields = (
'unique_field',
'other_field',
)
def get_or_create(self):
defaults = self.validated_data.copy()
identifier = defaults.pop('unique_field')
return MyObject.objects.get_or_create(unique_field=identifier, defaults=defaults)
def post(self, request, format=None):
serializer = MyObjectSerializer(data=request.data)
if serializer.is_valid():
instance, created = serializer.get_or_create()
if not created:
serializer.update(instance, serializer.validated_data)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
However, it doesn't seem to me that the resulting code is any more compact or easy to understand than if you query if the instance exists then update or save depending upon the result of the query.
#Groady's answer works, but you have now lost your ability to validate the uniqueness when creating new objects (UniqueValidator has been removed from your list of validators regardless the cicumstance). The whole idea of using a serializer is that you have a comprehensive way to create a new object that validates the integrity of the data you want to use to create the object. Removing validation isn't what you want. You DO want this validation to be present when creating new objects, you'd just like to be able to throw data at your serializer and get the right behavior under the hood (get_or_create), validation and all included.
I'd recommend overwriting your is_valid() method on the serializer instead. With the code below you first check to see if the object exists in your database, if not you proceed with full validation as usual. If it does exist you simply attach this object to your serializer and then proceed with validation as usual as if you'd instantiated the serializer with the associated object and data. Then when you hit serializer.save() you'll simply get back your already created object and you can have the same code pattern at a high level: instantiate your serializer with data, call .is_valid(), then call .save() and get returned your model instance (a la get_or_create). No need to overwrite .create() or .update().
The caveat here is that you will get an unnecessary UPDATE transaction on your database when you hit .save(), but the cost of one extra database call to have a clean developer API with full validation still in place seems worthwhile. It also allows you the extensibility of using custom models.Manager and custom models.QuerySet to uniquely identify your model from a few fields only (whatever the primary identifying fields may be) and then using the rest of the data in initial_data on the Serializer as an update to the object in question, thereby allowing you to grab unique objects from a subset of the data fields and treat the remaining fields as updates to the object (in which case the UPDATE call would not be extra).
Note that calls to super() are in Python3 syntax. If using Python 2 you'd want to use the old style: super(MyModelSerializer, self).is_valid(**kwargs)
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
class MyModelSerializer(serializers.ModelSerializer):
def is_valid(self, raise_exception=False):
if hasattr(self, 'initial_data'):
# If we are instantiating with data={something}
try:
# Try to get the object in question
obj = Security.objects.get(**self.initial_data)
except (ObjectDoesNotExist, MultipleObjectsReturned):
# Except not finding the object or the data being ambiguous
# for defining it. Then validate the data as usual
return super().is_valid(raise_exception)
else:
# If the object is found add it to the serializer. Then
# validate the data as usual
self.instance = obj
return super().is_valid(raise_exception)
else:
# If the Serializer was instantiated with just an object, and no
# data={something} proceed as usual
return super().is_valid(raise_exception)
class Meta:
model = models.MyModel
There are a couple of scenarios where a serializer might need to be able to get or create Objects based on data received by a view - where it's not logical for the view to do the lookup / create functionality - I ran into this this week.
Yes it is possible to have get_or_create functionality in a Serializer. There is a hint about this in the documentation here: http://www.django-rest-framework.org/api-guide/serializers#specifying-which-fields-should-be-write-only where:
restore_object method has been written to instantiate new users.
The instance attribute is fixed as None to ensure that this method is not used to update Users.
I think you can go further with this to put full get_or_create into the restore_object - in this instance loading Users from their email address which was posted to a view:
class UserFromEmailSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = [
'email',
]
def restore_object(self, attrs, instance=None):
assert instance is None, 'Cannot update users with UserFromEmailSerializer'
(user_object, created) = get_user_model().objects.get_or_create(
email=attrs.get('email')
)
# You can extend here to work on `user_object` as required - update etc.
return user_object
Now you can use the serializer in a view's post method, for example:
def post(self, request, format=None):
# Serialize "new" member's email
serializer = UserFromEmailSerializer(data=request.DATA)
if not serializer.is_valid():
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
# Loaded or created user is now available in the serializer object:
person=serializer.object
# Save / update etc.
A better way of doing this is to use the PUT verb instead, then override the get_object() method in the ModelViewSet. I answered this here: https://stackoverflow.com/a/35024782/3025825.
A simple workaround is to use to_internal_value method:
class MyModelSerializer(serializers.ModelSerializer):
def to_internal_value(self, validated_data):
instance, _ = models.MyModel.objects.get_or_create(**validated_data)
return instance
class Meta:
model = models.MyModel
I know it's a hack, but in case if you need a quick solution
P.S. Of course, editing is not supported
class ExpoDeviceViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, ]
serializer_class = ExpoDeviceSerializer
def get_queryset(self):
user = self.request.user
return ExpoDevice.objects.filter(user=user)
def perform_create(self, serializer):
existing_token = self.request.user.expo_devices.filter(
token=serializer.validated_data['token']).first()
if existing_token:
return existing_token
return serializer.save(user=self.request.user)
In case anyone needs to create an object if it does not exist on GET request:
class MyModelViewSet(viewsets.ModelViewSet):
queryset = models.MyModel.objects.all()
serializer_class = serializers.MyModelSerializer
def retrieve(self, request, pk=None):
instance, _ = models.MyModel.objects.get_or_create(pk=pk)
serializer = self.serializer_class(instance)
return response.Response(serializer.data)
Another solution, as I found that UniqueValidator wasn't in the validators for the serializer, but rather in the field's validators.
def is_valid(self, raise_exception=False):
self.fields["my_field_to_fix"].validators = [
v
for v in self.fields["my_field_to_fix"].validators
if not isinstance(v, validators.UniqueValidator)
]
return super().is_valid(raise_exception)

Django admin change to_python output based on request

I'm wondering how to change the behavior of a form field based on data in the request... especially as it relates to the Django Admin. For example, I'd like to decrypt a field in the admin based on request data (such as POST or session variables).
My thoughts are to start looking at overriding the change_view method in django/contrib/admin/options.py, since that has access to the request. However, I'm not sure how to affect how the field value displays the field depending on some value in the request. If the request has the correct value, the field value would be displayed; otherwise, the field value would return something like "NA".
My thought is that if I could somehow get that request value into the to_python() method, I could directly impact how the field is displayed. Should I try passing the request value into the form init and then somehow into the field init? Any suggestions how I might approach this?
Thanks for reading.
In models.py
class MyModel(models.Model):
hidden_data = models.CharField()
In admin.py
class MyModelAdmin(models.ModelAdmin):
class Meta:
model = MyModel
def change_view(self, request, object_id, extra_context=None):
.... # Perhaps this is where I'd do a lot of overriding?
....
return self.render_change_form(request, context, change=True, obj=obj)
I haven't tested this, but you could just overwrite the render_change_form method of the ModelAdmin to sneak in your code to change the field value between when the change_view is processed and the actual template rendered
class MyModelAdmin(admin.ModelAdmin):
...
def render_change_form(self, request, context, **kwargs):
# Here we have access to the request, the object being displayed and the context which contains the form
form = content['adminform'].form
field = form.fields['field_name']
...
if 'obj' in kwargs:
# Existing obj is being saved
else:
# New object is being created (an empty form)
return super(MyModelAdmin).render_change_form(request, context, **kwargs)

dynamic manipulation of django admin form

I have a modified admin form, where I added a field that shall modify the values of the current model's parent object. Now, depending on the user, I need to
alter the queryset of that extra field
set another field as readonly (or better, even hide it completely)
Basically my code below works as I'd expect it. A superuser gets the whole queryset and the other field is not readonly. All other users get a limited queryset and the other field is readonly. However, once I open that site in a different browser and as a non-superuser, even the superuser get the same result as the non-superusers. Seems like django somehow caches the result? If I put some print statements inside the conditional branches though, they get printed correctly. So the method still gets called each time and seems to still perform these steps. Only with a wrong outcome.
Is that a caching problem? Am I doing something entirely wrong? Can it be a bug in the django test server?
def get_form(self, request, obj=None, **kwargs):
form = super(MultishopProductAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
form.declared_fields['categories'].initial = obj.product.category.all()
if not request.user.is_superuser:
user_site = request.user.get_profile().site
form.declared_fields['categories'].queryset = Category.objects.filter(site__id=user_site.id)
self.readonly_fields = ('virtual_sites', )
if obj is not None:
form.declared_fields['categories'].initial = obj.product.category.filter(site__id=user_site.id)
return form
Yes you are doing it wrong. In Django 1.2+ you can use get_readonly_fields.
From this answer:
The ModelAdmin is only instantiated once for all requests that it receives. So when you define the readonly fields like that, you're setting it across the board permanently.
Regarding altering the queryset. From the documentation:
class MyModelAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyModelAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
To expand on dan-klasson's anwer: Do not ever set instance attributes in any ModelAdmin method (like self.readonly_fields) to prevent issues like the one you described. Only use Django ModelAdmin's options (which are class-level) or methods to manipulate any behavior. Regarding readonly fields, you can use the ModelAdmin.get_readonly_fields method which has this signature: get_readonly_fields(self, request, obj=None).
So I couldn't find a really clever way to do what I wanted with django admin's custom methods. What I ended up doing now is implementing the admin's change_view, setting up my own form manually and performing all my custom initializations from there.
I then provided a custom template by setting change_form_template, which is simply extending admin/change_form.html but rendering my own form instead of the default one. I also set extra_context['adminform'] = None so the default admin form gets removed.
That way I can now customize my form the way I need it to be but still use all the other admin conveniences. So far it seems to work very nicely. Not the very most elegant solution either I think, but the best I could think of.

Whole model as read-only

Is there a way to make a model read-only in the django admin? but I mean the whole model.
So, no adding, no deleting, no changing, just see the objects and the fields, everything as read-only?
ModelAdmin provides the hook get_readonly_fields() - the following is untested, my idea being to determine all fields the way ModelAdmin does it, without running into a recursion with the readonly fields themselves:
from django.contrib.admin.util import flatten_fieldsets
class ReadOnlyAdmin(ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if self.declared_fieldsets:
fields = flatten_fieldsets(self.declared_fieldsets)
else:
form = self.get_formset(request, obj).form
fields = form.base_fields.keys()
return fields
then subclass/mixin this admin whereever it should be a read-only admin.
For add/delete, and to make their buttons disappear, you'll probably also want to add
def has_add_permission(self, request):
# Nobody is allowed to add
return False
def has_delete_permission(self, request, obj=None):
# Nobody is allowed to delete
return False
P.S.: In ModelAdmin, if has_change_permission (lookup or your override) returns False, you don't get to the change view of an object - and the link to it won't even be shown. It would actually be cool if it did, and the default get_readonly_fields() checked the change permission and set all fields to readonly in that case, like above. That way non-changers could at least browse the data... given that the current admin structure assumes view=edit, as jathanism points out, this would probably require the introduction of a "view" permission on top of add/change/delete...
EDIT: regarding setting all fields readonly, also untested but looking promising:
readonly_fields = MyModel._meta.get_all_field_names()
EDIT: Here's another one
if self.declared_fieldsets:
return flatten_fieldsets(self.declared_fieldsets)
else:
return list(set(
[field.name for field in self.opts.local_fields] +
[field.name for field in self.opts.local_many_to_many]
))
As "view permissions" will not make it into Django 1.11, unfortunately, here's a solution that makes your ModelAdmin read-only by making both saving model changes and adding model history log entries a no-op.
def false(*args, **kwargs):
"""A simple no-op function to make our changes below readable."""
return False
class MyModelReadOnlyAdmin(admin.ModelAdmin):
list_display = [
# list your admin listview entries here (as usual)
]
readonly_fields = [
# list your read-only fields here (as usual)
]
actions = None
has_add_permission = false
has_delete_permission = false
log_change = false
message_user = false
save_model = false
(NOTE: Don't mistake the false no-op helper with the False builtin. If you don't sympathize with the helper function outside the class move it into the class, call it no_op or something else, or override the affected attributes by usual defs. Less DRY, but if you don't care...)
This will:
remove the actions drop-down box (with "delete") in the list view
disallow adding new model entries
disallow deleting existing model entries
avoid creating log entries in the model history
avoid displaying "was changed successfully" messages after saving
avoid saving changeform changes to the database
It will not:
remove or replace the two buttons "Save and continue editing" and "SAVE" (which would be nice to improve the user experience)
Note that get_all_field_names (as mentioned in the accepted answer) was removed in Django 1.10.
Tested with Django 1.10.5.
The selected answer doesn't work for Django 1.11, and I've found a much simpler way to do it so I thought I'd share:
class MyModelAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
return [f.name for f in obj._meta.fields]
def has_delete_permission(self, request, obj=None):
return False
def has_add_permission(self, request):
return False
You may customize your ModelAdmin classes with the readonly_fields attribute. See this answer for more.
I had a similar scenario where:
User should be able to create the model objects
User should be able to view listing of model objects
User SHOULD'NT be able to edit an object once it's been created
1. Overriding the Change View
Because it's possible to override the change_view() in a ModelAdmin, we can exploit that to prevent the editing of model instances once they have been created. Here's an example I've used:
def change_view(self, request, object_id, form_url='', extra_context=None):
messages.error(request, 'Sorry, but editing is NOT ALLOWED')
return redirect(request.META['HTTP_REFERER'])
2. Conditionally Change Edit Permissions
I also realized that the docs interpret the result of ModelAdmin.has_change_permission() in different ways:
Should return True if editing obj is permitted, False otherwise. If
obj is None, should return True or False to indicate whether editing
of objects of this type is permitted in general (e.g., False will be
interpreted as meaning that the current user is not permitted to edit
any object of this type).
Meaning I could check whether obj is None, in which case I return True, otherwise I return False, and this in effect allows users to view the change-list, but not be able to edit nor view the change_form after the model instance is saved.
def has_change_permission(self, request, obj = None, **kwargs):
if obj is None:
return True
else:
return False
Though am thinking this might also override any MODEL_can_change permissions allowing unwanted eyes from viewing the change-list?
According to my test on Django 1.8 we can not use following as noted on answer #3 but it works on Django 1.4:
## self.get_formset(request, obj) ##
answer 3 needs fix. Generally, alternative codes for this issue about below section
## form = self.get_formset(request, obj).form ##
## fields = form.base_fields.keys() ##
Can be something like:
#~ (A) or
[f.name for f in self.model._meta.fields]
#~ (B) or
MyModel._meta.get_all_field_names()
#~ (C)
list(set([field.name for field in self.opts.local_fields] +
[field.name for field in self.opts.local_many_to_many]
))