Diario de Python | #14. Color Choice: día productivo
Tags: devjournal, python, django, spanish
Published: 2023-04-13 13:03:02

Ayer fue un día altamente productivo: avancé mucho este proyecto, con múltiples ideas que ya están aplicadas, como siempre, en su [repositorio de GitHub](https://github.com/maxwellnewage/python-color-choice-api).

Hoy vamos a hablar de autenticación, información fake generada automáticamente y mucho más. Ajustate el cinturón, ¡porque la cosa viene fuerte!

## Validar si un voto ya fue emitido
Ya teniendo la funcionalidad de que un usuario pueda votar, la siguiente tenía que ser validarlo. Pensé en armar un endpoint distinto porque la idea era que en el momento de generar un frontend, pudiese consultar primero esto antes de generar el voto en si.

```python
@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
def is_voted(request):
    vote = Vote.objects.filter(user=request.user).first()

    if vote is None:
        return Response({
            "is_voted": False,
            "message": f"{request.user} no ha votado.",
        })
    else:
        return Response({
            "is_voted": True,
            "message": f"{request.user} ya ha votado el color {vote.color}.",
        })
```

Como no estoy dentro de un ViewSet, tuve que aplicar el decorador _permission_classes_. Consideré que GET era el método adecuado, dado que estamos obteniendo la información de un estado.

Luego hice el enrutamiento de toda la vida:

```python
path('votes/voted', is_voted, name='api-votes-voted'),
```

## Validadores de Clave desactivados
Muchos desarrolladores experimentados en Django me van a odiar, pido disculpas de antemano 🤣. Desactivé los validadores temporalmente porque me resultaba muy difícil estar generando usuarios con claves seguras todo el tiempo:

```python
AUTH_PASSWORD_VALIDATORS = [
    # {
    #     'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    # },
    # {
    #     'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    # },
    # {
    #     'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    # },
    # {
    #     'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    # },
]
```

A su vez aprendí que puedo definir estos valores a mi gusto, e incluso aplicar uno propio si quisiera. En el futuro seguramente le saque provecho 😄.

## Fix de id en ColorSerializer
Para continuar las pruebas de los votos, decidí generar colores nuevos, pero por la API en vez del administrador como venía haciendo hasta el momento.

Me sorprendí al recibir un error cuando quise agregar el color blanco:

```json
{
    "name": "blanco",
    "hexa": "#FFF"
}
```

Me pedía el campo _id_ 😳. Si, ese que se genera automáticamente en nuestra BD. Por suerte [logré resolverlo](https://github.com/maxwellnewage/python-color-choice-api/commit/a21881e6fb92a7a1662ba219d0312afa5ccb0dcb) rápidamente con un _readonly=True_ en este campo del serializador.

## No me toquen el endpoint Colors
Luego me di cuenta de que al endpoint colors podía acceder cualquiera, eliminando, agregando y modificando colores a su gusto. Esto requería una modificación, pero no podía restringir todo porque el usuario común tenía que poder listar los colores para verlos en el front y votar.

Así que utilicé el método get_permissions para determinar qué permisos tenía cada endpoint de colors:

```python
def get_permissions(self):
    if self.action == 'list':
        permission_classes = [permissions.AllowAny]
    else:
        permission_classes = [permissions.IsAdminUser]
    return [permission() for permission in permission_classes]
```

Recordemos que 'list' es el _GET api/colors_ que devuelve todos los registros.

## Pereza, y mucha
Una de nuestras virtudes fundamentales es [la pereza](https://dev.to/jmfayard/laziness-impatience-and-hubris-1ea9). Esto me llevó a pensar:
> Tengo que generar usuarios y colores para testear...uf.
No solo era una cantidad inmensa de trabajo, sino que también era extremadamente aburrido. Y eso me llevó a la solución que todos los devs amamos: automatizar cosas.

## Fake Users
Resulta que existe una librería en Python (gracias ChatGPT) que nos permite generar fake users (usuarios falsos, mala gente, en resumen). Esto no es más que un conjunto de strings aleatorios pero con una coherencia en la información como nombre, apellido o email.

Por lo tanto, decidí crear un módulo _utils_ y dentro un script en python que contendría la función _fake_users_:

```python
def fake_users(num_users):
    fake = Faker()
    user_list = []
    for i in range(num_users):
        username = fake.user_name()
        email = fake.email()
        password = fake.password()
        first_name = fake.first_name()
        last_name = fake.last_name()
        user = User.objects.create_user(username=username, email=email, password=password, first_name=first_name, last_name=last_name)
        user.save()
        user_list.append(user)

    return user_list
```

Mi idea inicial era ejecutarlo como un script suelto, pero las constantes de Django me resultaron un problema. Así que decidí no salirme del scope y generé un endpoint, por supuesto con permisos solo para el admin:

```python
def generate_fake_users(request, amount):
    user_list = fake_users(amount)
    user_serializer_list = [UserSerializer(user).data for user in user_list]

    return Response({"users": user_serializer_list})
```

Un detalle interesante, le pasé el amount (cantidad) por parámetro, asi puedo generar todos los que quiera. Además lo pasé por el UserSerializer, básicamente porque estaba inspirado 🎨.

## Fake Votes
Los colores los hice a mano, podría haberlo automatizado pero quería tener control sobre estos. Aparte debía existir una relación coherente entre los nombres y valores hexadecimales.

Por otro lado, los votos eran otro tema: asociar un voto por cada usuario me llevaría horas si lo hacía a mano, por eso me inventé un fake votes:

```python
def fake_votes(num_votes):
    vote_list = []
    colors = Color.objects.all()
    users = User.objects.all()

    while len(vote_list) < num_votes:
        random_color = choice(colors)
        random_user = choice(users)

        old_vote = Vote.objects.filter(user=random_user).first()
        if old_vote is None:
            vote = Vote.objects.create(user=random_user, color=random_color)
            vote.save()
            vote_list.append(vote)

    return vote_list
```

Básicamente me traigo todos los colores y usuarios, y luego con una función _choice_ elijo valores aleatorios. 

Va dentro de un while porque necesita persistir hasta que la cantidad de votos se cumpla. Esto es porque _random_user_ puede elegir un usuario que ya haya votado, y esto implicaría que no se va a guardar porque es una entrada duplicada.

Para evitar que explote (como tus finanzas 💸), valido si existe el voto (old_vote), y sino genero uno nuevo. Simple lógica de semáforo.

La vista es lo de siempre, pero para Vote:

```python
@api_view(['POST'])
@permission_classes([permissions.IsAdminUser])
def generate_fake_votes(request, amount):
    vote_list = fake_votes(amount)
    vote_serializer_list = [VoteSerializer(vote).data for vote in vote_list]

    return Response({"votes": vote_serializer_list})
```

## Las estadísticas
Ahora que tenía automatizada la parte de usuarios y votos, podía generar tantos registros como quisiera. Esto me permitió tener 50 o 60 votos emitidos, y eso tiene mucho olor a estadística.

Así que me puse a armar un método para mostrar cuantos votos tenía por color:

```python
@api_view(['GET'])
def votes_stats(request):
    votes_by_color = Vote.objects.values('color__name').annotate(count=Count('id'))
    result = {}
    for vote in votes_by_color:
        result[vote['color__name']] = vote['count']
    return Response(result)
```

Acá estan pasando varias cosas: _Vote.objects.values('color__name')_ toma cada atributo "name" de la tabla color, y luego genera un campo "count" con _annotate(count=Count('id'))_ tomando el id, que viene a ser clave primaria y valor único.

Luego arma un diccionario con clave en el nombre del color, y valor en su cantidad. Para ilustrar un resultado, miren mi json:

```python
{
    "amarillo": 4,
    "azul": 1,
    "blanco": 2,
    "negro": 4,
    "rojo": 2,
    "rosa": 3,
    "verde": 4,
    "violeta": 1
}
```

El potencial de este simple diccionario es infinito: gráficas de torta, de barra, incluso de dispersión.

## Conclusiones
Considero que si bien pueden surgir nuevos cambios en el futuro, ya tengo una API sólida para llevar al frontend y armar un sitio, una app o alguna interfaz de escritorio que aproveche estas funcionalidades. Ya veremos qué se me ocurre traerles en futuros artículos, de momento me voy a preparar un [gin tonic](https://es.wikipedia.org/wiki/Gin_tonic) y contemplar mi creación.

¡Gracias por acompañarme en este arduo camino de crear una API con Python! 🎉💖