feat: várias melhorias e evoluções no projeto
This commit is contained in:
@ -1,19 +1,21 @@
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import PyPDF2
|
||||
import requests
|
||||
from babel.dates import format_date
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
import PyPDF2
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
|
||||
class TipoDiarioOficial(models.Model):
|
||||
"""Representa um tipo de Diário Oficial (e.g., Municipal, Estadual, Federal)."""
|
||||
|
||||
nome = models.CharField(max_length=100, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
"""Retorna o nome do tipo de Diário Oficial."""
|
||||
return self.nome
|
||||
|
||||
class Meta:
|
||||
@ -21,6 +23,16 @@ class TipoDiarioOficial(models.Model):
|
||||
|
||||
|
||||
class DiarioOficial(models.Model):
|
||||
"""Modelo que representa um Diário Oficial, contendo data, arquivo PDF, tipo e link.
|
||||
|
||||
Attributes:
|
||||
data (DateField): Data de publicação do Diário Oficial.
|
||||
arquivo (FileField): Arquivo PDF do Diário Oficial (opcional).
|
||||
tipo (ForeignKey): Tipo do Diário Oficial (Municipal, Estadual, etc.).
|
||||
numero (CharField): Número de identificação único do Diário.
|
||||
link (URLField): URL para o Diário Oficial (opcional).
|
||||
"""
|
||||
|
||||
data = models.DateField()
|
||||
arquivo = models.FileField(upload_to="diarios_oficiais/", blank=True, null=True)
|
||||
tipo = models.ForeignKey(
|
||||
@ -32,63 +44,128 @@ class DiarioOficial(models.Model):
|
||||
)
|
||||
numero = models.CharField(max_length=20, unique=True)
|
||||
link = models.URLField(blank=True, null=True, unique=True)
|
||||
page_content = models.JSONField(encoder=DjangoJSONEncoder, blank=True, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Se houver um link, baixa o PDF e extrai o conteúdo
|
||||
if self.link and not self.arquivo:
|
||||
try:
|
||||
# Faz o download do PDF
|
||||
response = requests.get(self.link)
|
||||
response.raise_for_status() # Verifica se o download foi bem-sucedido
|
||||
|
||||
# Define o nome do arquivo a partir do link
|
||||
parsed_url = urlparse(self.link)
|
||||
file_name = (
|
||||
os.path.basename(parsed_url.path) or f"diario_{self.numero}.pdf"
|
||||
)
|
||||
|
||||
# Salva o arquivo no campo `arquivo`
|
||||
self.arquivo.save(file_name, ContentFile(response.content), save=False)
|
||||
|
||||
# Extrai o conteúdo do PDF
|
||||
pdf = PyPDF2.PdfReader(self.arquivo)
|
||||
pages_data = []
|
||||
|
||||
for i, pagina in enumerate(pdf.pages):
|
||||
page_text = pagina.extract_text()
|
||||
if page_text: # Ignora páginas sem conteúdo
|
||||
pages_data.append(
|
||||
{
|
||||
"number": i + 1,
|
||||
"content": page_text,
|
||||
}
|
||||
)
|
||||
|
||||
# Salva o conteúdo das páginas no campo `page_content`
|
||||
self.page_content = pages_data
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"Erro ao baixar o PDF: {e}")
|
||||
except PyPDF2.PdfReadError as e:
|
||||
print(f"Erro ao ler o PDF: {e}")
|
||||
except Exception as e:
|
||||
print(f"Erro inesperado: {e}")
|
||||
|
||||
# Salva o modelo
|
||||
"""Salva o Diário Oficial, baixa o PDF (se houver link) e extrai páginas."""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self.link and not self.arquivo:
|
||||
self._download_pdf_from_link()
|
||||
|
||||
if self.arquivo and not self.paginas:
|
||||
self._extract_pdf_pages()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
"""Valida o modelo antes de salvar (chamado automaticamente no admin/form)."""
|
||||
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):
|
||||
"""Verifica se o link é um PDF válido."""
|
||||
if not self.link.lower().endswith(".pdf"):
|
||||
raise ValidationError("O link deve apontar para um arquivo PDF.")
|
||||
|
||||
def _download_pdf_from_link(self):
|
||||
"""Faz download do PDF a partir do link e salva no campo `arquivo`.
|
||||
|
||||
Raises:
|
||||
ValidationError: Se o download falhar.
|
||||
"""
|
||||
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):
|
||||
"""Extrai o texto de cada página do PDF e salva no modelo `PageDiarioOficial`.
|
||||
|
||||
Raises:
|
||||
ValidationError: Se a extração falhar.
|
||||
"""
|
||||
try:
|
||||
with self.arquivo.open("rb") as pdf_file:
|
||||
pdf = PyPDF2.PdfReader(pdf_file)
|
||||
self._process_pdf_pages(pdf)
|
||||
except Exception as pdf_error:
|
||||
raise ValidationError(f"Não foi possível processar o PDF: {pdf_error}")
|
||||
|
||||
def _process_pdf_pages(self, pdf):
|
||||
"""Processa cada página do PDF e salva seu conteúdo.
|
||||
|
||||
Args:
|
||||
pdf (PdfReader): Objeto PDF carregado.
|
||||
"""
|
||||
self.paginas.all().delete()
|
||||
|
||||
for i, pagina in enumerate(pdf.pages):
|
||||
try:
|
||||
page_text = pagina.extract_text()
|
||||
if page_text and page_text.strip():
|
||||
PageDiarioOficial.objects.create(
|
||||
diario=self,
|
||||
numero=i + 1,
|
||||
conteudo=page_text.strip(),
|
||||
)
|
||||
except Exception as page_error:
|
||||
PageDiarioOficial.objects.create(
|
||||
diario=self,
|
||||
numero=i + 1,
|
||||
conteudo=f"[Erro na extração do texto: {str(page_error)}]",
|
||||
)
|
||||
continue
|
||||
|
||||
@property
|
||||
def data_formatada(self):
|
||||
"""Retorna a data formatada em português (e.g., '1 de Janeiro de 2023')."""
|
||||
return format_date(self.data, format="long", locale="pt_BR")
|
||||
|
||||
@property
|
||||
def is_online(self):
|
||||
return True if self.link else False
|
||||
"""Verifica se o Diário possui um link (online)."""
|
||||
return bool(self.link)
|
||||
|
||||
def __str__(self):
|
||||
return f"Diário {self.tipo.nome} nº {self.numero}, {self.data_formatada}"
|
||||
"""Representação em string do Diário Oficial."""
|
||||
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):
|
||||
"""Representa uma página de um Diário Oficial com seu conteúdo textual.
|
||||
|
||||
Attributes:
|
||||
diario (ForeignKey): Diário Oficial associado.
|
||||
layout_duas_colunas (BooleanField): Indica se a página tem duas colunas.
|
||||
numero (PositiveIntegerField): Número da página no Diário.
|
||||
conteudo (TextField): Texto extraído da página.
|
||||
"""
|
||||
|
||||
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 __str__(self):
|
||||
"""Representação em string da página (e.g., 'Página 1 do Diário 123')."""
|
||||
return f"Página {self.numero} do Diário {self.diario.numero}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user