Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make some fields in Entrega mandatory and add some tests #96

Merged
merged 8 commits into from
Jan 23, 2024
2 changes: 1 addition & 1 deletion src/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async def login_for_access_token(
tags=["Auth"],
)
async def get_users(
user_logged: Annotated[ # pylint: disable=unused-argument
user_logged: Annotated[ # pylint: disable=unused-argument
schemas.UsersSchema,
Depends(crud_auth.get_current_admin_user),
],
Expand Down
5 changes: 3 additions & 2 deletions src/create_admin_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
API_PGD_ADMIN_USER = os.environ.get("API_PGD_ADMIN_USER")
API_PGD_ADMIN_PASSWORD = os.environ.get("API_PGD_ADMIN_PASSWORD")


async def init_user_admin():
db_session = async_session_maker()

Expand All @@ -16,12 +17,12 @@ async def init_user_admin():
# b-crypt
password=get_password_hash(API_PGD_ADMIN_PASSWORD),
is_admin=True,
cod_SIAPE_instituidora=1
cod_SIAPE_instituidora=1,
)

async with db_session as session:
session.add(new_user)
await session.commit()
print(f"API_PGD_ADMIN: Usuário administrador `{API_PGD_ADMIN_USER}` criado")
else:
print(f"API_PGD_ADMIN: Usuário administrador `{API_PGD_ADMIN_USER}` já existe")
print(f"API_PGD_ADMIN: Usuário administrador `{API_PGD_ADMIN_USER}` já existe")
15 changes: 9 additions & 6 deletions src/crud_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):

return encoded_jwt


async def verify_token(token: str, db: DbContextManager):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand All @@ -116,12 +117,14 @@ async def verify_token(token: str, db: DbContextManager):

return user


async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: DbContextManager = Depends(DbContextManager),
):
return await verify_token(token, db)


async def get_user_by_token(
token: str,
db: DbContextManager = Depends(DbContextManager),
Expand Down Expand Up @@ -243,9 +246,10 @@ async def delete_user(

return f"Usuário `{email}` deletado"

async def user_reset_password(db_session: DbContextManager,
token: str,
new_password: str) -> str:

async def user_reset_password(
db_session: DbContextManager, token: str, new_password: str
) -> str:
"""Reset password of a user by passing a access token.

Args:
Expand All @@ -255,8 +259,7 @@ async def user_reset_password(db_session: DbContextManager,

Returns:
str: Message about updated password
"""

"""

user = await get_user_by_token(token, db_session)

Expand All @@ -268,4 +271,4 @@ async def user_reset_password(db_session: DbContextManager,
)
await session.commit()

return f"Senha do Usuário {user.email} atualizada"
return f"Senha do Usuário {user.email} atualizada"
12 changes: 7 additions & 5 deletions src/email_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
MAIL_SSL_TLS=False,
MAIL_PASSWORD=os.environ["MAIL_PASSWORD"],
USE_CREDENTIALS=False,
VALIDATE_CERTS=False
VALIDATE_CERTS=False,
)

async def send_reset_password_mail(email: str,
token: str,
) -> JSONResponse:

async def send_reset_password_mail(
email: str,
token: str,
) -> JSONResponse:
"""Envia o e-mail contendo token para redefinir a senha.

Args:
Expand Down Expand Up @@ -59,7 +61,7 @@ async def send_reset_password_mail(email: str,
subject="Recuperação de acesso",
recipients=[email],
body=body,
subtype=MessageType.html
subtype=MessageType.html,
)
fm = FastMail(conf)
await fm.send_message(message)
Expand Down
4 changes: 1 addition & 3 deletions src/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""Definições dos modelos de dados da API que serão persistidos no
banco pelo mapeamento objeto-relacional (ORM) do SQLAlchemy.
"""

from datetime import datetime
import enum
from sqlalchemy import (
Boolean,
Expand Down Expand Up @@ -559,4 +557,4 @@ class Users(Base):
DateTime,
nullable=False,
default=now(),
)
)
16 changes: 11 additions & 5 deletions src/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

from typing import List, Optional
from datetime import date, datetime
from datetime import date

from pydantic import BaseModel, ConfigDict, Field, EmailStr
from pydantic import model_validator, field_validator
Expand Down Expand Up @@ -281,16 +281,16 @@ class EntregaSchema(BaseModel):
title="Percentual de progresso realizado",
description=Entrega.percentual_progresso_realizado.comment,
)
data_entrega: Optional[date] = Field(
data_entrega: date = Field(
default=None, title="Data da entrega", description=Entrega.data_entrega.comment
)
nome_demandante: Optional[str] = Field(
nome_demandante: str = Field(
default=None,
title="Nome do demandante",
max_length=300,
description=Entrega.nome_demandante.comment,
)
nome_destinatario: Optional[str] = Field(
nome_destinatario: str = Field(
default=None,
title="Nome do destinatário",
max_length=300,
Expand Down Expand Up @@ -425,6 +425,7 @@ def must_be_in_period(self) -> "PlanoEntregasSchema":
entrega.data_entrega < self.data_inicio_plano_entregas
or entrega.data_entrega > self.data_termino_plano_entregas
for entrega in self.entregas
if entrega.data_entrega is not None
):
raise ValueError(
"Data de entrega precisa estar dentro do intervalo entre "
Expand Down Expand Up @@ -522,13 +523,16 @@ class ListaStatusParticipanteSchema(BaseModel):
description="Lista de Contribuições planejadas para o Plano de Trabalho.",
)


class Token(BaseModel):
access_token: str
token_type: str


class TokenData(BaseModel):
username: str | None = None


class UsersInputSchema(BaseModel):
__doc__ = Users.__doc__
model_config = ConfigDict(from_attributes=True)
Expand All @@ -537,6 +541,7 @@ class UsersInputSchema(BaseModel):
description=Users.email.comment,
)


class UsersGetSchema(UsersInputSchema):
__doc__ = Users.__doc__
model_config = ConfigDict(from_attributes=True)
Expand All @@ -555,6 +560,7 @@ class UsersGetSchema(UsersInputSchema):
description=Users.cod_SIAPE_instituidora.comment,
)


class UsersSchema(UsersGetSchema):
password: str = Field(
title="password encriptado",
Expand All @@ -566,4 +572,4 @@ class UsersSchema(UsersGetSchema):
def must_be_positive(cod_unidade: int) -> int:
if cod_unidade < 1:
raise ValueError("cod_SIAPE inválido")
return cod_unidade
return cod_unidade
58 changes: 2 additions & 56 deletions src/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,6 @@
import calendar
from datetime import date, timedelta

def sa_obj_to_dict(d: dict) -> dict:
"Copia os valores do objeto SQL Alchemy para um dicionário."
return {
k:
[
sa_obj_to_dict(item)
for item in v
] if isinstance(v, list) and v else v
for k, v in d.__dict__.items()
if not k.startswith("_")
}

def merge_dicts(d1: dict, d2:dict) -> dict:
"Atualiza d1 com os valores de d2."
# os que estão em d1, atualizar com d2
d = {
k: merge_dicts(v, d2.get(k, {})) \
if isinstance(v, dict) and v \
else d2.get(k, v)
for k, v in d1.items()
}
# os que estão em d2 mas não em d1, copiar de d2
d.update({
k: v
for k, v in d2.items()
if k not in d1.keys()
})
return d

def list_to_dict(l: list, id_attr: str) -> dict:
"Transforma uma lista em dicionário, tomando o id como chave."
return {
entry.get(id_attr): {
key: value
for key, value in entry.items() if key != id_attr
}
for entry in l
}

def dict_to_list(d: dict, id_attr: str) -> list:
"Transforma um dicionário em lista, tomando o id como chave."
return [
merge_dicts(
{
id_attr: item_id
},
{
key: value
for key, value in prop.items()
}
)
for item_id, prop in sorted(d.items())
]


def over_a_year(start: date, end: date) -> int:
"""Calculates wether or not the period from `start` to `end` comprises
Expand All @@ -75,8 +21,8 @@ def over_a_year(start: date, end: date) -> int:
add_leap = add_leap + 1
if calendar.isleap(end.year) and end.month > 3:
add_leap = add_leap + 1
if end - start == timedelta(days=365+add_leap):
if end - start == timedelta(days=365 + add_leap):
return 0
if end - start > timedelta(days=365+add_leap):
if end - start > timedelta(days=365 + add_leap):
return 1
return -1
17 changes: 8 additions & 9 deletions tests/entrypoint_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@

from httpx import Client
from fastapi import status
import pytest


def test_redirect_to_docs_html(client: Client):
"""
Testa se o acesso por um navegador na raiz redireciona para o /docs.
"""
response = client.get("/",
allow_redirects=False,
headers={"Accept": "text/html"})
response = client.get("/", allow_redirects=False, headers={"Accept": "text/html"})

assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT
assert response.headers['Location'] == "/docs"
assert response.headers["Location"] == "/docs"


def test_redirect_to_entrypoint_json(client: Client):
"""
Testa se o acesso por um script na raiz redireciona para o /openapi.json.
"""
response = client.get("/",
allow_redirects=False,
headers={"Accept": "application/json"})
response = client.get(
"/", allow_redirects=False, headers={"Accept": "application/json"}
)

assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT
assert response.headers['Location'] == "/openapi.json"
assert response.headers["Location"] == "/openapi.json"
3 changes: 2 additions & 1 deletion tests/participantes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ def test_put_participante_omit_optional_fields(
response_part[attribute] == partial_input_part[attribute]
for attributes in fields_participantes["mandatory"]
for attribute in attributes
) for response_part in response.json()["lista_status"]
)
for response_part in response.json()["lista_status"]
)


Expand Down
33 changes: 32 additions & 1 deletion tests/plano_entregas_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

fields_plano_entregas = {
"optional": (
["cancelado", "avaliacao_plano_entregas", "data_avaliacao_plano_entregas"],
["cancelado"],
["avaliacao_plano_entregas"],
["data_avaliacao_plano_entregas"],
),
"mandatory": (
["id_plano_entrega_unidade"],
Expand Down Expand Up @@ -191,6 +193,35 @@ def test_create_plano_entregas_entrega_omit_optional_fields(
assert_equal_plano_entregas(response.json(), input_pe)


@pytest.mark.parametrize("nulled_fields", enumerate(fields_entrega["optional"]))
def test_create_plano_entregas_entrega_null_optional_fields(
truncate_pe, # pylint: disable=unused-argument
input_pe: dict,
nulled_fields: list,
user1_credentials: dict,
header_usr_1: dict,
client: Client,
):
"""Tenta criar um novo plano de entregas com o valor null nos campos opcionais"""

offset, field_list = nulled_fields
for field in field_list:
for entrega in input_pe["entregas"]:
if field in entrega:
entrega[field] = None

input_pe["id_plano_entrega_unidade"] = 557 + offset
response = client.put(
f"/organizacao/{user1_credentials['cod_SIAPE_instituidora']}"
f"/plano_entregas/{input_pe['id_plano_entrega_unidade']}",
json=input_pe,
headers=header_usr_1,
)
print(response.json())
assert response.status_code == status.HTTP_201_CREATED
assert_equal_plano_entregas(response.json(), input_pe)


@pytest.mark.parametrize(
"missing_fields", enumerate(fields_plano_entregas["mandatory"])
)
Expand Down
Loading