Compare commits
9 Commits
c17f7c35f5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 22aa2efdeb | |||
| 75a3a68bec | |||
| 04517ea6ef | |||
| 46c83af8c1 | |||
| 8a4a945b1c | |||
| f5ceea0dae | |||
| 23e19c3ee1 | |||
| 6e8e24caeb | |||
| 28b12c5ab9 |
@ -4,13 +4,33 @@ set -o errexit
|
|||||||
set -o pipefail
|
set -o pipefail
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
# Garanta que os diretórios existam
|
echo "🧹 Limpando arquivos antigos..."
|
||||||
mkdir -p /app/staticfiles
|
|
||||||
mkdir -p /app/media
|
|
||||||
|
|
||||||
python /app/manage.py collectstatic --noinput --clear
|
# Garante que diretórios existem
|
||||||
|
mkdir -p /app/staticfiles
|
||||||
|
mkdir -p /app/diarios_oficiais_alems/media
|
||||||
|
|
||||||
|
# Limpa manualmente a pasta de arquivos estáticos
|
||||||
|
echo "🗑️ Removendo arquivos antigos de /app/staticfiles"
|
||||||
|
rm -rf /app/staticfiles/*
|
||||||
|
|
||||||
|
# Apaga cache de compressão
|
||||||
|
echo "🗑️ Removendo CACHE de django-compressor"
|
||||||
|
find /app/diarios_oficiais_search_alems/static -type d -name "CACHE" -exec rm -rf {} +
|
||||||
|
|
||||||
|
# Coleta arquivos estáticos novamente
|
||||||
|
echo "📦 Coletando arquivos estáticos..."
|
||||||
|
python /app/manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Comprime arquivos estáticos novamente
|
||||||
|
echo "🔧 Comprimindo arquivos estáticos..."
|
||||||
python /app/manage.py compress --force
|
python /app/manage.py compress --force
|
||||||
|
|
||||||
|
# Aplica migrações
|
||||||
|
echo "🗃️ Aplicando migrações..."
|
||||||
python /app/manage.py migrate
|
python /app/manage.py migrate
|
||||||
|
|
||||||
exec uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload
|
# Inicia o servidor
|
||||||
# exec python manage.py runserver 0.0.0.0:8000
|
echo "🚀 Iniciando servidor..."
|
||||||
|
exec uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --workers 4
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ if READ_DOT_ENV_FILE:
|
|||||||
# GENERAL
|
# GENERAL
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
|
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
|
||||||
DEBUG = env.bool("DJANGO_DEBUG", False)
|
DEBUG = env.bool("DJANGO_DEBUG", True)
|
||||||
# Local time zone. Choices are
|
# Local time zone. Choices are
|
||||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||||
# though not all of them may be available with every OS.
|
# though not all of them may be available with every OS.
|
||||||
@ -177,6 +177,7 @@ TEMPLATES = [
|
|||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
|
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
|
||||||
|
"string_if_invalid": "INVALID_TEMPLATE_VAR",
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
"django.template.context_processors.debug",
|
"django.template.context_processors.debug",
|
||||||
"django.template.context_processors.request",
|
"django.template.context_processors.request",
|
||||||
@ -210,10 +211,7 @@ FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),)
|
|||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
|
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
|
||||||
CSRF_COOKIE_HTTPONLY = True
|
CSRF_COOKIE_HTTPONLY = True
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = env.list("DJANGO_CSRF_TRUSTED_ORIGINS")
|
||||||
'http://localhost',
|
|
||||||
'http://192.168.235.234',
|
|
||||||
]
|
|
||||||
|
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
|
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
|
||||||
X_FRAME_OPTIONS = "DENY"
|
X_FRAME_OPTIONS = "DENY"
|
||||||
@ -298,7 +296,7 @@ ELASTICSEARCH_PASSWORD = env.str("ELASTICSEARCH_PASSWORD")
|
|||||||
ELASTICSEARCH_DSL = {
|
ELASTICSEARCH_DSL = {
|
||||||
"default": {
|
"default": {
|
||||||
"hosts": "http://elasticsearch:9200",
|
"hosts": "http://elasticsearch:9200",
|
||||||
"timeout": 60,
|
"timeout": 600,
|
||||||
"http_auth": (
|
"http_auth": (
|
||||||
ELASTICSEARCH_USER,
|
ELASTICSEARCH_USER,
|
||||||
ELASTICSEARCH_PASSWORD,
|
ELASTICSEARCH_PASSWORD,
|
||||||
@ -347,3 +345,5 @@ ELASTICSEARCH_INDEX_SETTINGS = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
API_BASE_URL = env.str("DJANGO_API_BASE_URL")
|
||||||
|
|||||||
@ -13,7 +13,7 @@ SECRET_KEY = env(
|
|||||||
"DJANGO_SECRET_KEY",
|
"DJANGO_SECRET_KEY",
|
||||||
)
|
)
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "192.168.235.234"] # noqa: S104
|
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS")
|
||||||
|
|
||||||
# CACHES
|
# CACHES
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|||||||
@ -20,13 +20,16 @@ urlpatterns = [
|
|||||||
# Django Admin, use {% url 'admin:index' %}
|
# Django Admin, use {% url 'admin:index' %}
|
||||||
path(settings.ADMIN_URL, admin.site.urls),
|
path(settings.ADMIN_URL, admin.site.urls),
|
||||||
# User management
|
# User management
|
||||||
# path("users/", include("diarios_oficiais_search_alems.users.urls", namespace="users")),
|
path("users/", include("diarios_oficiais_search_alems.users.urls", namespace="users")),
|
||||||
# path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
# Your stuff: custom urls includes go here
|
# Your stuff: custom urls includes go here
|
||||||
path("api/v1/", api.urls),
|
path("api/v1/", api.urls),
|
||||||
# Media files
|
# Media files
|
||||||
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
# Static file serving when using Gunicorn + Uvicorn for local web socket development
|
# Static file serving when using Gunicorn + Uvicorn for local web socket development
|
||||||
urlpatterns += staticfiles_urlpatterns()
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
|||||||
@ -3,16 +3,21 @@ from urllib.parse import urlparse
|
|||||||
import requests
|
import requests
|
||||||
from babel.dates import format_date
|
from babel.dates import format_date
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.db import models, transaction
|
from django.db import models
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
import fitz # PyMuPDF
|
import PyPDF2
|
||||||
|
import pdfplumber
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
|
import fitz
|
||||||
|
|
||||||
|
|
||||||
class TipoDiarioOficial(models.Model):
|
class TipoDiarioOficial(models.Model):
|
||||||
|
"""Representa um tipo de Diário Oficial (e.g., Municipal, Estadual, Federal)."""
|
||||||
|
|
||||||
nome = models.CharField(max_length=100, unique=True)
|
nome = models.CharField(max_length=100, unique=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""Retorna o nome do tipo de Diário Oficial."""
|
||||||
return self.nome
|
return self.nome
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -20,6 +25,16 @@ class TipoDiarioOficial(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class DiarioOficial(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()
|
data = models.DateField()
|
||||||
arquivo = models.FileField(upload_to="diarios_oficiais/", blank=True, null=True)
|
arquivo = models.FileField(upload_to="diarios_oficiais/", blank=True, null=True)
|
||||||
tipo = models.ForeignKey(
|
tipo = models.ForeignKey(
|
||||||
@ -33,30 +48,34 @@ class DiarioOficial(models.Model):
|
|||||||
link = models.URLField(blank=True, null=True, unique=True)
|
link = models.URLField(blank=True, null=True, unique=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
updated = False
|
"""Salva o Diário Oficial, baixa o PDF (se houver link) e extrai páginas."""
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if self.link and not self.arquivo:
|
if self.link and not self.arquivo:
|
||||||
self._download_pdf_from_link()
|
self._download_pdf_from_link()
|
||||||
updated = True
|
|
||||||
|
|
||||||
if self.arquivo and not self.paginas.exists():
|
if self.arquivo and not self.paginas:
|
||||||
self._extract_pdf_pages()
|
self._extract_pdf_pages()
|
||||||
updated = True
|
|
||||||
|
|
||||||
if updated:
|
super().save(*args, **kwargs)
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
"""Valida o modelo antes de salvar (chamado automaticamente no admin/form)."""
|
||||||
super().clean()
|
super().clean()
|
||||||
if not self.arquivo and not self.link:
|
if not self.arquivo and not self.link:
|
||||||
raise ValidationError("Informe um arquivo ou um link para o Diário.")
|
raise ValidationError("Informe um arquivo ou um link para o Diário.")
|
||||||
|
|
||||||
def _validar_link(self):
|
def _validar_link(self):
|
||||||
|
"""Verifica se o link é um PDF válido."""
|
||||||
if not self.link.lower().endswith(".pdf"):
|
if not self.link.lower().endswith(".pdf"):
|
||||||
raise ValidationError("O link deve apontar para um arquivo PDF.")
|
raise ValidationError("O link deve apontar para um arquivo PDF.")
|
||||||
|
|
||||||
def _download_pdf_from_link(self):
|
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:
|
try:
|
||||||
response = requests.get(self.link)
|
response = requests.get(self.link)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@ -68,74 +87,77 @@ class DiarioOficial(models.Model):
|
|||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise ValidationError(f"Não foi possível baixar o PDF: {e}")
|
raise ValidationError(f"Não foi possível baixar o PDF: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _extract_pdf_pages(self):
|
def _extract_pdf_pages(self):
|
||||||
|
"""Extrai o texto de cada página do PDF usando PyMuPDF."""
|
||||||
try:
|
try:
|
||||||
# Salvar temporariamente o PDF para abrir com o PyMuPDF
|
|
||||||
with self.arquivo.open("rb") as pdf_file:
|
with self.arquivo.open("rb") as pdf_file:
|
||||||
temp_pdf_path = f"/tmp/diario_{self.id}.pdf"
|
pdf_document = fitz.open(stream=pdf_file.read(), filetype="pdf")
|
||||||
with open(temp_pdf_path, "wb") as temp_file:
|
self.paginas.all().delete()
|
||||||
temp_file.write(pdf_file.read())
|
|
||||||
|
|
||||||
# Abrir e processar com fitz
|
for i, page in enumerate(pdf_document):
|
||||||
doc = fitz.open(temp_pdf_path)
|
try:
|
||||||
self._process_pdf_pages(doc)
|
# Extração simples
|
||||||
doc.close()
|
page_text = page.get_text("text")
|
||||||
|
|
||||||
|
# Você pode experimentar com outros métodos:
|
||||||
|
# page.get_text("blocks")
|
||||||
|
# page.get_text("words")
|
||||||
|
# para melhor tratamento de duas colunas
|
||||||
|
|
||||||
# Remover arquivo temporário
|
if page_text and page_text.strip():
|
||||||
os.remove(temp_pdf_path)
|
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)}]",
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as pdf_error:
|
except Exception as pdf_error:
|
||||||
raise ValidationError(f"Não foi possível processar o PDF: {pdf_error}")
|
raise ValidationError(f"Não foi possível processar o PDF: {pdf_error}")
|
||||||
|
|
||||||
def _process_pdf_pages(self, doc):
|
def _process_pdf_pages(self, pdf):
|
||||||
with transaction.atomic():
|
"""Processa cada página do PDF e salva seu conteúdo.
|
||||||
self.paginas.all().delete()
|
|
||||||
|
|
||||||
for i, page in enumerate(doc):
|
Args:
|
||||||
try:
|
pdf (PdfReader): Objeto PDF carregado.
|
||||||
blocks = page.get_text("blocks")
|
"""
|
||||||
# Ordenar os blocos por coordenadas (y, x) para manter a ordem de leitura
|
self.paginas.all().delete()
|
||||||
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
|
for i, pagina in enumerate(pdf.pages):
|
||||||
# PostgreSQL text fields cannot contain NUL (0x00) bytes
|
try:
|
||||||
cleaned_text = page_text.strip().replace('\x00', '')
|
page_text = pagina.extract_text()
|
||||||
|
if page_text and page_text.strip():
|
||||||
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(
|
PageDiarioOficial.objects.create(
|
||||||
diario=self,
|
diario=self,
|
||||||
numero=i + 1,
|
numero=i + 1,
|
||||||
conteudo=f"[Erro na extração do texto: {str(page_error)}]",
|
conteudo=page_text.strip(),
|
||||||
)
|
)
|
||||||
print(f"Erro ao processar a página {i+1} no Diario ID {self.id}: {page_error}")
|
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
|
@property
|
||||||
def data_formatada(self):
|
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")
|
return format_date(self.data, format="long", locale="pt_BR")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_online(self):
|
def is_online(self):
|
||||||
|
"""Verifica se o Diário possui um link (online)."""
|
||||||
return bool(self.link)
|
return bool(self.link)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""Representação em string do Diário Oficial."""
|
||||||
tipo_nome = self.tipo.nome if self.tipo else "Sem Tipo"
|
tipo_nome = self.tipo.nome if self.tipo else "Sem Tipo"
|
||||||
return f"Diário {tipo_nome} nº {self.numero}, {self.data_formatada}"
|
return f"Diário {tipo_nome} nº {self.numero}, {self.data_formatada}"
|
||||||
|
|
||||||
@ -145,6 +167,15 @@ class DiarioOficial(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class PageDiarioOficial(models.Model):
|
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(
|
diario = models.ForeignKey(
|
||||||
DiarioOficial, on_delete=models.CASCADE, related_name="paginas"
|
DiarioOficial, on_delete=models.CASCADE, related_name="paginas"
|
||||||
)
|
)
|
||||||
@ -157,11 +188,17 @@ class PageDiarioOficial(models.Model):
|
|||||||
verbose_name = "Página de Diário Oficial"
|
verbose_name = "Página de Diário Oficial"
|
||||||
verbose_name_plural = "Páginas de Diários Oficiais"
|
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):
|
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}"
|
return f"Página {self.numero} do Diário {self.diario.numero}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
from diarios.documents import DiarioOficialDocument
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
try:
|
||||||
|
print(f"Reindexando diario {self.diario}")
|
||||||
|
DiarioOficialDocument().update(self.diario)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao reindexar DiarioOficial {self.diario.id} (pai da página {self.id}) no Elasticsearch: {e}")
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,10 @@
|
|||||||
<!-- Alpine.js -->
|
<!-- Alpine.js -->
|
||||||
<script defer src="{% static 'js/alpine.min.js' %}"></script>
|
<script defer src="{% static 'js/alpine.min.js' %}"></script>
|
||||||
<!-- Estilos customizados -->
|
<!-- Estilos customizados -->
|
||||||
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
|
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||||
<link rel="icon" href="{% static 'images/favicon.ico' %}" type="image/x-icon">
|
<link rel="icon" href="{% static 'images/favicons/favicon.ico' %}" type="image/x-icon">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light-gradient">
|
<body>
|
||||||
<div class="container py-5" x-data="searchApp"> <!-- Container do Logo e Imagem Direita -->
|
<div class="container py-5" x-data="searchApp"> <!-- Container do Logo e Imagem Direita -->
|
||||||
<div class="row justify-content-center mb-4">
|
<div class="row justify-content-center mb-4">
|
||||||
<div class="col-12 col-lg-10">
|
<div class="col-12 col-lg-10">
|
||||||
@ -386,9 +386,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Bootstrap Bundle with Popper -->
|
<!-- Bootstrap Bundle with Popper -->
|
||||||
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
|
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
|
||||||
|
<script defer src="{% static 'js/alpine.min.js' %}"></script>
|
||||||
<!-- Script da aplicação -->
|
<!-- Script da aplicação -->
|
||||||
<script src="{% static 'js/config.js' %}"></script>
|
<script> window.API_BASE_URL = "{{ API_BASE_URL }}"; </script>
|
||||||
<script src="{% static 'js/script.js' %}"></script>
|
<script src="{% static 'js/script.js'%}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@ -9,11 +9,14 @@ from .search_service import (
|
|||||||
)
|
)
|
||||||
from .schemas import BuscaDiariosResponseSchema, SugestaoResponse, DiarioOficialIn, DiarioOficialOut
|
from .schemas import BuscaDiariosResponseSchema, SugestaoResponse, DiarioOficialIn, DiarioOficialOut
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
router = Router(tags=["Diários Oficiais"])
|
router = Router(tags=["Diários Oficiais"])
|
||||||
|
|
||||||
async def home(request):
|
async def home(request):
|
||||||
return render(request, 'diarios/index.html')
|
context = {"API_BASE_URL": settings.API_BASE_URL,}
|
||||||
|
return render(request, 'diarios/index.html', context)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/sugestao",
|
"/sugestao",
|
||||||
@ -110,4 +113,4 @@ def criar_diario(
|
|||||||
tipo_id=payload.tipo_id,
|
tipo_id=payload.tipo_id,
|
||||||
arquivo=arquivo_final,
|
arquivo=arquivo_final,
|
||||||
link=payload.link
|
link=payload.link
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,3 @@
|
|||||||
.bg-light-gradient {
|
|
||||||
background: linear-gradient(to right, #f8f9fa, #e9ecef);
|
|
||||||
}
|
|
||||||
.search-card {
|
.search-card {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
const API_BASE_URL = "http://109.199.98.226";
|
|
||||||
@ -1,242 +1,241 @@
|
|||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
Alpine.data('searchApp', () => ({
|
Alpine.data('searchApp', () => ({
|
||||||
searchParams: {
|
searchParams: {
|
||||||
q: '',
|
q: '',
|
||||||
numero_diario: '',
|
numero_diario: '',
|
||||||
data_inicio: '',
|
data_inicio: '',
|
||||||
data_fim: '',
|
data_fim: '',
|
||||||
modo_busca: 'exata',
|
modo_busca: 'exata',
|
||||||
ordenar_por: 'data_asc',
|
ordenar_por: 'data_asc',
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 10
|
page_size: 10
|
||||||
},
|
},
|
||||||
searchResults: null,
|
searchResults: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasSearched: false,
|
hasSearched: false,
|
||||||
error: null,
|
error: null,
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
expandedContents: {},
|
expandedContents: {},
|
||||||
suggestion: null,
|
suggestion: null,
|
||||||
ultimoTermoBuscado: '',
|
ultimoTermoBuscado: '',
|
||||||
|
|
||||||
get shouldShowSuggestion() {
|
get shouldShowSuggestion() {
|
||||||
if (!this.suggestion || !this.searchParams.q) return false;
|
if (!this.suggestion || !this.searchParams.q) return false;
|
||||||
|
|
||||||
// Função para remover acentos e converter para minúsculas
|
// Função para remover acentos e converter para minúsculas
|
||||||
const normalize = (text) => {
|
const normalize = (text) => {
|
||||||
return text.toLowerCase()
|
return text.toLowerCase()
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace("/", " ")
|
.replace("/", " ")
|
||||||
.replace(/[^a-z0-9\s]/g, " ") // Substitui todos os outros símbolos por espaço
|
.replace(/[^a-z0-9\s]/g, " ") // Substitui todos os outros símbolos por espaço
|
||||||
.replace(/[\u0300-\u036f]/g, '');
|
.replace(/[\u0300-\u036f]/g, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizedQuery = normalize(this.searchParams.q);
|
const normalizedQuery = normalize(this.searchParams.q);
|
||||||
const normalizedSuggestion = normalize(this.suggestion);
|
const normalizedSuggestion = normalize(this.suggestion);
|
||||||
|
|
||||||
// Só mostra a sugestão se for diferente do termo buscado (ignorando acentos e caixa)
|
// Só mostra a sugestão se for diferente do termo buscado (ignorando acentos e caixa)
|
||||||
return normalizedQuery !== normalizedSuggestion;
|
return normalizedQuery !== normalizedSuggestion;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Usa a sugestão como novo termo de busca
|
// Usa a sugestão como novo termo de busca
|
||||||
usesuggestion() {
|
usesuggestion() {
|
||||||
this.searchParams.q = this.suggestion;
|
this.searchParams.q = this.suggestion;
|
||||||
this.searchParams.page = 1;
|
this.searchParams.page = 1;
|
||||||
this.performSearch();
|
this.performSearch();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Verifica se um diário tem uma melhor correspondência
|
// Verifica se um diário tem uma melhor correspondência
|
||||||
hasBestMatch(diario) {
|
hasBestMatch(diario) {
|
||||||
return diario.paginas && diario.paginas.length > 0;
|
return diario.paginas && diario.paginas.length > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Muda a ordenação e faz uma nova busca
|
// Muda a ordenação e faz uma nova busca
|
||||||
changeOrder(order) {
|
changeOrder(order) {
|
||||||
if (this.searchParams.ordenar_por !== order) {
|
if (this.searchParams.ordenar_por !== order) {
|
||||||
this.searchParams.ordenar_por = order;
|
this.searchParams.ordenar_por = order;
|
||||||
this.searchParams.page = 1; // Volta para a primeira página
|
this.searchParams.page = 1; // Volta para a primeira página
|
||||||
this.performSearch();
|
this.performSearch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Obter sugestão da API
|
// Obter sugestão da API
|
||||||
async getSuggestion(query) {
|
async getSuggestion(query) {
|
||||||
if (!query) return null;
|
if (!query) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL('http://192.168.235.234/api/v1/diarios/sugestao');
|
const url = new URL(`${window.API_BASE_URL}/api/v1/diarios/sugestao`);
|
||||||
url.searchParams.append('q', query);
|
url.searchParams.append('q', query);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.sugestao;
|
return data.sugestao;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao buscar sugestão:', error);
|
console.error('Erro ao buscar sugestão:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
get totalPages() {
|
get totalPages() {
|
||||||
if (!this.searchResults) return 0;
|
if (!this.searchResults) return 0;
|
||||||
return Math.ceil(this.searchResults.total / this.searchResults.por_pagina);
|
return Math.ceil(this.searchResults.total / this.searchResults.por_pagina);
|
||||||
},
|
},
|
||||||
|
|
||||||
get paginationArray() {
|
get paginationArray() {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
const currentPage = this.searchParams.page;
|
const currentPage = this.searchParams.page;
|
||||||
const totalPages = this.totalPages;
|
const totalPages = this.totalPages;
|
||||||
|
|
||||||
// Função auxiliar para adicionar páginas
|
// Função auxiliar para adicionar páginas
|
||||||
const addPage = (page) => {
|
const addPage = (page) => {
|
||||||
if (page >= 1 && page <= totalPages && !pages.includes(page)) {
|
if (page >= 1 && page <= totalPages && !pages.includes(page)) {
|
||||||
pages.push(page);
|
pages.push(page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sempre mostrar primeira página, página atual, última página
|
// Sempre mostrar primeira página, página atual, última página
|
||||||
// e 1-2 páginas adjacentes à página atual
|
// e 1-2 páginas adjacentes à página atual
|
||||||
addPage(1);
|
addPage(1);
|
||||||
addPage(currentPage - 2);
|
addPage(currentPage - 2);
|
||||||
addPage(currentPage - 1);
|
addPage(currentPage - 1);
|
||||||
addPage(currentPage);
|
addPage(currentPage);
|
||||||
addPage(currentPage + 1);
|
addPage(currentPage + 1);
|
||||||
addPage(currentPage + 2);
|
addPage(currentPage + 2);
|
||||||
addPage(totalPages);
|
addPage(totalPages);
|
||||||
|
|
||||||
// Ordenar e adicionar separadores
|
// Ordenar e adicionar separadores
|
||||||
const result = pages.sort((a, b) => a - b);
|
const result = pages.sort((a, b) => a - b);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Retorna a melhor página (maior score) de um diário
|
// Retorna a melhor página (maior score) de um diário
|
||||||
getBestPage(paginas) {
|
getBestPage(paginas) {
|
||||||
if (!paginas || paginas.length === 0) return null;
|
if (!paginas || paginas.length === 0) return null;
|
||||||
|
|
||||||
// Ordena as páginas por score (se disponível) ou pelo número da página se não houver score
|
// Ordena as páginas por score (se disponível) ou pelo número da página se não houver score
|
||||||
const sortedPages = [...paginas].sort((a, b) => {
|
const sortedPages = [...paginas].sort((a, b) => {
|
||||||
if (a.score === undefined || b.score === undefined) return 0;
|
if (a.score === undefined || b.score === undefined) return 0;
|
||||||
if (a.score === undefined) return 1;
|
if (a.score === undefined) return 1;
|
||||||
if (b.score === undefined) return -1;
|
if (b.score === undefined) return -1;
|
||||||
return b.score - a.score;
|
return b.score - a.score;
|
||||||
});
|
});
|
||||||
|
|
||||||
return sortedPages[0];
|
return sortedPages[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Retorna todas as páginas exceto a melhor
|
// Retorna todas as páginas exceto a melhor
|
||||||
getOtherPages(paginas) {
|
getOtherPages(paginas) {
|
||||||
if (!paginas || paginas.length <= 1) return [];
|
if (!paginas || paginas.length <= 1) return [];
|
||||||
|
|
||||||
const bestPage = this.getBestPage(paginas);
|
const bestPage = this.getBestPage(paginas);
|
||||||
if (!bestPage) return paginas;
|
if (!bestPage) return paginas;
|
||||||
|
|
||||||
return paginas.filter(p => p.numero !== bestPage.numero)
|
return paginas.filter(p => p.numero !== bestPage.numero)
|
||||||
.sort((a, b) => a.numero - b.numero); // Ordena por número da página
|
.sort((a, b) => a.numero - b.numero); // Ordena por número da página
|
||||||
},
|
},
|
||||||
|
|
||||||
// Controla a exibição do conteúdo completo de uma página
|
// Controla a exibição do conteúdo completo de uma página
|
||||||
toggleFullContent(diarioIndex, paginaNumero) {
|
toggleFullContent(diarioIndex, paginaNumero) {
|
||||||
const key = `${diarioIndex}-${paginaNumero}`;
|
const key = `${diarioIndex}-${paginaNumero}`;
|
||||||
this.expandedContents[key] = !this.expandedContents[key];
|
this.expandedContents[key] = !this.expandedContents[key];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Verifica se um conteúdo está expandido
|
// Verifica se um conteúdo está expandido
|
||||||
isFullContentVisible(diarioIndex, paginaNumero) {
|
isFullContentVisible(diarioIndex, paginaNumero) {
|
||||||
const key = `${diarioIndex}-${paginaNumero}`;
|
const key = `${diarioIndex}-${paginaNumero}`;
|
||||||
return this.expandedContents[key] === true;
|
return this.expandedContents[key] === true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async performSearch() {
|
async performSearch() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.hasSearched = true;
|
this.hasSearched = true;
|
||||||
this.expandedContents = {}; // Resetar estados expandidos
|
this.expandedContents = {}; // Resetar estados expandidos
|
||||||
this.suggestion = null; // Resetar a sugestão
|
this.suggestion = null; // Resetar a sugestão
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let suggestionPromise = null;
|
let suggestionPromise = null;
|
||||||
if (this.searchParams.q) {
|
if (this.searchParams.q) {
|
||||||
suggestionPromise = this.getSuggestion(this.searchParams.q);
|
suggestionPromise = this.getSuggestion(this.searchParams.q);
|
||||||
}
|
}
|
||||||
if (this.searchParams.q !== this.ultimoTermoBuscado) {
|
if (this.searchParams.q !== this.ultimoTermoBuscado) {
|
||||||
this.searchParams.page = 1;
|
this.searchParams.page = 1;
|
||||||
}
|
}
|
||||||
this.ultimoTermoBuscado = this.searchParams.q;
|
this.ultimoTermoBuscado = this.searchParams.q;
|
||||||
// Usando agora o endpoint busca
|
// Usando agora o endpoint busca
|
||||||
const url = new URL('http://192.168.235.234/api/v1/diarios/busca');
|
const url = new URL(`${window.API_BASE_URL}/api/v1/diarios/busca`);
|
||||||
|
// Adicionar parâmetros à URL
|
||||||
|
Object.entries(this.searchParams).forEach(([key, value]) => {
|
||||||
|
if (value !== '' && value !== null) {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Adicionar parâmetros à URL
|
const response = await fetch(url);
|
||||||
Object.entries(this.searchParams).forEach(([key, value]) => {
|
|
||||||
if (value !== '' && value !== null) {
|
|
||||||
url.searchParams.append(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null);
|
||||||
|
throw new Error(errorData?.message || `Erro HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
this.searchResults = await response.json();
|
||||||
const errorData = await response.json().catch(() => null);
|
|
||||||
throw new Error(errorData?.message || `Erro HTTP: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchResults = await response.json();
|
// Processar os resultados para garantir que as páginas tenham score
|
||||||
|
if (this.searchResults && this.searchResults.resultados) {
|
||||||
|
this.searchResults.resultados.forEach(diario => {
|
||||||
|
if (diario.paginas) {
|
||||||
|
// Atribuir scores padrão se não existirem
|
||||||
|
diario.paginas.forEach((pagina, index) => {
|
||||||
|
if (pagina.score === undefined || pagina.score === null) {
|
||||||
|
pagina.score = diario.paginas.length - index; // Score inversamente proporcional ao índice
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Processar os resultados para garantir que as páginas tenham score
|
if (suggestionPromise) {
|
||||||
if (this.searchResults && this.searchResults.resultados) {
|
this.suggestion = await suggestionPromise;
|
||||||
this.searchResults.resultados.forEach(diario => {
|
}
|
||||||
if (diario.paginas) {
|
} catch (error) {
|
||||||
// Atribuir scores padrão se não existirem
|
console.error('Erro na busca:', error);
|
||||||
diario.paginas.forEach((pagina, index) => {
|
this.error = `Erro ao buscar diários: ${error.message}`;
|
||||||
if (pagina.score === undefined || pagina.score === null) {
|
} finally {
|
||||||
pagina.score = diario.paginas.length - index; // Score inversamente proporcional ao índice
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (suggestionPromise) {
|
formatDate(dateString) {
|
||||||
this.suggestion = await suggestionPromise;
|
const options = { day: '2-digit', month: '2-digit', year: 'numeric' };
|
||||||
}
|
return new Date(dateString + 'T00:00:00').toLocaleDateString('pt-BR', options);
|
||||||
} catch (error) {
|
},
|
||||||
console.error('Erro na busca:', error);
|
goToPage(page) {
|
||||||
this.error = `Erro ao buscar diários: ${error.message}`;
|
if (page < 1 || page > this.totalPages) return;
|
||||||
} finally {
|
this.searchParams.page = page;
|
||||||
this.isLoading = false;
|
this.performSearch();
|
||||||
}
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
},
|
},
|
||||||
|
|
||||||
formatDate(dateString) {
|
resetSearch() {
|
||||||
const options = { day: '2-digit', month: '2-digit', year: 'numeric' };
|
this.searchParams = {
|
||||||
return new Date(dateString + 'T00:00:00').toLocaleDateString('pt-BR', options);
|
q: '',
|
||||||
},
|
numero_diario: '',
|
||||||
goToPage(page) {
|
data_inicio: '',
|
||||||
if (page < 1 || page > this.totalPages) return;
|
data_fim: '',
|
||||||
this.searchParams.page = page;
|
modo_busca: 'exata',
|
||||||
this.performSearch();
|
ordenar_por: 'relevancia',
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
page: 1,
|
||||||
},
|
page_size: 10
|
||||||
|
};
|
||||||
resetSearch() {
|
this.searchResults = null;
|
||||||
this.searchParams = {
|
this.hasSearched = false;
|
||||||
q: '',
|
this.error = null;
|
||||||
numero_diario: '',
|
this.expandedContents = {};
|
||||||
data_inicio: '',
|
}
|
||||||
data_fim: '',
|
|
||||||
modo_busca: 'exata',
|
|
||||||
ordenar_por: 'relevancia',
|
|
||||||
page: 1,
|
|
||||||
page_size: 10
|
|
||||||
};
|
|
||||||
this.searchResults = null;
|
|
||||||
this.hasSearched = false;
|
|
||||||
this.error = null;
|
|
||||||
this.expandedContents = {};
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}
|
||||||
|
<!-- conteúdo específico virá aqui -->
|
||||||
|
{% endblock content %}
|
||||||
|
</div>
|
||||||
|
{% endblock main %}
|
||||||
|
|
||||||
@ -1,394 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="pt-BR">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Diários Oficiais</title>
|
|
||||||
<!-- Bootstrap 5 CSS -->
|
|
||||||
<link href="{% static 'css/bootstrap-custom.min.css' %}" rel="stylesheet">
|
|
||||||
<!-- Bootstrap Icons -->
|
|
||||||
<link rel="stylesheet" href="{% static 'css/bootstrap-icons.css' %}">
|
|
||||||
<!-- Alpine.js -->
|
|
||||||
<script defer src="{% static 'js/alpine.min.js' %}"></script>
|
|
||||||
<!-- Estilos customizados -->
|
|
||||||
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
|
|
||||||
<link rel="icon" href="{% static 'images/favicon.ico' %}" type="image/x-icon">
|
|
||||||
</head>
|
|
||||||
<body class="bg-light-gradient">
|
|
||||||
<div class="container py-5" x-data="searchApp"> <!-- Container do Logo e Imagem Direita -->
|
|
||||||
<div class="row justify-content-center mb-4">
|
|
||||||
<div class="col-12 col-lg-10">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-5">
|
|
||||||
<!-- Logo à esquerda -->
|
|
||||||
<div class="logo-container">
|
|
||||||
<img src="{% static 'images/logo.jpg' %}" alt="Logo" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
<!-- Imagem à direita -->
|
|
||||||
<div class="risco-container">
|
|
||||||
<img src="{% static 'images/risco.jpg' %}" alt="Risco" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row justify-content-center mb-4">
|
|
||||||
<div class="col-12 col-lg-10">
|
|
||||||
<div class="text-center mb-5">
|
|
||||||
<h1 class="display-5 fw-bold text-primary mb-3">
|
|
||||||
<i class="bi bi-search me-2"></i>Diários Oficiais
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card search-card mb-5">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<form @submit.prevent="performSearch" class="row g-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
||||||
<input type="text" class="form-control form-control-lg"
|
|
||||||
x-model="searchParams.q"
|
|
||||||
placeholder="Digite o termo de busca"
|
|
||||||
aria-label="Termo de busca">
|
|
||||||
<button class="btn btn-primary" type="submit">
|
|
||||||
<span x-show="!isLoading">Buscar</span>
|
|
||||||
<span x-show="isLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Opções básicas de busca (sempre visíveis) -->
|
|
||||||
<div class="col-12 mt-2">
|
|
||||||
<div class="row align-items-end">
|
|
||||||
<div class="col-md-4 mb-2 mb-md-0">
|
|
||||||
<label for="numero_diario" class="form-label">Número do Diário</label>
|
|
||||||
<input type="text" class="form-control" id="numero_diario"
|
|
||||||
x-model="searchParams.numero_diario"
|
|
||||||
placeholder="Ex: 1234">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-2 mb-md-0">
|
|
||||||
<label for="modo_busca" class="form-label">Modo de Busca</label>
|
|
||||||
<select class="form-select" id="modo_busca" x-model="searchParams.modo_busca">
|
|
||||||
<option value="exata">Busca exata</option>
|
|
||||||
<option value="qualquer">Qualquer termo</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 col-xl-4">
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
<label class="form-label">Ordenar por</label>
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
|
||||||
:class="{'active': searchParams.ordenar_por === 'relevancia'}"
|
|
||||||
@click="changeOrder('relevancia')">
|
|
||||||
<i class="bi bi-star me-1"></i>Relevância
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
|
||||||
:class="{'active': searchParams.ordenar_por === 'data_desc'}"
|
|
||||||
@click="changeOrder('data_desc')">
|
|
||||||
<i class="bi bi-sort-down-alt me-1"></i>Data<br>(Decrescente)
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
|
||||||
:class="{'active': searchParams.ordenar_por === 'data_asc'}"
|
|
||||||
@click="changeOrder('data_asc')">
|
|
||||||
<i class="bi bi-sort-down me-1"></i>Data<br>(Crescente)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 mt-3">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" @click="showAdvanced = !showAdvanced">
|
|
||||||
<span x-text="showAdvanced ? 'Ocultar filtros avançados' : 'Mostrar filtros avançados'"></span>
|
|
||||||
<i class="bi" :class="showAdvanced ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12" x-show="showAdvanced" x-transition>
|
|
||||||
<div class="row g-3 mt-1">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="data_inicio" class="form-label">Data inicial</label>
|
|
||||||
<input type="date" class="form-control date-picker" id="data_inicio" x-model="searchParams.data_inicio">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="data_fim" class="form-label">Data final</label>
|
|
||||||
<input type="date" class="form-control date-picker" id="data_fim" x-model="searchParams.data_fim">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-1">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="page_size" class="form-label">Resultados por página</label>
|
|
||||||
<select class="form-select" id="page_size" x-model="searchParams.page_size">
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
<option value="30">30</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Seção Você quis dizer -->
|
|
||||||
<template x-if="!isLoading && !error && searchResults && suggestion && shouldShowSuggestion">
|
|
||||||
<div class="mb-3 mt-3 alert alert-info d-flex align-items-center">
|
|
||||||
<i class="bi bi-lightbulb-fill me-2"></i>
|
|
||||||
<span>Você quis dizer:
|
|
||||||
<a href="#" @click.prevent="usesuggestion" class="alert-link" x-text="suggestion"></a>?
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<!-- Resultados -->
|
|
||||||
<div x-show="hasSearched" class="mb-4">
|
|
||||||
<template x-if="isLoading">
|
|
||||||
<div class="d-flex justify-content-center my-5">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Carregando...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="!isLoading && error">
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
||||||
<span x-text="error"></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="!isLoading && !error && searchResults">
|
|
||||||
<div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h2 class="h4 m-0">
|
|
||||||
<span x-text="searchResults.total"></span> Resultados encontrados
|
|
||||||
<template x-if="searchParams.q">
|
|
||||||
<span>para "<span x-text="searchParams.q"></span>"</span>
|
|
||||||
</template>
|
|
||||||
</h2>
|
|
||||||
<button @click="resetSearch" class="btn btn-sm btn-outline-secondary">
|
|
||||||
<i class="bi bi-arrow-counterclockwise me-1"></i> Nova busca
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template x-if="searchResults.total === 0">
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
<i class="bi bi-info-circle-fill me-2"></i>
|
|
||||||
Nenhum resultado encontrado para os critérios de busca informados.
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="searchResults.total > 0">
|
|
||||||
<div>
|
|
||||||
<!-- Lista de resultados -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<template x-for="(diario, diarioIndex) in searchResults.resultados" :key="diario.id">
|
|
||||||
<div class="card result-card mb-4 border-0 shadow-sm">
|
|
||||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="card-title mb-0">
|
|
||||||
<span class="badge bg-primary me-2" x-text="diario.tipo"></span>
|
|
||||||
<span x-text="diario.numero"></span>
|
|
||||||
</h5>
|
|
||||||
<span class="text-muted" x-text="formatDate(diario.data)"></span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<template x-if="diario.paginas && diario.paginas.length > 0">
|
|
||||||
<div>
|
|
||||||
<!-- Melhor página encontrada (mostrada apenas se estiver ordenado por relevância e tiver score) -->
|
|
||||||
<template x-if="hasBestMatch(diario)">
|
|
||||||
<div class="mb-4 p-3 best-match rounded">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<div>
|
|
||||||
<span class="page-badge me-2">Página <span x-text="getBestPage(diario.paginas).numero"></span></span>
|
|
||||||
<span class="match-score">
|
|
||||||
<i class="bi bi-star-fill me-1 small"></i>
|
|
||||||
Melhor correspondência no diário
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="page-content">
|
|
||||||
<div x-html="getBestPage(diario.paginas).conteudo"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Accordion de páginas -->
|
|
||||||
<template x-if="diario.paginas.length > 1">
|
|
||||||
<div class="accordion mt-3" :id="'accordionDiario' + diario.id">
|
|
||||||
<div class="accordion-item border-0 mb-2">
|
|
||||||
<h2 class="accordion-header">
|
|
||||||
<button class="accordion-button collapsed shadow-sm" type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
:data-bs-target="'#collapse' + diario.id"
|
|
||||||
aria-expanded="false">
|
|
||||||
<i class="bi bi-list-ul me-2"></i>
|
|
||||||
<template x-if="hasBestMatch(diario) && diario.paginas.length > 1">
|
|
||||||
<span>Ver mais <span class="mx-1" x-text="getOtherPages(diario.paginas).length"></span> páginas deste diário</span>
|
|
||||||
</template>
|
|
||||||
<template x-if="!hasBestMatch(diario) && diario.paginas.length > 1">
|
|
||||||
<span>Ver <span class="mx-1" x-text="diario.paginas.length"></span> páginas deste diário</span>
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div :id="'collapse' + diario.id" class="accordion-collapse collapse"
|
|
||||||
:data-bs-parent="'#accordionDiario' + diario.id">
|
|
||||||
<div class="accordion-body p-0">
|
|
||||||
<div class="list-group list-group-flush">
|
|
||||||
<template x-for="pagina in hasBestMatch(diario) ? getOtherPages(diario.paginas) : diario.paginas" :key="pagina.numero">
|
|
||||||
<div class="list-group-item border-0 py-3">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<span class="page-badge">Página <span x-text="pagina.numero"></span></span>
|
|
||||||
</div>
|
|
||||||
<div class="page-preview" x-html="pagina.conteudo.substring(0, 200) + '...'"></div>
|
|
||||||
<div x-show="isFullContentVisible(diarioIndex, pagina.numero)" x-transition class="mt-2 page-content border-top pt-3">
|
|
||||||
<div x-html="pagina.conteudo"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mt-3">
|
|
||||||
<a :href="diario.link" target="_blank" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="bi bi-file-earmark-pdf me-1"></i> Ver Diário Completo
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Paginação -->
|
|
||||||
<nav aria-label="Navegação de páginas" x-show="totalPages > 1">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
<li class="page-item" :class="{ 'disabled': searchParams.page <= 1 }">
|
|
||||||
<a class="page-link" href="#" @click.prevent="goToPage(searchParams.page - 1)" aria-label="Anterior">
|
|
||||||
<span aria-hidden="true">«</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<template x-for="page in paginationArray" :key="page">
|
|
||||||
<li class="page-item" :class="{ 'active': page === searchParams.page }">
|
|
||||||
<a class="page-link" href="#" @click.prevent="goToPage(page)" x-text="page"></a>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<li class="page-item" :class="{ 'disabled': searchParams.page >= totalPages }">
|
|
||||||
<a class="page-link" href="#" @click.prevent="goToPage(searchParams.page + 1)" aria-label="Próximo">
|
|
||||||
<span aria-hidden="true">»</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="bg-dark text-white py-4 mt-5">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h5>Diários Oficiais</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 text-md-end">
|
|
||||||
<p class="small mb-0">© 2025 Todos os direitos reservados</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<!-- Botão de ajuda que abre o modal -->
|
|
||||||
<button type="button" class="btn btn-outline-secondary position-fixed bottom-0 end-0 m-3" data-bs-toggle="modal" data-bs-target="#helpModal">
|
|
||||||
<i class="bi bi-question-circle"></i> Ajuda
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Modal de Ajuda -->
|
|
||||||
<div class="modal fade" id="helpModal" tabindex="-1" aria-labelledby="helpModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header bg-primary text-white">
|
|
||||||
<h5 class="modal-title" id="helpModalLabel">Ajuda - Sistema de Busca de Diários Oficiais</h5>
|
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fechar"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-4">
|
|
||||||
<h6>Atenção quanto à qualidade dos dados</h6>
|
|
||||||
<p>Alguns documentos podem conter erros de leitura devido à baixa qualidade das imagens dos PDFs originais. Isso pode afetar a precisão da busca, especialmente no modo de <strong>busca exata</strong>.</p>
|
|
||||||
<p>Nesses casos, recomendamos utilizar o modo <strong>qualquer termo</strong>, que é mais tolerante a pequenas falhas de reconhecimento de texto.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5>Como realizar buscas</h5>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h6>Busca básica</h6>
|
|
||||||
<p>Digite o termo que deseja buscar no campo principal e clique em "Buscar". O sistema irá localizar ocorrências desse termo nos Diários Oficiais.</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Número do Diário:</strong> Se souber o número específico do diário, digite-o neste campo para filtrar os resultados.</li>
|
|
||||||
<li><strong>Modo de Busca:</strong>
|
|
||||||
<ul>
|
|
||||||
<li><em>Busca exata</em> - Encontra apenas documentos que contenham exatamente o termo informado.</li>
|
|
||||||
<li><em>Qualquer termo</em> - Encontra documentos que contenham qualquer um dos termos informados.</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h6>Ordenação dos resultados</h6>
|
|
||||||
<p>Você pode ordenar os resultados de três formas:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Relevância:</strong> Mostra primeiro os documentos mais relevantes para sua busca.</li>
|
|
||||||
<li><strong>Data (Decrescente):</strong> Mostra os diários mais recentes primeiro.</li>
|
|
||||||
<li><strong>Data (Crescente):</strong> Mostra os diários mais antigos primeiro.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h6>Filtros avançados</h6>
|
|
||||||
<p>Clique em "Mostrar filtros avançados" para acessar opções adicionais:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Data inicial e Data final:</strong> Restringe a busca a diários publicados dentro do período informado.</li>
|
|
||||||
<li><strong>Resultados por página:</strong> Define quantos resultados serão exibidos em cada página.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h6>Resultados da busca</h6>
|
|
||||||
<p>Nos resultados, você verá:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Tipo e número do diário, com a data de publicação.</li>
|
|
||||||
<li>A página com melhor correspondência aparecerá destacada.</li>
|
|
||||||
<li>Para ver mais páginas do mesmo diário, clique no botão de expansão.</li>
|
|
||||||
<li>Use o botão "Ver Diário Completo" para abrir o arquivo PDF do diário inteiro.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h6>Sugestões de busca</h6>
|
|
||||||
<p>Se o sistema encontrar um termo semelhante ao que você buscou, mostrará uma sugestão que você pode clicar para realizar uma nova busca com o termo sugerido.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Entendi</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Bootstrap Bundle with Popper -->
|
|
||||||
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
|
|
||||||
<!-- Script da aplicação -->
|
|
||||||
<script src="{% static 'js/config.js' %}"></script>
|
|
||||||
<script src="{% static 'js/script.js' %}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@ -1,27 +1,29 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
diarios_oficiais_search_alems_local_postgres_data: {}
|
diarios_oficiais_search_alems_local_postgres_data: {}
|
||||||
diarios_oficiais_search_alems_local_postgres_data_backups: {}
|
diarios_oficiais_search_alems_local_postgres_data_backups: {}
|
||||||
staticfiles:
|
staticfiles:
|
||||||
media:
|
media:
|
||||||
esdata:
|
esdata:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v2.5
|
env_file:
|
||||||
|
- ./.envs/.local/.traefik
|
||||||
|
image: traefik:v2.11
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
command:
|
command:
|
||||||
- --api.insecure=true
|
- --api.insecure=true
|
||||||
- --providers.docker=true
|
- --providers.docker=true
|
||||||
- --providers.docker.exposedbydefault=false
|
- --providers.docker.exposedbydefault=false
|
||||||
- --entrypoints.web.address=:80
|
- --entrypoints.web.address=:80
|
||||||
|
# Adicionar logs para debug (opcional)
|
||||||
|
- --log.level=INFO
|
||||||
|
- --accesslog=true
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "8080:8080" # Dashboard (opcional)
|
- "8080:8080" # Dashboard
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- ./media:/app/diarios_oficiais_search_alems/media
|
|
||||||
|
|
||||||
django:
|
django:
|
||||||
build:
|
build:
|
||||||
@ -38,11 +40,15 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ./.envs/.local/.django
|
- ./.envs/.local/.django
|
||||||
- ./.envs/.local/.postgres
|
- ./.envs/.local/.postgres
|
||||||
|
- ./.envs/.local/.traefik
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.django.rule=Host(`192.168.235.234`)"
|
# Rota principal - captura tudo exceto /media/ e /static/
|
||||||
|
- "traefik.http.routers.django.rule=Host(`${TRAEFIK_HOST}`) && !PathPrefix(`/media/`) && !PathPrefix(`/static/`)"
|
||||||
- "traefik.http.routers.django.entrypoints=web"
|
- "traefik.http.routers.django.entrypoints=web"
|
||||||
- "traefik.http.services.django.loadbalancer.server.port=8000"
|
- "traefik.http.services.django.loadbalancer.server.port=8000"
|
||||||
|
# Prioridade maior para ser avaliada primeiro
|
||||||
|
- "traefik.http.routers.django.priority=100"
|
||||||
ports:
|
ports:
|
||||||
- '8000:8000'
|
- '8000:8000'
|
||||||
command: /start
|
command: /start
|
||||||
@ -67,3 +73,17 @@ services:
|
|||||||
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
|
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
|
||||||
volumes:
|
volumes:
|
||||||
- esdata:/usr/share/elasticsearch/data
|
- esdata:/usr/share/elasticsearch/data
|
||||||
|
|
||||||
|
media-server:
|
||||||
|
image: nginx:alpine
|
||||||
|
volumes:
|
||||||
|
- media:/usr/share/nginx/html/media:ro
|
||||||
|
- staticfiles:/usr/share/nginx/html/static:ro
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Rota específica para arquivos estáticos e media
|
||||||
|
- "traefik.http.routers.media.rule=Host(`${TRAEFIK_HOST}`) && (PathPrefix(`/media/`) || PathPrefix(`/static/`))"
|
||||||
|
- "traefik.http.routers.media.entrypoints=web"
|
||||||
|
- "traefik.http.services.media.loadbalancer.server.port=80"
|
||||||
|
# Prioridade menor para ser avaliada depois
|
||||||
|
- "traefik.http.routers.media.priority=200"
|
||||||
|
|||||||
0
erros_diarios.txt
Normal file
0
erros_diarios.txt
Normal file
@ -24,6 +24,7 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis
|
|||||||
django-elasticsearch-dsl
|
django-elasticsearch-dsl
|
||||||
PyPDF2
|
PyPDF2
|
||||||
PyMuPDF
|
PyMuPDF
|
||||||
|
pdfplumber
|
||||||
babel
|
babel
|
||||||
django-ninja
|
django-ninja
|
||||||
remote-pdb
|
remote-pdb
|
||||||
|
|||||||
Reference in New Issue
Block a user