3

I'm using Django 2.0 and Django REST Framework.

I have two models in contacts app

contacts/models.py

class Contact(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100, blank=True, null=True, default='')


class ContactPhoneNumber(models.Model):
    contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
    phone = models.CharField(max_length=100)
    primary = models.BooleanField(default=False)

    def __str__(self):
        return self.phone

contacts/serializers.py

class ContactPhoneNumberSerializer(serializers.ModelSerializer):
    class Meta:
        model = ContactPhoneNumber
        fields = ('id', 'phone', 'primary', 'created', 'modified')

and contacts/views.py

class ContactPhoneNumberViewSet(viewsets.ModelViewSet):
    serializer_class = ContactPhoneNumberSerializer

    def get_queryset(self):
        return ContactPhoneNumber.objects.filter(
            contact__user=self.request.user
        )

urls.py

router.register(r'contact-phone', ContactPhoneNumberViewSet, 'contact_phone_numbers')

What I want is following endpoints

  • GET: /contact-phone/{contact_id}/ list phones numbers of particular contact
  • POST:/contact-phone/{contact_id}/ add phone numbers to particular contact
  • PUT: /contact-phone/{contact_phone_number_id}/ update particular phone number
  • DELETE: /contact-phone/{contact_phone_number_id}/ delete particular phone number

PUT and Delete can be achieved as default action of ModelViewSet but how to make get_queryset to accept contact_id as required parameter?

Edit 2

I followed doc Binding ViewSets to URLs explicitly

update app/urls.py

router = routers.DefaultRouter()
router.register(r'contacts', ContactViewSet, 'contacts')
contact_phone_number_view_set = ContactPhoneNumberViewSet.as_view({
    'get': 'list/<contact_pk>/',
    'post': 'create/<contact_pk>/',
    'put': 'update',
    'delete': 'destroy'
})
router.register(r'contact-phone-number', contact_phone_number_view_set, 'contact_phone_numbers')

urlpatterns = [
    path('api/', include(router.urls)),
    url(r'^admin/', admin.site.urls),
]

But it is giving error

AttributeError: 'function' object has no attribute 'get_extra_actions'

1 Answer 1

2

You can add extra actions to the viewset using @action decorator:

class ContactPhoneNumberViewSet(viewsets.ModelViewSet):
    serializer_class = ContactPhoneNumberSerializer

    def get_queryset(self):
        return ContactPhoneNumber.objects.filter(
            contact__user=self.request.user
        )

    @action(methods=['post'], detail=False)
    def add_to_contact(self, request, contact_id=None):
        contact = Contact.objects.get(id=contact_id)
        serializer = ContactPhoneNumberSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(contact=contact)
            return Response(serializer.data)
        else:
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

    @action(methods=['get'], detail=False)
    def set_password(self, request, contact_id=None):
        contact = Contact.objects.get(id=contact_id)
        serializer = PasswordSerializer(contact.contactphonenumber_set.all(), many=True)
        return Response(serializer.data)

UPD

Since you don't need additional actions, you can override retrieve and create defaults methods:

class ContactPhoneNumberViewSet(viewsets.ModelViewSet):
        serializer_class = ContactPhoneNumberSerializer

        def get_queryset(self):
            return ContactPhoneNumber.objects.filter(
                contact__user=self.request.user
            )

        def create(self, request, pk=None):
            contact = Contact.objects.get(id=contact_id)
            serializer = ContactPhoneNumberSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save(contact=contact)
                return Response(serializer.data)
            else:
                return Response(serializer.errors,
                                status=status.HTTP_400_BAD_REQUEST)

        def retrieve(self, request, pk=None):
            contact = Contact.objects.get(pk=pk)
            serializer = PasswordSerializer(contact.contactphonenumber_set.all(), many=True)
            return Response(serializer.data)

To change standard create url use explicitly url binding:

contact_list = ContactPhoneNumberViewSet.as_view({
    'get': 'list',
    'post': 'create',
    'put': 'update',
    'delete': 'destroy'
})

urlpatterns = [
    path('api//contact-phone/<int:pk>/', contact_list, name='contact-list'),
    url(r'^admin/', admin.site.urls),
]
Sign up to request clarification or add additional context in comments.

14 Comments

then I would have to disable get_queryset and create methods, because I do not want to list all phone numbers at once. Also I want clean URL without adding extra action. can't I pass required additional parameter to get_querset?
@AnujTBE Not sure, but I think it's impossible. The problem is you need somehow to deside inside get_queryset method if selected kwarg was contact_phone_number_id or contact_id. But without extra actions you cannot distinguish them.
distinguishing is based on request methods. with request methods GET and POST contact_id will be passed and with rest contact_phone_number_id will be passed.
Thanks for your help. used your first answer by defining extra action.
@AnujTBE Don't have chance to test it. Try to use action's url_path argument like this: @action(methods=['post'], detail=False, url_path='delete_phone/<phone_pk>/').
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.