recipes_api/README3.md
2023-10-10 14:12:25 -03:00

14 KiB

Tags API

  • Añadir capadidad de agregar tags a las recetas
  • Creación del modelo para los tags
  • Añadir endpoints para la API tag
  • Actualizar el endpoint recipe, para agregar y listar tags

Tag endpoint

  • /api/recipe/tags
    Método HTTP Función
    POST Crear tag
    PUT/PATCH Actualizar tag
    DELETE Borrar tag
    GET Listar tags disponibles

Test Tag Model

core/tests/test_models.py

...

def create_user(email='user@example.com', password='testpass123'):
    """Create and return a new user."""
    return get_user_model().objects.create_user(email, password)
    ...

    def test_create_tag(self):
        """Test creating a tag is sucessfull."""
        user = create_user()
        tag = models.Tag.objects.create(user=user, name='Tag1')

        self.assertEqual(str(tag), tag.name)

Tag Model

  • name Nombre del tag a crear
  • user Usuario creador/dueño del tag

core/models.py

...

class Recipe(models.Model):
    """Recipe object."""
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    time_minutes = models.IntegerField()
    price = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
    link = models.CharField(max_length=255, blank=True)
    tags = models.ManyToManyField('Tag')  # <--- Nueva linea

    def __str__(self):
        return self.title


class Tag(models.Model):
    """Tag for filtering recipes."""
    name = models.CharField(max_length=255)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

    def __str__(self):
        return self.name

Crear migraciones

docker compose run --rm app sh -c python manage.py makemigrations

[+] Creating 1/0
  Container recipes_api_django-db-1  Running  0.0s
Migrations for 'core':
  core/migrations/0003_tag_recipe_tags.py
    - Create model Tag
    - Add field tags to recipe

Registrar aplicación tag en el administador

app/core/admin.py

admin.site.register(models.Tag) 

Test tags API

recipe/tests/test_tags_api.py

...

TAGS_URL = reverse('recipe:tag-list')

def create_user(email='user@example.com', password='testpass123'):
    """Create and return a user."""
    return get_user_model().objects.create_user(email=email, password=password)

class PublicTagsApiTests(TestCase):
    """Test unauthenticated API requests."""

    def setUp(self):
        self.client = APIClient()

    def test_auth_required(self):
        """Test auth is required for retriveing tags."""
        res = self.client.get(TAGS_URL)

        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)


class PrivateTagsApiTests(TestCase):
    """Test authenticated API requests."""

    def setUp(self):
        self.user = create_user()
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_retrieve_tags(self):
        """Test retrieving a list of tags."""
        Tag.objects.create(user=self.user, name='Vegan')
        Tag.objects.create(user=self.user, name='Omnivore')

        res = self.client.get(TAGS_URL)

        tags = Tag.objects.all().order_by('-name')
        serializer = TagSerializer(tags, many=True)
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, serializer.data)

    def test_tags_limited_to_user(self):
        """Test list of tags is limited to authenticated user."""
        user2 = create_user(email='user2@example.com')
        Tag.objects.create(user=user2, name='Fruity')
        tag = Tag.objects.create(user=self.user, name='Carnivore')

        res = self.client.get(TAGS_URL)

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(len(res.data), 1)
        self.assertEqual(res.data[0]['name'], tag.name)
        self.assertEqual(res.data[0]['id'], tag.id)

Serializador Tag API

recipe/serializers.py

...

class TagSerializer(serializers.ModelSerializer):
    """Serializer for tags."""

    class Meta:
        model = Tag
        fields = ['id', 'name']
        read_only_fields = ['id']

Views tags APIs

recipe/views.py

...

class TagViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    """Manage tags in the database."""
    serializer_class = serializers.TagSerializer
    queryset = Tag.objects.all()
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        """Filter queryset to authenticated user."""
        return self.queryset.filter(user=self.request.user).order_by('-id')

URLS tags APIs

recipe/urls.py

...
router.register('recipes', views.RecipeViewSet)
router.register('tags', views.TagViewSet)
...

Actualizar tags

Test Update Tags

test_tags_api.py

...

def detail_url(tag_id):
    """Create and return a tag detail url."""
    return reverse('recipe:tag-detail', args=[tag_id])
    ...

class PrivateTagsApiTests(TestCase):
    ...
    
    def test_update_tag(self):
        """Test updating a tag."""
        tag = Tag.objects.create(user=self.user, name='Despues de la cena')

        payload = {'name': 'Desierto'}
        url = detail_url(tag.id)
        res = self.client.patch(url, payload)

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        tag.refresh_from_db()
        self.assertEqual(tag.name, payload['name'])

Implementación update tag

recipe/views.py

-class TagViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
+class TagViewSet(mixins.UpdateModelMixin,
+                 mixins.ListModelMixin,
+                 viewsets.GenericViewSet):
     """Manage tags in the database."""
     serializer_class = serializers.TagSerializer
     ...

Borrando Tags

Test Deleting tags

test_tags_api.py

def test_delete_tag(self):                                                                        
    """Test deleting a tag."""
    tag = Tag.objects.create(user=self.user, name='Desayuno')
                              
    url = detail_url(tag.id)  
    res = self.client.delete(url)  
                              
    self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
    tags = Tag.objects.filter(user=self.user)  
    self.assertFalse(tags.exists())  

Implementación borrar tag

recipe/views.py

-class TagViewSet(mixins.UpdateModelMixin,
+class TagViewSet(mixins.DestroyModelMixin,
+                 mixins.UpdateModelMixin,
                  mixins.ListModelMixin,
                  viewsets.GenericViewSet):
     """Manage tags in the database."""
     serializer_class = serializers.TagSerializer
     ...

Serializadores anidados

  • Es un serializador dentro de un serializador
  • Usado para campos que son objetos

Ejemplo JSON resonse

{
    "title": "Un titulo",
    "user": "Usuario",
    "tags": [
      {"name": "Tag 1"},
      {"name": "Tag 2"}
    ]
}
class TagSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=100)

class RecipeSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=100)
    user = serializers.CharField(max_length=100)
    tags = TagSerializer(many=True)

Limitaciones

  • Por defecto son solo-lectura
  • Se debe crear lógica personalizada para hacerlos writable

Test crear tags a travez de la API recetas

tests/test_recipe_api.py

from core.models import Tag

    ...

    def test_create_recipe_with_new_tags(self):
        """Test create a recipe with new tags."""
        payload = {
            'title': 'Titulo receta de ejemplo tag anidado',
            'time_minutes': 1,
            'price': Decimal('2.50'),
            'tags': [{'name': 'anidado'}, {'name': 'cena'}]
        }
        res = self.client.post(RECIPES_URL, payload, format='json')

        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        recipes = Recipe.objects.filter(user=self.user)
        self.assertEqual(recipes.count(), 1)
        recipe = recipes[0]
        self.assertEqual(recipe.tags.count(), 2)
        for tag in payload['tags']:
            exists = recipe.tags.filter(
                name=tag['name'],
                user=self.user,
            ).exists()
            self.assertTrue(exists)

    def test_create_recipe_with_existing_tag(self):
        """Test creating a recipe with existing tag."""
        tag_peru = Tag.objects.create(user=self.user, name='Peruana')
        payload = {
            'title': 'Arroz con mariscos',
            'time_minutes': 45,
            'price': Decimal('8.50'),
            'tags': [{'name': 'Peruana'}, {'name': 'Almuerzo'}]
        }
        res = self.client.post(RECIPES_URL, payload, format='json')

        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        recipes = Recipe.objects.filter(user=self.user)
        self.assertEqual(recipes.count(), 1)
        recipe = recipes[0]
        self.assertEqual(recipe.tags.count(), 2)
        self.assertIn(tag_peru, recipe.tags.all())
        for tag in payload['tags']:
            exists = recipe.tags.filter(
                name=tag['name'],
                user=self.user,
            )
            self.assertTrue(exists)

Implementación creación de Tags al crear recetas

recipe/serializers.py

...

class TagSerializer(serializers.ModelSerializer):
    """Serializer for tags."""

    class Meta:
        model = Tag
        fields = ['id', 'name']
        read_only_fields = ['id']


class RecipeSerializer(serializers.ModelSerializer):
    """Serializer for recipes."""
    tags = TagSerializer(many=True, required=False)

    class Meta:
        model = Recipe
        fields = ['id', 'title', 'time_minutes', 'price', 'link', 'tags']
        read_only_fields = ['id']

    def create(self, validated_data):
        """Create a recipe."""
        tags = validated_data.pop('tags', [])
        recipe = Recipe.objects.create(**validated_data)
        auth_user = self.context['request'].user
        for tag in tags:
            tag_obj, created = Tag.objects.get_or_create(
                user=auth_user,
                **tag,
            )
            recipe.tags.add(tag_obj)

        return recipe
...

Modificar tag asignado a receta

Test modificar tag asignado a receta

recipe/tests/test_recipe_api.py

    ...

    def test_create_tag_on_update(self):
        """Test creating tag when updating a recipe."""
        recipe = create_recipe(user=self.user)

        payload = {'tags': [{'name': 'Cena'}]}
        url = detail_url(recipe.id)
        res = self.client.patch(url, payload, format='json')

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        new_tag = Tag.objects.get(user=self.user, name='Cena')
        self.assertIn(new_tag, recipe.tags.all())

    def test_update_recipe_assign_tag(self):
        """Test assigning an existing tag when updating a recipe."""
        tag_breakfast = Tag.objects.create(user=self.user, name='Desayuno')
        recipe = create_recipe(user=self.user)
        recipe.tags.add(tag_breakfast)

        tag_lunch = Tag.objects.create(user=self.user, name='Cena')
        payload = {'tags': [{'name': 'Cena'}]}
        url = detail_url(recipe.id)
        res = self.client.patch(url, payload, format='json')

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertIn(tag_lunch, recipe.tags.all())
        self.assertNotIn(tag_breakfast, recipe.tags.all())

    def test_clear_recipe_tags(self):
        """Test clearing a recipes tags."""
        tag = Tag.objets.create(user=self.user, name='Once')
        recipe = create_recipe(user=self.user)
        recipe.tags.add(tag)

        payload = {'tags': []}
        url = detail_url(recipe.id)
        res = self.client.patch(url, payload, format='json')

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(recipe.tags.count(), 0)

Implementación modificar tag asignado a receta

recipe/serializer.py

    ...

    def _get_or_create_tags(self, tags, recipe):
        """Handle getting or creating tags as needed."""
        auth_user = self.context['request'].user
        for tag in tags:
            tag_obj, created = Tag.objects.get_or_create(
                user=auth_user,
                **tag,
            )
            recipe.tags.add(tag_obj)

    
    def create(self, validated_data):
        """Create a recipe."""
        tags = validated_data.pop('tags', [])
        recipe = Recipe.objects.create(**validated_data)
        self._get_or_create_tags(tags, recipe)

        return recipe

    def update(self, instance, validated_data):
        """Update recipe."""
        tags = validated_data.pop('tags', None)
        if tags is not None:
            instance.tags.clear()
            self._get_or_create_tags(tags, instance)

        for attr, value in validated_data.items():
            setattr(instance, attr, value)

        instance.save()
        return instance

    ...

Levantar app y prueba navegador

URL localhost:8000/api/docs

Swagger

img

Django Admin

img