user5539517
user5539517

Reputation:

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

Upvotes: 24

Views: 29240

Answers (9)

wolfpan
wolfpan

Reputation: 81

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

Upvotes: 0

qin fei
qin fei

Reputation: 1

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

Upvotes: 0

Ohad the Lad
Ohad the Lad

Reputation: 1929

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)

Upvotes: 0

Greg Schmit
Greg Schmit

Reputation: 4574

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:

  1. 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', {})
  1. 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)
  1. 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', {}))

Upvotes: 10

Bud
Bud

Reputation: 1491

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.

Upvotes: 23

TheSergeX
TheSergeX

Reputation: 157

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': {}

Upvotes: 10

Joabe da Luz
Joabe da Luz

Reputation: 1020

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.

Upvotes: 1

Karina Klinkevičiūtė
Karina Klinkevičiūtė

Reputation: 1538

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')

Upvotes: -1

TankorSmash
TankorSmash

Reputation: 12747

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.

Upvotes: 2

Related Questions