'SalaryAdmin' object has no attribute 'request' - django

Hello I want to add method to list display in django admin but am getting error say SalryAdmin has no attribute request
here my admin
#admin.register(Salary)
class SalaryAdmin(admin.ModelAdmin):
list_display = ['user_name', 'action']
def action(self, obj):
if not self.request.user.is_superuser:
if obj.hr_state == 'request-change-approved' and self.request.user.user_role.position.code == 'HRM':
return True
else:
return False
else:
return True
any help appreciated

You are getting error for obvious reason - ModelAdmin class does not have request member. Instead of this, you should mark method as action and request would be passed as an argument to the method - like so:
#admin.register(Salary)
class SalaryAdmin(admin.ModelAdmin):
list_display = ['user_name', 'action']
actions = ['action']
#admin.action(description='')
def action(self, request, queryset):
obj = queryset.first() # TODO - implement object retrieval logic here
if not request.user.is_superuser:
if obj.hr_state == 'request-change-approved' and request.user.user_role.position.code == 'HRM':
return True
else:
return False
else:
return True
Here is the source to the official docs.

Related

Django forms: error not displaying in json

I am trying to set up my password forgot in Django using built-in validators, but I am getting no response if reproduce errors, but it supposed to show up the errors others classes, how can I fix this?
forms.py
class RestorePasswordForm(SetPasswordForm):
new_password1 = forms.CharField(widget=forms.PasswordInput(attrs={'class':'form-control','placeholder':'Password','required': True}))
new_password2 = forms.CharField(widget=forms.PasswordInput(attrs={'class':'form-control','placeholder':'Password','required': True}))
views
def post(self, request,token):
form = RestorePasswordForm(request.POST)
if form.is_valid():
obj = token(token)
if obj:
this_user = User.objects.get(id=obj.email_id)
if not this_user:
return False
this_user.set_password(request.POST.get('new_password2'))
this_user.save()
obj.token = None
obj.save()
return JsonResponse({"message":form.errors})
browser response display
{"message": {}}
When you are validating a form you should always check if it is posting and then only process it. Also 'forms.errors' is going to give you HTML string, but you already are aware of that.
The reason you are getting empty reason is because nothing is being posted to the form.
def post(self, request,token):
if request.method=="POST":
form = RestorePasswordForm(request.POST)
if form.is_valid():
obj = token(token)
if obj:
this_user = User.objects.get(id=obj.email_id)
if not this_user:
return False
this_user.set_password(request.POST.get('new_password2'))
this_user.save()
obj.token = None
obj.save()
else:
print("you didn't post anything to form")
return JsonResponse({"message":form.errors})

Is there a way to override the delete_selected method in ModelAdmin but keep confirmation?

I have:
class Person(admin.ModelAdmin):
actions = ['delete_selected']
def delete_selected(modeladmin, request, queryset):
# Show confirmation page.
for obj in queryset:
obj.custom_delete()
That comment I left there is where I'm struggling. I still want to show the confirmation page before I perform my custom delete.
Short answer: you should override delete_queryset [Django-doc], since this encapsulates the real logic to remove the objects.
You should not override delete_selected. This action is defined like [GitHub]:
def delete_selected(modeladmin, request, queryset):
# ...
# Populate deletable_objects, a data structure of all related objects that
# will also be deleted.
deletable_objects, model_count, perms_needed, protected = modeladmin.get_deleted_objects(queryset, request)
# The user has already confirmed the deletion.
# Do the deletion and return None to display the change list view again.
if request.POST.get('post') and not protected:
if perms_needed:
raise PermissionDenied
n = queryset.count()
if n:
for obj in queryset:
obj_display = str(obj)
modeladmin.log_deletion(request, obj, obj_display)
modeladmin.delete_queryset(request, queryset)
modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % {
"count": n, "items": model_ngettext(modeladmin.opts, n)
}, messages.SUCCESS)
# Return None to display the change list page again.
return None
# ...
context = {
# ...
}
request.current_app = modeladmin.admin_site.name
# Display the confirmation page
return TemplateResponse(request, modeladmin.delete_selected_confirmation_template or [
"admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.model_name),
"admin/%s/delete_selected_confirmation.html" % app_label,
"admin/delete_selected_confirmation.html"
], context)
delete_selected.allowed_permissions = ('delete',)
delete_selected.short_description = gettext_lazy("Delete selected %(verbose_name_plural)s")
The key part here is that this action will perform the proper checks, but the deletion itself is done through a call:
modeladmin.delete_queryset(request, queryset)
So it is sufficient to override delete_queryset instead, with:
class PersonAdmin(admin.ModelAdmin):
actions = ['delete_selected']
def delete_queryset(self, request, queryset):
for obj in queryset:
obj.custom_delete()
A ModelAdmin has a standard implementation for delete_queryset [GitHub]:
class ModelAdmin(BaseModelAdmin):
# ...
def delete_queryset(self, request, queryset):
"""Given a queryset, delete it from the database."""
queryset.delete()
My requirement was a bit different. I had to intercept the delete_selected action and display error depending on the condition. This is what I did -
In Model Admin
#admin.register(Model)
class ModelAdmin(model.Admin):
...
def get_actions(self, request):
actions = super().get_actions(request)
self.actions.append(delete_selected)
self.actions.append(...otheractions)
return actions
Outside the Model admin
#Import goes to top of the file
from django.contrib.admin.actions import delete_selected as default_delete_selected
def delete_selected(modeladmin, request, queryset):
response = HttpResponseRedirect(request.get_full_path())
#logic to validate
for obj in queryset:
if obj.name == 'test':
messages.error(request,'error message')
return response
return default_delete_selected(modeladmin, request, queryset)

Is there a way to access request object in django inline formset clean method?

Got this admin class with inline and form classes:
class InvoiceAdmin(admin.ModelAdmin):
....
inlines = [InvoiceLineInline, ]
form = InvoiceForm
....
class InvoiceForm(forms.ModelForm):
....
def clean():
....
class Meta:
model = Invoice
exclude = []
class InvoiceLineInline(admin.TabularInline):
model = InvoiceLine
formset = InvoiceLineInlineFormset
extra = 1
class InvoiceLineInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
super(InvoiceLineInlineFormset, self).clean()
count = 0
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
data = form.cleaned_data
try:
if data:
count += 1
else:
continue
except AttributeError:
pass
if Decimal(data.get('quantity', 0)) <= 0:
raise forms.ValidationError("Amount should be greater than 0.")
******************************************************
_stock_code = data.get('stock_code', None)
if not len(fetch_stocks_from_connector(request, stock_code=_stock_code)):
raise forms.ValidationError("{} Stock code does not exist at connector.".format(_stock_code))
******************************************************
if count < 1:
raise forms.ValidationError('Need one line at least.')
I need to do extra validation with an external method for the _stock_code value in each inlineform within InvoiceLineInlineFormset.clean as displayed above between the starred lines. But external method needs request object as argument to run properly.
Is it possible to pass request object to clean method?
The question is old but I'll share the solution that worked for me
ModelAdmin has a get_formset method. You can extend it like this
class YourAdminInline(admin.TabularInline):
model = YourModel
formset = YourInlineFormSet
def get_formset(self,request,obj=None,**kwargs):
formset = super(YourAdminInline,self).get_formset(request,obj,**kwargs)
formset.request = request
return formset
In your formset you can access the request object using self.request. For example in the clean method
class YourInlineFormset(forms.BaseInlineFormset):
def clean(self):
...
request = self.request
Base ModelAdmin class has _create_formsets() method which, well, generates formsets:
def _create_formsets(self, request, obj, change):
"Helper function to generate formsets for add/change_view."
formsets = []
inline_instances = []
prefixes = {}
get_formsets_args = [request]
if change:
get_formsets_args.append(obj)
for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset_params = {
'instance': obj,
'prefix': prefix,
'queryset': inline.get_queryset(request),
}
if request.method == 'POST':
formset_params.update({
'data': request.POST,
'files': request.FILES,
'save_as_new': '_saveasnew' in request.POST
})
formsets.append(FormSet(**formset_params))
inline_instances.append(inline)
return formsets, inline_instances
As you can see by extending formset_params with method's request argument in your ModelAdmin class you can then save extra kwarg with request in formset's class __init__() and later use it in clean() method via self.request.
Note that this is not the cleanest solution as method implementation does not allow to extend only kwargs easily so entire method needs to be moved to your ModelAdmin and with any Django's update of this part of code you will need to update your method accordingly.
I had pretty much the same question (how to get request.user stuff from inline methods) and I got this answer which worked for me:
Django: access to user info from admin.py for methods with no request object?

Django Rest Framework: Disable field update after object is created

I'm trying to make my User model RESTful via Django Rest Framework API calls, so that I can create users as well as update their profiles.
However, as I go through a particular verification process with my users, I do not want the users to have the ability to update the username after their account is created. I attempted to use read_only_fields, but that seemed to disable that field in POST operations, so I was unable to specify a username when creating the user object.
How can I go about implementing this? Relevant code for the API as it exists now is below.
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'password', 'email')
write_only_fields = ('password',)
def restore_object(self, attrs, instance=None):
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Thanks!
It seems that you need different serializers for POST and PUT methods. In the serializer for PUT method you are able to just except the username field (or set the username field as read only).
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = SerializerWithoutUsernameField
return serializer_class
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Check this question django-rest-framework: independent GET and PUT in same URL but different generics view
Another option (DRF3 only)
class MySerializer(serializers.ModelSerializer):
...
def get_extra_kwargs(self):
extra_kwargs = super(MySerializer, self).get_extra_kwargs()
action = self.context['view'].action
if action in ['create']:
kwargs = extra_kwargs.get('ro_oncreate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_oncreate_field'] = kwargs
elif action in ['update', 'partial_update']:
kwargs = extra_kwargs.get('ro_onupdate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_onupdate_field'] = kwargs
return extra_kwargs
Another method would be to add a validation method, but throw a validation error if the instance already exists and the value has changed:
def validate_foo(self, value):
if self.instance and value != self.instance.foo:
raise serializers.ValidationError("foo is immutable once set.")
return value
In my case, I wanted a foreign key to never be updated:
def validate_foo_id(self, value):
if self.instance and value.id != self.instance.foo_id:
raise serializers.ValidationError("foo_id is immutable once set.")
return value
See also: Level-field validation in django rest framework 3.1 - access to the old value
My approach is to modify the perform_update method when using generics view classes. I remove the field when update is performed.
class UpdateView(generics.UpdateAPIView):
...
def perform_update(self, serializer):
#remove some field
rem_field = serializer.validated_data.pop('some_field', None)
serializer.save()
I used this approach:
def get_serializer_class(self):
if getattr(self, 'object', None) is None:
return super(UserViewSet, self).get_serializer_class()
else:
return SerializerWithoutUsernameField
UPDATE:
Turns out Rest Framework already comes equipped with this functionality. The correct way of having a "create-only" field is by using the CreateOnlyDefault() option.
I guess the only thing left to say is Read the Docs!!!
http://www.django-rest-framework.org/api-guide/validators/#createonlydefault
Old Answer:
Looks I'm quite late to the party but here are my two cents anyway.
To me it doesn't make sense to have two different serializers just because you want to prevent a field from being updated. I had this exact same issue and the approach I used was to implement my own validate method in the Serializer class. In my case, the field I don't want updated is called owner. Here is the relevant code:
class BusinessSerializer(serializers.ModelSerializer):
class Meta:
model = Business
pass
def validate(self, data):
instance = self.instance
# this means it's an update
# see also: http://www.django-rest-framework.org/api-guide/serializers/#accessing-the-initial-data-and-instance
if instance is not None:
originalOwner = instance.owner
# if 'dataOwner' is not None it means they're trying to update the owner field
dataOwner = data.get('owner')
if dataOwner is not None and (originalOwner != dataOwner):
raise ValidationError('Cannot update owner')
return data
pass
pass
And here is a unit test to validate it:
def test_owner_cant_be_updated(self):
harry = User.objects.get(username='harry')
jack = User.objects.get(username='jack')
# create object
serializer = BusinessSerializer(data={'name': 'My Company', 'owner': harry.id})
self.assertTrue(serializer.is_valid())
serializer.save()
# retrieve object
business = Business.objects.get(name='My Company')
self.assertIsNotNone(business)
# update object
serializer = BusinessSerializer(business, data={'owner': jack.id}, partial=True)
# this will be False! owners cannot be updated!
self.assertFalse(serializer.is_valid())
pass
I raise a ValidationError because I don't want to hide the fact that someone tried to perform an invalid operation. If you don't want to do this and you want to allow the operation to be completed without updating the field instead, do the following:
remove the line:
raise ValidationError('Cannot update owner')
and replace it with:
data.update({'owner': originalOwner})
Hope this helps!
More universal way to "Disable field update after object is created"
- adjust read_only_fields per View.action
1) add method to Serializer (better to use your own base cls)
def get_extra_kwargs(self):
extra_kwargs = super(BasePerTeamSerializer, self).get_extra_kwargs()
action = self.context['view'].action
actions_readonly_fields = getattr(self.Meta, 'actions_readonly_fields', None)
if actions_readonly_fields:
for actions, fields in actions_readonly_fields.items():
if action in actions:
for field in fields:
if extra_kwargs.get(field):
extra_kwargs[field]['read_only'] = True
else:
extra_kwargs[field] = {'read_only': True}
return extra_kwargs
2) Add to Meta of serializer dict named actions_readonly_fields
class Meta:
model = YourModel
fields = '__all__'
actions_readonly_fields = {
('update', 'partial_update'): ('client', )
}
In the example above client field will become read-only for actions: 'update', 'partial_update' (ie for PUT, PATCH methods)
This post mentions four different ways to achieve this goal.
This was the cleanest way I think: [collection must not be edited]
class DocumentSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
if 'collection' in validated_data:
raise serializers.ValidationError({
'collection': 'You must not change this field.',
})
return super().update(instance, validated_data)
Another solution (apart from creating a separate serializer) would be to pop the username from attrs in the restore_object method if the instance is set (which means it's a PATCH / PUT method):
def restore_object(self, attrs, instance=None):
if instance is not None:
attrs.pop('username', None)
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
If you don't want to create another serializer, you may want to try customizing get_serializer_class() inside MyViewSet. This has been useful to me for simple projects.
# Your clean serializer
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
# Your hardworking viewset
class MyViewSet(MyParentViewSet):
serializer_class = MySerializer
model = MyModel
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# setting `exclude` while having `fields` raises an error
# so set `read_only_fields` if request is PUT/PATCH
setattr(serializer_class.Meta, 'read_only_fields', ('non_updatable_field',))
# set serializer_class here instead if you have another serializer for finer control
return serializer_class
setattr(object, name, value)
This is the counterpart of getattr(). The
arguments are an object, a string and an arbitrary value. The string
may name an existing attribute or a new attribute. The function
assigns the value to the attribute, provided the object allows it. For
example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123.
class UserUpdateSerializer(UserSerializer):
class Meta(UserSerializer.Meta):
fields = ('username', 'email')
class UserViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
return UserUpdateSerializer if self.action == 'update' else super().get_serializer_class()
djangorestframework==3.8.2
I would suggest also looking at Django pgtrigger
This allows you to install triggers for validation. I started using it and was very pleased with its simplicity:
Here's one of their examples that prevents a published post from being updated:
import pgtrigger
from django.db import models
#pgtrigger.register(
pgtrigger.Protect(
operation=pgtrigger.Update,
condition=pgtrigger.Q(old__status='published')
)
)
class Post(models.Model):
status = models.CharField(default='unpublished')
content = models.TextField()
The advantage of this approach is it also protects you from .update() calls that bypass .save()

How can I pass a detail object to custom authorization in tastypie?

How can I access the detail endpoint object being accessed in the request during a tastypie authorization?
I noticed that one of the overridden methods in the docs has an object parameter -- how can I set this?
In branch perms,
https://github.com/toastdriven/django-tastypie/blob/perms/tastypie/authorization.py
Class Authorization has a set of methods for example:
def read_detail(self, object_list, bundle):
"""
Returns either ``True`` if the user is allowed to read the object in
question or throw ``Unauthorized`` if they are not.
Returns ``True`` by default.
"""
return True
Here You can try to access the obj through bundle.obj
If You can't use the perms branch, I suggest you this way:
class MyBaseAuth(Authorization):
def get_object(self, request):
try:
pk = resolve(request.path)[2]['pk']
except IndexError, KeyError:
object = None # or raise Exception('Wrong URI')
else:
try:
object = self.resource_meta.object_class.objects.get(pk=pk)
except self.resource_meta.DoesNotExist:
object = None
return object
class FooResourceAuthorization(MyBaseAuth):
def is_authorized(self, request, object=None):
if request.method in ('GET', 'POST'):
return True
elif request.method == 'DELETE':
object = self.get_object(request)
if object.profile = request.user.profile:
return True
return False
Hackish, but with a simple way of getting to the object from the request URL (inspired by the code inside DjangoAuthorization).
def is_authorized(self, request, object=None):
meta = self.resource_meta
re_id = re.compile(meta.api_name + "/" + meta.resource_name + "/(\d+)/")
id = re_id.findall(request.path)
if id:
object = meta.object_class.objects.get(id=id[0])
# do whatever you want with the object
else:
# It's not an "object call"