0

I am trying to add reverse() to my automated test api calls instead of hard-coding the urls. I have a router in my urls.py that is directed to a ModelViewSet and ModelSerializer.

Here is my urls.py

router = DefaultRouter()
router.register('votes', VoteViewSet, basename='votes')

app_name = 'api'
urlpatterns = [
    path('', include(router.urls)),
]

Here is my views.py

class VoteViewSet(viewsets.ModelViewSet):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

Here is my serializers.py

class VoteSerializer(serializers.ModelSerializer):
    entry = serializers.SlugField()

    def create(self, validated_data):
        user = self.context['request'].user
        entry_slug = validated_data['entry']
        entry_queryset = Entry.objects.filter(slug=entry_slug)
        entry = entry_queryset[0]
        response = {
            'user': user,
            'entry': entry_queryset[0]
        }

        if entry_queryset.filter(author=user):
            raise PermissionDenied("Cannot upvote your own entry")
        elif Vote.objects.filter(entry=entry, user=user):
            raise PermissionDenied("Already voted")

        return super().create(response)

    class Meta:
        model = Vote
        fields = '__all__'
        read_only_fields = ['user']

The following test.py fails

class EntryViewSetTestCase(APITestCase):

    @classmethod
    def setUpTestData(cls):
        user = get_user_model()
        cls.user_data = {
            'email': 'user@localhost.com',
            'password': 'hgEYqf$nQ8x5Pms'
        }
        cls.user = user.objects.create_user(
            username='user', 
            email='user@localhost.com', 
            password='hgEYqf$nQ8x5Pms'
        )        
        cls.other_user_data = {
            'email': 'otheruser@localhost.com',
            'password': 'hgEYqf$nQ8x5Pms'
        }
        cls.other_user = user.objects.create_user(
            username='otheruser', 
            email='otheruser@localhost.com', 
            password='hgEYqf$nQ8x5Pms'
        )
        cls.entry_data = {
            'title': 'title',
            'description': 'description',
        }
        cls.entry = Entry(
            title='title',
            description='description',
            author=cls.user
        )
        cls.entry.save()    

    def test_vote_entry_success(self):
        token = self.client.post(reverse('token_obtain_pair'), self.other_user_data)
        self.client.credentials(
            HTTP_AUTHORIZATION='Bearer ' + token.data['access'])
        entry = {'entry': self.entry.slug}
        response = self.client.post(reverse('api:votes-detail', kwargs=entry))
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

This returns the following error

django.urls.exceptions.NoReverseMatch: Reverse for 'votes-detail' with keyword arguments '{'entry': 'd901e2-title'}' not found. 2 pattern(s) tried: ['api/votes/(?P<pk>[^/.]+)\\.(?P<format>[a-z0-9]+)/?$', 'api/votes/(?P<pk>[^/.]+)/$']

When I use the hard-coded url, the test passes

response = self.client.post('/api/votes/', entry)
timotaoh
  • 312
  • 5
  • 11

2 Answers2

1

Did you try reverse('api:votes-detail', kwargs=entry)? The api: is only needed if you registered your router using namespace= argument.

Regardless, I highly recommend using django-extension's show_urls. Take a look at this SO thread.

Tommy
  • 346
  • 1
  • 5
1

At least with automated tests, it looks like using *-list instead of *-detail was the solution. Thanks Tommy for recommending django-extensions to sort this out.

response = self.client.post(reverse('api:votes-list'), entry)
timotaoh
  • 312
  • 5
  • 11