I want to create a healthcheck endpoint for my django admin panel.
I want to register it via admin.site.register_view (I am using the adminplus package) but I can't figure out how to made it accessible to the public, without the need to authenticate first.
Any ideas?
So I ended up overriding def has_permission(self, request) in subclass of AdminPlusMixin:
from adminplus.sites import AdminPlusMixin
class MyAdmin(AdminPlusMixin, AdminSite):
def has_permission(self, request):
if request.resolver_match.url_name == 'admin-healthcheck':
return True
return super().has_permission(request)
I believe that package is outdated, instead you could do something like the following:
Created a proxy model in models.py such as:
class Proxy(models.Model):
id = models.BigAutoField(db_column='id', primary_key=True)
def str(self):
return "<Label: id: %d>" % self.id
class Meta:
managed = False
verbose_name_plural = 'proxies'
db_table = 'proxy'
ordering = ('id',)
Which is just a mysql view that a created from am existing table
create view proxy
as select id
from samples
LIMIT 10;
And finally in admin.py
#admin.register(Proxy)
class LabelAdmin(admin.ModelAdmin):
change_list_template = 'label_view.html'
def changelist_view(self, request, extra_context=None):
...
return render(request, "label_view.html", context)
This way it will show in the admin panel, inside the app you're working on.
Probably what you have is a function in your views.py, in this case you should replace that function content where the "..." are in the LabelAdmin class.
I'm need to add an additional filter property (in the background) to a django-filter request.
My Model:
class Event(models.Model):
name=models.CharField(max_length=254)
location=models.ForeignKey(Place)
invited_user=models.ManyToManyField(User,null=True, blank=True)
With a filter those entries with the same location can be filtered. This is working.
Further on I have to exclude all those entries where the invited_user is not the request.user (choosing this filter property is only possible if the user has permissions).
Is this possible with django-filter, and if yes how?
My filter Class:
import django_filters
from models import Event
class EventFilter(django_filters.FilterSet):
class Meta:
model = Event
fields = ['location']
My work is based on: How do I filter tables with Django generic views?
you can access the request object in FilterSet.qs property.
class EventFilter(django_filters.FilterSet):
class Meta:
model = Event
fields = ['location']
#property
def qs(self):
queryset=super(EventFilter, self).qs
if request.user.has_perm("app_label.has_permission"):
return queryset.exclude(invited_user!=self.request.user)
return queryset
docs https://rpkilby.github.io/django-filter/guide/usage.html#filtering-the-primary-qs
I think in your case you could do it by modifying the queryset in the view, where you should be able to access request.user. Therefore you wouldn't need to dig deep into django-filter,
In my case, when using dango_filters FilterView along with crispy forms to render the form, I wanted to hide fields from the form, along with additional filtering as you described, so I overrode get() for the FilterView, restricted the queryset to the user, and used crispy form's layout editing to pop the unwanted fields from the filter form:
def get(self, request, *args, **kwargs):
"""
original code from django-filters.views.BaseFilterView - added admin check
"""
filterset_class = self.get_filterset_class()
self.filterset = self.get_filterset(filterset_class)
self.object_list = self.filterset.qs
# If not admin, restrict to assessor's own centre and clients
if not request.user.get_profile().is_admin:
self.object_list = self.object_list.filter(attendee__assessor=request.user)
self.filterset.form.helper.layout[0].pop(2) # centres filter
self.filterset.form.helper.layout[0].pop(1) # assessors filter
context = self.get_context_data(filter=self.filterset,
object_list=self.object_list)
return self.render_to_response(context)
Try this:
class EventListView(BaseFilterView):
...
def get_filterset(self, *args, **kwargs):
fs = super().get_filterset(*args, **kwargs)
fs.filters['location'].field.queryset = fs.filters['location'].field.queryset.filter(user=self.request.user)
return fs
I would like to provide two different serializers and yet be able to benefit from all the facilities of ModelViewSet:
When viewing a list of objects, I would like each object to have an url which redirects to its details and every other relation appear using __unicode __ of the target model;
example:
{
"url": "http://127.0.0.1:8000/database/gruppi/2/",
"nome": "universitari",
"descrizione": "unitn!",
"creatore": "emilio",
"accesso": "CHI",
"membri": [
"emilio",
"michele",
"luisa",
"ivan",
"saverio"
]
}
When viewing the details of an object, I would like to use the default HyperlinkedModelSerializer
example:
{
"url": "http://127.0.0.1:8000/database/gruppi/2/",
"nome": "universitari",
"descrizione": "unitn!",
"creatore": "http://127.0.0.1:8000/database/utenti/3/",
"accesso": "CHI",
"membri": [
"http://127.0.0.1:8000/database/utenti/3/",
"http://127.0.0.1:8000/database/utenti/4/",
"http://127.0.0.1:8000/database/utenti/5/",
"http://127.0.0.1:8000/database/utenti/6/",
"http://127.0.0.1:8000/database/utenti/7/"
]
}
I managed to make all this work as I wish in the following way:
serializers.py
# serializer to use when showing a list
class ListaGruppi(serializers.HyperlinkedModelSerializer):
membri = serializers.RelatedField(many = True)
creatore = serializers.RelatedField(many = False)
class Meta:
model = models.Gruppi
# serializer to use when showing the details
class DettaglioGruppi(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.Gruppi
views.py
class DualSerializerViewSet(viewsets.ModelViewSet):
"""
ViewSet providing different serializers for list and detail views.
Use list_serializer and detail_serializer to provide them
"""
def list(self, *args, **kwargs):
self.serializer_class = self.list_serializer
return viewsets.ModelViewSet.list(self, *args, **kwargs)
def retrieve(self, *args, **kwargs):
self.serializer_class = self.detail_serializer
return viewsets.ModelViewSet.retrieve(self, *args, **kwargs)
class GruppiViewSet(DualSerializerViewSet):
model = models.Gruppi
list_serializer = serializers.ListaGruppi
detail_serializer = serializers.DettaglioGruppi
# etc.
Basically I detect when the user is requesting a list view or a detailed view and change serializer_class to suit my needs. I am not really satisfied with this code though, it looks like a dirty hack and, most importantly, what if two users request a list and a detail at the same moment?
Is there a better way to achieve this using ModelViewSets or do I have to fall back using GenericAPIView?
EDIT:
Here's how to do it using a custom base ModelViewSet:
class MultiSerializerViewSet(viewsets.ModelViewSet):
serializers = {
'default': None,
}
def get_serializer_class(self):
return self.serializers.get(self.action,
self.serializers['default'])
class GruppiViewSet(MultiSerializerViewSet):
model = models.Gruppi
serializers = {
'list': serializers.ListaGruppi,
'detail': serializers.DettaglioGruppi,
# etc.
}
Override your get_serializer_class method. This method is used in your model mixins to retrieve the proper Serializer class.
Note that there is also a get_serializer method which returns an instance of the correct Serializer
class DualSerializerViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == 'list':
return serializers.ListaGruppi
if self.action == 'retrieve':
return serializers.DettaglioGruppi
return serializers.Default # I dont' know what you want for create/destroy/update.
You may find this mixin useful, it overrides the get_serializer_class method and allows you to declare a dict that maps action and serializer class or fallback to the usual behavior.
class MultiSerializerViewSetMixin(object):
def get_serializer_class(self):
"""
Look for serializer class in self.serializer_action_classes, which
should be a dict mapping action name (key) to serializer class (value),
i.e.:
class MyViewSet(MultiSerializerViewSetMixin, ViewSet):
serializer_class = MyDefaultSerializer
serializer_action_classes = {
'list': MyListSerializer,
'my_action': MyActionSerializer,
}
#action
def my_action:
...
If there's no entry for that action then just fallback to the regular
get_serializer_class lookup: self.serializer_class, DefaultSerializer.
"""
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super(MultiSerializerViewSetMixin, self).get_serializer_class()
This answer is the same as the accepted answer but I prefer to do in this way.
Generic views
get_serializer_class(self):
Returns the class that should be used for the serializer. Defaults to returning the serializer_class attribute.
May be overridden to provide dynamic behavior, such as using different serializers for reading and write operations or providing different serializers to the different types of users.
the serializer_class attribute.
class DualSerializerViewSet(viewsets.ModelViewSet):
# mapping serializer into the action
serializer_classes = {
'list': serializers.ListaGruppi,
'retrieve': serializers.DettaglioGruppi,
# ... other actions
}
default_serializer_class = DefaultSerializer # Your default serializer
def get_serializer_class(self):
return self.serializer_classes.get(self.action, self.default_serializer_class)
Regarding providing different serializers, why is nobody going for the approach that checks the HTTP method? It's clearer IMO and requires no extra checks.
def get_serializer_class(self):
if self.request.method == 'POST':
return NewRackItemSerializer
return RackItemSerializer
Credits/source: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718
Based on #gonz and #user2734679 answers I've created this small python package that gives this functionality in form a child class of ModelViewset. Here is how it works.
from drf_custom_viewsets.viewsets.CustomSerializerViewSet
from myapp.serializers import DefaltSerializer, CustomSerializer1, CustomSerializer2
class MyViewSet(CustomSerializerViewSet):
serializer_class = DefaultSerializer
custom_serializer_classes = {
'create': CustomSerializer1,
'update': CustomSerializer2,
}
Just want to addon to existing solutions. If you want a different serializer for your viewset's extra actions (i.e. using #action decorator), you can add kwargs in the decorator like so:
#action(methods=['POST'], serializer_class=YourSpecialSerializer)
def your_extra_action(self, request):
serializer = self.get_serializer(data=request.data)
...
Although pre-defining multiple Serializers in or way or another does seem to be the most obviously documented way, FWIW there is an alternative approach that draws on other documented code and which enables passing arguments to the serializer as it is instantiated. I think it would probably tend to be more worthwhile if you needed to generate logic based on various factors, such as user admin levels, the action being called, perhaps even attributes of the instance.
The first piece of the puzzle is the documentation on dynamically modifying a serializer at the point of instantiation. That documentation doesn't explain how to call this code from a viewset or how to modify the readonly status of fields after they've been initated - but that's not very hard.
The second piece - the get_serializer method is also documented - (just a bit further down the page from get_serializer_class under 'other methods') so it should be safe to rely on (and the source is very simple, which hopefully means less chance of unintended side effects resulting from modification). Check the source under the GenericAPIView (the ModelViewSet - and all the other built in viewset classes it seems - inherit from the GenericAPIView which, defines get_serializer.
Putting the two together you could do something like this:
In a serializers file (for me base_serializers.py):
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Adding this next line to the documented example
read_only_fields = kwargs.pop('read_only_fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# another bit we're adding to documented example, to take care of readonly fields
if read_only_fields is not None:
for f in read_only_fields:
try:
self.fields[f].read_only = True
exceptKeyError:
#not in fields anyway
pass
Then in your viewset you might do something like this:
class MyViewSet(viewsets.ModelViewSet):
# ...permissions and all that stuff
def get_serializer(self, *args, **kwargs):
# the next line is taken from the source
kwargs['context'] = self.get_serializer_context()
# ... then whatever logic you want for this class e.g:
if self.action == "list":
rofs = ('field_a', 'field_b')
fs = ('field_a', 'field_c')
if self.action == “retrieve”:
rofs = ('field_a', 'field_c’, ‘field_d’)
fs = ('field_a', 'field_b’)
# add all your further elses, elifs, drawing on info re the actions,
# the user, the instance, anything passed to the method to define your read only fields and fields ...
# and finally instantiate the specific class you want (or you could just
# use get_serializer_class if you've defined it).
# Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
kwargs['read_only_fields'] = rofs
kwargs['fields'] = fs
return MyDynamicSerializer(*args, **kwargs)
And that should be it! Using MyViewSet should now instantiate your MyDynamicSerializer with the arguments you'd like - and assuming your serializer inherits from your DynamicFieldsModelSerializer, it should know just what to do.
Perhaps its worth mentioning that it can makes special sense if you want to adapt the serializer in some other ways …e.g. to do things like take in a read_only_exceptions list and use it to whitelist rather than blacklist fields (which I tend to do). I also find it useful to set the fields to an empty tuple if its not passed and then just remove the check for None ... and I set my fields definitions on my inheriting Serializers to 'all'. This means no fields that aren't passed when instantiating the serializer survive by accident and I also don't have to compare the serializer invocation with the inheriting serializer class definition to know what's been included...e.g within the init of the DynamicFieldsModelSerializer:
# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....
NB If I just wanted two or three classes that mapped to distinct actions and/or I didn't want any specially dynamic serializer behaviour, I might well use one of the approaches mentioned by others here, but I thought this worth presenting as an alternative, particularly given its other uses.
With all other solutions mentioned, I was unable to find how to instantiate the class using get_serializer_class function and unable to find custom validation function as well. For those who are still lost just like I was and want full implementation please check the answer below.
views.py
from rest_framework.response import Response
from project.models import Project
from project.serializers import ProjectCreateSerializer, ProjectIDGeneratorSerializer
class ProjectViewSet(viewsets.ModelViewSet):
action_serializers = {
'generate_id': ProjectIDGeneratorSerializer,
'create': ProjectCreateSerializer,
}
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
if hasattr(self, 'action_serializers'):
return self.action_serializers.get(self.action, self.serializer_class)
return super(ProjectViewSet, self).get_serializer_class()
# You can create custom function
def generate_id(self, request):
serializer = self.get_serializer_class()(data=request.GET)
serializer.context['user'] = request.user
serializer.is_valid(raise_exception=True)
return Response(serializer.validated_data, status=status.HTTP_200_OK)
def create(self, request, **kwargs):
serializer = self.get_serializer_class()(data=request.data)
serializer.context['user'] = request.user
serializer.is_valid(raise_exception=True)
return Response(serializer.validated_data, status=status.HTTP_200_OK)
serializers.py
import random
from rest_framework import serializers
from project.models import Project
class ProjectIDGeneratorSerializer(serializers.Serializer):
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass
projectName = serializers.CharField(write_only=True)
class Meta:
fields = ['projectName']
def validate(self, attrs):
project_name = attrs.get('projectName')
project_id = project_name.replace(' ', '-')
return {'projectID': project_id}
class ProjectCreateSerializer(serializers.Serializer):
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass
projectName = serializers.CharField(write_only=True)
projectID = serializers.CharField(write_only=True)
class Meta:
model = Project
fields = ['projectName', 'projectID']
def to_representation(self, instance: Project):
data = dict()
data['projectName'] = instance.name
data['projectID'] = instance.projectID
data['createdAt'] = instance.createdAt
data['updatedAt'] = instance.updatedAt
representation = {
'message': f'Project {instance.name} has been created.',
}
return representation
def validate(self, attrs):
print('attrs', dict(attrs))
project_name = attrs.get('projectName')
project_id = attrs.get('projectID')
if Project.objects.filter(projectID=project_id).first():
raise serializers.ValidationError(f'Project with ID {project_id} already exist')
project = Project.objects.create(projectID=project_id,
name=project_name)
print('user', self.context['user'])
project.user.add(self.context["user"])
project.save()
return self.to_representation(project)
urls.py
from django.urls import path
from .views import ProjectViewSet
urlpatterns = [
path('project/generateID', ProjectViewSet.as_view({'get': 'generate_id'})),
path('project/create', ProjectViewSet.as_view({'post': 'create'})),
]
models.py
# Create your models here.
from django.db import models
from authentication.models import User
class Project(models.Model):
id = models.AutoField(primary_key=True)
projectID = models.CharField(max_length=255, blank=False, db_index=True, null=False)
user = models.ManyToManyField(User)
name = models.CharField(max_length=255, blank=False)
createdAt = models.DateTimeField(auto_now_add=True)
updatedAt = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
You can map your all serializers with the action using a dictionary in class and then get them from "get_serializer_class" method. Here is what I am using to get different serializers in different cases.
class RushesViewSet(viewsets.ModelViewSet):
serializer_class = DetailedRushesSerializer
queryset = Rushes.objects.all().order_by('ingested_on')
permission_classes = (IsAuthenticated,)
filter_backends = (filters.SearchFilter,
django_filters.rest_framework.DjangoFilterBackend, filters.OrderingFilter)
pagination_class = ShortResultsSetPagination
search_fields = ('title', 'asset_version__title',
'asset_version__video__title')
filter_class = RushesFilter
action_serializer_classes = {
"create": RushesSerializer,
"update": RushesSerializer,
"retrieve": DetailedRushesSerializer,
"list": DetailedRushesSerializer,
"partial_update": RushesSerializer,
}
def get_serializer_context(self):
return {'request': self.request}
def get_serializer_class(self):
try:
return self.action_serializer_classes[self.action]
except (KeyError, AttributeError):
error_logger.error("---Exception occurred---")
return super(RushesViewSet, self).get_serializer_class()
Given the following models
class AnotherModel(models.Model):
n = models.IntegerField()
class MyModel(models.Model):
somefield = models.ForeignKey(AnotherModel)
and admin
class MyModelAdmin(admin.ModelAdmin):
list_filter = ('somefield',)
how can I filter the instances of AnotherModel to show only those with a given n value in my admin filter?
I need something like:
Filter
By somefield
all
[list of AnotherModel instances with given n]
See ModelAdmin.queryset and ModelAdmin.formfield_for_foreignkey. From the docs:
The queryset method on a ModelAdmin returns a QuerySet of all model instances that can be edited by the admin site. One use case for overriding this method is to show objects owned by the logged-in user:
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)
The formfield_for_foreignkey method on a ModelAdmin allows you to override the default formfield for a foreign keys field. For example, to return a subset of objects for this foreign key field based on the user:
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
This uses the HttpRequest instance to filter the Car foreign key field to only display the cars owned by the User instance.
[update]
Sorry, I failed to read the "filter" part. In Django >= 1.4 you can pass a subclass of django.contrib.admin.SimpleListFilter in the list_filter argument list, which you can use in order to override the lookups and queryset methods.
from datetime import date
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
class DecadeBornListFilter(admin.SimpleListFilter):
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = _('decade born')
# Parameter for the filter that will be used in the URL query.
parameter_name = 'decade'
def lookups(self, request, model_admin):
"""
Returns a list of tuples. The first element in each
tuple is the coded value for the option that will
appear in the URL query. The second element is the
human-readable name for the option that will appear
in the right sidebar.
"""
return (
('80s', _('in the eighties')),
('90s', _('in the nineties')),
)
def queryset(self, request, queryset):
"""
Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
# Compare the requested value (either '80s' or '90s')
# to decide how to filter the queryset.
if self.value() == '80s':
return queryset.filter(birthday__gte=date(1980, 1, 1),
birthday__lte=date(1989, 12, 31))
if self.value() == '90s':
return queryset.filter(birthday__gte=date(1990, 1, 1),
birthday__lte=date(1999, 12, 31))
class PersonAdmin(admin.ModelAdmin):
list_filter = (DecadeBornListFilter,)
Edit - this method has been pointed out to have issues, see below
You can do it like this:
Let's say you have a model called Animal, which has a ForeignKey field to a model called Species. In a particular admin list, you want to allow only certain species to be shown in the animals filter choices.
First, specify a custom ListFilter called SpeciesFilter in the Animal's ModelAdmin:
class AnimalAdmin(ModelAdmin):
list_filter = (('species', SpeciesFilter),)
Then define the SpeciesFilter:
from django.contrib.admin.filters import RelatedFieldListFilter
class SpeciesFilter(RelatedFieldListFilter):
def __init__(self, field, request, *args, **kwargs):
"""Get the species you want to limit it to.
This could be determined by the request,
But in this example we'll just specify an
arbitrary species"""
species = Species.objects.get(name='Tarantula')
#Limit the choices on the field
field.rel.limit_choices_to = {'species': species}
#Let the RelatedFieldListFilter do its magic
super(SpeciesFilter, self).__init__(field, request, *args, **kwargs)
That should do it.
I found another method similar to #seddonym, but doesn't mess with the caching. It is based on this Django code, but uses undocumented method field_choices, which can be subject to change in the future Django releases. The code for #seddonym's case would be:
from django.contrib.admin.filters import RelatedFieldListFilter
class SpeciesFilter(RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
return field.get_choices(include_blank=False, limit_choices_to={'name': 'Tarantula'})
Or in my case the working code is:
from django.contrib.admin.filters import RelatedFieldListFilter
class UnitFilter(RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
return field.get_choices(include_blank=False, limit_choices_to={'pk__in': request.user.administrated_units.all()})
I had to create my lookup fields from db table. I created custom filter class as below and displaying only related values to logged in user and filter accordingly:
class ShiftFilter_Org(admin.SimpleListFilter):
title = 'Organisation'
parameter_name = 'org'
def lookups(self, request, model_admin):
"""Get the organisations you want to limit"""
qs_org = Organisation.objects.filter(users=request.user)
list_org = []
for og in qs_org:
list_org.append(
(og.id, og.name)
)
return (
sorted(list_org, key=lambda tp:tp[1])
)
def queryset(self, request, queryset):
if self.value():
return queryset.filter(org=self.value())
For more visit Getting the most out of Django Admin filters
To use RelatedFieldListFilter like suggested in some answers, you need to pass a tuple to list_filter. Following the classes definition used in the original question, you would have:
class MyModelAdmin(admin.ModelAdmin):
class AnotherModelListFilter(admin.RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
ordering = self.field_admin_ordering(field, request, model_admin)
return field.get_choices(
include_blank=False,
ordering=ordering,
limit_choices_to=dict(n=<THE_VALUE_YOU_WANT>),
)
list_filter = (('somefield', AnotherModelListFilter),)