Django Rest Framework Read Only Model Serializer - python

I want to have a total read only ModelSerializer, i.e. just list/retrieve methods
what is the best way to do it?

You really want to do this at the view (or Viewset) level, which you can do with a ReadOnlyModelViewSet.
(You mentioned this in your comment but I'm leaving it as an answer for better visibility).
For example (from the documentation):
from rest_framework import viewsets
class AccountViewSet(viewsets.ReadOnlyModelViewSet):
"""
A simple ViewSet for viewing accounts.
"""
queryset = Account.objects.all()
serializer_class = AccountSerializer

If you do need a serializer to be read only, it's most concise and stable option to override the init method:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
setattr(self.Meta, 'read_only_fields', [*self.fields])
In comparison with the above:
#mgalgs's solution is best, if a view irrespective of the serializer used should be read only.
#thisisms solution is best if you only have a few and non-dynamic fields
#D W's approach will only work if there are no writable serializer fields that use the 'source' keyword.
EDIT better solution:
You can update the def get_fields method instead of the init method and create an abstract serializer:
class ReadOnlyModelSerializer(serializers.ModelSerializer):
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
for field in fields:
fields[field].read_only = True
return fields
To use it, just inherit from the abstract serializer:
def MySerializer(ReadOnlyModelSerializer):
class Meta:
model = MyModel
fields = '__all__'

If you're using a ModelSerializer you can achieve this with the following:
from rest_framework import serializers
class MyModelSerializer(serializers.ModelSerializer):
...
class Meta:
model = MyModel
fields = ("field_1", "field_2", etc)
read_only_fields = [f.name for f in MyModel._meta.get_fields()]
This solution uses the private _meta interface of the django model, but it's fairly stable and it's used fairly extensively by developers.
Here we're simply generating a list of all the fields in the relevant model and applying it to the read_only_fields option from django-rest-framework.
I prefer doing this at the serializer level as opposed to the view level as it doesn't tie you down to using Viewset's as suggested by mgalgs.
If you wanted to take this a step further, you could even wrap the functionality into a mixin which adds to the Meta options of your class. Something such as:
from rest_framework import serializers
from rest_framework.fields import Field
class ReadOnlyMixin(Field):
def __new__(cls, *args, **kwargs):
setattr(
cls.Meta,
"read_only_fields",
[f.name for f in cls.Meta.model._meta.get_fields()],
)
return super(ReadOnlyMixin, cls).__new__(cls, *args, **kwargs)
class MyModelSerializer(ReadOnlyMixin, serializers.ModelSerializer):
...
class Meta:
model = MyModel
fields = ("field_1", "field_2", etc)

The only thing you have to do is create a serializer like this.
serializers.py
class YourdataSerializer(serializers.ModelSerializer):
class Meta:
model = Yourdata
# some data
fields = ('id', 'city', 'pincode', 'created')
read_only_fields = ('id', 'city', 'pincode', 'created')
Views something like this
class YourdataList(APIView):
def get(self, request, format=None):
yourdata = YourdataList.objects.all()
serializer = YourdataSerializer(yourdata, many=True)
return Response(serializer.data)
detail view
class YourdataDetail(APIView):
def get_object(self, pk):
try:
return Yourdata.objects.get(pk=pk)
except Yourdata.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = YourdataSerializer(snippet)
return Response(serializer.data)
This will do it.

Ok dear Mohsen,To implement a read-only serializer using the BaseSerializer class, we just need to override the .to_representation()method. Let's take a look at an example using a simple Django model:
class HighScore(models.Model):
created = models.DateTimeField(auto_now_add=True)
player_name=models.CharField(max_length=10)
score = models.IntegerField()
It's simple to create a read-only serializer for converting HighScore instances into primitive data types.
class HighScoreSerializer(serializers.BaseSerializer):
def to_representation(self, obj):
return { 'score': obj.score, 'player_name': obj.player_name }

two solutions for this?
from rest_framework.utils import model_meta
class CustomSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
model = getattr(self.Meta, 'model')
declared_fields = copy.deepcopy(self._declared_fields)
info = model_meta.get_field_info(model)
field_names = self.get_field_names(declared_fields, info)
_, hidden_fields = self.get_uniqueness_extra_kwargs(
field_names, declared_fields, extra_kwargs)
fields_names.extend(hidden_fields.keys())
setattr(self.Meta, 'read_only_fields', list(field_names))
or
class CustomSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__()
setattr(self.Meta, 'read_only_fields', list(self.fields))

Related

Django pass field from serializer to model.save() that is not present in the model

I need to pass fields that are present in serializer, but not present in model to model save method (I have complicated saving logic and I want to make some decisions in object creation based on these fields). How can I do that? I tried to add
non_db_field = property to model, but I still get error MyModel() got an unexpected keyword argument 'negative_amount'
Let's say my model is
class MyModel(AbstractModel):
field1 = models.DateTimeField()
field2 = models.BigIntegerField()
My serializer is
class MyModelSerializer(AbstractSerializer):
field3 = serializers.BooleanField(required=False)
class Meta(AbstractSerializer.Meta):
model = MyModel
fields = '__all__'
And my viewset is
class MyModelViewSet(AbstractViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You should handle this behavior in serializer.save method, for example, you can pop it from validated_data like that:
def save(self, **kwargs):
self.validated_data.pop("negative_amount")
return super().save(**kwargs)
You can use fields=['field1', 'field2', 'field3'] in serializer instead of fields='__all__'.
I found a solution based partly on Sharpek's answer and partly based on this answer:
In serializer I override save method:
def save(self, **kwargs):
if 'field3' in self.validated_data:
kwargs['field3'] = self.validated_data.pop('field3')
return super().save(**kwargs)
In models I override init method and define field:
field3 = None
def __init__(self, *args, **kwargs):
if 'field3' in kwargs:
self.field3 = kwargs.pop('field3')
super(Reading, self).__init__(*args, **kwargs)

Django Admin - Filter ManyToManyField with through model

How can I filter a queryset inside the Admin page of an object that has a ManyToManyField relation with a manually defined through model?
Given models.py
class Foo(models.Model):
foo_field1 = models.CharField(max_length=50)
class Main(models.Model):
main_field1 = models.CharField(max_length=50)
m2mfield = models.ManyToManyField(Foo, through="FooBar")
class FooBar(models.Model):
main = models.ForeignKey(Main, on_delete=models.CASCADE)
foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
new_field = models.CharField(max_length=50)
Inside admin.py
class M2MInlineAdmin(admin.TabularInline):
model = Main.m2mfield.through
extra = 1
class MainAdmin(admin.ModelAdmin):
inlines = [M2MInlineAdmin,]
...
def formfield_for_manytomany(self, db_field, request, **kwargs):
print('called formfield_for_manytomany')
return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_field_queryset(self, db, db_field, request):
print('called get_field_queryset')
return super().get_field_queryset(db, db_field, request)
I try to access both of these methods, but none of them are called if I specify a through table. However, they do get called if the ManyToMany relation is simply defined as like this:
class Main(models.Model):
main_field1 = models.CharField(max_length=50)
m2mfield = models.ManyToManyField(Foo)
Is there a method to filter the queryset when a through table is specified (while being able to access the request context)?
EDIT:
The methods are indeed called when the ManyToManyField has a through model specified, only if there are no fieldsets specified inside the modelAdmin class.
How to access these methods when fieldsets are defined?
formfield_for_manytomany method seems to be called only when default form is used. When fieldsets is defined, it is using a different form which is why above method is not getting called.
Since you are using tabular admin for many to many field, you can override get_queryset to filter with field.
class M2MInlineAdmin(admin.TabularInline):
model = Main.fruits.through
extra = 1
def get_queryset(self, request):
qs = super(M2MInlineAdmin, self).get_queryset(request)
qs = qs.filter(some_arg=some_value)
return qs
Alternatively, you can write a custom model form and use it in admin instead of default form.
class MainAdminForm(forms.ModelForm):
class Meta:
model = Main
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# custom setup
class MainAdmin(admin.ModelAdmin):
form = MainAdminForm
You can use the formfield_for_foreignkey() method on the inline class.
class M2MInlineAdmin(admin.TabularInline):
model = Main.m2mfield.through
extra = 1
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

django rest framework hide specific fields in list display?

I want to hide specific fields of a model on the list display at persons/ and show all the fields on the detail display persons/jane
I am relatively new to the rest framework and the documentation feels like so hard to grasp.
Here's what I am trying to accomplish.
I have a simple Person model,
# model
class Person(models.Model):
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
nickname = models.CharField(max_length=20)
slug = models.SlugField()
address = models.TextField(max_length=300, blank=True)
and the serializer class
# serializers
class PersonListSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('nickname', 'slug')
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
and the viewsets.
# view sets (api.py)
class PersonListViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonListSerializer
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
at the url persons I want to dispaly list of persons, just with fields nickname and slug and at the url persons/[slug] I want to display all the fields of the model.
my router configurations,
router = routers.DefaultRouter()
router.register(r'persons', api.PersonListViewSet)
router.register(r'persons/{slug}', api.PersonViewSet)
I guess the second configuration is wrong, How can I achieve what I am trying to do?
update:
the output to persons/slug is {"detail":"Not found."} but it works for person/pk
Thank you
For anyone else stumbling across this, I found overriding get_serializer_class on the viewset and defining a serializer per action was the DRY-est option (keeping a single viewset but allowing for dynamic serializer choice):
class MyViewset(viewsets.ModelViewSet):
serializer_class = serializers.ListSerializer
permission_classes = [permissions.IsAdminUser]
renderer_classes = (renderers.AdminRenderer,)
queryset = models.MyModel.objects.all().order_by('-updated')
def __init__(self, *args, **kwargs):
super(MyViewset, self).__init__(*args, **kwargs)
self.serializer_action_classes = {
'list':serializers.AdminListSerializer,
'create':serializers.AdminCreateSerializer,
'retrieve':serializers.AdminRetrieveSerializer,
'update':serializers.AdminUpdateSerializer,
'partial_update':serializers.AdminUpdateSerializer,
'destroy':serializers.AdminRetrieveSerializer,
}
def get_serializer_class(self, *args, **kwargs):
"""Instantiate the list of serializers per action from class attribute (must be defined)."""
kwargs['partial'] = True
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super(MyViewset, self).get_serializer_class()
Hope this helps someone else.
You can override the 'get_fields' method your serializer class and to add something like that:
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
request = self.context.get('request')
if request is not None and not request.parser_context.get('kwargs'):
fields.pop('your_field', None)
return fields
In this case when you get detail-view there is 'kwargs': {'pk': 404} and when you get list-view there is 'kwargs': {}
I wrote an extension called drf-action-serializer (pypi) that adds a serializer called ModelActionSerializer that allows you to define fields/exclude/extra_kwargs on a per-action basis (while still having the normal fields/exclude/extra_kwargs to fall back on).
The implementation is nice because you don't have to override your ViewSet get_serializer method because you're only using a single serializer. The relevant change is that in the get_fields and get_extra_kwargs methods of the serializer, it inspects the view action and if that action is present in the Meta.action_fields dictionary, then it uses that configuration rather than the Meta.fields property.
In your example, you would do this:
from action_serializer import ModelActionSerializer
class PersonSerializer(ModelActionSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
action_fields = {
'list': {'fields': ('nickname', 'slug')}
}
Your ViewSet would look something like:
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
And your router would look normal, too:
router = routers.DefaultRouter()
router.register(r'persons', api.PersonViewSet)
Implementation
If you're curious how I implemented this:
I added a helper method called get_action_config which gets the current view action and returns that entry in the action_fields dict:
def get_action_config(self):
"""
Return the configuration in the `Meta.action_fields` dictionary for this
view's action.
"""
view = getattr(self, 'context', {}).get('view', None)
action = getattr(view, 'action', None)
action_fields = getattr(self.Meta, 'action_fields', {})
I changed get_field_names of ModelSerializer:
From:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
To:
action_config = self.get_action_config()
if action_config:
fields = action_config.get('fields', None)
exclude = action_config.get('exclude', None)
else:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
Finally, I changed the get_extra_kwargs method:
From:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
To:
action_config = self.get_action_config()
if action_config:
extra_kwargs = copy.deepcopy(action_config.get('extra_kwargs', {}))
else:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
If you want to change what fields are displayed in the List vs Detail view, the only thing you can do is change the Serializer used. There's no field that I know of that lets you specify which fields of the Serializer gets used.
The field selection on you serializers should be working, but I don't know what might be happening exactly. I have two solutions you can try:
1 Try to change the way you declare you serializer object
#If you aren't using Response:
from rest_framework.response import Response
class PersonListViewSet(viewsets.ModelViewSet):
def get(self, request):
queryset = Person.objects.all()
serializer_class = PersonListSerializer(queryset, many=True) #It may change the things
return Response(serializer_class.data)
class PersonViewSet(viewsets.ModelViewSet):
def get(self, request, pk): #specify the method is cool
queryset = Person.objects.all()
serializer_class = PersonSerializer(queryset, many=True) #Here as well
#return Response(serializer_class.data)
2 The second way around would change your serializers
This is not the most normal way, since the field selector should be working but you can try:
class PersonListSerializer(serializers.ModelSerializer):
nickname = serializers.SerializerMethodField() #Will get the attribute my the var name
slug = serializers.SerializerMethodField()
class Meta:
model = Person
def get_nickname(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.nickname
def get_slug(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.slug
I hope it helps. Try to see the APIview class for building your view too.
Somehow close:
If you just want to skip fields in the serilaizer
class UserSerializer(serializers.ModelSerializer):
user_messages = serializers.SerializerMethodField()
def get_user_messages(self, obj):
if self.context.get('request').user != obj:
# do somthing here check any value from the request:
# skip others msg
return
# continue with your code
return SystemMessageController.objects.filter(user=obj, read=False)
I rewrite ModelViewSet list function to modify serializer_class.Meta.fields attribute, code like this:
class ArticleBaseViewSet(BaseViewSet):
def list(self, request, *args, **kwargs):
exclude = ["content"]
self.serializer_class.Meta.fields = [f.name for f in self.serializer_class.Meta.model._meta.fields if f.name not in exclude]
queryset = self.filter_queryset(self.get_queryset()).filter(is_show=True, is_check=True)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class BannerArticleViewSet(ArticleBaseViewSet):
queryset = BannerArticle.objects.filter(is_show=True, is_check=True).all()
serializer_class = BannerArticleSerializer
permission_classes = (permissions.AllowAny,)
But it looks not stable, so i will not use it, just share to figure out the best way
My solution.
class BaseSerializerMixin(_ModelSerializer):
class Meta:
exclude: tuple[str, ...] = ()
exclude_in_list: tuple[str, ...] = ()
model: Type[_models.Model]
def get_action(self) -> Optional[str]:
if 'request' not in self.context:
return None
return self.context['request'].parser_context['view'].action
def get_fields(self):
fields = super().get_fields()
if self.get_action() == 'list':
[fields.pop(i) for i in list(fields) if i in self.Meta.exclude_in_list]
return fields
I think it should be like this:
router.register(r'persons/?P<slug>/', api.PersonViewSet)
and you should include a line like this:
lookup_field='slug'
in your serializer class. Like this:
class PersonSerializer(serializers.ModelSerializer):
lookup_field='slug'
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')

Django admin inline: select_related

Using Django 1.8 on Python 3.4.1 with models:
class Product(models.Model):
name = models.CharField(max_length=255)
# some more fields here
def __str__(self):
return self.name
class PricedProduct(models.Model):
product = models.ForeignKey(Product, related_name='prices')
# some more fields here
def __str__(self):
return str(self.product)
class Coming(models.Model):
# some unimportant fields here
class ComingProducts(models.Model):
coming = models.ForeignKey(Coming)
priced_product = models.ForeignKey(PricedProduct)
# more unimportant fields
and the following admin.py:
class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
model = ComingProducts
class ComingAdmin(admin.ModelAdmin):
inlines = [ComingProductsInline]
Of course, i have a problem with multiply queries to database: i have a query for each item in list and a query for each line. So, having 100 items i get 100 ^ 2 queries.
I've solved the problem with queries for each line with Caching queryset choices for ModelChoiceField or ModelMultipleChoiceField in a Django form
But i still having problem with str method. I've tried the following:
1) adding prefetch_related to ComingAdmin:
def get_queryset(self, request):
return super(ComingAdmin, self).get_queryset(request). \
prefetch_related('products__product')
2) adding select_related to ComingProductInline:
def get_queryset(self, request):
return super(ComingProductsInline, self).get_queryset(request). \
select_related('priced_product__product')
3) Defining custom form for inline and adding select_related to field queryset:
class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
model = ComingProducts
form = ComingProductsAdminForm
class ComingProductsAdminForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ComingProductsAdminForm, self).__init__(args, kwargs)
self.fields['priced_product'].queryset = PricedProduct.objects.all(). \
select_related('product')
class Meta:
model = ComingProducts
fields = '__all__'
4) Defining a custom formset:
class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
model = ComingProducts
formset = MyInlineFormset
class MyInlineFormset(BaseInlineFormSet):
def __init__(self, data=None, files=None, instance=None,
save_as_new=False, prefix=None, queryset=None, **kwargs):
super(MyInlineFormset, self).__init__(data, files, instance,
save_as_new, prefix, queryset, **kwargs)
self.queryset = ComingProducts.objects.all(). \
prefetch_related('priced_product__product')
5) Different combinations for previous 4 methods
And nothing helps: each call of str for PricedProduct makes Django to perform a query for Product table. All of these methods were mentioned on stackoverflow, but they treated ModelAdmin, and do not help with Inline. What do i miss?
The formset solution does work for me, but with a slightly different approach:
class MyInlineFormset(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInlineFormset, self).__init__(*args, **kwargs)
self.queryset = self.queryset.prefetch_related('priced_product__product')
The BaseInlineFormSet class filters the queryset for you, and you need to take that filtered queryset and add the prefetch. With your formset implementation (the all() queryset) you get unrelated ComingProduct objects and it probably takes much too long to render. When it's the filtered queryset it renders very quickly.
You will find this approach very useful:
project/admin.py
from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.db.models.constants import LOOKUP_SEP
class AdminBaseWithSelectRelated(BaseModelAdmin):
"""
Admin Base using list_select_related for get_queryset related fields
"""
list_select_related = []
def get_queryset(self, request):
return super(AdminBaseWithSelectRelated, self).get_queryset(request).select_related(*self.list_select_related)
def form_apply_select_related(self, form):
for related_field in self.list_select_related:
splitted = related_field.split(LOOKUP_SEP)
if len(splitted) > 1:
field = splitted[0]
related = LOOKUP_SEP.join(splitted[1:])
form.base_fields[field].queryset = form.base_fields[field].queryset.select_related(related)
class AdminInlineWithSelectRelated(admin.TabularInline, AdminBaseWithSelectRelated):
"""
Admin Inline using list_select_related for get_queryset and get_formset related fields
"""
def get_formset(self, request, obj=None, **kwargs):
formset = super(AdminInlineWithSelectRelated, self).get_formset(request, obj, **kwargs)
self.form_apply_select_related(formset.form)
return formset
class AdminWithSelectRelated(admin.ModelAdmin, AdminBaseWithSelectRelated):
"""
Admin using list_select_related for get_queryset and get_form related fields
"""
def get_form(self, request, obj=None, **kwargs):
form = super(AdminWithSelectRelated, self).get_form(request, obj, **kwargs)
self.form_apply_select_related(form)
return form
class FilterWithSelectRelated(admin.RelatedFieldListFilter):
list_select_related = []
def field_choices(self, field, request, model_admin):
return [
(getattr(x, field.remote_field.get_related_field().attname), str(x))
for x in self.get_queryset(field)
]
def get_queryset(self, field):
return field.remote_field.model._default_manager.select_related(*self.list_select_related)
app/admin.py
from django.contrib import admin
from project.admin import AdminWithSelectRelated, AdminInlineWithSelectRelated, FilterWithSelectRelated
from .models import FormaPago, Comprobante, ItemServicio, ItemBazar
class ItemServicioInlineAdmin(AdminInlineWithSelectRelated):
model = ItemServicio
list_select_related = (
'alumno_servicio__alumno__estudiante__profile',
'alumno_servicio__servicio__grado',
'comprobante__forma_pago',
)
class ItemBazarInlineAdmin(AdminInlineWithSelectRelated):
model = ItemBazar
list_select_related = (
'alumno_item__alumno__estudiante__profile',
'alumno_item__item__anio_lectivo',
'comprobante__forma_pago',
)
class ComprobanteAdmin(AdminWithSelectRelated):
list_display = ('__str__', 'total', 'estado', 'fecha_generado', 'forma_pago', 'tipo', )
list_filter = ('estado', 'forma_pago', )
list_select_related = ('forma_pago', )
inlines = (ItemServicioInlineAdmin, ItemBazarInlineAdmin, )
class AlumnoFilter(FilterWithSelectRelated):
list_select_related = ('estudiante__profile', )
class ItemServicioAdmin(AdminWithSelectRelated):
list_display = ('nombre', 'alumno', 'monto_pagado', 'comprobante', )
list_filter = (
'alumno_servicio__alumno__seccion__grado',
('alumno_servicio__alumno', AlumnoFilter),
)
list_select_related = (
'comprobante__forma_pago',
'alumno_servicio__alumno__estudiante__profile',
'alumno_servicio__alumno__seccion__grado',
'alumno_servicio__servicio__grado',
)
class ItemBazarAdmin(AdminWithSelectRelated):
list_display = ('nombre', 'alumno', 'monto_pagado', 'comprobante', )
list_filter = (
'alumno_item__alumno__seccion__grado',
('alumno_item__alumno', AlumnoFilter),
)
list_select_related = (
'comprobante__forma_pago',
'alumno_item__alumno__estudiante__profile',
'alumno_item__alumno__seccion__grado',
'alumno_item__item__anio_lectivo',
)
admin.site.register(FormaPago)
admin.site.register(Comprobante, ComprobanteAdmin)
admin.site.register(ItemServicio, ItemServicioAdmin)
admin.site.register(ItemBazar, ItemBazarAdmin)
All I have to do is define the select_related fields, and the Custom AdminWithSelectRelated, AdminInlineWithSelectRelated, and FilterWithSelectRelated make use of them for Changelists, Changeforms, and even inline Formsets.
Works like a charm.
Inspired from #helpse answer you can also do the following if you just want to override the queryset for a single admin inline:
class ComingProductsInline(admin.TabularInline):
model = ComingProducts
def get_formset(self, request, obj=None, **kwargs):
formset = super(ComingProductsInline, self).get_formset(request, obj, **kwargs)
queryset = formset.form.base_fields["priced_product"].queryset
queryset = queryset.select_related("product")
formset.form.base_fields["priced_product"].queryset = queryset
return formset
It might be sufficient for most of the cases.
I'm working currently on a similar problem. What I have found is documented at this thread: Translatable Manytomany fields in admin generate many queries
One important observation I did is that my solution works only for Django 1.7x and not for 1.8. Exactly same code, with d1.7 I have order of 10^1 queries, and with new installation of d1.8 I have 10^4.

how to override django admin form Foreignkey based on request.user

admin.py
class PromoAdmin(admin.modelAdmin)
list_display = ( 'name', 'id', 'category', 'promo_type', 'store', 'brand', 'date_start' )
form = SampleForm
forms.py
class SampleForm(forms.ModelForm):
class Meta:
model = Promo
def __init__(self, request *args, **kwargs):
super(PromoAdminForm, self).__init__(*args, **kwargs)
self.fields["store"].queryset = Store.objects.filter(onwer=request.user)
got an error on request
Django Version: 1.3.1
Exception Type: TypeError
Exception Value:
init() takes at least 2 arguments (1 given)
You cannot initiate the store field with request.user in the field declaration. You can try the following:
class MyAwesomeForm(forms.ModelForm):
store = forms.ModelChoiceField(Store.objects)
class Meta:
model = Promo
def __init__(self, user, *args, **kwargs):
super(MyAwesomeForm, self).__init__(*args, **kwargs)
self.fields['store'].queryset = Store.objects.filter(owner=user)
While instantiating the form you can pass the request.user object.
myform = MyAwesomeForm(request.user)
If you want to achieve this in the admin you might try this
For providing only the objects related to the logged-in user in the admin provides the possibility to overwrite ModelAdmin.queryset function:
class MyModelAdmin(admin.ModelAdmin):
form = MyAwesomeAdminForm()
def queryset(self, request):
qs = super(MyModelAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(store__owner=request.user)
class MyAwesomeAdminForm(forms.ModelForm):
class Meta:
model = Promo
Note that store__owner only works if you have a foreign key field stored in your promo model as such:
class Promo(models.Model):
store = models.ForeignKey(Store)
class Store(models.Model):
owner = models.ForeignKey(User)
I assume it should also be possible to somehow pass the request to the init method of the form. But did not find a suitable approach to do it.

Categories

Resources