Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django rest framework hide specific fields in list display?

Tags:

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

like image 236
Deena Avatar asked May 05 '16 23:05

Deena


2 Answers

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.

like image 194
Liz Avatar answered Sep 30 '22 10:09

Liz


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', {}))
like image 25
Greg Schmit Avatar answered Sep 30 '22 09:09

Greg Schmit