adiciona o aplicativo de diarios
This commit is contained in:
167
diarios/models.py
Normal file
167
diarios/models.py
Normal file
@ -0,0 +1,167 @@
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
from babel.dates import format_date
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models, transaction
|
||||
from django.core.exceptions import ValidationError
|
||||
import fitz # PyMuPDF
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
|
||||
class TipoDiarioOficial(models.Model):
|
||||
nome = models.CharField(max_length=100, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.nome
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Tipos de Diários Oficiais"
|
||||
|
||||
|
||||
class DiarioOficial(models.Model):
|
||||
data = models.DateField()
|
||||
arquivo = models.FileField(upload_to="diarios_oficiais/", blank=True, null=True)
|
||||
tipo = models.ForeignKey(
|
||||
TipoDiarioOficial,
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="diarios",
|
||||
)
|
||||
numero = models.CharField(max_length=20, unique=True)
|
||||
link = models.URLField(blank=True, null=True, unique=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
updated = False
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self.link and not self.arquivo:
|
||||
self._download_pdf_from_link()
|
||||
updated = True
|
||||
|
||||
if self.arquivo and not self.paginas.exists():
|
||||
self._extract_pdf_pages()
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.arquivo and not self.link:
|
||||
raise ValidationError("Informe um arquivo ou um link para o Diário.")
|
||||
|
||||
def _validar_link(self):
|
||||
if not self.link.lower().endswith(".pdf"):
|
||||
raise ValidationError("O link deve apontar para um arquivo PDF.")
|
||||
|
||||
def _download_pdf_from_link(self):
|
||||
try:
|
||||
response = requests.get(self.link)
|
||||
response.raise_for_status()
|
||||
|
||||
parsed_url = urlparse(self.link)
|
||||
file_name = os.path.basename(parsed_url.path) or f"diario_{self.numero}.pdf"
|
||||
|
||||
self.arquivo.save(file_name, ContentFile(response.content), save=True)
|
||||
except requests.RequestException as e:
|
||||
raise ValidationError(f"Não foi possível baixar o PDF: {e}")
|
||||
|
||||
def _extract_pdf_pages(self):
|
||||
try:
|
||||
# Salvar temporariamente o PDF para abrir com o PyMuPDF
|
||||
with self.arquivo.open("rb") as pdf_file:
|
||||
temp_pdf_path = f"/tmp/diario_{self.id}.pdf"
|
||||
with open(temp_pdf_path, "wb") as temp_file:
|
||||
temp_file.write(pdf_file.read())
|
||||
|
||||
# Abrir e processar com fitz
|
||||
doc = fitz.open(temp_pdf_path)
|
||||
self._process_pdf_pages(doc)
|
||||
doc.close()
|
||||
|
||||
# Remover arquivo temporário
|
||||
os.remove(temp_pdf_path)
|
||||
|
||||
except Exception as pdf_error:
|
||||
raise ValidationError(f"Não foi possível processar o PDF: {pdf_error}")
|
||||
|
||||
def _process_pdf_pages(self, doc):
|
||||
with transaction.atomic():
|
||||
self.paginas.all().delete()
|
||||
|
||||
for i, page in enumerate(doc):
|
||||
try:
|
||||
blocks = page.get_text("blocks")
|
||||
# Ordenar os blocos por coordenadas (y, x) para manter a ordem de leitura
|
||||
blocks.sort(key=lambda b: (b[1], b[0]))
|
||||
page_text = ""
|
||||
for block in blocks:
|
||||
text = block[4].strip()
|
||||
if text:
|
||||
page_text += text + "\n"
|
||||
|
||||
# Crucial: Remove NULL bytes from the extracted text
|
||||
# PostgreSQL text fields cannot contain NUL (0x00) bytes
|
||||
cleaned_text = page_text.strip().replace('\x00', '')
|
||||
|
||||
if cleaned_text:
|
||||
PageDiarioOficial.objects.create(
|
||||
diario=self,
|
||||
numero=i + 1,
|
||||
conteudo=cleaned_text,
|
||||
)
|
||||
else:
|
||||
PageDiarioOficial.objects.create(
|
||||
diario=self,
|
||||
numero=i + 1,
|
||||
conteudo="[Conteúdo não extraído ou vazio]",
|
||||
)
|
||||
|
||||
except Exception as page_error:
|
||||
PageDiarioOficial.objects.create(
|
||||
diario=self,
|
||||
numero=i + 1,
|
||||
conteudo=f"[Erro na extração do texto: {str(page_error)}]",
|
||||
)
|
||||
print(f"Erro ao processar a página {i+1} no Diario ID {self.id}: {page_error}")
|
||||
|
||||
@property
|
||||
def data_formatada(self):
|
||||
return format_date(self.data, format="long", locale="pt_BR")
|
||||
|
||||
@property
|
||||
def is_online(self):
|
||||
return bool(self.link)
|
||||
|
||||
def __str__(self):
|
||||
tipo_nome = self.tipo.nome if self.tipo else "Sem Tipo"
|
||||
return f"Diário {tipo_nome} nº {self.numero}, {self.data_formatada}"
|
||||
|
||||
class Meta:
|
||||
constraints = [models.UniqueConstraint(fields=["numero"], name="unique_numero")]
|
||||
verbose_name_plural = "Diários Oficiais"
|
||||
|
||||
|
||||
class PageDiarioOficial(models.Model):
|
||||
diario = models.ForeignKey(
|
||||
DiarioOficial, on_delete=models.CASCADE, related_name="paginas"
|
||||
)
|
||||
layout_duas_colunas = models.BooleanField(default=False)
|
||||
numero = models.PositiveIntegerField()
|
||||
conteudo = models.TextField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("diario", "numero")
|
||||
verbose_name = "Página de Diário Oficial"
|
||||
verbose_name_plural = "Páginas de Diários Oficiais"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.diario:
|
||||
self.diario.save()
|
||||
|
||||
def __str__(self):
|
||||
return f"Página {self.numero} do Diário {self.diario.numero}"
|
||||
|
||||
Reference in New Issue
Block a user