Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.env
docs/
*.pyc
tests/.*/
tests/.*/
tests\.tmp_arquivo.pdf
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion app/services/jobs.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down
20 changes: 14 additions & 6 deletions tests/test_jobs.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -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):
Expand All @@ -47,4 +55,4 @@ def __enter__(self):
return self.valor

def __exit__(self, *args):
return False
return False
Loading