Django Admin: Show only some objects in ManyToMany field? - django

I have relatively simple data model with User, Group and Task. Each group has its own tasks and users. Users can be only assigned to one group.
Tasks belong to groups and each task has manyToMany field for users, so multiple users can have the same task assigned.
In my admin when assigning users to task it shows all created users, I want it to only show users from the same group as the task.
What would be the best approach?
I have checked available customization options for admin.ModelAdmin but haven't found anything related to my problem.

you can use formfield_for_manytomany
the formfield_for_manytomany method can be overridden to change the default formfield for a many to many field
in your case change your admin.py to :
class TaskAdmin(admin.ModelAdmin):
def get_object(self, request, object_id, s):
# Hook obj for use in formfield_for_manytomany
self.obj = super(TaskAdmin, self).get_object(request, object_id)
# print ("Got object:", self.obj)
return self.obj
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "user":
kwargs["queryset"] = User.objects.filter(task=self.obj)
return super().formfield_for_manytomany(db_field, request, **kwargs)
admin.site.register(Task, TaskAdmin)

You can customize the model admin using a method: formfield_for_manytomany
class TaskAdmin(admin.ModelAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "users":
# Filter your users below as per your condition
kwargs["queryset"] = Users.objects.filter()
return super(TaskAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)

Related

DRF options request on viewset with multiple serializers

I'm using a mixin on my viewset so that multiple serializers can be used accross different viewset actions and any custom actions.
I have an extra action called invoice which is just a normal update but using a different serializer. I need to perform an OPTIONS request at the endpoint to get options for a <select> element. The problem is that when I perform the request it's picking up the serializer from the default update - OrderSerializer instead of InvoiceSerializer. How can I pick up the options from the correct serializer?
class MultipleSerializerMixin:
"""
Mixin that allows for multiple serializers based on the view's
`serializer_action_classes` attribute.
ex.
serializer_action_classes = {
'list': ReadOnlyListSerializer,
'retrieve': ReadOnlyDetailSerializer,
}
"""
def get_serializer_class(self):
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super().get_serializer_class()
class OrderAPIViewSet(MultipleSerializerMixin,
viewsets.ModelViewSet):
queryset = Order.objects.all()
serializer_class = serializers.OrderSerializer
serializer_action_classes = {
'invoice': serializers.InvoiceSerializer,
}
#action(detail=True, methods=['put'], url_name='invoice')
def invoice(self, request, *args, **kwargs):
"""
Invoice the order and order lines.
"""
return self.update(request, *args, **kwargs)
Update:
So after inspecting the determine_actions method in metadata.SimpleMetadata it would seem that when performing an OPTIONS request view.action is metadata instead of invoice which explains why the serializer is defaulting to view.serializer_class.
One workaround is to create an extra action as a schema endpoint that could be accessed via a GET request that manually sets the action to invoice.
#action(detail=True, methods=['get', 'put'])
def invoice_schema(self, request, *args, **kwargs):
self.action = 'invoice'
data = self.metadata_class().determine_metadata(request, self)
return Response(data, status=status.HTTP_200_OK)
A more DRY solution if you have multiple actions that use different serializers would be to override the view's options method and set the action from the query parameters. This could be added to MultipleSerializerMixin to make it the default behaviour for all views that use this mixin.
def options(self, request, *args, **kwargs):
self.action = request.query_params.get('action')
return super().options(request, *args, **kwargs)
Override get_serializer_class method is enough and OPTIONS request will detect which serializer to use :
def get_serializer_class(self):
if self.request.method == 'GET':
return ReadOnlyShopSerializer
return ShopSerializer

Django filter_horizontal filtering

I have 2 models related by M2M type of relationship. I use filter_horizontal in the admin for editing my entities.
However, I would like to have a control on what is presented in the left side of the filter_horizontal widget. For example, I would like to filter and show only those entities that match some certain criteria.
I think I found it!
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "cars":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
ModelAdmin.formfield_for_manytomany(db_field, request, **kwargs)
This subject is always tricky in the Django admin. I suppose that in the inline defenition you can do something like this:
class BAdmin(admin.TabularInline):
...
def get_queryset(self, request):
qs = super(BAdmin, self).get_queryset(request)
return qs.filter(user=request.user)
https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_queryset

Django admin: prevent add, change, but not delete

I want to be able to only delete values from admin.
I wrote following code for this:
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return True
However, in this case I can't find link to delete object.
How this can be resolved?
It makes sense that the change list view is disabled. I noticed that visiting /admin/app/model/1/delete/ will let you delete the object.
So you have basically two options:
Create a custom admin page listing the models objects. Each object
with a delete button that links to /admin/app/model/pk/delete/.
Hook this into your admin somehow.
Set has_change_permission
to True and make sure the detail page displays a custom form, all
fields with readonly widgets.
I would go for 2. Because it is less work, gives you all the benefits of the change list page (filters, actions) and keeps the default admin structure. A large benefit is that the user can see what he is about to delete.
I would do something like this (not tested):
class ItemForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs['readonly'] = True
class Meta:
model = Item
exclude = []
class ItemAdmin(admin.ModelAdmin):
form = ItemForm
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return True
def has_delete_permission(self, request, obj=None):
return True

Exclude a field in django admin for users not super admin

how i'll exclude a field in django admin if the users are not super admin?
thanks
I did it in this way:
admin.py
def add_view(self, request, form_url='', extra_context=None):
if not request.user.is_superuser:
self.exclude=('activa', )
return super(NoticiaAdmin, self).add_view(request, form_url='', extra_context=None)
Overriding the exclude property is a little dangerous unless you remember to set it back for other permissions, a better way might be to override the get_form method.
see: Django admin: exclude field on change form only
In the future, it looks like there will be a get_fields hook. But it's only in the master branch, not 1.5 or 1.6.
def get_fields(self, request, obj=None):
"""
Hook for specifying fields.
"""
return self.fields
https://github.com/django/django/blob/master/django/contrib/admin/options.py
If you have a lot of views, you can use this decorator :
def exclude(fields=(), permission=None):
"""
Exclude fields in django admin with decorator
"""
def _dec(func):
def view(self, request, *args, **kwargs):
if not request.user.has_perm(permission):
self.exclude=fields
return func(self, request, *args, **kwargs)
return view
return _dec
usage:
#exclude(fields=('fonction', 'fonction_ar'))

Django: How to get current user in admin forms?

In Django's ModelAdmin, I need to display forms customized according to the permissions a user has. Is there a way of getting the current user object into the form class, so that i can customize the form in its __init__ method?
I think saving the current request in a thread local would be a possibility but this would be my last resort because I'm thinking it is a bad design approach.
Here is what i did recently for a Blog:
class BlogPostAdmin(admin.ModelAdmin):
form = BlogPostForm
def get_form(self, request, **kwargs):
form = super(BlogPostAdmin, self).get_form(request, **kwargs)
form.current_user = request.user
return form
I can now access the current user in my forms.ModelForm by accessing self.current_user
EDIT: This is an old answer, and looking at it recently I realized the get_form method should be amended to be:
def get_form(self, request, *args, **kwargs):
form = super(BlogPostAdmin, self).get_form(request, *args, **kwargs)
form.current_user = request.user
return form
(Note the addition of *args)
Joshmaker's answer doesn't work for me on Django 1.7. Here is what I had to do for Django 1.7:
class BlogPostAdmin(admin.ModelAdmin):
form = BlogPostForm
def get_form(self, request, obj=None, **kwargs):
form = super(BlogPostAdmin, self).get_form(request, obj, **kwargs)
form.current_user = request.user
return form
For more details on this method, please see this relevant Django documentation
This use case is documented at ModelAdmin.get_form
[...] if you wanted to offer additional fields to superusers, you could swap in a different base form like so:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
kwargs['form'] = MySuperuserForm
return super().get_form(request, obj, **kwargs)
If you just need to save a field, then you could just override ModelAdmin.save_model
from django.contrib import admin
class ArticleAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.user = request.user
super().save_model(request, obj, form, change)
I think I found a solution that works for me: To create a ModelForm Django uses the admin's formfield_for_db_field-method as a callback.
So I have overwritten this method in my admin and pass the current user object as an attribute with every field (which is probably not the most efficient but appears cleaner to me than using threadlocals:
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(MyAdmin, self).formfield_for_dbfield(db_field, **kwargs)
field.user = kwargs.get('request', None).user
return field
Now I can access the current user object in the forms __init__ with something like:
current_user=self.fields['fieldname'].user
stumbled upon same thing and this was first google result on my page.Dint helped, bit more googling and worked!!
Here is how it works for me (django 1.7+) :
class SomeAdmin(admin.ModelAdmin):
# This is important to have because this provides the
# "request" object to "clean" method
def get_form(self, request, obj=None, **kwargs):
form = super(SomeAdmin, self).get_form(request, obj=obj, **kwargs)
form.request = request
return form
class SomeAdminForm(forms.ModelForm):
class Meta(object):
model = SomeModel
fields = ["A", "B"]
def clean(self):
cleaned_data = super(SomeAdminForm, self).clean()
logged_in_email = self.request.user.email #voila
if logged_in_email in ['abc#abc.com']:
raise ValidationError("Please behave, you are not authorised.....Thank you!!")
return cleaned_data
Another way you can solve this issue is by using Django currying which is a bit cleaner than just attaching the request object to the form model.
from django.utils.functional import curry
class BlogPostAdmin(admin.ModelAdmin):
form = BlogPostForm
def get_form(self, request, **kwargs):
form = super(BlogPostAdmin, self).get_form(request, **kwargs)
return curry(form, current_user=request.user)
This has the added benefit making your init method on your form a bit more clear as others will understand that it's being passed as a kwarg and not just randomly attached attribute to the class object before initialization.
class BlogPostForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.current_user = kwargs.pop('current_user')
super(BlogPostForm, self).__init__(*args, **kwargs)