alterações gerais

This commit is contained in:
2025-06-16 13:11:57 -04:00
parent 9dca0d6022
commit f196773705
45 changed files with 6774 additions and 222 deletions

View File

@ -3,4 +3,7 @@
USE_DOCKER=yes
IPYTHONDIR=/app/.ipython
ELASTICSEARCH_USER=elastic
ELASTICSEARCH_PASSWORD=Euamooelasticsearch123.
ELASTICSEARCH_PASSWORD=RwtC6t+BLWkLj44=cp-*
DJANGO_SECRET_KEY=tYdYl0MP5zgpMlMmjBuYHvH4Dp3JDN5q3sxWBdFejemZSr0qpI9IrvrvTm17F0aW
DJANGO_ADMIN_URL=manage-panel/
DJANGO_ALLOWED_HOSTS=192.168.235.234,localhost,127.0.0.1,django

1
backup.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,4 @@
# FROM docker.io/python:3.12.9-slim-bookworm AS python # Linha removida, já definida abaixo
# Python build stage - Mantenha apenas dependências de BUILD aqui
FROM docker.io/python:3.12.9-slim-bookworm AS python-build-stage
ARG BUILD_ENVIRONMENT=local
@ -44,9 +42,10 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
tesseract-ocr \
tesseract-ocr-por \
ghostscript \
openjdk-17-jdk \
# libtesseract-dev \
# Utilitários do devcontainer e outros
sudo git bash-completion vim ssh \
sudo git bash-completion vim ssh curl \
# Limpeza
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*

View File

@ -4,7 +4,17 @@ set -o errexit
set -o pipefail
set -o nounset
# Aplica migrações antes de iniciar o servidor
python manage.py migrate
exec python manage.py runserver_plus 0.0.0.0:8005
# Coleta arquivos estáticos
python manage.py collectstatic --noinput
python manage.py compress --force
# Inicia o servidor com Gunicorn
exec gunicorn config.wsgi:application \
--bind 0.0.0.0:8005 \
--workers 4 \
--timeout 120
# python manage.py runserver_plus 0.0.0.0:8005

View File

@ -0,0 +1,44 @@
upstream django {
server django:8005;
}
server {
listen 80;
server_name 192.168.235.234 localhost;
client_max_body_size 10M;
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
# Timeouts
proxy_connect_timeout 75s;
proxy_read_timeout 300s;
}
location /static/ {
alias /app/staticfiles/;
expires 30d;
access_log off;
add_header Cache-Control "public, no-transform";
}
location /media/ {
alias /app/media/;
expires 30d;
access_log off;
add_header Cache-Control "public, no-transform";
}
# Bloqueia acesso a arquivos ocultos
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

View File

@ -1,4 +1,3 @@
# define an alias for the specific python version used in this file.
FROM docker.io/python:3.12.9-slim-bookworm AS python
@ -9,77 +8,66 @@ ARG BUILD_ENVIRONMENT=production
# Install apt packages
RUN apt-get update && apt-get install --no-install-recommends -y \
# dependencies for building Python packages
build-essential \
# psycopg dependencies
libpq-dev
libpq-dev \
wait-for-it \
&& rm -rf /var/lib/apt/lists/*
# Requirements are installed here to ensure they will be cached.
COPY ./requirements .
# Create Python Dependency and Sub-Dependency Wheels.
RUN pip wheel --wheel-dir /usr/src/app/wheels \
RUN pip wheel --wheel-dir /usr/src/app/wheels \
-r ${BUILD_ENVIRONMENT}.txt
# Python 'run' stage
FROM python AS python-run-stage
ARG BUILD_ENVIRONMENT=production
ARG APP_HOME=/app
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV BUILD_ENV=${BUILD_ENVIRONMENT}
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
BUILD_ENV=${BUILD_ENVIRONMENT} \
PATH="/home/django/.local/bin:${PATH}"
WORKDIR ${APP_HOME}
RUN addgroup --system django \
&& adduser --system --ingroup django django
# Create system user
RUN addgroup --system django && \
adduser --system --ingroup django django
# Install required system dependencies
RUN apt-get update && apt-get install --no-install-recommends -y \
# psycopg dependencies
libpq-dev \
# Translations dependencies
gettext \
# entrypoint
wait-for-it \
# cleaning up unused files
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*
# All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction
# copy python dependency wheels from python-build-stage
COPY --from=python-build-stage /usr/src/app/wheels /wheels/
# Copy python dependency wheels
COPY --from=python-build-stage /usr/src/app/wheels /wheels/
# use wheels to install python dependencies
# Install python dependencies
RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \
&& rm -rf /wheels/
# Copy entrypoint and start scripts
COPY --chown=django:django ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
RUN sed -i 's/\r$//g' /entrypoint && \
chmod +x /entrypoint
COPY --chown=django:django ./compose/production/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
RUN sed -i 's/\r$//g' /start && \
chmod +x /start
# copy application code to WORKDIR
# Copy application code
COPY --chown=django:django . ${APP_HOME}
# make django owner of the WORKDIR directory as well.
RUN chown -R django:django ${APP_HOME}
# Fix permissions
RUN chown -R django:django ${APP_HOME} && \
find ${APP_HOME} -type d -exec chmod 755 {} \; && \
find ${APP_HOME} -type f -exec chmod 644 {} \;
USER django
RUN DATABASE_URL="" \
DJANGO_SETTINGS_MODULE="config.settings.test" \
python manage.py compilemessages
ENTRYPOINT ["/entrypoint"]

View File

@ -1,29 +1,51 @@
#!/bin/bash
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
# Configurações ajustáveis via variáveis de ambiente
PORT=${GUNICORN_PORT:-8005}
WORKERS=${GUNICORN_WORKERS:-$(( $(nproc) * 2 + 1 ))}
TIMEOUT=${GUNICORN_TIMEOUT:-120}
MAX_REQUESTS=${GUNICORN_MAX_REQUESTS:-1000}
# Aplica migrações do banco de dados (com tratamento de erro)
python /app/manage.py migrate --noinput || {
echo "⚠️ Falha nas migrações do banco de dados!";
exit 1;
}
# Coleta arquivos estáticos
python /app/manage.py collectstatic --noinput
# Verifica e comprime arquivos (se compressor ativado)
compress_enabled() {
python << END
python << END
import sys
from environ import Env
env = Env(COMPRESS_ENABLED=(bool, True))
if env('COMPRESS_ENABLED'):
sys.exit(0)
else:
sys.exit(1)
sys.exit(0 if env('COMPRESS_ENABLED') else 1)
END
}
if compress_enabled; then
# NOTE this command will fail if django-compressor is disabled
python /app/manage.py compress
python /app/manage.py compress --verbosity=0 || {
echo "⚠️ Falha ao comprimir arquivos (django-compressor pode estar desativado)";
}
fi
exec /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app
# Inicia o Gunicorn com configurações otimizadas
exec /usr/local/bin/gunicorn config.wsgi:application \
--bind 0.0.0.0:${PORT} \
--workers ${WORKERS} \
--timeout ${TIMEOUT} \
--max-requests ${MAX_REQUESTS} \
--worker-class sync \
--name diarios_oficiais_alems \
--access-logfile - \
--error-logfile - \
--chdir=/app

View File

@ -0,0 +1,39 @@
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 180;
gzip on;
server {
listen 80;
server_name _;
location / {
proxy_pass http://django:8005;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /usr/share/nginx/static/;
expires 30d;
access_log off;
}
location /media/ {
alias /usr/share/nginx/media/;
expires 30d;
access_log off;
}
}
}

13
config/asgi.py Normal file
View File

@ -0,0 +1,13 @@
import os
from pathlib import Path
import sys
from django.core.asgi import get_asgi_application
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
sys.path.append(str(BASE_DIR / "diarios_oficiais_alems"))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
application = get_asgi_application()

View File

@ -19,7 +19,7 @@ if READ_DOT_ENV_FILE:
# GENERAL
# ------------------------------------------------------------------------------
# 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
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# though not all of them may be available with every OS.
@ -67,7 +67,7 @@ DJANGO_APPS = [
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
# "django.contrib.humanize", # Handy template tags
"django.contrib.humanize",
"django.contrib.admin",
"django.forms",
]
@ -137,9 +137,8 @@ MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
@ -210,11 +209,28 @@ FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),)
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
SESSION_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_TRUSTED_ORIGINS = [
'http://192.168.235.234:8005',
'http://192.168.235.234:80',
'http://192.168.235.234',
'http://localhost:8005',
'http://localhost:80',
'http://localhost',
'http://127.0.0.1:8005',
'http://127.0.0.1:80',
'http://127.0.0.1',
]
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
X_FRAME_OPTIONS = "DENY"
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'http')
# EMAIL
# ------------------------------------------------------------------------------
@ -229,7 +245,7 @@ EMAIL_TIMEOUT = 5
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL.
ADMIN_URL = "admin/"
ADMIN_URL = "manage-panel/"
# https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = [("""Antonio Roberto""", "antonio-roberto@example.com")]
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
@ -267,7 +283,7 @@ REDIS_SSL = REDIS_URL.startswith("rediss://")
# django-allauth
# ------------------------------------------------------------------------------
ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True)
ACCOUNT_ALLOW_REGISTRATION = False
# https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_LOGIN_METHODS = {"username"}
# https://docs.allauth.org/en/latest/account/configuration.html
@ -349,6 +365,7 @@ ELASTICSEARCH_INDEX_SETTINGS = {
}
CORS_ALLOWED_ORIGINS = [
"http://192.168.235.234",
"http://109.199.98.226:8006",
"http://109.199.98.226:8005",
"http://localhost:8006",

View File

@ -14,7 +14,7 @@ SECRET_KEY = env(
default="tYdYl0MP5zgpMlMmjBuYHvH4Dp3JDN5q3sxWBdFejemZSr0qpI9IrvrvTm17F0aW",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "109.199.98.226"] # noqa: S104
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "192.168.235.234"] # noqa: S104
# CACHES
# ------------------------------------------------------------------------------

View File

@ -10,7 +10,7 @@ from .base import env
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env("DJANGO_SECRET_KEY")
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["example.com"])
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[])
# DATABASES
# ------------------------------------------------------------------------------
@ -36,7 +36,7 @@ CACHES = {
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=False)
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure
SESSION_COOKIE_SECURE = True
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-name

View File

@ -10,8 +10,8 @@ from .api import api
urlpatterns = [
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
path('', include('diarios.urls')),
# path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
path("", include('diarios.urls')),
path(
"about/",
TemplateView.as_view(template_name="pages/about.html"),
@ -23,8 +23,6 @@ urlpatterns = [
# User management
path("users/", include("diarios_oficiais_alems.users.urls", namespace="users")),
# path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here
path("", include("diarios.urls")),
# Media files
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
]

View File

@ -128,7 +128,7 @@ class PageDiarioOficialAdmin(admin.ModelAdmin):
list_display = ("id", "diario_link", "numero", "conteudo_resumido")
list_display_links = ('id', 'numero')
list_filter = ("diario__tipo", "diario__data", "layout_duas_colunas")
search_fields = ("conteudo", "diario__numero")
search_fields = ("diario__numero",)
readonly_fields = (
"diario_link",
)

179
diarios/crud_service.py Normal file
View File

@ -0,0 +1,179 @@
from typing import Optional, List
from datetime import date
from diarios.models import DiarioOficial, TipoDiarioOficial
from django.core.exceptions import ObjectDoesNotExist
from ninja.errors import HttpError
class DiarioOficialService:
@staticmethod
def criar_diario(
data: date,
numero: str,
tipo_id: Optional[int] = None,
arquivo=None,
link: Optional[str] = None
) -> DiarioOficial:
"""
Cria um novo diário oficial
Args:
data: Data do diário
numero: Número do diário
tipo_id: ID do tipo de diário
arquivo: Arquivo PDF (opcional)
link: URL do PDF (opcional)
Returns:
DiarioOficial: O diário criado
Raises:
HttpError: Se ocorrer algum erro na criação
"""
try:
tipo = TipoDiarioOficial.objects.get(pk=tipo_id) if tipo_id else None
diario = DiarioOficial(
data=data,
numero=numero,
tipo=tipo,
link=link
)
if arquivo:
diario.arquivo = arquivo
diario.full_clean()
diario.save()
return diario
except ObjectDoesNotExist:
raise HttpError(404, "Tipo de diário não encontrado")
except Exception as e:
raise HttpError(400, f"Erro ao criar diário: {str(e)}")
@staticmethod
def obter_diario_por_id(id: int) -> DiarioOficial:
"""
Obtém um diário pelo ID
Args:
id: ID do diário
Returns:
DiarioOficial: O diário encontrado
Raises:
HttpError: Se o diário não for encontrado
"""
try:
return DiarioOficial.objects.get(pk=id)
except ObjectDoesNotExist:
raise HttpError(404, "Diário não encontrado")
@staticmethod
def listar_diarios(
tipo_id: Optional[int] = None,
data_inicio: Optional[date] = None,
data_fim: Optional[date] = None,
numero: Optional[str] = None
) -> List[DiarioOficial]:
"""
Lista diários com filtros opcionais
Args:
tipo_id: ID do tipo de diário para filtrar
data_inicio: Data inicial para filtrar
data_fim: Data final para filtrar
numero: Número do diário para filtrar
Returns:
List[DiarioOficial]: Lista de diários filtrados
"""
queryset = DiarioOficial.objects.all().order_by('-data')
if tipo_id:
queryset = queryset.filter(tipo_id=tipo_id)
if data_inicio and data_fim:
queryset = queryset.filter(data__range=[data_inicio, data_fim])
elif data_inicio:
queryset = queryset.filter(data__gte=data_inicio)
elif data_fim:
queryset = queryset.filter(data__lte=data_fim)
if numero:
queryset = queryset.filter(numero__icontains=numero)
return list(queryset)
@staticmethod
def atualizar_diario(
id: int,
data: Optional[date] = None,
numero: Optional[str] = None,
tipo_id: Optional[int] = None,
arquivo=None,
link: Optional[str] = None
) -> DiarioOficial:
"""
Atualiza um diário existente
Args:
id: ID do diário a ser atualizado
data: Nova data (opcional)
numero: Novo número (opcional)
tipo_id: Novo tipo (opcional)
arquivo: Novo arquivo (opcional)
link: Novo link (opcional)
Returns:
DiarioOficial: O diário atualizado
Raises:
HttpError: Se ocorrer algum erro na atualização
"""
try:
diario = DiarioOficial.objects.get(pk=id)
if data is not None:
diario.data = data
if numero is not None:
diario.numero = numero
if tipo_id is not None:
tipo = TipoDiarioOficial.objects.get(pk=tipo_id) if tipo_id else None
diario.tipo = tipo
if arquivo is not None:
diario.arquivo = arquivo
if link is not None:
diario.link = link
diario.full_clean()
diario.save()
return diario
except ObjectDoesNotExist:
raise HttpError(404, "Diário ou tipo não encontrado")
except Exception as e:
raise HttpError(400, f"Erro ao atualizar diário: {str(e)}")
@staticmethod
def deletar_diario(id: int) -> None:
"""
Remove um diário
Args:
id: ID do diário a ser removido
Raises:
HttpError: Se o diário não for encontrado
"""
try:
diario = DiarioOficial.objects.get(pk=id)
diario.delete()
except ObjectDoesNotExist:
raise HttpError(404, "Diário não encontrado")

View File

@ -3,19 +3,16 @@ 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
from django.db import models, transaction
from django.core.exceptions import ValidationError
import PyPDF2
import fitz # PyMuPDF
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:
@ -23,16 +20,6 @@ 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(
@ -46,34 +33,30 @@ class DiarioOficial(models.Model):
link = models.URLField(blank=True, null=True, unique=True)
def save(self, *args, **kwargs):
"""Salva o Diário Oficial, baixa o PDF (se houver link) e extrai páginas."""
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:
if self.arquivo and not self.paginas.exists():
self._extract_pdf_pages()
updated = True
super().save(*args, **kwargs)
if updated:
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()
@ -86,55 +69,73 @@ class DiarioOficial(models.Model):
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:
# Salvar temporariamente o PDF para abrir com o PyMuPDF
with self.arquivo.open("rb") as pdf_file:
pdf = PyPDF2.PdfReader(pdf_file)
self._process_pdf_pages(pdf)
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, pdf):
"""Processa cada página do PDF e salva seu conteúdo.
def _process_pdf_pages(self, doc):
with transaction.atomic():
self.paginas.all().delete()
Args:
pdf (PdfReader): Objeto PDF carregado.
"""
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"
for i, pagina in enumerate(pdf.pages):
try:
page_text = pagina.extract_text()
if page_text and page_text.strip():
# 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=page_text.strip(),
conteudo=f"[Erro na extração do texto: {str(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
print(f"Erro ao processar a página {i+1} no Diario ID {self.id}: {page_error}")
@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):
"""Verifica se o Diário possui um link (online)."""
return bool(self.link)
def __str__(self):
"""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}{self.numero}, {self.data_formatada}"
@ -144,15 +145,6 @@ class DiarioOficial(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(
DiarioOficial, on_delete=models.CASCADE, related_name="paginas"
)
@ -165,7 +157,11 @@ class PageDiarioOficial(models.Model):
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):
"""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}"

View File

@ -1,5 +1,7 @@
from ninja import Schema
from ninja import Schema, ModelSchema, UploadedFile
from typing import List, Optional
from .models import TipoDiarioOficial
from datetime import date
class PaginaSchema(Schema):
@ -61,3 +63,64 @@ class SugestaoResponse(Schema):
"""
sugestao: Optional[str]
class TipoDiarioSchema(ModelSchema):
class Config:
model = TipoDiarioOficial
model_fields = ["id", "nome"]
class DiarioOficialIn(Schema):
"""Schema para criação de Diário Oficial"""
data: date
numero: str
tipo_id: Optional[int] = None
link: Optional[str] = None
arquivo: Optional[UploadedFile] = None # Adicionando o campo de arquivo
class Config:
# Configuração adicional para o Swagger UI
json_schema_extra = {
"example": {
"data": "2023-12-01",
"numero": "1234/2023",
}
}
class DiarioOficialOut(Schema):
"""Schema para retorno de Diário Oficial (com detalhes completos)"""
id: int
data: str # Será formatado como ISO (YYYY-MM-DD)
numero: str
link: Optional[str]
tipo: Optional[TipoDiarioSchema]
total_paginas: int
@staticmethod
def resolve_data(obj):
return obj.data.isoformat()
@staticmethod
def resolve_total_paginas(obj):
return obj.paginas.count()
class DiarioOficialUpdate(Schema):
"""Schema para atualização de Diário Oficial"""
data: Optional[date] = None
numero: Optional[str] = None
tipo_id: Optional[int] = None
link: Optional[str] = None
class DiarioListagem(Schema):
"""Schema simplificado para listagem de Diários"""
id: int
data: str
numero: str
tipo_nome: Optional[str]
@staticmethod
def resolve_data(obj):
return obj.data.isoformat()
@staticmethod
def resolve_tipo_nome(obj):
return obj.tipo.nome if obj.tipo else None

View File

@ -2,7 +2,7 @@ import re
from datetime import datetime
from typing import Optional, Dict, Any, List
from elasticsearch import Elasticsearch, AsyncElasticsearch
from .schemas import ResultadoSchema, PaginaSchema
from .schemas import BuscaDiariosResponseSchema, ResultadoSchema, PaginaSchema
import unicodedata
import asyncio
from django.conf import settings
@ -588,16 +588,19 @@ async def processar_paginas_encontradas(
Returns:
List[PaginaSchema]: Lista de páginas encontradas
"""
if not query:
return []
paginas = []
source = hit.get("_source", {})
if not query and "paginas" in source:
for pagina in source["paginas"]:
paginas.append(
PaginaSchema(
numero=pagina.get("numero", 0), conteudo=pagina.get("conteudo", "")
)
)
#if not query and "paginas" in source:
# for pagina in source["paginas"]:
# paginas.append(
# PaginaSchema(
# numero=pagina.get("numero", 0), conteudo=pagina.get("conteudo", "")
# )
# )
# Se temos uma query e há inner_hits, processamos todas as páginas correspondentes
if query and "inner_hits" in hit and "paginas" in hit["inner_hits"]:
paginas_inner_hits = hit["inner_hits"]["paginas"]["hits"]["hits"]
@ -731,3 +734,115 @@ async def buscar_diarios_simples(
# Processar e retornar os resultados
return await processar_resultados(response, query, page, page_size)
async def list_diarios(
es_client: AsyncElasticsearch,
page: int = 1,
page_size: int = 10,
data_inicio: Optional[str] = None,
data_fim: Optional[str] = None,
tipo_diario: Optional[str] = None,
numero_diario: Optional[str] = None,
ordenar_por: str = "data_desc"
) -> Dict[str, any]:
"""
Lista diários com paginação e filtros opcionais
Args:
es_client: Cliente do Elasticsearch
page: Número da página
page_size: Itens por página
data_inicio: Data inicial (YYYY-MM-DD)
data_fim: Data final (YYYY-MM-DD)
tipo_diario: Filtro por tipo de diário
numero_diario: Filtro por número do diário
ordenar_por: Campo para ordenação (data_desc, data_asc)
Returns:
Dict com total de itens e lista de diários
"""
# Construir query de filtros
filters = []
# Filtro por data
if data_inicio or data_fim:
date_range = {}
if data_inicio:
try:
datetime.strptime(data_inicio, "%Y-%m-%d")
date_range["gte"] = data_inicio
except ValueError:
pass
if data_fim:
try:
datetime.strptime(data_fim, "%Y-%m-%d")
date_range["lte"] = data_fim
except ValueError:
pass
if date_range:
filters.append({"range": {"data": date_range}})
# Filtro por tipo
if tipo_diario:
filters.append({"term": {"tipo.nome": tipo_diario}})
# Filtro por número
if numero_diario:
filters.append({"wildcard": {"numero": f"*{numero_diario}*"}})
# Construir query completa
query = {
"bool": {
"must": [{"match_all": {}}],
"filter": filters
}
}
# Definir ordenação
sort = [{"data": {"order": "asc" if ordenar_por == "data_asc" else "desc"}}]
# Executar consulta
try:
response = await es_client.search(
index="diario_oficial",
body={
"query": query,
"sort": sort,
"from": (page - 1) * page_size,
"size": page_size,
"_source": ["numero", "data", "tipo", "link"]
}
)
hits = response["hits"]["hits"]
total = response["hits"]["total"]["value"]
# Processar resultados
diarios = []
for hit in hits:
source = hit["_source"]
diarios.append(BuscaDiariosResponseSchema(
id=hit["_id"],
numero=source.get("numero"),
data=source.get("data"),
tipo=source.get("tipo", {}).get("nome"),
link=source.get("link")
))
return {
"total": total,
"page": page,
"page_size": page_size,
"results": diarios
}
except Exception as e:
print(f"Erro ao listar diários: {e}")
return {
"total": 0,
"page": page,
"page_size": page_size,
"results": []
}

View File

@ -2,5 +2,5 @@ from django.urls import path
from . import views
urlpatterns = [
path('busca/', views.index, name='index')
path("", views.home, name='home')
]

View File

@ -1,19 +1,19 @@
from ninja import Router
from ninja import Router, File, Form
from ninja.files import UploadedFile
from typing import Optional
from django.http import HttpRequest
from django.http import HttpRequest, HttpResponse
from .search_service import (
buscar_diarios,
sugestao_termo,
buscar_diarios_simples,
)
from .schemas import BuscaDiariosResponseSchema, SugestaoResponse
from .schemas import BuscaDiariosResponseSchema, SugestaoResponse, DiarioOficialIn, DiarioOficialOut
from django.shortcuts import render
router = Router(tags=["Diários Oficiais"])
async def index(request):
return render(request, 'diarios/busca.html')
async def home(request):
return render(request, 'diarios/index.html')
@router.get(
"/sugestao",
@ -86,3 +86,28 @@ async def busca_diarios_oficiais_simples(
)
return resultado
@router.post("/", response=DiarioOficialOut, summary="Criar novo diário oficial")
def criar_diario(
request: HttpRequest,
# Usamos Form para os dados normais e File para o upload
payload: DiarioOficialIn = Form(...),
arquivo: UploadedFile = File(None)
):
"""
Cria um novo diário oficial.
Observações:
- Aceita tanto upload de arquivo PDF quanto link para o diário
- Se ambos (arquivo e link) forem fornecidos, o arquivo terá prioridade
- O arquivo deve ser um PDF válido
"""
# Prioriza o arquivo se ambos existirem
arquivo_final = arquivo if arquivo else payload.arquivo
return DiarioOficialService.criar_diario(
data=payload.data,
numero=payload.numero,
tipo_id=payload.tipo_id,
arquivo=arquivo_final,
link=payload.link
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
const API_BASE_URL = "http://109.199.98.226";

View File

@ -64,7 +64,7 @@ document.addEventListener('alpine:init', () => {
if (!query) return null;
try {
const url = new URL('http://109.199.98.226:8005/api/v1/diarios/sugestao');
const url = new URL('http://192.168.235.234/api/v1/diarios/sugestao');
url.searchParams.append('q', query);
const response = await fetch(url);
@ -168,7 +168,7 @@ document.addEventListener('alpine:init', () => {
}
this.ultimoTermoBuscado = this.searchParams.q;
// Usando agora o endpoint busca
const url = new URL('http://109.199.98.226:8005/api/v1/diarios/busca');
const url = new URL('http://192.168.235.234/api/v1/diarios/busca');
// Adicionar parâmetros à URL
Object.entries(this.searchParams).forEach(([key, value]) => {

View File

@ -90,17 +90,6 @@
{# URL provided by django-allauth/account/urls.py #}
<a class="nav-link" href="{% url 'account_logout' %}">{% translate "Sign Out" %}</a>
</li>
{% else %}
{% if ACCOUNT_ALLOW_REGISTRATION %}
<li class="nav-item">
{# URL provided by django-allauth/account/urls.py #}
<a id="sign-up-link" class="nav-link" href="{% url 'account_signup' %}">{% translate "Sign Up" %}</a>
</li>
{% endif %}
<li class="nav-item">
{# URL provided by django-allauth/account/urls.py #}
<a id="log-in-link" class="nav-link" href="{% url 'account_login' %}">{% translate "Sign In" %}</a>
</li>
{% endif %}
</ul>
</div>

View File

@ -1,25 +1,40 @@
{% 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>Sistema de Busca de Diários Oficiais</title>
<title>Diários Oficiais</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="{% static 'css/bootstrap-custom.min.css' %}" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'css/bootstrap-icons.css' %}">
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.1/dist/cdn.min.js"></script>
<script defer src="{% static 'js/alpine.min.js' %}"></script>
<!-- Estilos customizados -->
<link rel="stylesheet" href="styles.css">
<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">
<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>Sistema de Busca de Diários Oficiais
<i class="bi bi-search me-2"></i>Diários Oficiais
</h1>
</div>
@ -114,7 +129,7 @@
</form>
</div>
</div>
<!-- Adicionando a seção de "Você quis dizer" após iniciar a busca -->
<!-- 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>
@ -283,8 +298,7 @@
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>Sistema de Busca de Diários Oficiais</h5>
<p class="small">Uma ferramenta avançada para pesquisa em diários oficiais.</p>
<h5>Diários Oficiais</h5>
</div>
<div class="col-md-6 text-md-end">
<p class="small mb-0">&copy; 2025 Todos os direitos reservados</p>
@ -292,10 +306,88 @@
</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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
<!-- Script da aplicação -->
<script src="script.js"></script>
<script src="{% static 'js/config.js' %}"></script>
<script src="{% static 'js/script.js' %}"></script>
</body>
</html>
</html>

View File

@ -1,7 +1,7 @@
volumes:
diarios_oficiais_alems_local_postgres_data: {}
diarios_oficiais_alems_local_postgres_data_backups: {}
esdata:
esdata:
services:
django:
@ -12,6 +12,7 @@ services:
container_name: diarios_oficiais_alems_local_django
depends_on:
- postgres
- elasticsearch
volumes:
- .:/app:z
env_file:
@ -20,6 +21,21 @@ services:
ports:
- '8005:8005'
command: /start
restart: unless-stopped
nginx:
image: nginx:latest
container_name: diarios_oficiais_alems_local_nginx
volumes:
- ./compose/local/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./staticfiles:/app/staticfiles:ro
- ./mediafiles:/app/mediafiles:ro
ports:
- "80:80"
depends_on:
- django
restart: unless-stopped
postgres:
build:
@ -37,15 +53,8 @@ services:
image: docker.elastic.co/elasticsearch/elasticsearch:8.17.3
environment:
- discovery.type=single-node
- xpack.security.enabled=true
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
volumes:
- esdata:/usr/share/elasticsearch/data
frontend:
image: nginx:latest
container_name: frontend_diarios
volumes:
- ./frontend:/usr/share/nginx/html:ro
ports:
- "8006:80"

View File

@ -1,25 +1,31 @@
version: '3.8'
volumes:
production_postgres_data: {}
production_postgres_data_backups: {}
production_traefik: {}
production_esdata: {}
production_django_static: {}
production_django_media: {}
services:
django:
build:
context: .
dockerfile: ./compose/production/django/Dockerfile
image: diarios_oficiais_alems_production_django
volumes:
- production_django_media:/app/diarios_oficiais_alems/media
container_name: diarios_oficiais_alems_production_django
restart: unless-stopped
depends_on:
- postgres
- redis
- elasticsearch
volumes:
- production_django_media:/app/diarios_oficiais_alems/media
- production_django_static:/app/diarios_oficiais_alems/static
env_file:
- ./.envs/.production/.django
- ./.envs/.production/.postgres
ports:
- '8005:8005'
command: /start
postgres:
@ -27,34 +33,53 @@ services:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
image: diarios_oficiais_alems_production_postgres
container_name: diarios_oficiais_alems_production_postgres
restart: unless-stopped
volumes:
- production_postgres_data:/var/lib/postgresql/data
- production_postgres_data_backups:/backups
env_file:
- ./.envs/.production/.postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U debug"]
interval: 10s
timeout: 5s
retries: 5
traefik:
build:
context: .
dockerfile: ./compose/production/traefik/Dockerfile
image: diarios_oficiais_alems_production_traefik
depends_on:
- django
volumes:
- production_traefik:/etc/traefik/acme
ports:
- '0.0.0.0:80:80'
- '0.0.0.0:443:443'
redis:
image: docker.io/redis:6
nginx:
build:
context: .
dockerfile: ./compose/production/nginx/Dockerfile
image: diarios_oficiais_alems_production_nginx
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.17.3
container_name: diarios_oficiais_alems_production_elasticsearch
restart: unless-stopped
environment:
- discovery.type=single-node
- xpack.security.enabled=true
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
- bootstrap.memory_lock=true
volumes:
- production_esdata:/usr/share/elasticsearch/data
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
frontend:
image: nginx:alpine
container_name: diarios_oficiais_alems_production_frontend
restart: unless-stopped
depends_on:
- django
volumes:
- production_django_static:/usr/share/nginx/static:ro
- production_django_media:/usr/share/nginx/media:ro
- ./compose/production/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
- "443:443"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3

2914
erros_processamento.txt Normal file

File diff suppressed because it is too large Load Diff

5
frontend/custom.scss Normal file
View File

@ -0,0 +1,5 @@
$primary: #448AAD;
$secondary: #5C944E;
@import "node_modules/bootstrap/scss/bootstrap";

View File

@ -3,23 +3,38 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistema de Busca de Diários Oficiais</title>
<title>Diários Oficiais</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/bootstrap-custom.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="./css/bootstrap-icons.css">
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.1/dist/cdn.min.js"></script>
<script defer src="./js/alpine.min.js"></script>
<!-- Estilos customizados -->
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="./css/styles.css">
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
</head>
<body class="bg-light-gradient">
<div class="container py-5" x-data="searchApp">
<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="img/logo.jpg" alt="Logo" class="img-fluid">
</div>
<!-- Imagem à direita -->
<div class="risco-container">
<img src="img/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>Sistema de Busca de Diários Oficiais
<i class="bi bi-search me-2"></i>Diários Oficiais
</h1>
</div>
@ -283,8 +298,7 @@
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>Sistema de Busca de Diários Oficiais</h5>
<p class="small">Uma ferramenta avançada para pesquisa em diários oficiais.</p>
<h5>Diários Oficiais</h5>
</div>
<div class="col-md-6 text-md-end">
<p class="small mb-0">&copy; 2025 Todos os direitos reservados</p>
@ -292,12 +306,89 @@
</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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="./js/bootstrap.bundle.min.js"></script>
<!-- Script da aplicação -->
<script src="js/config.js"></script>
<script src="js/script.js"></script>
<script src="./js/config.js"></script>
<script src="./js/script.js"></script>
</body>
</html>

View File

@ -1 +0,0 @@
const API_BASE_URL = "http://109.199.98.226:8005";

781
frontend/package-lock.json generated Normal file
View File

@ -0,0 +1,781 @@
{
"name": "frontend",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"alpinejs": "^3.14.9",
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"sass": "^1.89.1"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@vue/reactivity": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
"dependencies": {
"@vue/shared": "3.1.5"
}
},
"node_modules/@vue/shared": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
},
"node_modules/alpinejs": {
"version": "3.14.9",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.9.tgz",
"integrity": "sha512-gqSOhTEyryU9FhviNqiHBHzgjkvtukq9tevew29fTj+ofZtfsYriw4zPirHHOAy9bw8QoL3WGhyk7QqCh5AYlw==",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/bootstrap": {
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/bootstrap-icons": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
"integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
]
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"optional": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"optional": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"optional": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/immutable": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"optional": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"optional": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"optional": true,
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"optional": true
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"optional": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass": {
"version": "1.89.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz",
"integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"optional": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
}
},
"dependencies": {
"@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"optional": true,
"requires": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
}
},
"@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"optional": true
},
"@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"optional": true
},
"@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"optional": true
},
"@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"optional": true
},
"@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"optional": true
},
"@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"optional": true
},
"@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"optional": true
},
"@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"optional": true
},
"@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"optional": true
},
"@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"optional": true
},
"@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"optional": true
},
"@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"optional": true
},
"@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"optional": true
},
"@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"peer": true
},
"@vue/reactivity": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
"requires": {
"@vue/shared": "3.1.5"
}
},
"@vue/shared": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
},
"alpinejs": {
"version": "3.14.9",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.9.tgz",
"integrity": "sha512-gqSOhTEyryU9FhviNqiHBHzgjkvtukq9tevew29fTj+ofZtfsYriw4zPirHHOAy9bw8QoL3WGhyk7QqCh5AYlw==",
"requires": {
"@vue/reactivity": "~3.1.1"
}
},
"bootstrap": {
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"requires": {}
},
"bootstrap-icons": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
"integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw=="
},
"braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"optional": true,
"requires": {
"fill-range": "^7.1.1"
}
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"requires": {
"readdirp": "^4.0.1"
}
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"optional": true
},
"fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"optional": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"immutable": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"optional": true
},
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"optional": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"optional": true
},
"micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"optional": true,
"requires": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
}
},
"node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"optional": true
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"optional": true
},
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
},
"sass": {
"version": "1.89.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz",
"integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==",
"requires": {
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
}
},
"source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"optional": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
}

8
frontend/package.json Normal file
View File

@ -0,0 +1,8 @@
{
"dependencies": {
"alpinejs": "^3.14.9",
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"sass": "^1.89.1"
}
}

View File

@ -21,6 +21,7 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis
# ------------------------------------------------------------------------------
django-elasticsearch-dsl
PyPDF2
PyMuPDF
babel
django-ninja
remote-pdb

View File

@ -34,3 +34,4 @@ pytest-django==4.10.0 # https://github.com/pytest-dev/pytest-django
debugpy
black
language-tool-python