diff --git a/database/01_create_database.sql b/database/01_create_database.sql index ff982ac..7fa1741 100644 --- a/database/01_create_database.sql +++ b/database/01_create_database.sql @@ -27,4 +27,34 @@ CREATE TABLE "public"."problem" ( REFERENCES "public"."category" ("id") ON DELETE RESTRICT -); \ No newline at end of file +); + +CREATE TABLE "public"."request" ( + id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY(start 1), + attendant_name VARCHAR(250), + applicant_name VARCHAR(250), + applicant_phone VARCHAR(20), + place VARCHAR(250), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + workstation_id INTEGER, + CONSTRAINT "PK_id_request" PRIMARY KEY ("id") +); + +CREATE TYPE "public"."priority" AS ENUM ('low', 'normal', 'hight', 'urgent'); +CREATE TYPE "public"."status" AS ENUM ('pending', 'in_progress', 'not_solved', 'outsourced', 'solved'); + +CREATE TABLE "public"."has" ( + problem_id INTEGER, + request_id INTEGER, + request_status "public"."status" NOT NULL DEFAULT 'pending', + event_date TIMESTAMP, + is_event BOOLEAN NOT NULL DEFAULT FALSE, + priority "public"."priority" NOT NULL DEFAULT 'normal', + CONSTRAINT "FK_problem_id" FOREIGN KEY ("problem_id") + REFERENCES "public"."problem" ("id") + ON DELETE RESTRICT, + CONSTRAINT "FK_request_id" FOREIGN KEY ("request_id") + REFERENCES request ("id") + ON DELETE SET NULL +); diff --git a/src/main.py b/src/main.py index 06a8ba4..61fbd43 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,14 @@ from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware -from routers import category, problem +from routers import category, problem, request app = FastAPI() +app.include_router(request.router) +app.include_router(problem.router) +app.include_router(category.router) + app.add_middleware( CORSMiddleware, allow_origins=["*"], diff --git a/src/models.py b/src/models.py index e0a51f3..ba8aab5 100644 --- a/src/models.py +++ b/src/models.py @@ -1,9 +1,50 @@ -from sqlalchemy import TIMESTAMP, Boolean, Column, ForeignKey, Integer, String +import enum + +from sqlalchemy import (TIMESTAMP, Boolean, Column, Enum, ForeignKey, Integer, + String, Table, Text) +from sqlalchemy.orm import relationship from sqlalchemy.sql import func from database import Base +class EnumStatus(str, enum.Enum): + pending = "pending" + in_progress = "in_progress" + not_solved = "not_solved" + outsourced = "outsourced" + solved = "solved" + + +class EnumPriority(str, enum.Enum): + low = "low" + normal = "normal" + hight = "hight" + urgent = "urgent" + + +has = Table( + "has", + Base.metadata, + Column("problem_id", Integer, ForeignKey("problem.id")), + Column("request_id", Integer, ForeignKey("request.id")), + Column("is_event", Boolean, nullable=True), + Column("event_date", TIMESTAMP, nullable=True), + Column( + "request_status", + Enum(EnumStatus), + default=EnumStatus.pending, + nullable=False, + ), + Column( + "priority", + Enum(EnumPriority), + default=EnumPriority.normal, + nullable=False, + ), +) + + class Category(Base): __tablename__ = "category" id = Column(Integer, primary_key=True) @@ -29,3 +70,21 @@ class Problem(Base): onupdate=func.current_timestamp(), ) category_id = Column(Integer, ForeignKey(Category.id)) + requests = relationship( + "Request", secondary=has, back_populates="problems" + ) + + +class Request(Base): + __tablename__ = "request" + id = Column(Integer, primary_key=True) + attendant_name = Column(String(250), nullable=False) + applicant_name = Column(String(250), nullable=False) + applicant_phone = Column(String(20), nullable=False) + place = Column(String(250), nullable=False) + description = Column(Text, nullable=True) + created_at = Column(TIMESTAMP, server_default=func.current_timestamp()) + workstation_id = Column(Integer, nullable=False) + problems = relationship( + "Problem", secondary=has, back_populates="requests" + ) diff --git a/src/routers/request.py b/src/routers/request.py new file mode 100644 index 0000000..6724478 --- /dev/null +++ b/src/routers/request.py @@ -0,0 +1,301 @@ +from typing import List, Union + +from fastapi import APIRouter, Depends, status +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from sqlalchemy import insert +from sqlalchemy.orm import Session + +from database import engine, get_db +from models import Base, Request, has + +router = APIRouter() + + +class UpdateHasModel(BaseModel): + problem_id: int + is_event: bool = False + event_date: str | None = None + request_status: str | None = "pending" + priority: str | None = "normal" + + +class UpdateRequestModel(BaseModel): + attendant_name: str | None = None + applicant_name: str = None + applicant_phone: str = None + place: str = None + description: str | None = None + created_at: str | None = None + workstation_id: int = None + problems: List[UpdateHasModel] + + class Config: + schema_extra = { + "example": { + "applicant_name": "Fulano de Tal", + "applicant_phone": "999999999", + "place": "Sala de Testes", + "description": "Ta tudo dando errado nos testes.", + "workstation_id": 2, + "problems": [ + { + "problem_id": 1, + "is_event": False, + "event_date": None, + "request_status": "pending", + "priority": "hight", + }, + { + "problem_id": 2, + "is_event": True, + "event_date": "2020-01-01T00:00:00", + "request_status": "pending", + "priority": "urgent", + }, + ], + } + } + + +class hasModel(BaseModel): + problem_id: int + is_event: bool = False + event_date: str | None = None + request_status: str = "pending" + priority: str = "normal" + + +class RequestModel(BaseModel): + attendant_name: str + applicant_name: str + applicant_phone: str + place: str + description: str | None = None + created_at: str | None = None + workstation_id: int + problems: List[hasModel] + + class Config: + schema_extra = { + "example": { + "attendant_name": "Fulano", + "applicant_name": "Ciclano", + "applicant_phone": "1111111111", + "place": "Sala de Reuniões", + "description": "Chamado aberto para acesso a internet.", + "workstation_id": 1, + "problems": [ + { + "problem_id": 1, + "is_event": False, + "event_date": None, + "request_status": "pending", + "priority": "normal", + }, + { + "problem_id": 2, + "is_event": True, + "event_date": "2020-01-01T00:00:00", + "request_status": "pending", + "priority": "normal", + }, + ], + } + } + + +Base.metadata.create_all(bind=engine) + + +def get_error_response(e: Exception): + return { + "message": "Erro ao processar dados", + "error": str(e), + "data": None, + } + + +@router.post("/chamado", tags=["Chamado"], response_model=RequestModel) +async def post_request(data: RequestModel, db: Session = Depends(get_db)): + try: + data_dict = data.dict() + problems = data_dict.pop("problems") + + new_object = Request(**data_dict) + db.add(new_object) + db.commit() + db.refresh(new_object) + new_object = jsonable_encoder(new_object) + + for problem in problems: + problem["request_id"] = new_object["id"] + db.execute(insert(has).values(**problem)) + + db.commit() + + response_data = jsonable_encoder( + { + "message": "Dado cadastrado com sucesso", + "error": None, + "data": new_object, + } + ) + + return JSONResponse( + content=response_data, status_code=status.HTTP_201_CREATED + ) + except Exception as e: + return JSONResponse( + content=get_error_response(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +def get_has_data(query, db: Session): + final_list = [] + for request in query: + request_dict = jsonable_encoder(request) + requests = ( + db.query(has).filter(has.c.request_id == request_dict["id"]).all() + ) + request_dict["problems"] = requests + final_list.append(request_dict) + return final_list + + +@router.get("/chamado", tags=["Chamado"]) +async def get_chamado( + problem_id: Union[int, None] = None, db: Session = Depends(get_db) +): + try: + if problem_id: + query = ( + db.query(Request) + .filter(Request.problems.any(id=problem_id)) + .all() + ) + + if query: + final_list = get_has_data(query, db) + query = jsonable_encoder(final_list) + message = "Dados buscados com sucesso" + status_code = status.HTTP_200_OK + else: + message = "Nenhum chamado com esse tipo de problema encontrado" + status_code = status.HTTP_200_OK + + response_data = {"message": message, "error": None, "data": query} + + return JSONResponse( + content=jsonable_encoder(response_data), + status_code=status_code, + ) + else: + query = db.query(Request).all() + all_data = get_has_data(query, db) + all_data = jsonable_encoder(all_data) + response_data = { + "message": "Dados buscados com sucesso", + "error": None, + "data": all_data, + } + return JSONResponse( + content=dict(response_data), status_code=status.HTTP_200_OK + ) + + except Exception as e: + return JSONResponse( + content=get_error_response(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +@router.delete("/chamado", tags=["Chamado"]) +async def delete_chamado( + request_id: int, problem_id: int, db: Session = Depends(get_db) +): + try: + query = ( + db.query(has) + .filter(has.c.request_id == request_id) + .filter(has.c.problem_id == problem_id) + .update({"request_status": "solved"}) + ) + + if query: + db.commit() + query_data = ( + db.query(has) + .filter(has.c.request_id == request_id) + .filter(has.c.problem_id == problem_id) + .first() + ) + query_data = jsonable_encoder(query_data) + message = "Chamado marcado como resolvido" + else: + message = "Chamado não encontrado" + query_data = None + + response_data = {"message": message, "error": None, "data": query_data} + + return JSONResponse( + content=response_data, status_code=status.HTTP_200_OK + ) + + except Exception as e: + return JSONResponse( + content=get_error_response(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +@router.put("/chamado/{request_id}", tags=["Chamado"]) +async def update_chamado( + data: UpdateRequestModel, request_id: int, db: Session = Depends(get_db) +): + if data.attendant_name: + return JSONResponse( + content={ + "message": "Não é possível alterar o nome do atendente", + "error": None, + "data": None, + }, + status_code=status.HTTP_400_BAD_REQUEST, + ) + + try: + data_dict = data.dict(exclude_none=True) + problems = data_dict.pop("problems") + to_update = ( + db.query(Request) + .filter(Request.id == request_id) + .update(data_dict) + ) + if to_update: + db.commit() + for problem in problems: + problem["request_id"] = request_id + db.query(has).filter(has.c.request_id == request_id).filter( + has.c.problem_id == problem["problem_id"] + ).update(problem) + db.commit() + query = db.query(Request).filter(Request.id == request_id).all() + final_list = get_has_data(query, db) + query = jsonable_encoder(final_list) + message = "Dados atualizados com sucesso" + status_code = status.HTTP_200_OK + + response_data = {"message": message, "error": None, "data": query} + else: + message = "Chamado não encontrado" + status_code = status.HTTP_200_OK + return JSONResponse( + content=jsonable_encoder(response_data), status_code=status_code + ) + except Exception as e: + return JSONResponse( + content=get_error_response(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) diff --git a/tests/conftest.py b/tests/conftest.py index cdb2ffc..78d3abc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,6 +37,14 @@ def session(): session.execute(f.read()) session.commit() + with open("tests/data/insert_request.sql", "r") as f: + session.execute(f.read()) + session.commit() + + with open("tests/data/insert_has.sql", "r") as f: + session.execute(f.read()) + session.commit() + yield session os.remove("test.db") diff --git a/tests/data/insert_has.sql b/tests/data/insert_has.sql new file mode 100644 index 0000000..e177afa --- /dev/null +++ b/tests/data/insert_has.sql @@ -0,0 +1,14 @@ +INSERT INTO has (problem_id, request_id, request_status, event_date, is_event, priority) VALUES +(1, 1, 'pending', '2021-02-26 00:00:00', TRUE, 'normal'), +(2, 1, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(3, 1, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(4, 1, 'pending', '2021-02-26 00:00:00', TRUE, 'normal'), +(5, 2, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(6, 2, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(7, 3, 'pending', '2021-02-26 00:00:00', TRUE, 'normal'), +(8, 4, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(9, 5, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(10, 6, 'pending', '2021-02-26 00:00:00', TRUE, 'normal'), +(9, 7, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(10, 8, 'pending', '2022-03-28 12:00:00', TRUE, 'normal'), +(10, 9, 'pending', '2021-02-26 00:00:00', TRUE, 'normal'); \ No newline at end of file diff --git a/tests/data/insert_request.sql b/tests/data/insert_request.sql new file mode 100644 index 0000000..83c5b4b --- /dev/null +++ b/tests/data/insert_request.sql @@ -0,0 +1,14 @@ +INSERT INTO request (attendant_name, applicant_name, applicant_phone, place, description, created_at, workstation_id) VALUES +('João', 'Maria', '999999999', 'Sala 1', 'Problema com a impressora', '2021-02-26 00:00:00', 1), +('Joaquim', 'Jose', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('Maria', 'João', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('João', 'Maria', '999999999', 'Sala 1', 'Problema com a impressora', '2021-02-26 00:00:00', 1), +('Joaquim', 'Jose', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('Maria', 'João', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('João', 'Maria', '999999999', 'Sala 1', 'Problema com a impressora', '2021-02-26 00:00:00', 1), +('Joaquim', 'Jose', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('Maria', 'João', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('João', 'Maria', '999999999', 'Sala 1', 'Problema com a impressora', '2021-02-26 00:00:00', 1), +('Joaquim', 'Jose', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('Maria', 'João', '985658745', 'Escritório do delegado', 'Problema com internet', '2022-03-28 12:00:00', 2), +('João', 'Maria', '999999999', 'Sala 1', 'Problema com a impressora', '2021-02-26 00:00:00', 1); diff --git a/tests/test_delete_request.py b/tests/test_delete_request.py new file mode 100644 index 0000000..dce2755 --- /dev/null +++ b/tests/test_delete_request.py @@ -0,0 +1,41 @@ +def test_delete_request(client): + response = client.delete("/chamado?request_id=1&problem_id=1") + assert response.status_code == 200 + assert response.json()["message"] == "Chamado marcado como resolvido" + assert response.json()["data"]["request_status"] == "solved" + + +def test_delete_invalid_request(client): + response = client.delete("/chamado?request_id=99&problem_id=99") + assert response.status_code == 200 + assert response.json()["message"] == "Chamado não encontrado" + + +def test_delete_request_without_problem_id(client): + response = client.delete("/chamado?request_id=1") + assert response.status_code == 422 + + +def test_delete_request_without_request_id(client): + response = client.delete("/chamado?problem_id=1") + assert response.status_code == 422 + + +def test_delete_request_without_request_id_and_problem_id(client): + response = client.delete("/chamado") + assert response.status_code == 422 + + +def test_delete_request_with_invalid_request_id(client): + response = client.delete("/chamado?request_id=abc&problem_id=1") + assert response.status_code == 422 + + +def test_delete_request_with_invalid_problem_id(client): + response = client.delete("/chamado?request_id=1&problem_id=abc") + assert response.status_code == 422 + + +def test_delete_request_with_invalid_request_id_and_problem_id(client): + response = client.delete("/chamado?request_id=abc&problem_id=abc") + assert response.status_code == 422 diff --git a/tests/test_get_request.py b/tests/test_get_request.py new file mode 100644 index 0000000..13fc7f4 --- /dev/null +++ b/tests/test_get_request.py @@ -0,0 +1,21 @@ +def test_get_request(client): + url = "/chamado" + response = client.get(url) + assert response.status_code == 200 + assert response.json()["message"] == "Dados buscados com sucesso" + + +def test_get_requestid(client): + url = "/chamado?problem_id=1" + response = client.get(url) + assert response.status_code == 200 + + +def test_get_requestid_invalid(client): + url = "/chamado?problem_id=99" + response = client.get(url) + assert response.status_code == 200 + assert ( + response.json()["message"] + == "Nenhum chamado com esse tipo de problema encontrado" + ) diff --git a/tests/test_post_request.py b/tests/test_post_request.py new file mode 100644 index 0000000..34196b1 --- /dev/null +++ b/tests/test_post_request.py @@ -0,0 +1,84 @@ +def test_post_request(client): + response = client.post( + "/chamado", + json={ + "attendant_name": "Fulano", + "applicant_name": "Ciclano", + "applicant_phone": "1111111111", + "place": "Sala de Reuniões", + "description": "Chamado aberto para acesso a internet.", + "workstation_id": 1, + "problems": [ + { + "problem_id": 1, + "is_event": False, + "request_status": "pending", + "priority": "normal", + }, + { + "problem_id": 2, + "is_event": False, + "request_status": "pending", + "priority": "normal", + }, + ], + }, + ) + assert response.status_code == 201 + + +def test_post_request_invalid(client): + response = client.post( + "/chamado", + json={ + "attendant_name": "Fulano", + "applicant_name": "Ciclano", + "applicant_phone": "1111111111", + "place": "Sala de Reuniões", + "description": "Chamado aberto para acesso a internet.", + "problems": [ + { + "problem_id": 1, + "is_event": False, + "request_status": "pending", + "priority": "normal", + }, + { + "problem_id": 2, + "is_event": False, + "request_status": "pending", + "priority": "normal", + }, + ], + }, + ) + assert response.status_code == 422 + + +# def test_post_request_500(client): +# response = client.post( +# "/chamado", +# json={ +# "attendant_name": "Fulano", +# "applicant_name": "Ciclano", +# "applicant_phone": "1111111111", +# "place": "Sala de Reuniões", +# "description": "Chamado aberto para acesso a internet.", +# "workstation_id": 1, +# "problems": [ +# { +# "problem_id": 99, +# "is_event": False, +# "request_status": "pending", +# "priority": "normal", +# }, +# { +# "problem_id": 98, +# "is_event": False, +# "request_status": "pending", +# "priority": "normal", +# }, +# ], +# }, +# ) +# assert response.status_code == 500 diff --git a/tests/test_put_request.py b/tests/test_put_request.py new file mode 100644 index 0000000..0ba445b --- /dev/null +++ b/tests/test_put_request.py @@ -0,0 +1,57 @@ +def test_put_request(client): + response = client.put( + "/chamado/1", + json={ + "applicant_name": "Fulano de Tal", + "applicant_phone": "999999999", + "place": "Sala de Testes", + "description": "Ta tudo dando errado nos testes.", + "workstation_id": 2, + "problems": [ + { + "problem_id": 1, + "is_event": False, + "request_status": "pending", + "priority": "hight", + }, + { + "problem_id": 2, + "is_event": False, + "request_status": "pending", + "priority": "urgent", + }, + ], + }, + ) + assert response.status_code == 200 + assert response.json()["message"] == "Dados atualizados com sucesso" + assert response.json()["data"][0]["applicant_name"] == "Fulano de Tal" + + +def test_put_request_attendant_name(client): + response = client.put( + "/chamado/1", + json={ + "attendant_name": "Fulano", + "applicant_name": "Fulano de Tal", + "applicant_phone": "999999999", + "place": "Sala de Testes", + "description": "Ta tudo dando errado nos testes.", + "workstation_id": 2, + "problems": [ + { + "problem_id": 1, + "is_event": False, + "request_status": "pending", + "priority": "hight", + }, + { + "problem_id": 2, + "is_event": False, + "request_status": "pending", + "priority": "urgent", + }, + ], + }, + ) + assert response.status_code == 400