From a04319d648254e1d4d9417a623d7465e91a67585 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Wed, 23 Sep 2026 20:06:53 -0300 Subject: [PATCH 1/2] =?UTF-8?q?feat/jobs:=20implementa=20l=C3=B3gica=20par?= =?UTF-8?q?a=20determinar=20a=20pasta=20de=20armazenamento=20no=20Cloudina?= =?UTF-8?q?ry=20com=20base=20na=20empresa=20e=20ciclo=20do=20anexo,=20al?= =?UTF-8?q?=C3=A9m=20de=20atualizar=20a=20fun=C3=A7=C3=A3o=20de=20processa?= =?UTF-8?q?mento=20de=20uploads=20para=20usar=20essa=20nova=20l=C3=B3gica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- app/services/jobs.py | 35 ++++++++++++++++++++++++++++++++++- tests/test_jobs.py | 20 ++++++++++++++------ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index d276bf8..a56c37d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.env docs/ *.pyc -tests/.*/ \ No newline at end of file +tests/.*/ +tests\.tmp_arquivo.pdf \ No newline at end of file diff --git a/app/services/jobs.py b/app/services/jobs.py index ab67d07..206dc83 100644 --- a/app/services/jobs.py +++ b/app/services/jobs.py @@ -1,5 +1,8 @@ +import re +import unicodedata from pathlib import Path from tempfile import NamedTemporaryFile +from uuid import NAMESPACE_URL, uuid5 from rq import get_current_job from app.database.postgres import conn_worker from app.models.enums import Status @@ -17,7 +20,14 @@ def processar_upload(job: UploadJob) -> None: caminho_temp = arquivo_temp.name _atualizar_status(conn, job.id_anexo, job.id_usuario, Status.PROCESSANDO) - resultado = enviar_cloudinary(caminho_temp, job.nome_original, job.extensao, public_id=f'anexos/{job.id_anexo}') + pasta = _obter_pasta_cloudinary(conn, job.id_anexo) + resultado = enviar_cloudinary( + caminho_temp, + job.nome_original, + job.extensao, + pasta=pasta, + public_id=str(job.id_anexo) + ) _atualizar_sucesso(conn, job.id_anexo, job.id_usuario, resultado) except Exception: @@ -35,6 +45,29 @@ def _configurar_usuario(conn, id_usuario: int) -> None: with conn.cursor() as cursor: cursor.execute("SELECT set_config('app.current_user_id', %s, true)", (str(id_usuario),),) +def _obter_pasta_cloudinary(conn, id_anexo: int) -> str: + with conn.cursor() as cursor: + cursor.execute( + """ + SELECT e.id, e.nome, a.id_ciclo + FROM pdca.anexo a + JOIN public.empresa e ON e.id = a.id_empresa + WHERE a.id = %s + """, + (id_anexo,) + ) + empresa = cursor.fetchone() + + if empresa is None: + raise ValueError('Empresa do anexo não encontrada') + + id_empresa, nome_empresa, id_ciclo = empresa + nome_normalizado = unicodedata.normalize('NFKD', nome_empresa).encode('ascii', 'ignore').decode().lower() + nome_normalizado = re.sub(r'[^a-z0-9]+', '-', nome_normalizado).strip('-') + uuid_curto = uuid5(NAMESPACE_URL, f'acta:empresa:{id_empresa}').hex[:8] + + return f'acta-arquivos/{nome_normalizado}-{uuid_curto}/ciclo-{id_ciclo}' + def _atualizar_status(conn, id_anexo: int, id_usuario: int, status: Status) -> None: _configurar_usuario(conn, id_usuario) with conn.cursor() as cursor: diff --git a/tests/test_jobs.py b/tests/test_jobs.py index a537810..1700d1e 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,11 +1,13 @@ -from pathlib import Path +from unittest.mock import Mock + from app.models.enums import Status from app.schemas import UploadJob from app.services import jobs class Cursor: - def __init__(self): + def __init__(self, retorno=(5, "Instituto J&F", 7)): self.executados = [] + self.retorno = retorno def __enter__(self): return self @@ -16,6 +18,9 @@ def __exit__(self, *args): def execute(self, sql, parametros): self.executados.append((sql, parametros)) + def fetchone(self): + return self.retorno + class Connection: def __init__(self): self.cursor_obj = Cursor() @@ -28,16 +33,19 @@ def commit(self): self.commits += 1 def test_processa_upload_com_sucesso(monkeypatch): - caminho = Path("tests/.tmp_arquivo.pdf") - caminho.write_bytes(b"pdf") conexao = Connection() job = UploadJob(id_anexo=42, id_usuario=3, conteudo=b"pdf", nome_original="arquivo.pdf", extensao="pdf") + enviar_cloudinary = Mock(return_value={"url": "https://arquivo", "public_id": "42", "resource_type": "image"}) monkeypatch.setattr(jobs, "conn_worker", lambda: _contexto(conexao)) - monkeypatch.setattr(jobs, "enviar_cloudinary", lambda *args, **kwargs: {"url": "https://arquivo", "public_id": "42", "resource_type": "image"}) + monkeypatch.setattr(jobs, "enviar_cloudinary", enviar_cloudinary) jobs.processar_upload(job) assert any(Status.ATIVO.value in parametros for _, parametros in conexao.cursor_obj.executados) + assert enviar_cloudinary.call_args.kwargs == { + "pasta": "acta-arquivos/instituto-j-f-f6ce836f/ciclo-7", + "public_id": "42" + } class _contexto: def __init__(self, valor): @@ -47,4 +55,4 @@ def __enter__(self): return self.valor def __exit__(self, *args): - return False \ No newline at end of file + return False From 81d1cd450197f50d56cfb15715902060c2a67021 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Wed, 23 Sep 2026 20:12:03 -0300 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20atualiza=20a=20se=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20armazenamento=20de=20anexos=20no=20README=20para=20incluir?= =?UTF-8?q?=20a=20nova=20estrutura=20de=20diret=C3=B3rios=20e=20normaliza?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20nomes=20de=20empresas=20com=20UUIDs=20curt?= =?UTF-8?q?os?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6e3132b..1e1433f 100644 --- a/README.md +++ b/README.md @@ -241,11 +241,14 @@ O worker: - atualiza o status do anexo; - envia o arquivo ao Cloudinary; +- organiza o arquivo em `acta-arquivos/{nome-empresa}-{uuid-curto}/ciclo-{id_ciclo}/{id_anexo}`; - salva o `secure_url` retornado; - altera o status para `ATIVO` em caso de sucesso; - altera o status para `ERRO` em caso de falha definitiva; - remove o arquivo temporário após o processamento concluído. +O nome da empresa é normalizado e recebe um UUID curto e estável, mantendo os anexos separados por empresa e ciclo. + Falhas de enfileiramento retornam `503`. Arquivos vazios retornam `400` e arquivos acima de 10 MB retornam `413`. ## 🧪 Testes