Lucas Simon

Web Developer. lucassrod@gmail.com

Série API em Flask - Parte 10 - Editando um usuário

Hoje vamos implementar a atualização de dados, refatorar algumas linhas de código e criar uma validação simples no campo e-mail.

Para entendimento, segue os capítulos que serão abordados.

O repositório com todo o código fonte esta aqui. Os capítulos estão em branches.

Alterar o nome dos endpoints

Uma mudança que tive de fazer é o nome das rotas. E para não dar conflitos altere a url /admin/users/<int:page_id> para /admin/users/page/<int:page_id>

# rotas para os admins
api.add_resource(AdminUserPageList, '/admin/users/page/<int:page_id>')
api.add_resource(AdminUserResource, '/admin/users/<string:user_id>')

Criar uma nova mensagem

Como de praxe, precisamos padronizar as mensagens da aplicação. Para este caso vamos criar uma mensagem referenciando à um recurso atualizado. Em nosso messages.py adicione:

MSG_RESOURCE_UPDATED = '{} atualizado(a).'

Refatorar a busca do usuário

Reparei que o código, abaixo iria se repetir nos métodos GET, PUT e DELETE.

# -*- coding: utf-8 -*-

try:
    # Buscando usuário por id
    user = User.objects.get(id=user_id)

except FieldDoesNotExist as e:
    return resp_exception('Users', description=e.__str__())

except Exception as e:
    return resp_exception('Users', description=e.__str__())

Logo surgiu a necessidade de refatorar e reaproveitar esse código em uma única função. Para isso dentro de nosso apps/users/utils.py vamos criar o meodo get_user_by_id contendo a lógica acima.

def get_user_by_id(user_id: str):
    try:
        # buscamos todos os usuários da base utilizando o paginate
        return User.objects.get(id=user_id)

    except DoesNotExist as e:
        return resp_does_not_exist('Users', 'Usuário')

    except FieldDoesNotExist as e:
        return resp_exception('Users', description=e.__str__())

    except Exception as e:
        return resp_exception('Users', description=e.__str__())

De acordo com o bloco try, caso encontre o usuário a sua instância retornarda. Em caso de exceções elas são retornadas como resposta de requisição do flask. Outro ponto a observar, é que foi incluido mais um tratamento de exceção DoesNotExist.

Feito isso, vamos refatorar nosso método GET do recurso AdminUserResource. Praticamente todo o bloco try/except será substituido por 3 linhas.


from .utils import get_user_by_id


class AdminUserResource(Resource):

    def get(self, user_id):
        result = None
        schema = UserSchema()

        user = get_user_by_id(user_id)

        if not isinstance(user, User):
            return user

        result = schema.dump(user)

        return resp_ok(
            'Users', MSG_RESOURCE_FETCHED.format('Usuários'),  data=result.data
        )

Novo schema para atualizar dados

Precisaremos, de um novo schema para atualizar os dados de um determinado usuário. Logo seguindo a lógica os campos devem ser não obrigatórios já que podemos atualizar qualquer campo.

Logo em nosso schema.py, adicionamos a seguinte classe:

from marshmallow.fields import Email, Str, Boolean, Nested

# ... códigos anteriores

class UserUpdateSchema(Schema):
    full_name = Str()
    email = Email()
    cpf_cnpj = Str()
    address = Nested(AddressSchema)

Verifica se existe um email já cadastrado

Afim de verificar previamente se existe um email ja cadastrado na base de dados e lançar uma resposta antes de chamar o método save() do modelo de User, vamos adicionar o seguinte código em nosso utils.py.

def exists_email_in_users(email: str, instance=None):
    """
    Verifico se existe um usuário com aquele email
    """
    user = None

    try:
        user = User.objects.get(email=email)

    except DoesNotExist:
        return False

    except MultipleObjectsReturned:
        return True

    # verifico se o id retornado na pesquisa é mesmo da minha instancia
    # informado no parâmetro
    if instance and instance.id == user.id:
        return False

    return True

O método PUT

O código abaixo refere-se a implementação do método PUT.

# -*- coding: utf-8 -*-

# Flask
from flask import request

# Third
from flask_restful import Resource
from mongoengine.errors import FieldDoesNotExist
from mongoengine.errors import NotUniqueError, ValidationError

# Apps
from apps.responses import resp_ok, resp_exception, resp_data_invalid, resp_already_exists

from apps.messages import MSG_RESOURCE_FETCHED_PAGINATED, MSG_RESOURCE_FETCHED
from apps.messages import MSG_NO_DATA, MSG_RESOURCE_UPDATED, MSG_INVALID_DATA
from apps.messages import MSG_ALREADY_EXISTS

# Local
from .models import User
from .schemas import UserSchema, UserUpdateSchema
from .utils import get_user_by_id, exists_email_in_users


class AdminUserPageList(Resource):
    # ...

class AdminUserResource(Resource):

    def get(self, user_id):
        # ...

    def put(self, user_id):
        result = None
        schema = UserSchema()
        update_schema = UserUpdateSchema()
        req_data = request.get_json() or None
        email = None

        # Valido se o payload está vazio
        if req_data is None:
            return resp_data_invalid('Users', [], msg=MSG_NO_DATA)

        # Busco o usuário na coleção users pelo seu id
        user = get_user_by_id(user_id)

        # se não for uma instancia do modelo User retorno uma resposta
        # da requisição http do flask
        if not isinstance(user, User):
            return user

        # carrego meus dados de acordo com o schema de atualização
        data, errors = update_schema.load(req_data)

        # em caso de erros retorno uma resposta 422 com os erros de
        # validação do schema
        if errors:
            return resp_data_invalid('Users', errors)

        email = data.get('email', None)

        # Valido se existe um email na coleção de usuários
        if email and exists_email_in_users(email, user):
            return resp_data_invalid(
                'Users', [{'email': [MSG_ALREADY_EXISTS.format('usuário')]}]
            )

        try:
            # para cada chave dentro do dados do update schema
            # atribuimos seu valor
            for i in data.keys():
                user[i] = data[i]

            user.save()

        except NotUniqueError:
            return resp_already_exists('Users', 'usuário')

        except ValidationError as e:
            return resp_exception('Users', msg=MSG_INVALID_DATA, description=e.__str__())

        except Exception as e:
            return resp_exception('Users', description=e.__str__())

        result = schema.dump(user)

        return resp_ok(
            'Users', MSG_RESOURCE_UPDATED.format('Usuário'),  data=result.data
        )

Testando

$ http -v PUT 0.0.0.0:5000/admin/users/5bbeaf52fb5d1b0a32466c93 full_name="teste sobrenome"
PUT /admin/users/5bbeaf52fb5d1b0a32466c93 HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 32
Content-Type: application/json
Host: 0.0.0.0:5000
User-Agent: HTTPie/0.9.8

{
    "full_name": "teste sobrenome"
}

HTTP/1.0 200 OK
Content-Length: 268
Content-Type: application/json
Date: Wed, 17 Oct 2018 17:39:17 GMT
Server: Werkzeug/0.14.1 Python/3.6.5

{
    "data": {
        "active": false,
        "cpf_cnpj": "1234567",
        "email": "teste@teste.com",
        "full_name": "teste sobrenome",
        "id": "5bbeaf52fb5d1b0a32466c93"
    },
    "message": "Usuário atualizado(a).",
    "resource": "Users",
    "status": 200
}

Graças a refatoração de código podemos economizar algumas linhas de código. Com isso finalizamos a atualização de dados do usuário e estamos quase terminando o nosso CRUD.

Próximo artigo: Deletando um usuário