diff --git a/.dockerignore b/.dockerignore index 318700ad6..4ff7fdc82 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,3 +12,10 @@ bower .travis.yml .env .idea +.DS_Store +.coveragerc +*.swp +.coveragerc +.drone.yml +.github +release.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f9ef2fe97..000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: python - -python: - - 3.7 - -services: - - postgresql - -install: - - pip install -r requirements/test-requirements.txt - -before_script: - - cp sapl/.env_test sapl/.env - - psql -c "CREATE USER sapl WITH PASSWORD 'sapl'" -U postgres; - - psql -c "CREATE DATABASE sapl OWNER sapl;" -U postgres - - ./scripts/django/check_migrations.sh - -script: - - ./manage.py migrate - - py.test --create-db - # - ./scripts/django/test_and_check_qa.sh - -addons: - hosts: - - 127.0.0.1 sapldb diff --git a/CHANGES.md b/CHANGES.md index 2c103d882..efac508cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,94 @@ +3.1.165-RC3 / 2026-09-01 +======================== + + * Corrige SQL injection em DispositivoSearchFragmentFormView (#3856) + * Remove limite de caracteres do campo Informação Geral da Casa (#3848) + * Oculta sessões plenárias não iniciadas da consulta pública (#3845) + * hot-fix: Adiciona MIME Media Types opcionais para CSV. + * hot-fix: Remove alerta errado + * Corrige ordem dos expedientes no PDF do Resumo da Sessão (#3844) + * hot-fix: imprime somente contéudo da ocorrência + * Fix duplicate normas/matérias in author reports and prevent unfiltered queries + * Fix ValueError in audiencia form and AttributeError on HEAD requests + * Fix panel clock source and prevent overlapping polling + * hot-fix: recover epoch from server always + * Corrige polling excessivo e bloqueia não-votantes em /voto-individual/ (#3831) + * Hot-fix: corrige cópia de request.data + * Release: 3.1.165-RC2 + * #3821 centralizar geração próximo número para create de matérias legislativas (#3822) + +3.1.166-RC0 / 2026-09-01 +======================== + + + +3.1.165-RC2 / 2026-03-26 +======================== + + * Aumenta RATE_LIMITER_RATE default de 10/m para 35/m (#3824) + * #3821 centralizar geração próximo número para create de matérias legislativas (#3822) + +3.1.165-RC1 / 2026-02-24 +======================== + + * Hot-fix: fixes setup tool for Haystack + +3.1.165-RC0 / 2026-02-20 +======================== + + * Implementa Header LastModified na Api (#3808) + * Impl expansão dinâmica de campos na API (#3809) + * Seleciona tipo de votação para múltiplas matérias ao incluir na Ordem do Dia ou Expediente (#3781) + * Adiciona requestId em requisições + * Hot-fix: conserta erro em JS de voto individual + * Coloca negrito em resultado de votação + * Inversão da ordem de ano e número em Adicionar Norma + * Aumento de nome de parlamentar + * Aumento do tamanho de Nome e Tema da Audiência Pública + * Conserta bug na pesquisa do Relatório de Votações Nominais (#3803) + * feat: impl filtro m2m com lookup '__in' para buscas com multiplos ids (#3807) + * Refatora rate limiter + * Release: 3.1.164-RC5 + +3.1.164-RC5 / 2025-09-22 +======================== + + * Adiciona smoke test for rate limiter + * Hot-fix: rate limiter get ip + +3.1.164-RC4 / 2025-09-22 +======================== + + * Fix recibo proposição e adiciona rate limiter em matéria e norma + +3.1.164-RC3 / 2025-09-16 +======================== + + * Health and Ready endpoints (#3788) + * Fix read-only mount on k8s + * Remove setup.py do projeto SAPL + +3.1.164-RC2 / 2025-09-08 +======================== + + * Restaura prod settings de docker-compose + * Restringe tipos de arquivos para upload (#3794) + * feat: cria script para remocao de proposicoes de um autor (#3703) + * HOT-Fix: Conserta erro introduzido no PR #3785 + * Atualização da imagem base Docker (#3787) + * Ajustes solicitados - Relatório de Votações Nominais (#3785) + * Refatora lógica de geração de CSV/json/xlsx (#3786) + * Remove btn libras de media print (#3777) + +3.1.164-RC1 / 2025-08-05 +======================== + + * Adiciona botoes de impressao na pesquisa de doc adm + * Fix: conserta erro em historico proposicao + * Desabilita detalhe de Justificativa Ausência para acesso público + * Adiciona homepage do parlamentar a perfil público + 3.1.164-RC0 / 2025-07-09 ======================== diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 7c730520d..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -include README.rst LICENSE.txt -include sapl/webpack-stats.json -recursive-include sapl *.html *.yaml -recursive-include sapl/static * -recursive-include sapl/relatorios/templates *.py -recursive-include sapl/compilacao *.sql -global-exclude __pycache__ -global-exclude *.py[co] diff --git a/README.rst b/README.rst index 5481ba985..fc082b9d5 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,4 @@ -.. image:: https://travis-ci.org/interlegis/sapl.svg?branch=3.1.x - :target: https://travis-ci.org/interlegis/sapl + *********************************************** diff --git a/docker/Dockerfile b/docker/Dockerfile index 6e5f27864..831627ec8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,73 +1,123 @@ -FROM python:3.9-slim-buster - -# Setup env -ENV LANG C.UTF-8 -ENV LC_ALL C.UTF-8 -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED=1 -ENV DEBIAN_FRONTEND noninteractive - -ENV BUILD_PACKAGES apt-utils apt-file libpq-dev graphviz-dev build-essential git pkg-config \ - python3-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \ - libcairo2-dev software-properties-common python3-setuptools python3-pip - -## NAO EH PRA TIRAR O vim DA LISTA DE COMANDOS INSTALADOS!!! -ENV RUN_PACKAGES graphviz python3-lxml python3-magic postgresql-client python3-psycopg2 \ - poppler-utils curl jq bash vim python3-venv tzdata nodejs \ - fontconfig ttf-dejavu python nginx - -RUN mkdir -p /var/interlegis/sapl +# ---------- 1) BUILDER ---------- +FROM python:3.12-slim-bookworm AS builder + +ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + VENV_DIR=/opt/venv \ + PIP_NO_CACHE_DIR=on + +# Dev headers e toolchain só no builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git pkg-config \ + libpq-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \ + libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev libharfbuzz-dev \ + libfreetype6-dev zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Venv independente do sistema +RUN python -m venv "${VENV_DIR}" \ + && "${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel + +WORKDIR /build + +# Copie APENAS os requirements primeiro para maximizar cache +COPY requirements/ ./requirements/ + +# Instale os requisitos de produção +# ATENÇÃO: se seu código importa prompt_toolkit em runtime, inclua em requirements.txt: +# prompt_toolkit>=3,<4 +RUN "${VENV_DIR}/bin/pip" install -r requirements/requirements.txt + +# Opcional: verificação de conflitos (falha cedo se faltar algo) +RUN "${VENV_DIR}/bin/pip" check || true + + +# ---------- 2) RUNTIME ---------- +FROM python:3.12-slim-bookworm AS runtime + +ARG WITH_GRAPHVIZ=1 +ARG WITH_POPPLER=1 +ARG WITH_PSQL_CLIENT=1 +ARG WITH_NGINX=1 + +ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + VENV_DIR=/opt/venv \ + PATH=/opt/venv/bin:$PATH \ + PIP_NO_CACHE_DIR=on + +# Pacotes de runtime (sem *-dev) +# Removi python3/python3-venv (já temos o Python da base) +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + curl jq bash tzdata fontconfig tini libmagic1 \ + libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 \ + libharfbuzz0b libfreetype6 libjpeg62-turbo zlib1g fonts-dejavu-core; \ + if [ "$WITH_GRAPHVIZ" = "1" ]; then apt-get install -y --no-install-recommends graphviz; fi; \ + if [ "$WITH_POPPLER" = "1" ]; then apt-get install -y --no-install-recommends poppler-utils; fi; \ + if [ "$WITH_PSQL_CLIENT" = "1" ]; then apt-get install -y --no-install-recommends postgresql-client; fi; \ + if [ "$WITH_NGINX" = "1" ]; then apt-get install -y --no-install-recommends nginx; fi; \ + rm -rf /var/lib/apt/lists/* + +# Usuários/grupos (idempotente) +RUN useradd --system --no-create-home --shell /usr/sbin/nologin sapl || true \ + && groupadd -g 101 -r nginx || true \ + && usermod -aG nginx www-data || true \ + && usermod -aG nginx sapl || true + +# Estrutura de diretórios +RUN mkdir -p /var/interlegis/sapl /var/interlegis/sapl/data /var/interlegis/sapl/media /var/interlegis/sapl/run \ + && chown -R root:nginx /var/interlegis/sapl /var/interlegis/sapl/run \ + && chmod -R g+rwX /var/interlegis/sapl \ + && chmod 2775 /var/interlegis/sapl /var/interlegis/sapl/run \ + && find /var/interlegis/sapl -type d -exec chmod g+s {} + WORKDIR /var/interlegis/sapl/ -ADD . /var/interlegis/sapl/ - -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y --no-install-recommends $BUILD_PACKAGES $RUN_PACKAGES && \ - fc-cache -fv && \ - pip3 install --no-cache-dir --upgrade pip setuptools && \ - rm -f /etc/nginx/conf.d/* && \ - pip install --no-cache-dir -r /var/interlegis/sapl/requirements/dev-requirements.txt --upgrade setuptools && \ - SUDO_FORCE_REMOVE=yes apt-get purge -y --auto-remove $BUILD_PACKAGES && \ - apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* - -WORKDIR /var/interlegis/sapl/ -ADD . /var/interlegis/sapl/ - -COPY docker/start.sh $HOME -COPY docker/solr_cli.py $HOME -COPY docker/wait-for-pg.sh $HOME -COPY docker/wait-for-solr.sh $HOME -COPY docker/create_admin.py $HOME -COPY docker/genkey.py $HOME -COPY docker/gunicorn_start.sh $HOME - -COPY docker/config/nginx/sapl.conf /etc/nginx/conf.d -COPY docker/config/nginx/nginx.conf /etc/nginx/nginx.conf +# Traga o venv pré-instalado +COPY --from=builder ${VENV_DIR} ${VENV_DIR} + +# Código da aplicação (depois do venv para aproveitar cache) +COPY . /var/interlegis/sapl/ + +# Nginx (somente se instalado) +RUN if [ "$WITH_NGINX" = "1" ]; then \ + rm -f /etc/nginx/conf.d/*; \ + cp docker/config/nginx/sapl.conf /etc/nginx/conf.d/sapl.conf; \ + cp docker/config/nginx/nginx.conf /etc/nginx/nginx.conf; \ + fi + +# Scripts + gunicorn.conf no diretório da app +RUN install -m 755 docker/startup_scripts/start.sh /var/interlegis/sapl/start.sh \ + && install -m 755 docker/startup_scripts/wait-for-pg.sh /var/interlegis/sapl/wait-for-pg.sh \ + && install -m 755 docker/startup_scripts/wait-for-solr.sh /var/interlegis/sapl/wait-for-solr.sh \ + && install -m 644 docker/startup_scripts/solr_cli.py /var/interlegis/sapl/solr_cli.py \ + && install -m 644 docker/startup_scripts/create_admin.py /var/interlegis/sapl/create_admin.py \ + && install -m 644 docker/startup_scripts/genkey.py /var/interlegis/sapl/genkey.py \ + && install -m 644 docker/startup_scripts/gunicorn.conf.py /var/interlegis/sapl/gunicorn.conf.py + +# (Se possível, evite copiar .env no build. Use secrets/variáveis em runtime.) COPY docker/config/env_dockerfile /var/interlegis/sapl/sapl/.env -RUN python3 manage.py collectstatic --noinput --clear - -# Remove .env(fake) e sapl.db da imagem -RUN rm -rf /var/interlegis/sapl/sapl/.env && \ - rm -rf /var/interlegis/sapl/sapl.db - -RUN chmod +x /var/interlegis/sapl/start.sh && \ - chmod +x /var/interlegis/sapl/wait-for-solr.sh && \ - chmod +x /var/interlegis/sapl/wait-for-pg.sh && \ - ln -sf /dev/stdout /var/log/nginx/access.log && \ - ln -sf /dev/stderr /var/log/nginx/error.log && \ - mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \ - ln -s /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log - -# Debian não possui usuário 'nginx' necessário para o Debian -RUN useradd --no-create-home nginx +# Logs (só se nginx estiver presente) +RUN if [ "$WITH_NGINX" = "1" ]; then \ + ln -sf /dev/stdout /var/log/nginx/access.log; \ + ln -sf /dev/stderr /var/log/nginx/error.log; \ + fi \ + && mkdir -p /var/log/sapl/ \ + && ln -sf /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log -ENV DEBIAN_FRONTEND teletype +# Cache de fontes e collectstatic +# NÃO atualizamos pip aqui (já veio pronto do builder) +RUN fc-cache -fv \ + && python manage.py collectstatic --noinput --clear \ + && rm -f /var/interlegis/sapl/sapl/.env /var/interlegis/sapl/sapl.db || true -EXPOSE 80/tcp 443/tcp +ENV DEBIAN_FRONTEND=teletype +EXPOSE 80 443 VOLUME ["/var/interlegis/sapl/data", "/var/interlegis/sapl/media", "/var/log/sapl/"] +ENTRYPOINT ["/usr/bin/tini","--"] CMD ["/var/interlegis/sapl/start.sh"] diff --git a/docker/config/nginx/nginx.conf b/docker/config/nginx/nginx.conf index 29b9e805c..e002a6905 100644 --- a/docker/config/nginx/nginx.conf +++ b/docker/config/nginx/nginx.conf @@ -1,4 +1,4 @@ -user nginx; +user www-data nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; diff --git a/docker/config/nginx/sapl.conf b/docker/config/nginx/sapl.conf index 18641045e..015538c96 100644 --- a/docker/config/nginx/sapl.conf +++ b/docker/config/nginx/sapl.conf @@ -4,10 +4,17 @@ upstream sapl_server { } +# Se o cliente já manda X-Request-ID, reaproveita; senão, usa $request_id (nginx) +map $http_x_request_id $req_id { + default $http_x_request_id; + "" $request_id; +} + + server { listen 80; - server_name sapl.test; + server_name sapl.prod; client_max_body_size 4G; @@ -30,7 +37,9 @@ server { return 204; } + proxy_set_header X-Request-ID $req_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://sapl_server; @@ -45,10 +54,11 @@ server { } location / { + proxy_set_header X-Request-ID $req_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; - proxy_pass http://sapl_server; } diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 436f5e588..46b5bd627 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -9,7 +9,9 @@ services: POSTGRES_PASSWORD: sapl POSTGRES_USER: sapl POSTGRES_DB: sapl - PGDATA : /var/lib/postgresql/data/ + PGDATA: /var/lib/postgresql/data/ + TZ: UTC + PG_TZ: UTC volumes: - sapldb_data:/var/lib/postgresql/data/ ports: @@ -31,7 +33,7 @@ services: networks: - sapl-net sapl: - image: interlegis/sapl:3.1.164-RC0 + image: interlegis/sapl:3.1.165-RC3 # build: # context: ../ # dockerfile: ./docker/Dockerfile diff --git a/docker/gunicorn_start.sh b/docker/gunicorn_start.sh deleted file mode 100755 index 9ef4b7982..000000000 --- a/docker/gunicorn_start.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash - -## -## -## PARA USO EXCLUSIVO DO CONTAINER DOCKER DO SAPL!!! -## EVITE USAR PARA CHAMADA DIRETAS -## -## - -# As seen in http://tutos.readthedocs.org/en/latest/source/ndg.html - -SAPL_DIR="/var/interlegis/sapl" - -# Seta um novo diretório foi passado como raiz para o SAPL -# caso esse tenha sido passado como parâmetro -if [ "$1" ] -then - SAPL_DIR="$1" -fi - -NAME="SAPL" # Name of the application (*) -DJANGODIR=/var/interlegis/sapl/ # Django project directory (*) -SOCKFILE=/var/interlegis/sapl/run/gunicorn.sock # we will communicate using this unix socket (*) -USER=`whoami` # the user to run as (*) -GROUP=`whoami` # the group to run as (*) -NUM_WORKERS=3 # how many worker processes should Gunicorn spawn (*) - # NUM_WORKERS = 2 * CPUS + 1 -TIMEOUT=300 -MAX_REQUESTS=100 # number of requests before restarting worker -DJANGO_SETTINGS_MODULE=sapl.settings # which settings file should Django use (*) -DJANGO_WSGI_MODULE=sapl.wsgi # WSGI module name (*) - -echo "Starting $NAME as `whoami` on base dir $SAPL_DIR" - -# Create the run directory if it doesn't exist -RUNDIR=$(dirname $SOCKFILE) -test -d $RUNDIR || mkdir -p $RUNDIR - -# Start your Django Unicorn -# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon) -exec gunicorn ${DJANGO_WSGI_MODULE}:application \ - --name $NAME \ - --log-level debug \ - --timeout $TIMEOUT \ - --workers $NUM_WORKERS \ - --max-requests $MAX_REQUESTS \ - --user $USER \ - --access-logfile /var/log/sapl/access.log \ - --error-logfile /var/log/sapl/error.log \ - --bind=unix:$SOCKFILE diff --git a/docker/k8s/sapl-deploy.sh b/docker/k8s/sapl-deploy.sh new file mode 100755 index 000000000..0133704b8 --- /dev/null +++ b/docker/k8s/sapl-deploy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +kubectl create namespace sapl +mkdir -p ./sapl-secret-data +kubectl -n sapl create secret generic sapl-secretkey --from-file=./sapl-secret-data/ +kubectl apply -f sapl-k8s.yaml + +kubectl rollout status deployment/sapl -n sapl + +POD=$(kubectl get pod -n sapl -l app=sapl -o jsonpath='{.items[0].metadata.name}') +kubectl exec -n sapl "$POD" -- ls -l /var/interlegis/sapl/data + diff --git a/docker/k8s/sapl-k8s.yaml b/docker/k8s/sapl-k8s.yaml new file mode 100644 index 000000000..8a45e015e --- /dev/null +++ b/docker/k8s/sapl-k8s.yaml @@ -0,0 +1,218 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: sapl +--- +apiVersion: v1 +kind: Service +metadata: + name: sapldb + namespace: sapl +spec: + selector: + app: sapldb + ports: + - name: postgres + port: 5432 + targetPort: 5432 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sapldb-data + namespace: sapl +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path + resources: + requests: + storage: 5Gi # or 1Gi for solr-configsets +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: sapldb + namespace: sapl +spec: + serviceName: sapldb + replicas: 1 + selector: + matchLabels: + app: sapldb + template: + metadata: + labels: + app: sapldb + spec: + containers: + - name: postgres + image: postgres:10.5-alpine + env: + - name: POSTGRES_PASSWORD + value: "sapl" + - name: POSTGRES_USER + value: "sapl" + - name: POSTGRES_DB + value: "sapl" + - name: PGDATA + value: /var/lib/postgresql/data/ + - name: TZ + value: UTC + - name: PG_TZ + value: UTC + ports: + - containerPort: 5432 + volumeMounts: + - name: sapldb-data + mountPath: /var/lib/postgresql/data/ + volumes: + - name: sapldb-data + persistentVolumeClaim: + claimName: sapldb-data +--- +apiVersion: v1 +kind: Service +metadata: + name: saplsolr + namespace: sapl +spec: + selector: + app: saplsolr + ports: + - name: solr + port: 8983 + targetPort: 8983 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: solr-data + namespace: sapl +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path + resources: + requests: + storage: 5Gi # or 1Gi for solr-configsets +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: solr-configsets + namespace: sapl +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path + resources: + requests: + storage: 5Gi # or 1Gi for solr-configsets +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: saplsolr + namespace: sapl +spec: + serviceName: saplsolr + replicas: 1 + selector: + matchLabels: + app: saplsolr + template: + metadata: + labels: + app: saplsolr + spec: + containers: + - name: solr + image: solr:8.11 + command: ["bash","-lc","bin/solr start -c -f"] + ports: + - containerPort: 8983 + volumeMounts: + - name: solr-data + mountPath: /var/solr + - name: solr-configsets + mountPath: /opt/solr/server/solr/configsets + volumes: + - name: solr-data + persistentVolumeClaim: + claimName: solr-data + - name: solr-configsets + persistentVolumeClaim: + claimName: solr-configsets +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sapl-media + namespace: sapl +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path + resources: + requests: + storage: 5Gi # or 1Gi for solr-configsets +--- +apiVersion: v1 +kind: Service +metadata: + name: sapl + namespace: sapl +spec: + selector: + app: sapl + type: NodePort + ports: + - name: http + port: 80 + targetPort: 80 + nodePort: 30080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sapl + namespace: sapl +spec: + replicas: 1 + selector: + matchLabels: + app: sapl + template: + metadata: + labels: + app: sapl + spec: + containers: + - name: sapl + image: eribeiro/sapl:debug-k8s-1 + ports: + - containerPort: 80 + volumeMounts: + - name: data + mountPath: /var/interlegis/sapl/data + readOnly: true # secrets are always mounted read-only + volumes: + - name: data + secret: + secretName: sapl-secretkey + defaultMode: 0440 # ensures read-only + env: + - name: ADMIN_PASSWORD + value: "interlegis" + - name: ADMIN_EMAIL + value: "email@dominio.net" + - name: DEBUG + value: "False" + - name: EMAIL_PORT + value: "587" + - name: EMAIL_USE_TLS + value: "False" + - name: EMAIL_HOST + value: "smtp.dominio.net" + - name: EMAIL_HOST_USER + value: "usuariosmtp" + - name: EMAIL_SEND_USER + + diff --git a/docker/simple_gunicorn.sh b/docker/simple_gunicorn.sh index 977d44288..196e19dec 100755 --- a/docker/simple_gunicorn.sh +++ b/docker/simple_gunicorn.sh @@ -12,4 +12,5 @@ export PYTHONPATH=$DJANGODIR:$PYTHONPATH # Get eth0 IP and filter out the netmask portion (/24, e.g.) IP=`ip addr | grep 'inet .* eth0' | awk '{print $2}' | sed 's/\/[0-9]*//'` -gunicorn --bind $IP:8000 sapl.wsgi:application +#gunicorn --bind $IP:8000 sapl.wsgi:application +gunicorn -c gunicorn.conf.py sapl.wsgi:application diff --git a/docker/start.sh b/docker/start.sh deleted file mode 100755 index 7a9345fbb..000000000 --- a/docker/start.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash - -create_env() { - echo "[ENV FILE] creating .env file..." - # check if file exists - if [ -f "/var/interlegis/sapl/data/secret.key" ]; then - KEY=`cat /var/interlegis/sapl/data/secret.key` - else - KEY=`python3 genkey.py` - echo $KEY > data/secret.key - fi - - FILENAME="/var/interlegis/sapl/sapl/.env" - - if [ -z "${DATABASE_URL:-}" ]; then - DATABASE_URL="postgresql://sapl:sapl@sapldb:5432/sapl" - fi - - # ALWAYS replace the content of .env variable - # If want to conditionally create only if absent then use IF below - # if [ ! -f $FILENAME ]; then - - touch $FILENAME - - # explicitly use '>' to erase any previous content - echo "SECRET_KEY="$KEY > $FILENAME - # now only appends - echo "DATABASE_URL = "$DATABASE_URL >> $FILENAME - echo "DEBUG = ""${DEBUG-False}" >> $FILENAME - echo "EMAIL_USE_TLS = ""${USE_TLS-True}" >> $FILENAME - echo "EMAIL_PORT = ""${EMAIL_PORT-587}" >> $FILENAME - echo "EMAIL_HOST = ""${EMAIL_HOST-''}" >> $FILENAME - echo "EMAIL_HOST_USER = ""${EMAIL_HOST_USER-''}" >> $FILENAME - echo "EMAIL_HOST_PASSWORD = ""${EMAIL_HOST_PASSWORD-''}" >> $FILENAME - echo "EMAIL_SEND_USER = ""${EMAIL_HOST_USER-''}" >> $FILENAME - echo "DEFAULT_FROM_EMAIL = ""${EMAIL_HOST_USER-''}" >> $FILENAME - echo "SERVER_EMAIL = ""${EMAIL_HOST_USER-''}" >> $FILENAME - echo "USE_SOLR = ""${USE_SOLR-False}" >> $FILENAME - echo "SOLR_COLLECTION = ""${SOLR_COLLECTION-sapl}" >> $FILENAME - echo "SOLR_URL = ""${SOLR_URL-http://localhost:8983}" >> $FILENAME - echo "IS_ZK_EMBEDDED = ""${IS_ZK_EMBEDDED-False}" >> $FILENAME - echo "ENABLE_SAPN = ""${ENABLE_SAPN-False}" >> $FILENAME - - echo "[ENV FILE] done." -} - -create_env - -/bin/bash wait-for-pg.sh $DATABASE_URL - -yes yes | python3 manage.py migrate - - -## SOLR -USE_SOLR="${USE_SOLR:=False}" -SOLR_URL="${SOLR_URL:=http://localhost:8983}" -SOLR_COLLECTION="${SOLR_COLLECTION:=sapl}" -NUM_SHARDS=${NUM_SHARDS:=1} -RF=${RF:=1} -MAX_SHARDS_PER_NODE=${MAX_SHARDS_PER_NODE:=1} -IS_ZK_EMBEDDED="${IS_ZK_EMBEDDED:=False}" - -if [ "${USE_SOLR-False}" == "True" ] || [ "${USE_SOLR-False}" == "true" ]; then - - echo "Solr configurations" - echo "===================" - echo "URL: $SOLR_URL" - echo "COLLECTION: $SOLR_COLLECTION" - echo "NUM_SHARDS: $NUM_SHARDS" - echo "REPLICATION FACTOR: $RF" - echo "MAX SHARDS PER NODE: $MAX_SHARDS_PER_NODE" - echo "ASSUME ZK EMBEDDED: $IS_ZK_EMBEDDED" - echo "=========================================" - - echo "running Solr script" - /bin/bash wait-for-solr.sh $SOLR_URL - CHECK_SOLR_RETURN=$? - - if [ $CHECK_SOLR_RETURN == 1 ]; then - echo "Connecting to Solr..." - - - if [ "${IS_ZK_EMBEDDED-False}" == "True" ] || [ "${IS_ZK_EMBEDDED-False}" == "true" ]; then - ZK_EMBEDDED="--embedded_zk" - echo "Assuming embedded ZooKeeper instalation..." - fi - - python3 solr_cli.py -u $SOLR_URL -c $SOLR_COLLECTION -s $NUM_SHARDS -rf $RF -ms $MAX_SHARDS_PER_NODE $ZK_EMBEDDED & - # Enable SOLR switch on, creating if it doesn't exist on database - ./manage.py waffle_switch SOLR_SWITCH on --create - else - echo "Solr is offline, not possible to connect." - # Disable Solr switch off, creating if it doesn't exist on database - ./manage.py waffle_switch SOLR_SWITCH off --create - fi - -else - echo "Solr support is not initialized." - # Disable Solr switch off, creating if it doesn't exist on database - ./manage.py waffle_switch SOLR_SWITCH off --create -fi - -## Enable/Disable SAPN -if [ "${ENABLE_SAPN-False}" == "True" ] || [ "${ENABLE_SAPN-False}" == "true" ]; then - echo "Enabling SAPN" - ./manage.py waffle_switch SAPLN_SWITCH on --create -else - echo "Enabling SAPL" - ./manage.py waffle_switch SAPLN_SWITCH off --create -fi - - -echo "Creating admin user..." - -user_created=$(python3 create_admin.py 2>&1) - -echo $user_created - -cmd=$(echo $user_created | grep 'ADMIN_USER_EXISTS') -user_exists=$? - -cmd=$(echo $user_created | grep 'MISSING_ADMIN_PASSWORD') -lack_pwd=$? - -if [ $user_exists -eq 0 ]; then - echo "[SUPERUSER CREATION] User admin already exists. Not creating" -fi - -if [ $lack_pwd -eq 0 ]; then - echo "[SUPERUSER] Environment variable $ADMIN_PASSWORD for superuser admin was not set. Leaving container" - # return -1 -fi - -# Backfilling AuditLog's JSON field -time ./manage.py backfill_auditlog & - -echo "-------------------------------------" -echo "| ███████╗ █████╗ ██████╗ ██╗ |" -echo "| ██╔════╝██╔══██╗██╔══██╗██║ |" -echo "| ███████╗███████║██████╔╝██║ |" -echo "| ╚════██║██╔══██║██╔═══╝ ██║ |" -echo "| ███████║██║ ██║██║ ███████╗ |" -echo "| ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝ |" -echo "-------------------------------------" - -/bin/sh gunicorn_start.sh & -/usr/sbin/nginx -g "daemon off;" diff --git a/docker/create_admin.py b/docker/startup_scripts/create_admin.py similarity index 100% rename from docker/create_admin.py rename to docker/startup_scripts/create_admin.py diff --git a/docker/genkey.py b/docker/startup_scripts/genkey.py similarity index 100% rename from docker/genkey.py rename to docker/startup_scripts/genkey.py diff --git a/docker/startup_scripts/gunicorn.conf.py b/docker/startup_scripts/gunicorn.conf.py new file mode 100644 index 000000000..6bdcacb02 --- /dev/null +++ b/docker/startup_scripts/gunicorn.conf.py @@ -0,0 +1,81 @@ +# /var/interlegis/sapl/gunicorn.conf.py + +import os +import pathlib +import multiprocessing + +# ---- SAPL app configuration ---- +NAME = "SAPL" +DJANGODIR = "/var/interlegis/sapl" +SOCKFILE = f"unix:{DJANGODIR}/run/gunicorn.sock" +USER = "sapl" +GROUP = "nginx" +NUM_WORKERS = int(os.getenv("WEB_CONCURRENCY", "3")) +THREADS = int(os.getenv("GUNICORN_THREADS", "8")) +TIMEOUT = int(os.getenv("GUNICORN_TIMEOUT", "300")) +MAX_REQUESTS = 1000 +WORKER_CLASS = "gthread" +DJANGO_SETTINGS = "sapl.settings" +WSGI_APP = "sapl.wsgi:application" + +# ---- gunicorn settings ---- +# Equivalent of: --name +proc_name = NAME + +# Equivalent of: --bind=unix:... +# For quick testing via browser, you can switch to: bind = "0.0.0.0:8000" +bind = f"unix:{SOCKFILE}" +umask = 0o007 +user = USER +group = GROUP + +# Ensure imports work like in your script’s working dir +chdir = DJANGODIR + +# Allow starting with just: gunicorn -c gunicorn.conf.py +wsgi_app = WSGI_APP + +# Logs +loglevel = "debug" +accesslog = "/var/log/sapl/access.log" +errorlog = "/var/log/sapl/error.log" +# errorlog = "-" # send to stderr (so you see it in docker logs or terminal) +# accesslog = "-" # send to stdout +capture_output = True # capture print/tracebacks from app + +# Worker/process lifecycle +workers = NUM_WORKERS +worker_class = WORKER_CLASS +threads = THREADS +timeout = TIMEOUT +graceful_timeout = 30 +keepalive = 10 +backlog = 2048 +max_requests = MAX_REQUESTS +max_requests_jitter = 200 +worker_max_memory_per_child = 300 * 1024 * 1024 # 300 MB cap + +# Environment (same as exporting before running) +raw_env = [ + f"DJANGO_SETTINGS_MODULE={DJANGO_SETTINGS}", + # If you’re using ReportLab and seeing segfaults with PDFs, keep this: + # "RL_NOACCEL=1", +] + +# If you previously enabled preload and saw segfaults with native libs, keep it off: +preload_app = False + + +# Create the run/ directory for the UNIX socket (your script did this) +def on_starting(server): + pathlib.Path(SOCKFILE).parent.mkdir(parents=True, exist_ok=True) + + +# Close DB connections after fork (safer when using preload or certain DB drivers) +def post_fork(server, worker): + try: + from django import db + db.connections.close_all() + except Exception: + # Django not initialized yet or not available + pass diff --git a/docker/solr_cli.py b/docker/startup_scripts/solr_cli.py similarity index 95% rename from docker/solr_cli.py rename to docker/startup_scripts/solr_cli.py index d452d1fe9..e7600914a 100755 --- a/docker/solr_cli.py +++ b/docker/startup_scripts/solr_cli.py @@ -20,6 +20,8 @@ from kazoo.client import KazooClient # logging.basicConfig() +logging.captureWarnings(True) +logger = logging.getLogger(__name__) SECURITY_FILE_TEMPLATE = """ { @@ -49,6 +51,7 @@ def solr_hash_password(password: str, salt: str = None): salt (optional): base64 salt string returns: sha256 hash of password and salt (both base64 strings) """ + logger.debug("Generating Solr password") m = sha256() if salt is None: salt = secrets.token_bytes(32) @@ -67,32 +70,32 @@ def solr_hash_password(password: str, salt: str = None): def create_security_file(username, password): - print("Creating security.json file...") + logger.info("Creating security.json file...") with open("security.json", "w") as f: cypher, salt = solr_hash_password(password) f.write(SECURITY_FILE_TEMPLATE % (username, cypher, salt, username)) - print("file created!") + logger.info("file created!") def upload_security_file(zk_host): zk_port = 9983 # embedded ZK port - print(f"Uploading security file to Solr, ZK server={zk_host}:{zk_port}...") + logger.info(f"Uploading security file to Solr, ZK server={zk_host}:{zk_port}...") try: with open('security.json', 'r') as f: data = f.read() zk = KazooClient(hosts=f"{zk_host}:{zk_port}") zk.start() - print("Uploading security.json file...") + logger.info("Uploading security.json file...") if zk.exists('/security.json'): zk.set("/security.json", str.encode(data)) else: zk.create("/security.json", str.encode(data)) data, stat = zk.get('/security.json') - print("file uploaded!") - print(data.decode('utf-8')) + logger.info("file uploaded!") + logger.info(data.decode('utf-8')) zk.stop() except Exception as e: - print(e) + logger.error(e) sys.exit(-1) @@ -250,6 +253,7 @@ def setup_embedded_zk(solr_url): _, solr_user, solr_pwd, solr_host, solr_port = match.groups() if solr_user and solr_pwd and solr_host: + print(f"Creating Solr user {solr_user} with password {solr_pwd}") create_security_file(solr_user, solr_pwd) upload_security_file(solr_host) else: diff --git a/docker/startup_scripts/start.sh b/docker/startup_scripts/start.sh new file mode 100755 index 000000000..bd98bdfc0 --- /dev/null +++ b/docker/startup_scripts/start.sh @@ -0,0 +1,292 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' + +APP_DIR="/var/interlegis/sapl" +DATA_DIR="/var/interlegis/sapl/data" +RUN_DIR="/var/interlegis/sapl/run" + +ENV_FILE="$APP_DIR/.env" +SECRET_FILE="$DATA_DIR/secret.key" + +chown -R root:nginx "$RUN_DIR" || true +chmod -R g+rwX "$RUN_DIR" || true + +# setgid bit on our writable trees (not data/) +find "$RUN_DIR" -type d -exec chmod g+s {} + 2>/dev/null || true + +log() { printf '[%s] %s\n' "$(date -Is)" "$*"; } +err() { printf '[%s] ERROR: %s\n' "$(date -Is)" "$*" >&2; } + +cleanup() { jobs -p | xargs -r kill 2>/dev/null || true; } +trap cleanup TERM INT EXIT + +# --- new function --- +configure_pg_timezone() { + : "${DATABASE_URL:=postgresql://sapl:sapl@sapldb:5432/sapl}" + : "${DB_TIMEZONE:=America/Sao_Paulo}" + : "${DB_NAME:=}" + : "${DB_ROLE:=}" + + log "Checking database/role timezone defaults…" + + # Detect DB and role if not provided + if [[ -z "$DB_NAME" ]]; then + DB_NAME="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'select current_database();')" + fi + if [[ -z "$DB_ROLE" ]]; then + DB_ROLE="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'select current_user;')" + fi + + # What is the effective timezone for this DB/role right now? + current_tz="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'show time zone;')" + current_tz_lower="${current_tz,,}" + + # Consider these as already UTC + if [[ "$current_tz_lower" == "utc" || "$current_tz_lower" == "etc/utc" ]]; then + log "Timezone already UTC for DB='$DB_NAME' ROLE='$DB_ROLE' (SHOW TIME ZONE => $current_tz). Skipping ALTERs." + return + fi + + log "Timezone is '$current_tz' (not UTC). Applying persistent defaults…" + + # Persist at database level (requires DB owner or superuser) + if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q \ + -c "ALTER DATABASE \"$DB_NAME\" SET timezone TO '$DB_TIMEZONE';"; then + log "ALTER DATABASE \"$DB_NAME\" SET timezone TO '$DB_TIMEZONE' applied." + else + err "ALTER DATABASE \"$DB_NAME\" failed. Need DB owner or superuser." + exit 1 + fi + + # Persist at role level (requires superuser) + if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q \ + -c "ALTER ROLE \"$DB_ROLE\" SET timezone TO '$DB_TIMEZONE';"; then + log "ALTER ROLE \"$DB_ROLE\" SET timezone TO '$DB_TIMEZONE' applied." + else + err "ALTER ROLE \"$DB_ROLE\" failed. Need superuser privileges." + exit 1 + fi + + # Re-check (new session shows the new default) + verify_tz="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'show time zone;')" + log "SHOW TIME ZONE now => $verify_tz (new sessions will inherit the defaults)." +} + + +create_secret() { + if [[ -f "$SECRET_FILE" ]]; then + SECRET_KEY="$(<"$SECRET_FILE")" + else + log "Generating SECRET_KEY..." + SECRET_KEY="$(python3 genkey.py)" + umask 177 + printf '%s\n' "$SECRET_KEY" > "$SECRET_FILE" + fi + export SECRET_KEY +} + +write_env_file() { + : "${DATABASE_URL:=postgresql://sapl:sapl@sapldb:5432/sapl}" + : "${DEBUG:=False}" + : "${EMAIL_USE_TLS:=True}" + : "${EMAIL_PORT:=587}" + : "${EMAIL_HOST:=}" + : "${EMAIL_HOST_USER:=}" + : "${EMAIL_HOST_PASSWORD:=}" + : "${DEFAULT_FROM_EMAIL:=$EMAIL_HOST_USER}" + : "${SERVER_EMAIL:=$EMAIL_HOST_USER}" + : "${USE_SOLR:=False}" + : "${SOLR_COLLECTION:=sapl}" + : "${SOLR_URL:=http://localhost:8983}" + : "${IS_ZK_EMBEDDED:=False}" + : "${NUM_SHARDS:=1}" + : "${RF:=1}" + : "${MAX_SHARDS_PER_NODE:=1}" + : "${ENABLE_SAPN:=False}" + + tmp="$(mktemp)" + { + printf 'SECRET_KEY=%s\n' "$SECRET_KEY" + printf 'DATABASE_URL=%s\n' "$DATABASE_URL" + printf 'DEBUG=%s\n' "$DEBUG" + printf 'EMAIL_USE_TLS=%s\n' "$EMAIL_USE_TLS" + printf 'EMAIL_PORT=%s\n' "$EMAIL_PORT" + printf 'EMAIL_HOST=%s\n' "$EMAIL_HOST" + printf 'EMAIL_HOST_USER=%s\n' "$EMAIL_HOST_USER" + printf 'EMAIL_HOST_PASSWORD=%s\n' "$EMAIL_HOST_PASSWORD" + printf 'EMAIL_SEND_USER=%s\n' "$EMAIL_HOST_USER" + printf 'DEFAULT_FROM_EMAIL=%s\n' "$DEFAULT_FROM_EMAIL" + printf 'SERVER_EMAIL=%s\n' "$SERVER_EMAIL" + printf 'USE_SOLR=%s\n' "$USE_SOLR" + printf 'SOLR_COLLECTION=%s\n' "$SOLR_COLLECTION" + printf 'SOLR_URL=%s\n' "$SOLR_URL" + printf 'IS_ZK_EMBEDDED=%s\n' "$IS_ZK_EMBEDDED" + printf 'NUM_SHARDS=%s\n' "$NUM_SHARDS" + printf 'RF=%s\n' "$RF" + printf 'MAX_SHARDS_PER_NODE=%s\n' "$MAX_SHARDS_PER_NODE" + printf 'ENABLE_SAPN=%s\n' "$ENABLE_SAPN" + } > "$tmp" + + chmod 600 "$tmp" + mv -f "$tmp" "$ENV_FILE" + log "[ENV] wrote $ENV_FILE" +} + +wait_for_pg() { + : "${DATABASE_URL:=postgresql://sapl:sapl@sapldb:5432/sapl}" + log "Waiting for Postgres..." + /bin/bash wait-for-pg.sh "$DATABASE_URL" +} + +migrate_db() { + log "Running Django migrations..." + python3 manage.py migrate --noinput +} + +# In start.sh (near your other helpers) +configure_solr() { + # respect envs, with sane defaults + local USE="${USE_SOLR:-False}" + local URL="${SOLR_URL:-http://admin:solr@localhost:8983}" + local COL="${SOLR_COLLECTION:-sapl}" + local SHARDS="${NUM_SHARDS:-1}" + local RF="${RF:-1}" + local MS="${MAX_SHARDS_PER_NODE:-1}" + local IS_ZK="${IS_ZK_EMBEDDED:-False}" + + # total wait time before we give up (seconds) + local WAIT_TIMEOUT="${SOLR_WAIT_TIMEOUT:-30}" + # per probe max seconds + local PROBE_TIMEOUT="${SOLR_PROBE_TIMEOUT:-3}" + # sleep between probes + local SLEEP_SECS="${SOLR_WAIT_INTERVAL:-2}" + + # feature flag OFF by default unless we confirm Solr + ./manage.py waffle_switch SOLR_SWITCH off --create || true + + # Fast exit if disabled + if [[ "${USE,,}" != "true" ]]; then + echo "[SOLR] USE_SOLR=$USE → skipping Solr initialization." + return 0 + fi + + echo "[SOLR] Best-effort wait (<= ${WAIT_TIMEOUT}s): $URL, collection=$COL" + + local deadline=$((SECONDS + WAIT_TIMEOUT)) + while (( SECONDS < deadline )); do + # Try a cheap SolrCloud endpoint; swap for /solr/admin/info/system if you prefer + if curl -fsS --max-time "${PROBE_TIMEOUT}" \ + "${URL%/}/solr/admin/collections?action=LIST" >/dev/null; then + echo "[SOLR] Reachable. Kicking off background configuration…" + + # optional flag if ZK is embedded + local ZK_FLAG="" + if [[ "${IS_ZK,,}" == "true" ]]; then + ZK_FLAG="--embedded_zk" + fi + + ( + set -Eeuo pipefail + python3 solr_cli.py \ + -u "$URL" -c "$COL" -s "$SHARDS" -rf "$RF" -ms "$MS" $ZK_FLAG + ./manage.py waffle_switch SOLR_SWITCH on --create + echo "[SOLR] Configuration done, SOLR_SWITCH=on." + ) >/var/log/sapl/solr_init.log 2>&1 & disown + + return 0 + fi + sleep "${SLEEP_SECS}" + done + + echo "[SOLR] Not reachable within ${WAIT_TIMEOUT}s. Proceeding without Solr (SOLR_SWITCH=off)." + return 0 +} + +configure_sapn() { + if [[ "${ENABLE_SAPN,,}" == "true" ]]; then + log "Enabling SAPN" + python3 manage.py waffle_switch SAPN_SWITCH on --create + else + log "Disabling SAPN" + python3 manage.py waffle_switch SAPN_SWITCH off --create + fi +} + +create_admin() { + log "Creating admin user..." + out="$(python3 create_admin.py 2>&1 || true)" + printf '%s\n' "$out" + + if grep -q 'MISSING_ADMIN_PASSWORD' <<<"$out"; then + err "[SUPERUSER] ADMIN_PASSWORD not set. Exiting." + exit 1 + fi +} + +fix_logging_and_socket_perms() { + local APP_DIR="/var/interlegis/sapl" + local LOG_FILE="$APP_DIR/sapl.log" + + # dirs + mkdir -p "$APP_DIR/run" + chmod 2775 "$APP_DIR/run" + + # new files/sockets → 660 + umask 0007 + + # ensure log file is owned by sapl and writable + install -Dm0660 /dev/null "$LOG_FILE" + chown sapl:nginx "$LOG_FILE" + + # stale socket cleanup (if any) + rm -f "$APP_DIR/run/gunicorn.sock" 2>/dev/null || true +} + +setup_cache_dir() { + # if you later move cache under /var/interlegis/sapl/cache, this line can read an env var + local CACHE_DIR="${DJANGO_CACHE_DIR:-/var/tmp/django_cache}" + + mkdir -p "$CACHE_DIR" + chown -R sapl:nginx "$CACHE_DIR" + chmod -R 2775 "$CACHE_DIR" + find "$CACHE_DIR" -type d -exec chmod g+s {} + + + # keep your global umask; 0007 ensures new files are rw for owner+group + umask 0007 +} + +start_services() { + log "Starting gunicorn..." + gunicorn -c gunicorn.conf.py & + log "Starting nginx..." + exec /usr/sbin/nginx -g "daemon off;" +} + +main() { + create_secret + write_env_file + wait_for_pg + configure_pg_timezone + migrate_db + configure_solr || true + configure_sapn + create_admin + setup_cache_dir + fix_logging_and_socket_perms + + cat <<'BANNER' +------------------------------------- +| ███████╗ █████╗ ██████╗ ██╗ | +| ██╔════╝██╔══██╗██╔══██╗██║ | +| ███████╗███████║██████╔╝██║ | +| ╚════██║██╔══██║██╔═══╝ ██║ | +| ███████║██║ ██║██║ ███████╗ | +| ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝ | +------------------------------------- +BANNER + + start_services +} + +main "$@" diff --git a/docker/wait-for-pg.sh b/docker/startup_scripts/wait-for-pg.sh similarity index 100% rename from docker/wait-for-pg.sh rename to docker/startup_scripts/wait-for-pg.sh diff --git a/docker/wait-for-solr.sh b/docker/startup_scripts/wait-for-solr.sh similarity index 100% rename from docker/wait-for-solr.sh rename to docker/startup_scripts/wait-for-solr.sh diff --git a/drfautoapi/drfautoapi.py b/drfautoapi/drfautoapi.py index e0fd0c348..3f9e44292 100644 --- a/drfautoapi/drfautoapi.py +++ b/drfautoapi/drfautoapi.py @@ -1,28 +1,34 @@ from collections import OrderedDict +from functools import cached_property +import django_filters import importlib import inspect import logging import re -from django.apps.config import AppConfig -from django.apps.registry import apps from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields.jsonb import JSONField +from django.contrib.auth import get_user_model from django.db.models.base import ModelBase from django.db.models.fields import TextField, CharField from django.db.models.fields.files import FileField +from django.db.models.fields.related import ManyToManyField from django.template.defaultfilters import capfirst from django.utils.translation import ugettext_lazy as _ -import django_filters +from django.urls import reverse from django_filters.constants import ALL_FIELDS, EMPTY_VALUES +from django_filters.fields import ModelMultipleChoiceField from django_filters.filters import CharFilter from django_filters.filterset import FilterSet from django_filters.rest_framework.backends import DjangoFilterBackend from django_filters.utils import resolve_field, get_all_model_fields from rest_framework import serializers as rest_serializers +from rest_framework.relations import ManyRelatedField from rest_framework.response import Response from rest_framework.routers import DefaultRouter from rest_framework.viewsets import ModelViewSet +from rest_framework.urlpatterns import format_suffix_patterns logger = logging.getLogger(__name__) @@ -65,9 +71,25 @@ class SplitStringCharFilter(django_filters.CharFilter): return qs +class M2MFilter(django_filters.ModelMultipleChoiceFilter): + + class M2MFieldFormField(ModelMultipleChoiceField): + def clean(self, value): + if value in EMPTY_VALUES: + return super().clean(value) + values = list(map(lambda x: x.replace(' ', '').split(','), value)) + values = [v for subvalues in values for v in subvalues] + cleaned_values = tuple(super().clean(values)) + return (cleaned_values,) if cleaned_values else cleaned_values + + field_class = M2MFieldFormField + distinct = True + + class ApiFilterSetMixin(FilterSet): o = CharFilter(method='filter_o') + id__in = CharFilter(method='filter_id__in') class Meta: fields = '__all__' @@ -92,6 +114,13 @@ class ApiFilterSetMixin(FilterSet): }, } + def filter_id__in(self, queryset, name, value): + try: + ids = [int(v.strip()) for v in value.split(',')] + return queryset.filter(id__in=ids) + except: + return queryset + def filter_o(self, queryset, name, value): try: return queryset.order_by( @@ -117,7 +146,7 @@ class ApiFilterSetMixin(FilterSet): f = model._meta.get_field(f_str) if f.many_to_many: - fields[f_str] = ['exact'] + fields[f_str] = ['exact', 'in', ] continue fields[f_str] = ['exact'] @@ -173,6 +202,14 @@ class ApiFilterSetMixin(FilterSet): return filter_class(**default) return None + @classmethod + def filter_for_lookup(cls, field, lookup_type): + f, p = super().filter_for_lookup(field, lookup_type) + + if lookup_type == 'in' and isinstance(field, ManyToManyField): + return M2MFilter, p + return f, p + class BusinessRulesNotImplementedMixin: http_method_names = ['get', 'head', 'options', 'trace'] @@ -194,6 +231,17 @@ class ApiViewSetConstrutor(): class ApiViewSet(ModelViewSet): filter_backends = (DjangoFilterBackend,) + class LastModifiedDecorator(object): + def __init__(self): + pass + def __call__(self, cls): + return cls + + @classmethod + def last_modified_class(cls, klass): + cls.LastModifiedDecorator = klass + return cls + @classmethod def get_viewset_for_model(cls, model): return cls._built_sets[model._meta.app_config][model] @@ -208,7 +256,7 @@ class ApiViewSetConstrutor(): importlib.import_module(m) @classmethod - def router(cls, router_class=DefaultRouter): + def router(cls, router_class = DefaultRouter): router = router_class() for app, built_sets in cls._built_sets.items(): for model, viewset in built_sets.items(): @@ -309,6 +357,7 @@ class ApiViewSetConstrutor(): if not hasattr(_meta_filterset, 'model'): model = _model + @cls.LastModifiedDecorator() class ModelApiViewSet(ApiViewSetConstrutor.ApiViewSet): queryset = _model.objects.all() filterset_class = ApiFilterSet @@ -407,3 +456,183 @@ class customize(object): ApiViewSetConstrutor._built_sets[ self.model._meta.app_config][self.model] = _ApiViewSet return _ApiViewSet + + +class DrfAutoApiSerializerMixin(rest_serializers.ModelSerializer): + """ + Mixin de Serializer que implementa expansão dinâmica de campos + via parâmetros de query string: + - `expand`: expande os campos informados + - `include`: inclui apenas os campos informados + - `exclude`: exclui os campos informados + + Exemplo: + - ?expand=campo1;campo2.sub_campo1,sub_campo2;campo3.sub_campo1.sub_sub_campo1,sub_sub_campo2 + - ?include=campo1;campo2.sub_campo1,sub_campo2;campo3.sub_campo1.sub_sub_campo1,sub_sub_campo2 + - ?exclude=campo1;campo2.sub_campo1,sub_campo2;campo3.sub_campo1.sub_sub_campo1,sub_sub_campo2 + - ?expand=campo1&include=campo1.id,name&exclude=campo1.secret_field + + Onde: + - campo1, campo2, campo3 são campos do model raiz + - sub_campo1, sub_campo2 são campos relacionados do campo2 + - sub_sub_campo1, sub_sub_campo2 são campos relacionados do sub_campo1 + + Ou seja: + ";" separa campos independentes do nível atual + "," separa campos relacionados do mesmo campo pai + "." indica o nível de profundidade dos campos relacionados + + e ainda: + - `expand` pode ser usado para expansão direta, ou seja, campo1.sub_campo1 já expande campo1 + - `expand`, `include` e `exclude` podem ser usados juntos na mesma requisição + - `include` e `exclude` só funcionam em subniveis se o campo pai estiver expandido + - `include` tem precedência sobre `exclude` e já remove todo o resto + - `exclude` remove o campo do resultado final, mesmo que esteja em `include` + - Se nenhum dos parâmetros for informado, nenhum campo será expandido + - Filtros da API, paginação (`page` e `page_size`) e ordenação (`o`) + podem ser usados normalmente para filtrar os resultados + + Atenção: + - A expansão não é aplicada para o model User do Django + - A expansão não é aplicada para campos customizados que utilizam SerializerMethodField + - Uma exceção é lançada e registrada no log caso ocorra algum erro na expansão de algum campo, + inclusive devido a recursão infinita + - A expansão automática de todos os campos relacionados (expand=all) está desabilitada + por necessidade de controle mais refinado. + - A expansão de campos relacionados ForeignKey e OneToOne é suportada. + - A expansão de campos relacionados ManyToMany é suportada. + - A expansão de campos relacionados reversos (related_name) não é suportada, mas pode ser implementada manualmente no serializer customizado, ou vir a ser implementada no futuro. + """ + + __str__ = rest_serializers.SerializerMethodField() + + class Meta: + fields = '__all__' + + def get___str__(self, obj) -> str: + return str(obj) + + @cached_property + def user_model(self): + return get_user_model() + + def get_control_fields(self, control_field='expand'): + request = self.context.get('request', None) + if request: + param = request.query_params.get(control_field, '') + param = [e.strip() for e in param.split(';') if e.strip()] + param = [e.split('.') for e in param] + return param + return [] + + def get_fields(self): + fields = super().get_fields() + + if not hasattr(self.root, 'drf_expand_fields'): + self.root.drf_expand_fields = self.get_control_fields('expand') + self.root.drf_include_fields = self.get_control_fields('include') + self.root.drf_exclude_fields = self.get_control_fields('exclude') + if not (self.root.drf_expand_fields or self.root.drf_include_fields or self.root.drf_exclude_fields): + return fields + + model = self.Meta.model + expand_fields = self.root.drf_expand_fields + include_fields = self.root.drf_include_fields + exclude_fields = self.root.drf_exclude_fields + + expand_all = False #['all'] in expand_fields + + #if expand_all: + # request = self.context.get('request', None) + # user = getattr(request, 'user', None) + # expand_all = user and user.is_superuser + + def parents(nd): + if not nd: + return [] + return parents(nd.parent) + [nd.field_name] + + sources = parents(self) + sources = list(filter(lambda x: x, sources)) + + if expand_fields: + exps = [] + for exp in expand_fields: + if len(exp) > len(sources) and exp[0:len(sources)] == sources: + exps.extend(exp[len(sources)].split(',')) + expand_fields = exps + + if include_fields: + incls = [] + for inf in include_fields: + if len(inf) - 1 == len(sources) and inf[:-1] == sources: + incls.extend(inf[-1].split(',')) + if incls: + fields = OrderedDict( + [(k, v) for k, v in fields.items() if k in incls] + ) + + if exclude_fields: + excls = [] + for inf in exclude_fields: + if len(inf) - 1 == len(sources) and inf[:-1] == sources: + excls.extend(inf[-1].split(',')) + if excls: + fields = OrderedDict( + [(k, v) for k, v in fields.items() if k not in excls] + ) + + fields_with_relations_map_model = {f.name: f.related_model for f in model._meta.get_fields() + if f.is_relation and f.name in fields} + + set_fields_with_relations = set(fields_with_relations_map_model.keys()) + set_expand_fields = set(expand_fields) + set_fields_serialized = set( + map( + lambda kv: kv[0], + filter( + lambda kv: not isinstance(kv[1], rest_serializers.SerializerMethodField), + fields.items() + ) + ) + ) + + expand_fields = set_fields_with_relations.intersection(set_fields_serialized) + + if not expand_all: + expand_fields = expand_fields.intersection(set_expand_fields) + + # remove o User model da expansão + if self.user_model in fields_with_relations_map_model.values(): + expand_fields = [f for f in expand_fields + if fields_with_relations_map_model[f] != self.user_model] + + if not expand_fields: + return fields + + for field_name in expand_fields: + field = fields[field_name] + + model = fields_with_relations_map_model[field_name] + + if model: + try: + serializer_class = ApiViewSetConstrutor.get_viewset_for_model(model).serializer_class + if hasattr(field, 'many') and field.many or isinstance(field, ManyRelatedField): + serializer_class = serializer_class( + many=True, + read_only=True, + context=self.context + ) + else: + serializer_class = serializer_class( + read_only=True, + context=self.context + ) + + fields[field_name] = serializer_class + + except Exception as e: + logger.error(f'Erro ao expandir campo {field_name} do model {model}: {e}') + + return fields diff --git a/frontend/src/__apps/compilacao/js/old/compilacao.js b/frontend/src/__apps/compilacao/js/old/compilacao.js index f7176c473..8853d6425 100644 --- a/frontend/src/__apps/compilacao/js/old/compilacao.js +++ b/frontend/src/__apps/compilacao/js/old/compilacao.js @@ -279,6 +279,69 @@ function DispositivoSearch (opts) { }) } +// Escala de leitura do texto articulado, preferência de cada navegador. +// Aplicada via variável CSS (--cp-font-scale) para não alterar o conteúdo +// dos dispositivos e para valer também nos que são recarregados via ajax. +const EscalaFonte = Object.freeze({ + CHAVE: 'compilacao_escala_fonte', + PADRAO: 1, + PASSO: 0.1, + MIN: 0.8, + MAX: 2.5 +}) +let escalaFonteAtual = null + +// Limita num ao intervalo [lower, upper]. +function clamp (num, lower, upper) { + return Math.max(lower, Math.min(num, upper)) +} + +function lerEscalaFonte () { + if (escalaFonteAtual !== null) { + return escalaFonteAtual + } + let valor = null + try { + valor = window.localStorage.getItem(EscalaFonte.CHAVE) + } catch (e) { + // localStorage indisponível (ex.: navegação privada bloqueada) + console.warn('Escala de fonte: não foi possível ler o localStorage.', e) + return EscalaFonte.PADRAO + } + if (valor === null) { + return EscalaFonte.PADRAO + } + const escala = parseFloat(valor) + if (!Number.isFinite(escala)) { + console.warn('Escala de fonte: valor inválido no localStorage, usando o padrão.', valor) + return EscalaFonte.PADRAO + } + return clamp(escala, EscalaFonte.MIN, EscalaFonte.MAX) +} + +function aplicarEscalaFonte (escala) { + document.documentElement.style.setProperty('--cp-font-scale', escala) + if (window.tinymce) { + window.tinymce.get().forEach(function (editor) { + if (editor.getBody()) { + editor.getBody().style.fontSize = (escala * 100) + '%' + } + }) + } +} + +function salvarEscalaFonte (escala) { + escala = Math.round(clamp(escala, EscalaFonte.MIN, EscalaFonte.MAX) * 10) / 10 + try { + window.localStorage.setItem(EscalaFonte.CHAVE, escala) + } catch (e) { + // localStorage indisponível: a escala vale apenas até recarregar a página + console.warn('Escala de fonte: não foi possível salvar no localStorage.', escala, e) + } + escalaFonteAtual = escala + aplicarEscalaFonte(escala) +} + function InitViewTAs () { setTimeout(function () { var href = location.href.split('#') @@ -297,11 +360,12 @@ function InitViewTAs () { } }, 100) + aplicarEscalaFonte(lerEscalaFonte()) $('#btn_font_menos').click(function () { - $('.dpt').css('font-size', '-=1') + salvarEscalaFonte(lerEscalaFonte() - EscalaFonte.PASSO) }) $('#btn_font_mais').click(function () { - $('.dpt').css('font-size', '+=1') + salvarEscalaFonte(lerEscalaFonte() + EscalaFonte.PASSO) }) $('.dpt.bloco_alteracao .dpt').each(function () { @@ -327,5 +391,7 @@ export default { ReadCookie, insertWaitAjax, InitViewTAs, + lerEscalaFonte, + aplicarEscalaFonte, DispositivoSearch } diff --git a/frontend/src/__apps/compilacao/js/old/compilacao_edit.js b/frontend/src/__apps/compilacao/js/old/compilacao_edit.js index ba651188b..27060135b 100644 --- a/frontend/src/__apps/compilacao/js/old/compilacao_edit.js +++ b/frontend/src/__apps/compilacao/js/old/compilacao_edit.js @@ -244,7 +244,9 @@ window.DispositivoEdit = function () { if (editortype !== 'construct') { dpt_form.html(data) if (editortype === 'tinymce') { - window.initTextRichEditor(null, false, false) + window.initTextRichEditor(null, false, false).then(function () { + window.aplicarEscalaFonte(window.lerEscalaFonte()) + }) } // OptionalCustomFrontEnd().init() } diff --git a/frontend/src/__apps/compilacao/scss/compilacao.scss b/frontend/src/__apps/compilacao/scss/compilacao.scss index 82c52f7f6..842436b24 100644 --- a/frontend/src/__apps/compilacao/scss/compilacao.scss +++ b/frontend/src/__apps/compilacao/scss/compilacao.scss @@ -164,12 +164,21 @@ a:link:after, a:visited:after { } .cp { + /* --cp-font-scale: escala de leitura escolhida pelo usuário (a/A) */ + font-size: calc(100% * var(--cp-font-scale, 1)); + + .dpt-form textarea.form-control { + font-size: calc(1rem * var(--cp-font-scale, 1)); + } + .vigencia-active { margin-top: 30px; display: block; } .cp-linha-vigencias { + /* navegação entre vigências não acompanha a escala de leitura */ + font-size: calc(100% / var(--cp-font-scale, 1)); display: flex; flex-direction: row; list-style: none; diff --git a/frontend/src/__apps/parlamentar/main.js b/frontend/src/__apps/parlamentar/main.js index 97f5e5c75..4392fa9ee 100644 --- a/frontend/src/__apps/parlamentar/main.js +++ b/frontend/src/__apps/parlamentar/main.js @@ -3,6 +3,12 @@ import Vue from 'vue' import { FormSelectPlugin } from 'bootstrap-vue' import axios from 'axios' +// TODO: refatorar para aproveitar cache e LastModified no frontend oficial +// Configuração global para todas as requisições GET do axios +axios.defaults.headers.get['Cache-Control'] = 'no-cache, no-store, must-revalidate' +axios.defaults.headers.get['Pragma'] = 'no-cache' // Suporte para navegadores mais antigos +axios.defaults.headers.get['Expires'] = '0' // Expira imediatamente + axios.defaults.xsrfCookieName = 'csrftoken' axios.defaults.xsrfHeaderName = 'X-CSRFToken' diff --git a/frontend/src/__global/js/functions.js b/frontend/src/__global/js/functions.js index 1f9b44a6a..cfc86fa4a 100644 --- a/frontend/src/__global/js/functions.js +++ b/frontend/src/__global/js/functions.js @@ -66,7 +66,8 @@ window.autorModal = function () { $('#pesquisar').click(function () { const json_data = { - q: $('#q').val() + q: $('#q').val(), + utc_time: Date.now(), // get_all: true } $.get('/api/base/autor', json_data, function (data) { diff --git a/frontend/src/__global/js/tinymce/index.js b/frontend/src/__global/js/tinymce/index.js index 51eed1efa..ee20a7089 100644 --- a/frontend/src/__global/js/tinymce/index.js +++ b/frontend/src/__global/js/tinymce/index.js @@ -14,7 +14,23 @@ import 'tinymce/plugins/table' import './langs/pt_BR.js' window.tinymce = tinymce -window.initTextRichEditor = function (elements, readonly = false, paste_as_text = false) { +// Títulos (h1-h6) vindos de estilos de título do Word ou de páginas web +// chegam em negrito e não podem ser desfeitos pelo botão de negrito nem +// por "remover formatação". Converte-os em parágrafos, mantendo o alinhamento. +function colarTitulosComoParagrafo (editor, args) { + args.node.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(function (titulo) { + const paragrafo = document.createElement('p') + if (titulo.style.textAlign) { + paragrafo.style.textAlign = titulo.style.textAlign + } + while (titulo.firstChild) { + paragrafo.appendChild(titulo.firstChild) + } + titulo.replaceWith(paragrafo) + }) +} + +window.initTextRichEditor = function (elements, readonly = false, paste_as_text = false, paste_titulos_como_paragrafo = false) { const configTinymce = { selector: elements === null || elements === undefined ? 'textarea' : elements, language: 'pt_BR', @@ -22,14 +38,17 @@ window.initTextRichEditor = function (elements, readonly = false, paste_as_text forced_root_block: 'p', paste_as_text, plugins: 'table lists advlist link code', - toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | code | removeformat ', + toolbar: 'undo redo | blocks | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | code | removeformat ', menubar: 'file edit view insert format table', license_key: 'gpl' } + if (paste_titulos_como_paragrafo) { + configTinymce.paste_postprocess = colarTitulosComoParagrafo + } if (readonly) { configTinymce.readonly = 1 configTinymce.menubar = false configTinymce.toolbar = false } - window.tinymce.init(configTinymce) + return window.tinymce.init(configTinymce) } diff --git a/frontend/src/__global/scss/layouts/_globals.scss b/frontend/src/__global/scss/layouts/_globals.scss index bc89c2f65..3cb9c6f82 100644 --- a/frontend/src/__global/scss/layouts/_globals.scss +++ b/frontend/src/__global/scss/layouts/_globals.scss @@ -187,4 +187,7 @@ small { a[href]:after { content: none !important; } + .btn-vlibras { + display: none !important; + } } diff --git a/frontend/webpack-stats.json b/frontend/webpack-stats.json index d5f5209bc..900bf80c9 100644 --- a/frontend/webpack-stats.json +++ b/frontend/webpack-stats.json @@ -3,1024 +3,1024 @@ "assets": { "audio/ring.mp3": { "name": "audio/ring.mp3", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/audio/ring.mp3", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/audio/ring.mp3", "publicPath": "/static/sapl/frontend/audio/ring.mp3" }, "css/chunk-vendors.045ec640.css": { "name": "css/chunk-vendors.045ec640.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css", "publicPath": "/static/sapl/frontend/css/chunk-vendors.045ec640.css" }, "css/chunk-vendors.045ec640.css.gz": { "name": "css/chunk-vendors.045ec640.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css.gz", "publicPath": "/static/sapl/frontend/css/chunk-vendors.045ec640.css.gz" }, "css/compilacao.0baf3580.css": { "name": "css/compilacao.0baf3580.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/compilacao.0baf3580.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/compilacao.0baf3580.css", "publicPath": "/static/sapl/frontend/css/compilacao.0baf3580.css" }, "css/compilacao.0baf3580.css.gz": { "name": "css/compilacao.0baf3580.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/compilacao.0baf3580.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/compilacao.0baf3580.css.gz", "publicPath": "/static/sapl/frontend/css/compilacao.0baf3580.css.gz" }, - "css/global.45591136.css": { - "name": "css/global.45591136.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/global.45591136.css", - "publicPath": "/static/sapl/frontend/css/global.45591136.css" + "css/global.042f6737.css": { + "name": "css/global.042f6737.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/global.042f6737.css", + "publicPath": "/static/sapl/frontend/css/global.042f6737.css" }, - "css/global.45591136.css.gz": { - "name": "css/global.45591136.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/global.45591136.css.gz", - "publicPath": "/static/sapl/frontend/css/global.45591136.css.gz" + "css/global.042f6737.css.gz": { + "name": "css/global.042f6737.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/global.042f6737.css.gz", + "publicPath": "/static/sapl/frontend/css/global.042f6737.css.gz" }, "css/painel.e2b9504e.css": { "name": "css/painel.e2b9504e.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/painel.e2b9504e.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/painel.e2b9504e.css", "publicPath": "/static/sapl/frontend/css/painel.e2b9504e.css" }, "css/painel.e2b9504e.css.gz": { "name": "css/painel.e2b9504e.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/painel.e2b9504e.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/painel.e2b9504e.css.gz", "publicPath": "/static/sapl/frontend/css/painel.e2b9504e.css.gz" }, "css/parlamentar.cd5dc5a8.css": { "name": "css/parlamentar.cd5dc5a8.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/css/parlamentar.cd5dc5a8.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/parlamentar.cd5dc5a8.css", "publicPath": "/static/sapl/frontend/css/parlamentar.cd5dc5a8.css" }, "fonts/fa-brands-400.5d18d427.ttf": { "name": "fonts/fa-brands-400.5d18d427.ttf", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf", "publicPath": "/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf" }, "fonts/fa-brands-400.5d18d427.ttf.gz": { "name": "fonts/fa-brands-400.5d18d427.ttf.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf.gz", "publicPath": "/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf.gz" }, "fonts/fa-brands-400.87587a68.woff2": { "name": "fonts/fa-brands-400.87587a68.woff2", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.87587a68.woff2", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.87587a68.woff2", "publicPath": "/static/sapl/frontend/fonts/fa-brands-400.87587a68.woff2" }, "fonts/fa-regular-400.3ccdbd3d.woff2": { "name": "fonts/fa-regular-400.3ccdbd3d.woff2", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.3ccdbd3d.woff2", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.3ccdbd3d.woff2", "publicPath": "/static/sapl/frontend/fonts/fa-regular-400.3ccdbd3d.woff2" }, "fonts/fa-regular-400.81482cd4.ttf": { "name": "fonts/fa-regular-400.81482cd4.ttf", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf", "publicPath": "/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf" }, "fonts/fa-regular-400.81482cd4.ttf.gz": { "name": "fonts/fa-regular-400.81482cd4.ttf.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf.gz", "publicPath": "/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf.gz" }, "fonts/fa-solid-900.0b0cc8a6.woff2": { "name": "fonts/fa-solid-900.0b0cc8a6.woff2", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.0b0cc8a6.woff2", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.0b0cc8a6.woff2", "publicPath": "/static/sapl/frontend/fonts/fa-solid-900.0b0cc8a6.woff2" }, "fonts/fa-solid-900.69d3141a.ttf": { "name": "fonts/fa-solid-900.69d3141a.ttf", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf", "publicPath": "/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf" }, "fonts/fa-solid-900.69d3141a.ttf.gz": { "name": "fonts/fa-solid-900.69d3141a.ttf.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf.gz", "publicPath": "/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf.gz" }, "fonts/fa-v4compatibility.2c070fd2.ttf": { "name": "fonts/fa-v4compatibility.2c070fd2.ttf", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf", "publicPath": "/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf" }, "fonts/fa-v4compatibility.2c070fd2.ttf.gz": { "name": "fonts/fa-v4compatibility.2c070fd2.ttf.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf.gz", "publicPath": "/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf.gz" }, "img/arrow.png": { "name": "img/arrow.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/arrow.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/arrow.png", "publicPath": "/static/sapl/frontend/img/arrow.png" }, "img/authenticated.png": { "name": "img/authenticated.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/authenticated.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/authenticated.png", "publicPath": "/static/sapl/frontend/img/authenticated.png" }, "img/avatar.png": { "name": "img/avatar.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/avatar.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/avatar.png", "publicPath": "/static/sapl/frontend/img/avatar.png" }, "img/beta.png": { "name": "img/beta.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/beta.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/beta.png", "publicPath": "/static/sapl/frontend/img/beta.png" }, "img/bg.png": { "name": "img/bg.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/bg.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/bg.png", "publicPath": "/static/sapl/frontend/img/bg.png" }, "img/brasao_transp.gif": { "name": "img/brasao_transp.gif", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/brasao_transp.gif", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/brasao_transp.gif", "publicPath": "/static/sapl/frontend/img/brasao_transp.gif" }, "img/down_arrow_select.jpg": { "name": "img/down_arrow_select.jpg", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/down_arrow_select.jpg", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/down_arrow_select.jpg", "publicPath": "/static/sapl/frontend/img/down_arrow_select.jpg" }, "img/down_arrow_select.jpg.gz": { "name": "img/down_arrow_select.jpg.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/down_arrow_select.jpg.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/down_arrow_select.jpg.gz", "publicPath": "/static/sapl/frontend/img/down_arrow_select.jpg.gz" }, "img/etiqueta.png": { "name": "img/etiqueta.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/etiqueta.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/etiqueta.png", "publicPath": "/static/sapl/frontend/img/etiqueta.png" }, "img/favicon.ico": { "name": "img/favicon.ico", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/favicon.ico", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/favicon.ico", "publicPath": "/static/sapl/frontend/img/favicon.ico" }, "img/file.png": { "name": "img/file.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/file.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/file.png", "publicPath": "/static/sapl/frontend/img/file.png" }, "img/hand-note.png": { "name": "img/hand-note.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/hand-note.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/hand-note.png", "publicPath": "/static/sapl/frontend/img/hand-note.png" }, "img/icon_comissoes.png": { "name": "img/icon_comissoes.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_comissoes.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_comissoes.png", "publicPath": "/static/sapl/frontend/img/icon_comissoes.png" }, "img/icon_delete_white.png": { "name": "img/icon_delete_white.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_delete_white.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_delete_white.png", "publicPath": "/static/sapl/frontend/img/icon_delete_white.png" }, "img/icon_materia_legislativa.png": { "name": "img/icon_materia_legislativa.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_materia_legislativa.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_materia_legislativa.png", "publicPath": "/static/sapl/frontend/img/icon_materia_legislativa.png" }, "img/icon_mesa_diretora.png": { "name": "img/icon_mesa_diretora.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_mesa_diretora.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_mesa_diretora.png", "publicPath": "/static/sapl/frontend/img/icon_mesa_diretora.png" }, "img/icon_normas_juridicas_destaque.png": { "name": "img/icon_normas_juridicas_destaque.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_normas_juridicas_destaque.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_normas_juridicas_destaque.png", "publicPath": "/static/sapl/frontend/img/icon_normas_juridicas_destaque.png" }, "img/icon_normas_juridicas.png": { "name": "img/icon_normas_juridicas.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_normas_juridicas.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_normas_juridicas.png", "publicPath": "/static/sapl/frontend/img/icon_normas_juridicas.png" }, "img/icon_parlamentares.png": { "name": "img/icon_parlamentares.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_parlamentares.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_parlamentares.png", "publicPath": "/static/sapl/frontend/img/icon_parlamentares.png" }, "img/icon_pautas.png": { "name": "img/icon_pautas.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_pautas.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_pautas.png", "publicPath": "/static/sapl/frontend/img/icon_pautas.png" }, "img/icon_plenarias.png": { "name": "img/icon_plenarias.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_plenarias.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_plenarias.png", "publicPath": "/static/sapl/frontend/img/icon_plenarias.png" }, "img/icon_relatorios.png": { "name": "img/icon_relatorios.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_relatorios.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_relatorios.png", "publicPath": "/static/sapl/frontend/img/icon_relatorios.png" }, "img/icon_save_white.png": { "name": "img/icon_save_white.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/icon_save_white.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_save_white.png", "publicPath": "/static/sapl/frontend/img/icon_save_white.png" }, "img/lexml.gif": { "name": "img/lexml.gif", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/lexml.gif", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/lexml.gif", "publicPath": "/static/sapl/frontend/img/lexml.gif" }, "img/logo_cc.png": { "name": "img/logo_cc.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/logo_cc.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/logo_cc.png", "publicPath": "/static/sapl/frontend/img/logo_cc.png" }, "img/logo_interlegis.png": { "name": "img/logo_interlegis.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/logo_interlegis.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/logo_interlegis.png", "publicPath": "/static/sapl/frontend/img/logo_interlegis.png" }, "img/logo.png": { "name": "img/logo.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/logo.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/logo.png", "publicPath": "/static/sapl/frontend/img/logo.png" }, "img/manual.png": { "name": "img/manual.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/manual.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/manual.png", "publicPath": "/static/sapl/frontend/img/manual.png" }, "img/pdflogo.png": { "name": "img/pdflogo.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/pdflogo.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/pdflogo.png", "publicPath": "/static/sapl/frontend/img/pdflogo.png" }, "img/perfil.png": { "name": "img/perfil.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/perfil.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/perfil.png", "publicPath": "/static/sapl/frontend/img/perfil.png" }, "img/search-gray.png": { "name": "img/search-gray.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/search-gray.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/search-gray.png", "publicPath": "/static/sapl/frontend/img/search-gray.png" }, "img/search.png": { "name": "img/search.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/search.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/search.png", "publicPath": "/static/sapl/frontend/img/search.png" }, "img/user.png": { "name": "img/user.png", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/img/user.png", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/user.png", "publicPath": "/static/sapl/frontend/img/user.png" }, - "js/chunk-vendors.e8ab4373.js": { - "name": "js/chunk-vendors.e8ab4373.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/chunk-vendors.e8ab4373.js", - "publicPath": "/static/sapl/frontend/js/chunk-vendors.e8ab4373.js" - }, - "js/chunk-vendors.e8ab4373.js.gz": { - "name": "js/chunk-vendors.e8ab4373.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/chunk-vendors.e8ab4373.js.gz", - "publicPath": "/static/sapl/frontend/js/chunk-vendors.e8ab4373.js.gz" - }, - "js/chunk-vendors.e8ab4373.js.LICENSE.txt": { - "name": "js/chunk-vendors.e8ab4373.js.LICENSE.txt", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/chunk-vendors.e8ab4373.js.LICENSE.txt", - "publicPath": "/static/sapl/frontend/js/chunk-vendors.e8ab4373.js.LICENSE.txt" - }, - "js/chunk-vendors.e8ab4373.js.LICENSE.txt.gz": { - "name": "js/chunk-vendors.e8ab4373.js.LICENSE.txt.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/chunk-vendors.e8ab4373.js.LICENSE.txt.gz", - "publicPath": "/static/sapl/frontend/js/chunk-vendors.e8ab4373.js.LICENSE.txt.gz" - }, - "js/compilacao.d3adb1c1.js": { - "name": "js/compilacao.d3adb1c1.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/compilacao.d3adb1c1.js", - "publicPath": "/static/sapl/frontend/js/compilacao.d3adb1c1.js" - }, - "js/compilacao.d3adb1c1.js.gz": { - "name": "js/compilacao.d3adb1c1.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/compilacao.d3adb1c1.js.gz", - "publicPath": "/static/sapl/frontend/js/compilacao.d3adb1c1.js.gz" - }, - "js/global.8ef6e932.js": { - "name": "js/global.8ef6e932.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/global.8ef6e932.js", - "publicPath": "/static/sapl/frontend/js/global.8ef6e932.js" - }, - "js/global.8ef6e932.js.gz": { - "name": "js/global.8ef6e932.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/global.8ef6e932.js.gz", - "publicPath": "/static/sapl/frontend/js/global.8ef6e932.js.gz" - }, - "js/global.8ef6e932.js.LICENSE.txt": { - "name": "js/global.8ef6e932.js.LICENSE.txt", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/global.8ef6e932.js.LICENSE.txt", - "publicPath": "/static/sapl/frontend/js/global.8ef6e932.js.LICENSE.txt" - }, - "js/painel.73a4fb17.js": { - "name": "js/painel.73a4fb17.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/painel.73a4fb17.js", - "publicPath": "/static/sapl/frontend/js/painel.73a4fb17.js" - }, - "js/painel.73a4fb17.js.gz": { - "name": "js/painel.73a4fb17.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/painel.73a4fb17.js.gz", - "publicPath": "/static/sapl/frontend/js/painel.73a4fb17.js.gz" - }, - "js/parlamentar.f0710dca.js": { - "name": "js/parlamentar.f0710dca.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/parlamentar.f0710dca.js", - "publicPath": "/static/sapl/frontend/js/parlamentar.f0710dca.js" - }, - "js/parlamentar.f0710dca.js.gz": { - "name": "js/parlamentar.f0710dca.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/parlamentar.f0710dca.js.gz", - "publicPath": "/static/sapl/frontend/js/parlamentar.f0710dca.js.gz" + "js/chunk-vendors.f2d6067d.js": { + "name": "js/chunk-vendors.f2d6067d.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.f2d6067d.js", + "publicPath": "/static/sapl/frontend/js/chunk-vendors.f2d6067d.js" + }, + "js/chunk-vendors.f2d6067d.js.gz": { + "name": "js/chunk-vendors.f2d6067d.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.f2d6067d.js.gz", + "publicPath": "/static/sapl/frontend/js/chunk-vendors.f2d6067d.js.gz" + }, + "js/chunk-vendors.f2d6067d.js.LICENSE.txt": { + "name": "js/chunk-vendors.f2d6067d.js.LICENSE.txt", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.f2d6067d.js.LICENSE.txt", + "publicPath": "/static/sapl/frontend/js/chunk-vendors.f2d6067d.js.LICENSE.txt" + }, + "js/chunk-vendors.f2d6067d.js.LICENSE.txt.gz": { + "name": "js/chunk-vendors.f2d6067d.js.LICENSE.txt.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.f2d6067d.js.LICENSE.txt.gz", + "publicPath": "/static/sapl/frontend/js/chunk-vendors.f2d6067d.js.LICENSE.txt.gz" + }, + "js/compilacao.148cb66b.js": { + "name": "js/compilacao.148cb66b.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/compilacao.148cb66b.js", + "publicPath": "/static/sapl/frontend/js/compilacao.148cb66b.js" + }, + "js/compilacao.148cb66b.js.gz": { + "name": "js/compilacao.148cb66b.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/compilacao.148cb66b.js.gz", + "publicPath": "/static/sapl/frontend/js/compilacao.148cb66b.js.gz" + }, + "js/global.d557d20c.js": { + "name": "js/global.d557d20c.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.d557d20c.js", + "publicPath": "/static/sapl/frontend/js/global.d557d20c.js" + }, + "js/global.d557d20c.js.gz": { + "name": "js/global.d557d20c.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.d557d20c.js.gz", + "publicPath": "/static/sapl/frontend/js/global.d557d20c.js.gz" + }, + "js/global.d557d20c.js.LICENSE.txt": { + "name": "js/global.d557d20c.js.LICENSE.txt", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.d557d20c.js.LICENSE.txt", + "publicPath": "/static/sapl/frontend/js/global.d557d20c.js.LICENSE.txt" + }, + "js/painel.f3190acc.js": { + "name": "js/painel.f3190acc.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/painel.f3190acc.js", + "publicPath": "/static/sapl/frontend/js/painel.f3190acc.js" + }, + "js/painel.f3190acc.js.gz": { + "name": "js/painel.f3190acc.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/painel.f3190acc.js.gz", + "publicPath": "/static/sapl/frontend/js/painel.f3190acc.js.gz" + }, + "js/parlamentar.f63027a7.js": { + "name": "js/parlamentar.f63027a7.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/parlamentar.f63027a7.js", + "publicPath": "/static/sapl/frontend/js/parlamentar.f63027a7.js" + }, + "js/parlamentar.f63027a7.js.gz": { + "name": "js/parlamentar.f63027a7.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/parlamentar.f63027a7.js.gz", + "publicPath": "/static/sapl/frontend/js/parlamentar.f63027a7.js.gz" }, "js/skins/content/dark/content.css": { "name": "js/skins/content/dark/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.css", "publicPath": "/static/sapl/frontend/js/skins/content/dark/content.css" }, "js/skins/content/dark/content.css.gz": { "name": "js/skins/content/dark/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/dark/content.css.gz" }, "js/skins/content/dark/content.js": { "name": "js/skins/content/dark/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.js", "publicPath": "/static/sapl/frontend/js/skins/content/dark/content.js" }, "js/skins/content/dark/content.js.gz": { "name": "js/skins/content/dark/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/content/dark/content.js.gz" }, "js/skins/content/dark/content.min.css": { "name": "js/skins/content/dark/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/content/dark/content.min.css" }, "js/skins/content/dark/content.min.css.gz": { "name": "js/skins/content/dark/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/dark/content.min.css.gz" }, "js/skins/content/default/content.css": { "name": "js/skins/content/default/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.css", "publicPath": "/static/sapl/frontend/js/skins/content/default/content.css" }, "js/skins/content/default/content.css.gz": { "name": "js/skins/content/default/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/default/content.css.gz" }, "js/skins/content/default/content.js": { "name": "js/skins/content/default/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.js", "publicPath": "/static/sapl/frontend/js/skins/content/default/content.js" }, "js/skins/content/default/content.js.gz": { "name": "js/skins/content/default/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/content/default/content.js.gz" }, "js/skins/content/default/content.min.css": { "name": "js/skins/content/default/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/content/default/content.min.css" }, "js/skins/content/default/content.min.css.gz": { "name": "js/skins/content/default/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/default/content.min.css.gz" }, "js/skins/content/document/content.css": { "name": "js/skins/content/document/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.css", "publicPath": "/static/sapl/frontend/js/skins/content/document/content.css" }, "js/skins/content/document/content.css.gz": { "name": "js/skins/content/document/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/document/content.css.gz" }, "js/skins/content/document/content.js": { "name": "js/skins/content/document/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.js", "publicPath": "/static/sapl/frontend/js/skins/content/document/content.js" }, "js/skins/content/document/content.js.gz": { "name": "js/skins/content/document/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/content/document/content.js.gz" }, "js/skins/content/document/content.min.css": { "name": "js/skins/content/document/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/content/document/content.min.css" }, "js/skins/content/document/content.min.css.gz": { "name": "js/skins/content/document/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/document/content.min.css.gz" }, "js/skins/content/tinymce-5-dark/content.css": { "name": "js/skins/content/tinymce-5-dark/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css" }, "js/skins/content/tinymce-5-dark/content.css.gz": { "name": "js/skins/content/tinymce-5-dark/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css.gz" }, "js/skins/content/tinymce-5-dark/content.js": { "name": "js/skins/content/tinymce-5-dark/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.js", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.js" }, "js/skins/content/tinymce-5-dark/content.js.gz": { "name": "js/skins/content/tinymce-5-dark/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.js.gz" }, "js/skins/content/tinymce-5-dark/content.min.css": { "name": "js/skins/content/tinymce-5-dark/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css" }, "js/skins/content/tinymce-5-dark/content.min.css.gz": { "name": "js/skins/content/tinymce-5-dark/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css.gz" }, "js/skins/content/tinymce-5/content.css": { "name": "js/skins/content/tinymce-5/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.css", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.css" }, "js/skins/content/tinymce-5/content.css.gz": { "name": "js/skins/content/tinymce-5/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.css.gz" }, "js/skins/content/tinymce-5/content.js": { "name": "js/skins/content/tinymce-5/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.js", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.js" }, "js/skins/content/tinymce-5/content.js.gz": { "name": "js/skins/content/tinymce-5/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.js.gz" }, "js/skins/content/tinymce-5/content.min.css": { "name": "js/skins/content/tinymce-5/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css" }, "js/skins/content/tinymce-5/content.min.css.gz": { "name": "js/skins/content/tinymce-5/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css.gz" }, "js/skins/content/writer/content.css": { "name": "js/skins/content/writer/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.css", "publicPath": "/static/sapl/frontend/js/skins/content/writer/content.css" }, "js/skins/content/writer/content.css.gz": { "name": "js/skins/content/writer/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/writer/content.css.gz" }, "js/skins/content/writer/content.js": { "name": "js/skins/content/writer/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.js", "publicPath": "/static/sapl/frontend/js/skins/content/writer/content.js" }, "js/skins/content/writer/content.js.gz": { "name": "js/skins/content/writer/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/content/writer/content.js.gz" }, "js/skins/content/writer/content.min.css": { "name": "js/skins/content/writer/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/content/writer/content.min.css" }, "js/skins/content/writer/content.min.css.gz": { "name": "js/skins/content/writer/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/content/writer/content.min.css.gz" }, "js/skins/ui/oxide-dark/content.css": { "name": "js/skins/ui/oxide-dark/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.css" }, "js/skins/ui/oxide-dark/content.css.gz": { "name": "js/skins/ui/oxide-dark/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.css.gz" }, "js/skins/ui/oxide-dark/content.inline.css": { "name": "js/skins/ui/oxide-dark/content.inline.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css" }, "js/skins/ui/oxide-dark/content.inline.css.gz": { "name": "js/skins/ui/oxide-dark/content.inline.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css.gz" }, "js/skins/ui/oxide-dark/content.inline.js": { "name": "js/skins/ui/oxide-dark/content.inline.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.js" }, "js/skins/ui/oxide-dark/content.inline.js.gz": { "name": "js/skins/ui/oxide-dark/content.inline.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.js.gz" }, "js/skins/ui/oxide-dark/content.inline.min.css": { "name": "js/skins/ui/oxide-dark/content.inline.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css" }, "js/skins/ui/oxide-dark/content.inline.min.css.gz": { "name": "js/skins/ui/oxide-dark/content.inline.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css.gz" }, "js/skins/ui/oxide-dark/content.js": { "name": "js/skins/ui/oxide-dark/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.js" }, "js/skins/ui/oxide-dark/content.js.gz": { "name": "js/skins/ui/oxide-dark/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.js.gz" }, "js/skins/ui/oxide-dark/content.min.css": { "name": "js/skins/ui/oxide-dark/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css" }, "js/skins/ui/oxide-dark/content.min.css.gz": { "name": "js/skins/ui/oxide-dark/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css.gz" }, "js/skins/ui/oxide-dark/skin.css": { "name": "js/skins/ui/oxide-dark/skin.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css" }, "js/skins/ui/oxide-dark/skin.css.gz": { "name": "js/skins/ui/oxide-dark/skin.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css.gz" }, "js/skins/ui/oxide-dark/skin.js": { "name": "js/skins/ui/oxide-dark/skin.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.js" }, "js/skins/ui/oxide-dark/skin.js.gz": { "name": "js/skins/ui/oxide-dark/skin.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.js.gz" }, "js/skins/ui/oxide-dark/skin.min.css": { "name": "js/skins/ui/oxide-dark/skin.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css" }, "js/skins/ui/oxide-dark/skin.min.css.gz": { "name": "js/skins/ui/oxide-dark/skin.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css.gz" }, "js/skins/ui/oxide-dark/skin.shadowdom.css": { "name": "js/skins/ui/oxide-dark/skin.shadowdom.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css" }, "js/skins/ui/oxide-dark/skin.shadowdom.css.gz": { "name": "js/skins/ui/oxide-dark/skin.shadowdom.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css.gz" }, "js/skins/ui/oxide-dark/skin.shadowdom.js": { "name": "js/skins/ui/oxide-dark/skin.shadowdom.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.js" }, "js/skins/ui/oxide-dark/skin.shadowdom.js.gz": { "name": "js/skins/ui/oxide-dark/skin.shadowdom.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.js.gz" }, "js/skins/ui/oxide-dark/skin.shadowdom.min.css": { "name": "js/skins/ui/oxide-dark/skin.shadowdom.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css" }, "js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz": { "name": "js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz" }, "js/skins/ui/oxide/content.css": { "name": "js/skins/ui/oxide/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.css" }, "js/skins/ui/oxide/content.css.gz": { "name": "js/skins/ui/oxide/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.css.gz" }, "js/skins/ui/oxide/content.inline.css": { "name": "js/skins/ui/oxide/content.inline.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.css" }, "js/skins/ui/oxide/content.inline.css.gz": { "name": "js/skins/ui/oxide/content.inline.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.css.gz" }, "js/skins/ui/oxide/content.inline.js": { "name": "js/skins/ui/oxide/content.inline.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.js" }, "js/skins/ui/oxide/content.inline.js.gz": { "name": "js/skins/ui/oxide/content.inline.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.js.gz" }, "js/skins/ui/oxide/content.inline.min.css": { "name": "js/skins/ui/oxide/content.inline.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css" }, "js/skins/ui/oxide/content.inline.min.css.gz": { "name": "js/skins/ui/oxide/content.inline.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css.gz" }, "js/skins/ui/oxide/content.js": { "name": "js/skins/ui/oxide/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.js" }, "js/skins/ui/oxide/content.js.gz": { "name": "js/skins/ui/oxide/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.js.gz" }, "js/skins/ui/oxide/content.min.css": { "name": "js/skins/ui/oxide/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.min.css" }, "js/skins/ui/oxide/content.min.css.gz": { "name": "js/skins/ui/oxide/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.min.css.gz" }, "js/skins/ui/oxide/skin.css": { "name": "js/skins/ui/oxide/skin.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.css" }, "js/skins/ui/oxide/skin.css.gz": { "name": "js/skins/ui/oxide/skin.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.css.gz" }, "js/skins/ui/oxide/skin.js": { "name": "js/skins/ui/oxide/skin.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.js" }, "js/skins/ui/oxide/skin.js.gz": { "name": "js/skins/ui/oxide/skin.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.js.gz" }, "js/skins/ui/oxide/skin.min.css": { "name": "js/skins/ui/oxide/skin.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.min.css" }, "js/skins/ui/oxide/skin.min.css.gz": { "name": "js/skins/ui/oxide/skin.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.min.css.gz" }, "js/skins/ui/oxide/skin.shadowdom.css": { "name": "js/skins/ui/oxide/skin.shadowdom.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css" }, "js/skins/ui/oxide/skin.shadowdom.css.gz": { "name": "js/skins/ui/oxide/skin.shadowdom.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css.gz" }, "js/skins/ui/oxide/skin.shadowdom.js": { "name": "js/skins/ui/oxide/skin.shadowdom.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.js", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.js" }, "js/skins/ui/oxide/skin.shadowdom.js.gz": { "name": "js/skins/ui/oxide/skin.shadowdom.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.js.gz" }, "js/skins/ui/oxide/skin.shadowdom.min.css": { "name": "js/skins/ui/oxide/skin.shadowdom.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css" }, "js/skins/ui/oxide/skin.shadowdom.min.css.gz": { "name": "js/skins/ui/oxide/skin.shadowdom.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css.gz" }, "js/skins/ui/tinymce-5-dark/content.css": { "name": "js/skins/ui/tinymce-5-dark/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css" }, "js/skins/ui/tinymce-5-dark/content.css.gz": { "name": "js/skins/ui/tinymce-5-dark/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css.gz" }, "js/skins/ui/tinymce-5-dark/content.inline.css": { "name": "js/skins/ui/tinymce-5-dark/content.inline.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css" }, "js/skins/ui/tinymce-5-dark/content.inline.css.gz": { "name": "js/skins/ui/tinymce-5-dark/content.inline.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css.gz" }, "js/skins/ui/tinymce-5-dark/content.inline.js": { "name": "js/skins/ui/tinymce-5-dark/content.inline.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.js" }, "js/skins/ui/tinymce-5-dark/content.inline.js.gz": { "name": "js/skins/ui/tinymce-5-dark/content.inline.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.js.gz" }, "js/skins/ui/tinymce-5-dark/content.inline.min.css": { "name": "js/skins/ui/tinymce-5-dark/content.inline.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css" }, "js/skins/ui/tinymce-5-dark/content.inline.min.css.gz": { "name": "js/skins/ui/tinymce-5-dark/content.inline.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css.gz" }, "js/skins/ui/tinymce-5-dark/content.js": { "name": "js/skins/ui/tinymce-5-dark/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.js" }, "js/skins/ui/tinymce-5-dark/content.js.gz": { "name": "js/skins/ui/tinymce-5-dark/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.js.gz" }, "js/skins/ui/tinymce-5-dark/content.min.css": { "name": "js/skins/ui/tinymce-5-dark/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css" }, "js/skins/ui/tinymce-5-dark/content.min.css.gz": { "name": "js/skins/ui/tinymce-5-dark/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css.gz" }, "js/skins/ui/tinymce-5-dark/skin.css": { "name": "js/skins/ui/tinymce-5-dark/skin.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css" }, "js/skins/ui/tinymce-5-dark/skin.css.gz": { "name": "js/skins/ui/tinymce-5-dark/skin.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css.gz" }, "js/skins/ui/tinymce-5-dark/skin.js": { "name": "js/skins/ui/tinymce-5-dark/skin.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.js" }, "js/skins/ui/tinymce-5-dark/skin.js.gz": { "name": "js/skins/ui/tinymce-5-dark/skin.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.js.gz" }, "js/skins/ui/tinymce-5-dark/skin.min.css": { "name": "js/skins/ui/tinymce-5-dark/skin.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css" }, "js/skins/ui/tinymce-5-dark/skin.min.css.gz": { "name": "js/skins/ui/tinymce-5-dark/skin.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css.gz" }, "js/skins/ui/tinymce-5-dark/skin.shadowdom.css": { "name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css" }, "js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz": { "name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz" }, "js/skins/ui/tinymce-5-dark/skin.shadowdom.js": { "name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.js" }, "js/skins/ui/tinymce-5-dark/skin.shadowdom.js.gz": { "name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.js.gz" }, "js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css": { "name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css" }, "js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz": { "name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz" }, "js/skins/ui/tinymce-5/content.css": { "name": "js/skins/ui/tinymce-5/content.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.css" }, "js/skins/ui/tinymce-5/content.css.gz": { "name": "js/skins/ui/tinymce-5/content.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.css.gz" }, "js/skins/ui/tinymce-5/content.inline.css": { "name": "js/skins/ui/tinymce-5/content.inline.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css" }, "js/skins/ui/tinymce-5/content.inline.css.gz": { "name": "js/skins/ui/tinymce-5/content.inline.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css.gz" }, "js/skins/ui/tinymce-5/content.inline.js": { "name": "js/skins/ui/tinymce-5/content.inline.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.js" }, "js/skins/ui/tinymce-5/content.inline.js.gz": { "name": "js/skins/ui/tinymce-5/content.inline.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.js.gz" }, "js/skins/ui/tinymce-5/content.inline.min.css": { "name": "js/skins/ui/tinymce-5/content.inline.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css" }, "js/skins/ui/tinymce-5/content.inline.min.css.gz": { "name": "js/skins/ui/tinymce-5/content.inline.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css.gz" }, "js/skins/ui/tinymce-5/content.js": { "name": "js/skins/ui/tinymce-5/content.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.js" }, "js/skins/ui/tinymce-5/content.js.gz": { "name": "js/skins/ui/tinymce-5/content.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.js.gz" }, "js/skins/ui/tinymce-5/content.min.css": { "name": "js/skins/ui/tinymce-5/content.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css" }, "js/skins/ui/tinymce-5/content.min.css.gz": { "name": "js/skins/ui/tinymce-5/content.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css.gz" }, "js/skins/ui/tinymce-5/skin.css": { "name": "js/skins/ui/tinymce-5/skin.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css" }, "js/skins/ui/tinymce-5/skin.css.gz": { "name": "js/skins/ui/tinymce-5/skin.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css.gz" }, "js/skins/ui/tinymce-5/skin.js": { "name": "js/skins/ui/tinymce-5/skin.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.js" }, "js/skins/ui/tinymce-5/skin.js.gz": { "name": "js/skins/ui/tinymce-5/skin.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.js.gz" }, "js/skins/ui/tinymce-5/skin.min.css": { "name": "js/skins/ui/tinymce-5/skin.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css" }, "js/skins/ui/tinymce-5/skin.min.css.gz": { "name": "js/skins/ui/tinymce-5/skin.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css.gz" }, "js/skins/ui/tinymce-5/skin.shadowdom.css": { "name": "js/skins/ui/tinymce-5/skin.shadowdom.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css" }, "js/skins/ui/tinymce-5/skin.shadowdom.css.gz": { "name": "js/skins/ui/tinymce-5/skin.shadowdom.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css.gz" }, "js/skins/ui/tinymce-5/skin.shadowdom.js": { "name": "js/skins/ui/tinymce-5/skin.shadowdom.js", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.js", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.js", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.js" }, "js/skins/ui/tinymce-5/skin.shadowdom.js.gz": { "name": "js/skins/ui/tinymce-5/skin.shadowdom.js.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.js.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.js.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.js.gz" }, "js/skins/ui/tinymce-5/skin.shadowdom.min.css": { "name": "js/skins/ui/tinymce-5/skin.shadowdom.min.css", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css" }, "js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz": { "name": "js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz", - "path": "/Users/joaohortencio/projects/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz", + "path": "/home/leandrojatai/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz", "publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz" } }, "chunks": { "compilacao": [ "css/chunk-vendors.045ec640.css", - "js/chunk-vendors.e8ab4373.js", + "js/chunk-vendors.f2d6067d.js", "css/compilacao.0baf3580.css", - "js/compilacao.d3adb1c1.js" + "js/compilacao.148cb66b.js" ], "global": [ "css/chunk-vendors.045ec640.css", - "js/chunk-vendors.e8ab4373.js", - "css/global.45591136.css", - "js/global.8ef6e932.js" + "js/chunk-vendors.f2d6067d.js", + "css/global.042f6737.css", + "js/global.d557d20c.js" ], "painel": [ "css/chunk-vendors.045ec640.css", - "js/chunk-vendors.e8ab4373.js", + "js/chunk-vendors.f2d6067d.js", "css/painel.e2b9504e.css", - "js/painel.73a4fb17.js" + "js/painel.f3190acc.js" ], "parlamentar": [ "css/chunk-vendors.045ec640.css", - "js/chunk-vendors.e8ab4373.js", + "js/chunk-vendors.f2d6067d.js", "css/parlamentar.cd5dc5a8.css", - "js/parlamentar.f0710dca.js" + "js/parlamentar.f63027a7.js" ] }, "publicPath": "/static/sapl/frontend/" diff --git a/release.sh b/release.sh index 3ee43b85a..138e92f66 100755 --- a/release.sh +++ b/release.sh @@ -46,13 +46,11 @@ function change_files { if [[ "$OSTYPE" == "darwin"* ]]; then # MacOS (BSD sed) sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" docker/docker-compose.yaml - sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" setup.py sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/templates/base.html sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/settings.py else # Linux (GNU sed) sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" docker/docker-compose.yaml - sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" setup.py sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/templates/base.html sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/settings.py fi @@ -76,9 +74,6 @@ function set_rc_version { fi FINAL_VERSION=$NEXT_RC_VERSION -## DEBUG -# echo "OLD_VERSION: $OLD_VERSION" -# echo "FINAL_VERSION: $FINAL_VERSION" } # Function to display Yes/No prompt with colored message @@ -96,7 +91,7 @@ prompt_yes_no() { function commit_and_push { echo -e "${green_color}Committing new release $FINAL_VERSION...${color_reset}" - git add docker/docker-compose.yaml setup.py sapl/settings.py sapl/templates/base.html + git add docker/docker-compose.yaml sapl/settings.py sapl/templates/base.html git changelog --tag $FINAL_VERSION --prune-old -x > latest_changes.md cat latest_changes.md CHANGES.md > CHANGES.tmp mv CHANGES.tmp CHANGES.md diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt index 56cdb1bb1..b7b662d5e 100644 --- a/requirements/dev-requirements.txt +++ b/requirements/dev-requirements.txt @@ -1,11 +1,11 @@ -r test-requirements.txt -autopep8==1.2.4 -beautifulsoup4==4.9.1 -django-debug-toolbar==1.11.1 -ipdb==0.13.3 -fancycompleter==0.9.1 -pdbpp==0.10.3 -pip-review==0.4 -pipdeptree==0.10.1 +autopep8==2.3.2 +beautifulsoup4==4.13.5 +django-debug-toolbar==3.2.4 +ipdb==0.13.13 +fancycompleter==0.11.1 +pdbpp==0.11.7 +pip-review==1.3.0 +pipdeptree==2.28.0 pydevd-pycharm~=203.7148.7 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a53ec010a..ca05c71b7 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,7 +2,6 @@ django==2.2.28 django-haystack==3.1.1 django-filter==2.4.0 djangorestframework==3.12.4 -dj-database-url==0.5.0 django-braces==1.14.0 django-crispy-forms==1.7.2 django-contrib-postgres==0.0.1 @@ -16,14 +15,17 @@ drf-spectacular==0.18.2 django-ratelimit==3.0.1 easy-thumbnails==2.8.5 python-decouple==3.1 -psycopg2-binary==2.8.6 +dj-database-url==0.5.0 +psycopg2-binary==2.9.9 pyyaml==6.0.1 pytz==2019.3 python-magic==0.4.15 unipath==1.1 -WeasyPrint==51 Pillow==10.3.0 -gunicorn==22.0.0 +reportlab==3.6.13 +WeasyPrint==66 +trml2pdf==0.6 +gunicorn==23.0.0 more-itertools==8.2.0 pysolr==3.6.0 PyPDF4==1.27.0 @@ -37,5 +39,6 @@ django-prometheus==2.2.0 asn1crypto==1.5.1 XlsxWriter==3.2.0 -git+https://github.com/interlegis/trml2pdf +setuptools==80.9.0 + git+https://github.com/interlegis/django-admin-bootstrapped diff --git a/requirements/test-requirements.txt b/requirements/test-requirements.txt index 5116d1fe7..2da40408f 100644 --- a/requirements/test-requirements.txt +++ b/requirements/test-requirements.txt @@ -1,11 +1,10 @@ -r requirements.txt -coverage==4.4 -django-webtest==1.9.7 -flake8==2.6.2 -isort==4.2.5 -model-bakery==1.1.0 -pep8==1.7.0 -pytest==5.4.3 -pytest-cov==2.10.0 -pytest-django==3.8.0 -webtest==2.0.21 +coverage==7.6.1 +django-webtest==1.9.8 +flake8==7.1.1 +isort==5.13.2 +model-bakery==1.5.0 +pycodestyle==2.12.1 +pytest==8.3.3 +pytest-cov==5.0.0 +WebTest==3.0.6 diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index 903051d10..b8bb06b4d 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -4,6 +4,8 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db.models import Q +from django.urls import reverse +from drfautoapi.drfautoapi import DrfAutoApiSerializerMixin from image_cropping.utils import get_backend from rest_framework import serializers from rest_framework.fields import SerializerMethodField @@ -13,16 +15,13 @@ from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura from sapl.sessao.models import OrdemDia, SessaoPlenaria -class SaplSerializerMixin(serializers.ModelSerializer): - __str__ = SerializerMethodField() +class SaplSerializerMixin(DrfAutoApiSerializerMixin): + link_detail_backend = serializers.SerializerMethodField() metadata = SerializerMethodField() - class Meta: + class Meta(DrfAutoApiSerializerMixin.Meta): fields = '__all__' - def get___str__(self, obj) -> str: - return str(obj) - def get_metadata(self, obj) -> dict: try: metadata = Metadata.objects.get( @@ -35,6 +34,13 @@ class SaplSerializerMixin(serializers.ModelSerializer): finally: return metadata + def get_link_detail_backend(self, obj) -> str: + try: + return reverse(f'{self.Meta.model._meta.app_config.name}:{self.Meta.model._meta.model_name}_detail', + kwargs={'pk': obj.pk}) + except: + return '' + class ChoiceSerializer(serializers.Serializer): value = serializers.SerializerMethodField() diff --git a/sapl/api/urls.py b/sapl/api/urls.py index a51ea2a80..1ab636707 100644 --- a/sapl/api/urls.py +++ b/sapl/api/urls.py @@ -1,15 +1,13 @@ - from django.conf.urls import include, url from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, \ SpectacularRedocView from rest_framework.authtoken.views import obtain_auth_token from sapl.api.deprecated import SessaoPlenariaViewSet -from sapl.api.views import AppVersionView, recria_token,\ - SaplApiViewSetConstrutor +from sapl.api.views import recria_token, SaplApiViewSetConstrutor from .apps import AppConfig - +from .views_health import HealthzView, ReadyzView app_name = AppConfig.name @@ -38,7 +36,6 @@ urlpatterns = [ url(r'^api/', include(urlpatterns_api_doc)), url(r'^api/', include(urlpatterns_router)), - url(r'^api/version', AppVersionView.as_view()), url(r'^api/auth/token$', obtain_auth_token), url(r'^api/recriar-token/(?P\d*)$', recria_token, name="recria_token"), ] diff --git a/sapl/api/views.py b/sapl/api/views.py index a9c0686e8..c2b8ae786 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -1,6 +1,7 @@ import logging from django.conf import settings +from django.http import HttpResponse, JsonResponse from rest_framework.authtoken.models import Token from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated, IsAdminUser @@ -8,6 +9,9 @@ from rest_framework.response import Response from rest_framework.views import APIView from drfautoapi.drfautoapi import ApiViewSetConstrutor +from django.views.decorators.http import condition as django_condition + +from sapl.base.models import AuditLog logger = logging.getLogger(__name__) @@ -20,22 +24,122 @@ def recria_token(request, pk): return Response({"message": "Token recriado com sucesso!", "token": token.key}) - -class AppVersionView(APIView): - permission_classes = (IsAuthenticated,) - - def get(self, request): - content = { - 'name': 'SAPL', - 'description': 'Sistema de Apoio ao Processo Legislativo', - 'version': settings.SAPL_VERSION, - 'user': request.user.username, - 'is_authenticated': request.user.is_authenticated, - } - return Response(content) - +class LastModifiedDecorator: + """ - Decorator para adicionar suporte a Last-Modified em ViewSets + - Baseado em django.views.decorators.http.condition + + - Por padrão utiliza o AuditLog para determinar a data do last_modified + - Pode ser sobrescrito na customização do ViewSet caso necessário + - Existe um exemplo de sobrescrita em sapl/api/views_materia.py + + - Devolve Last-Modified baseado no timestamp do último AuditLog + - Se for uma listagem, considera o último AuditLog de todos os objetos + retornados na listagem levando em consideração resultado de FilterSet. + - Retorna 304 Not Modified se o recurso não foi modificado + """ + def __call__(self, cls): + + original_dispatch = cls.dispatch + self.model = cls.queryset.model + + def wrapped_dispatch(view, request, *args, **kwargs): + drf_request = request + wsgi_request = view.initialize_request(request, *args, **kwargs) + + last_modified_func = self.last_modified_func if not hasattr(view, 'last_modified_func') else view.last_modified_func + + def patched_viewset_method(*_args, **_kwargs): + return original_dispatch(view, drf_request, *args, **kwargs) + + django_decorator = django_condition(last_modified_func=last_modified_func) + decorated_viewset_method = django_decorator(patched_viewset_method) + return decorated_viewset_method(wsgi_request, *args, view=view, **kwargs) + + cls.dispatch = wrapped_dispatch + return cls + + def last_modified_func(self, request, *args, **kwargs): + + default_fields_for_timestamp = ( + 'data_ultima_atualizacao', + 'ultima_edicao' + ) + + # tenta encontrar um dos campos acima para usar como last_modified + fields = tuple( + filter( + lambda f: f in default_fields_for_timestamp, + map( + lambda f: f.name, self.model._meta.get_fields() + ) + ) + ) + field_name = fields[0] if fields else None + + # Retornando None. que indica para o decorator retornar 200 OK sem Last-Modified + # Na prática, desativando o AuditLog como last_modified + # Porém, mantendo a estrutura para possível uso futuro + if not field_name: + # sem campo definido para last_modified, usa AuditLog + return None # self.last_modified_func__auditlog(request, *args, **kwargs) + + pk = kwargs.get('pk', None) + if pk: + return self.model.objects.filter(pk=pk).values_list(field_name, flat=True)[:1].first() + + view = kwargs.get('view', None) + + if view: + queryset = view.get_queryset() + for backend in list(view.filter_backends): + queryset = backend().filter_queryset(request, queryset, view) + else: + queryset = self.model.objects.all() + + timestamp = queryset.order_by(f'-{field_name}').values_list(field_name, flat=True)[:1].first() + return timestamp + + def last_modified_func__auditlog(self, request, *args, **kwargs): + """ - Método padrão para obter o last_modified baseado no AuditLog + - Pode ser sobrescrito na customização do ViewSet caso necessário + - Existe um exemplo de sobrescrita em sapl/api/views_materia.py + """ + try: + if 'pk' in kwargs: + obj_id = kwargs['pk'] + last_log = AuditLog.objects.filter( + model_name=self.model._meta.model_name, + object_id=obj_id + ).order_by('-timestamp').values_list('timestamp', flat=True)[:1].first() + else: + view = kwargs.get('view', None) + if view: + for backend in list(view.filter_backends): + queryset = backend().filter_queryset(request, view.queryset, view) + + if queryset.exists(): + last_log = AuditLog.objects.filter( + model_name=self.model._meta.model_name, + object_id__in=queryset.values_list('pk', flat=True) + ).order_by('-timestamp').values_list('timestamp', flat=True)[:1].first() + else: + last_log = None + else: + last_log = AuditLog.objects.filter( + model_name=self.model._meta.model_name, + object_id__in=self.model.objects.values_list('pk', flat=True) + ).order_by('-timestamp').values_list('timestamp', flat=True)[:1].first() + + if last_log: + return last_log + + except Exception as e: + logger.error(f"Erro ao obter last_modified: {e}") + + return None SaplApiViewSetConstrutor = ApiViewSetConstrutor +SaplApiViewSetConstrutor.last_modified_class(LastModifiedDecorator) SaplApiViewSetConstrutor.import_modules([ 'sapl.api.views_audiencia', 'sapl.api.views_base', @@ -52,7 +156,7 @@ SaplApiViewSetConstrutor.import_modules([ """ 1. ApiViewSetConstrutor constroi uma rest_framework.viewsets.ModelViewSet - para todos os models de todas as app_configs passadas no list + para todos os models de todas as app_configs passadas no list 2. Define DjangoFilterBackend como ferramenta de filtro dos campos 3. Define Serializer como a seguir: 3.1 - Define um Serializer genérico para cada módel @@ -60,7 +164,7 @@ SaplApiViewSetConstrutor.import_modules([ recupera Serializer customizados no módulo DEFAULT_SERIALIZER_MODULE 3.2 - Para todo model é opcional a existência de {model}Serializer. Caso não seja definido um Serializer customizado, utiliza-se o genérico - 3.3 - Caso exista GLOBAL_SERIALIZER_MIXIN definido, + 3.3 - Caso exista GLOBAL_SERIALIZER_MIXIN definido, utiliza este Serializer para construir o genérico de 3.1 4. Define um FilterSet como a seguir: 4.1 - Define um FilterSet genérico para cada módel @@ -68,13 +172,13 @@ SaplApiViewSetConstrutor.import_modules([ recupera o FilterSet customizado no módulo DEFAULT_FILTER_MODULE 4.2 - Para todo model é opcional a existência de {model}FilterSet. Caso não seja definido um FilterSet customizado, utiliza-se o genérico - 4.3 - Caso exista GLOBAL_FILTERSET_MIXIN definido, + 4.3 - Caso exista GLOBAL_FILTERSET_MIXIN definido, utiliza este FilterSet para construir o genérico de 4.1 - 4.4 - Caso não exista GLOBAL_FILTERSET_MIXIN, será aplicado + 4.4 - Caso não exista GLOBAL_FILTERSET_MIXIN, será aplicado drfautoapi.drjautoapi.ApiFilterSetMixin que inclui parametro para: - order_by: através do parâmetro "o" - - amplia os lookups aceitos pelo FilterSet default - para os aceitos pelo django sem a necessidade de criar + - amplia os lookups aceitos pelo FilterSet default + para os aceitos pelo django sem a necessidade de criar fields específicos em um FilterSet customizado. 5. ApiViewSetConstrutor não cria padrões e/ou exige conhecimento alem dos diff --git a/sapl/api/views_health.py b/sapl/api/views_health.py new file mode 100644 index 000000000..bd7e6eaad --- /dev/null +++ b/sapl/api/views_health.py @@ -0,0 +1,106 @@ +import logging +import traceback + +from django.http import HttpResponse, JsonResponse +from django.utils import timezone +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from django.conf import settings +from sapl.health import check_app, check_db, check_cache + +COMMON_HEADERS = { + "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", + "Pragma": "no-cache", +} + + +def _format_plain(ok: bool) -> HttpResponse: + return HttpResponse("OK\n" if ok else "UNHEALTHY\n", + status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, + content_type="text/plain") + + +class HealthzView(APIView): + authentication_classes = [] + permission_classes = [] + + logger = logging.getLogger(__name__) + + def get(self, request): + try: + ok, msg, ms = check_app() + payload = { + "status": "OK" if ok else "UNHEALTHY", + "checks": {"app": {"ok": ok, "latency_ms": round(ms, 1), "error": msg}}, + "version": settings.SAPL_VERSION, + "time": timezone.now().isoformat(), + } + if request.query_params.get("fmt") == "txt": + return _format_plain(ok) + return Response(payload, + status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, + headers=COMMON_HEADERS) + except Exception as e: + self.logger.error(traceback.format_exc()) + return "An internal error has occurred!" + + + +class ReadyzView(APIView): + authentication_classes = [] + permission_classes = [] + + logger = logging.getLogger(__name__) + + def get(self, request): + try: + checks = { + "app": check_app(), + "db": check_db(), + "cache": check_cache(), + } + payload_checks = { + name: {"ok": r[0], "latency_ms": round(r[2], 1), "error": r[1]} + for name, r in checks.items() + } + ok = all(r[0] for r in checks.values()) + payload = { + "status": "ok" if ok else "unhealthy", + "checks": payload_checks, + "version": settings.SAPL_VERSION, + "time": timezone.now().isoformat(), + } + if request.query_params.get("fmt") == "txt": + return _format_plain(ok) + return Response(payload, + status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, + headers=COMMON_HEADERS) + except Exception as e: + self.logger.error(traceback.format_exc()) + return "An internal error has occurred!" + + +class AppzVersionView(APIView): + authentication_classes = [] + permission_classes = [] + + logger = logging.getLogger(__name__) + + def get(self, request): + try: + payload = { + 'name': 'SAPL', + 'description': 'Sistema de Apoio ao Processo Legislativo', + 'version': settings.SAPL_VERSION, + } + if request.query_params.get("fmt") == "txt": + return HttpResponse(f"{payload['version']} {payload['name']}", + status=status.HTTP_200_OK, + content_type="text/plain") + return Response(payload, + status=status.HTTP_200_OK, + headers=COMMON_HEADERS) + except Exception as e: + self.logger.error(traceback.format_exc()) + return "An internal error has occurred!" diff --git a/sapl/api/views_materia.py b/sapl/api/views_materia.py index 0d5356b0b..fc345bb90 100644 --- a/sapl/api/views_materia.py +++ b/sapl/api/views_materia.py @@ -1,7 +1,9 @@ from django.apps.registry import apps +from django.db import IntegrityError, transaction from django.db.models import Q from rest_framework.decorators import action +from rest_framework.status import HTTP_201_CREATED, HTTP_409_CONFLICT from rest_framework.response import Response from drfautoapi.drfautoapi import ApiViewSetConstrutor, \ @@ -90,6 +92,60 @@ class _MateriaLegislativaViewSet: class Meta: ordering = ['-ano', 'tipo', 'numero'] + _MAX_RETRIES_NUMERO = 3 + + def create(self, request, *args, **kwargs): + data = request.data.copy() + tipo = data.get('tipo', None) + numero = data.get('numero', None) + ano = data.get('ano', None) + + if tipo and not numero: + # Número não fornecido pelo cliente: auto-gerar próximo disponível. + # select_for_update() em get_proximo_numero previne race conditions. + # Retry como camada extra de segurança contra IntegrityError residual. + for tentativa in range(self._MAX_RETRIES_NUMERO): + try: + with transaction.atomic(): + numero_gerado, ano_gerado = MateriaLegislativa.get_proximo_numero( + tipo=tipo, ano=ano + ) + data['numero'] = numero_gerado + data['ano'] = ano_gerado + + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) + except IntegrityError: + if tentativa == self._MAX_RETRIES_NUMERO - 1: + return Response( + {'detail': 'Não foi possível gerar um número único após ' + '%d tentativas. Tente novamente.' % self._MAX_RETRIES_NUMERO}, + status=HTTP_409_CONFLICT + ) + continue + + # Número fornecido pelo cliente (ou tipo ausente): + # respeitar os dados enviados. Se houver conflito de unicidade, + # retornar erro explícito em vez de sobrescrever silenciosamente. + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + try: + with transaction.atomic(): + self.perform_create(serializer) + except IntegrityError: + return Response( + {'numero': [ + 'O número %s já está em uso para este tipo/ano. ' + 'Remova o campo "numero" para auto-gerar o próximo disponível.' % numero + ]}, + status=HTTP_409_CONFLICT + ) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) + @action(detail=True, methods=['GET']) def ultima_tramitacao(self, request, *args, **kwargs): @@ -110,6 +166,25 @@ class _MateriaLegislativaViewSet: self.queryset = self.get_object().anexadas.all() return self.list(request, *args, **kwargs) +""" + O próprio decorator LastModifiedDecorator já implementa o método last_modified_func que atende o caso + específico de MateriaLegislativa, baseado no campo data_ultima_atualizacao. + Portanto, não há necessidade de reimplementar este método aqui. Mas segue o código comentado + para referência futura caso necessário e como exemplo de implementação customizada que sobrescreve + o comportamento padrão do decorator. + + def last_modified_func(self, request, *args, **kwargs): + pk = kwargs.get('pk', None) + if pk: + return MateriaLegislativa.objects.filter(pk=pk).values_list('data_ultima_atualizacao', flat=True)[:1].first() + + queryset = self.get_queryset() + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(request, self.queryset, self) + + timestamp = queryset.order_by('-data_ultima_atualizacao').values_list('data_ultima_atualizacao', flat=True)[:1].first() + return timestamp +""" @customize(DocumentoAcessorio) diff --git a/sapl/audiencia/forms.py b/sapl/audiencia/forms.py index 211db9855..d2c7dd45b 100755 --- a/sapl/audiencia/forms.py +++ b/sapl/audiencia/forms.py @@ -10,9 +10,9 @@ from django.utils.translation import ugettext_lazy as _ from crispy_forms.layout import Button, Column, Fieldset, HTML, Layout from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica, AnexoAudienciaPublica +from sapl.base.models import Autor from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, SaplFormLayout, to_row from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa -from sapl.parlamentares.models import Parlamentar from sapl.utils import timezone, FileFieldCheckMixin, validar_arquivo @@ -44,10 +44,10 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm): widget=forms.HiddenInput(), queryset=MateriaLegislativa.objects.all()) - parlamentar_autor = forms.ModelChoiceField( - label=_("Parlamentar Autor"), + autor = forms.ModelChoiceField( + label=_("Autor"), required=False, - queryset=Parlamentar.objects.all()) + queryset=Autor.objects.all().order_by('nome')) requerimento = forms.ModelChoiceField( label=_("Requerimento"), @@ -58,7 +58,7 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm): model = AudienciaPublica fields = ['tipo', 'numero', 'ano', 'nome', 'tema', 'data', 'hora_inicio', 'hora_fim', - 'observacao', 'audiencia_cancelada', 'parlamentar_autor', 'requerimento', 'url_audio', + 'observacao', 'audiencia_cancelada', 'autor', 'requerimento', 'url_audio', 'url_video', 'upload_pauta', 'upload_ata', 'upload_anexo', 'tipo_materia', 'numero_materia', 'ano_materia', 'materia'] @@ -85,7 +85,7 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm): materia = cleaned_data['numero_materia'] ano_materia = cleaned_data['ano_materia'] tipo_materia = cleaned_data['tipo_materia'] - parlamentar_autor = cleaned_data["parlamentar_autor"] + autor = cleaned_data["autor"] requerimento = cleaned_data["requerimento"] if cleaned_data["ano"] != cleaned_data["data"].year: @@ -115,7 +115,7 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm): numero=materia, ano=ano_materia, tipo=tipo_materia) - except ObjectDoesNotExist: + except (ObjectDoesNotExist, ValueError): msg = _('A matéria %s nº %s/%s não existe no cadastro' ' de matérias legislativas.' % (tipo_materia, materia, ano_materia)) self.logger.warning( @@ -157,12 +157,12 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm): raise ValidationError(msg) # requerimento é optativo - if parlamentar_autor and requerimento: - if parlamentar_autor.autor.first() not in requerimento.autores.all(): - raise ValidationError("Parlamentar Autor selecionado não faz" + if autor and requerimento: + if autor not in requerimento.autores.all(): + raise ValidationError("Autor selecionado não faz" " parte da autoria do Requerimento " "selecionado.") - elif parlamentar_autor: + elif autor: raise ValidationError("Para informar um autor deve-se informar um requerimento.") elif requerimento: raise ValidationError("Para informar um requerimento deve-se informar um autor.") diff --git a/sapl/audiencia/migrations/0020_auto_20251201_1450.py b/sapl/audiencia/migrations/0020_auto_20251201_1450.py new file mode 100644 index 000000000..6afbd9b0e --- /dev/null +++ b/sapl/audiencia/migrations/0020_auto_20251201_1450.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.28 on 2025-12-01 17:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audiencia', '0019_auto_20240711_1400'), + ] + + operations = [ + migrations.AlterField( + model_name='audienciapublica', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='audienciapublica', + name='nome', + field=models.CharField(max_length=250, verbose_name='Nome da Audiência Pública'), + ), + migrations.AlterField( + model_name='audienciapublica', + name='tema', + field=models.CharField(max_length=250, verbose_name='Tema da Audiência Pública'), + ), + ] diff --git a/sapl/audiencia/migrations/0021_auto_20260623_1938.py b/sapl/audiencia/migrations/0021_auto_20260623_1938.py new file mode 100644 index 000000000..a2e101c64 --- /dev/null +++ b/sapl/audiencia/migrations/0021_auto_20260623_1938.py @@ -0,0 +1,59 @@ +# Generated by Django 2.2.28 on 2026-06-23 22:38 + +from django.db import migrations, models +import django.db.models.deletion + + +def copia_parlamentar_para_autor(apps, schema_editor): + """Converte o antigo parlamentar_autor no Autor genérico correspondente.""" + AudienciaPublica = apps.get_model('audiencia', 'AudienciaPublica') + Autor = apps.get_model('base', 'Autor') + ContentType = apps.get_model('contenttypes', 'ContentType') + try: + ct = ContentType.objects.get( + app_label='parlamentares', model='parlamentar') + except ContentType.DoesNotExist: + return + for ap in AudienciaPublica.objects.filter(parlamentar_autor__isnull=False): + autor = Autor.objects.filter( + content_type=ct, object_id=ap.parlamentar_autor_id).first() + if autor: + ap.autor = autor + ap.save(update_fields=['autor']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0060_auto_20240812_1628'), + ('audiencia', '0020_auto_20251201_1450'), + ] + + operations = [ + migrations.AddField( + model_name='audienciapublica', + name='autor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='base.Autor', verbose_name='Autor'), + ), + migrations.RunPython( + copia_parlamentar_para_autor, migrations.RunPython.noop), + # Mantém a coluna parlamentar_autor no banco (removida apenas do estado + # do Django). Assim uma casa pode dar rollback da imagem sem perder o + # vínculo, e o código novo simplesmente ignora a coluna. O DROP + # definitivo fica para uma migração posterior, quando toda a frota + # estiver estável nesta versão. + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name='audienciapublica', + name='parlamentar_autor', + ), + ], + database_operations=[], + ), + migrations.AlterField( + model_name='audienciapublica', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + ] diff --git a/sapl/audiencia/migrations/0022_audienciapublica_data_ultima_atualizacao.py b/sapl/audiencia/migrations/0022_audienciapublica_data_ultima_atualizacao.py new file mode 100644 index 000000000..15f49d3fa --- /dev/null +++ b/sapl/audiencia/migrations/0022_audienciapublica_data_ultima_atualizacao.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2026-06-26 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audiencia', '0021_auto_20260623_1938'), + ] + + operations = [ + migrations.AddField( + model_name='audienciapublica', + name='data_ultima_atualizacao', + field=models.DateTimeField(auto_now=True, null=True, verbose_name='Data da Última Atualização'), + ), + ] diff --git a/sapl/audiencia/models.py b/sapl/audiencia/models.py index e5d011762..8ba14f29f 100755 --- a/sapl/audiencia/models.py +++ b/sapl/audiencia/models.py @@ -3,7 +3,7 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from model_utils import Choices from sapl.materia.models import MateriaLegislativa -from sapl.parlamentares.models import (CargoMesa, Parlamentar) +from sapl.parlamentares.models import CargoMesa from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericRelation, restringe_tipos_de_arquivo_txt, texto_upload_path, @@ -65,9 +65,9 @@ class AudienciaPublica(models.Model): ano = models.PositiveSmallIntegerField(verbose_name=_('Ano'), choices=RANGE_ANOS) nome = models.CharField( - max_length=100, verbose_name=_('Nome da Audiência Pública')) + max_length=250, verbose_name=_('Nome da Audiência Pública')) tema = models.CharField( - max_length=100, verbose_name=_('Tema da Audiência Pública')) + max_length=250, verbose_name=_('Tema da Audiência Pública')) data = models.DateField(verbose_name=_('Data')) hora_inicio = models.CharField( max_length=5, verbose_name=_('Horário Início(hh:mm)')) @@ -79,12 +79,15 @@ class AudienciaPublica(models.Model): default=False, choices=YES_NO_CHOICES, verbose_name=_('Audiência Cancelada?')) - parlamentar_autor = models.ForeignKey( - Parlamentar, + autor = models.ForeignKey( + 'base.Autor', on_delete=models.PROTECT, null=True, blank=True, - verbose_name=_('Parlamentar Autor')) + verbose_name=_('Autor')) + data_ultima_atualizacao = models.DateTimeField( + blank=True, null=True, auto_now=True, + verbose_name=_('Data da Última Atualização')) requerimento = models.ForeignKey( MateriaLegislativa, null=True, diff --git a/sapl/audiencia/views.py b/sapl/audiencia/views.py index 443fc81e8..d5ea7f710 100755 --- a/sapl/audiencia/views.py +++ b/sapl/audiencia/views.py @@ -9,6 +9,12 @@ from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, MasterDetailCrud from .forms import AudienciaForm, AnexoAudienciaPublicaForm from .models import AudienciaPublica, AnexoAudienciaPublica +from ratelimit.decorators import ratelimit +from django.utils.decorators import method_decorator + +from ..settings import RATE_LIMITER_RATE +from ..utils import ratelimit_ip + def index(request): return HttpResponse("Audiência Pública") @@ -105,6 +111,3 @@ class AnexoAudienciaPublicaCrud(MasterDetailCrud): qs = super(MasterDetailCrud.ListView, self).get_queryset() kwargs = {self.crud.parent_field: self.kwargs['pk']} return qs.filter(**kwargs).order_by('-data', '-id') - - class DetailView(AudienciaPublicaMixin, MasterDetailCrud.DetailView): - pass diff --git a/sapl/base/apps.py b/sapl/base/apps.py index 403d74bd1..7e4305ea8 100644 --- a/sapl/base/apps.py +++ b/sapl/base/apps.py @@ -1,4 +1,3 @@ - import django from django.utils.translation import ugettext_lazy as _ diff --git a/sapl/base/email_utils.py b/sapl/base/email_utils.py index 3ccf64690..2781e9876 100644 --- a/sapl/base/email_utils.py +++ b/sapl/base/email_utils.py @@ -21,7 +21,7 @@ def load_email_templates(templates, context={}): tpl = loader.get_template(t) email = tpl.render(context) if t.endswith(".html"): - email = email.replace('\n', '').replace('\r', '') + email = email.replace('\\n', '').replace('\r', '') emails.append(email) return emails diff --git a/sapl/base/migrations/0061_casalegislativa_informacao_geral.py b/sapl/base/migrations/0061_casalegislativa_informacao_geral.py new file mode 100644 index 000000000..5fec68acd --- /dev/null +++ b/sapl/base/migrations/0061_casalegislativa_informacao_geral.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.28 on 2026-08-05 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0060_auto_20240812_1628'), + ] + + operations = [ + migrations.AlterField( + model_name='casalegislativa', + name='informacao_geral', + field=models.TextField(blank=True, + verbose_name='Informação Geral'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 692c55138..b31c532ab 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -73,8 +73,10 @@ class CasaLegislativa(models.Model): max_length=100, blank=True, verbose_name=_('HomePage')) email = models.EmailField( max_length=100, blank=True, verbose_name=_('E-mail')) + # Campo editado via editor de texto rico (TinyMCE), portanto o valor + # armazenado é HTML. Não deve ter max_length: as tags consomem o limite + # e impedem o salvamento de textos curtos. informacao_geral = models.TextField( - max_length=100, blank=True, verbose_name=_('Informação Geral')) diff --git a/sapl/base/receivers.py b/sapl/base/receivers.py index 595323197..f0e5365c6 100644 --- a/sapl/base/receivers.py +++ b/sapl/base/receivers.py @@ -225,7 +225,7 @@ def signed_files_extraction_function(sender, instance, **kwargs): issuer = cert.native['tbs_certificate']['issuer'] oname = issuer.get('organization_name', '') - if oname == 'Gov-Br': + if oname in ('Gov-Br', '1Doc'): nome = subject['common_name'].split(':')[0] continue @@ -336,7 +336,7 @@ def signed_files_extraction_function(sender, instance, **kwargs): issuer = cert.native['tbs_certificate']['issuer'] oname = issuer.get('organization_name', '') - if oname == 'Gov-Br': + if oname in ('Gov-Br', '1Doc'): nome = subject['common_name'].split(':')[0] continue diff --git a/sapl/base/search_indexes.py b/sapl/base/search_indexes.py index e76f00168..d0432aded 100644 --- a/sapl/base/search_indexes.py +++ b/sapl/base/search_indexes.py @@ -108,7 +108,7 @@ class TextExtractField(CharField): continue data += getattr(self, func)(value) + ' ' - data = data.replace('\n', ' ') + data = data.replace('\\n', ' ') return data diff --git a/sapl/base/templatetags/common_tags.py b/sapl/base/templatetags/common_tags.py index 9a60bf85e..84f61b64f 100644 --- a/sapl/base/templatetags/common_tags.py +++ b/sapl/base/templatetags/common_tags.py @@ -300,7 +300,7 @@ def youtube_url(value): # Test if YouTube video # tested on https://pythex.org/ value = value.lower() - youtube_pattern = "^((https?://)?(www\.)?youtube\.com\/watch\?v=)" + youtube_pattern = r"^((https?://)?(www\.)?youtube\.com\/watch\?v=)" r = re.findall(youtube_pattern, value) return True if r else False @@ -308,7 +308,7 @@ def youtube_url(value): @register.filter def facebook_url(value): value = value.lower() - facebook_pattern = "^((https?://)?((www|pt-br)\.)?facebook\.com(\/.+)?\/videos(\/.*)?)" + facebook_pattern = r"^((https?://)?((www|pt-br)\.)?facebook\.com(\/.+)?\/videos(\/.*)?)" r = re.findall(facebook_pattern, value) return True if r else False diff --git a/sapl/base/tests/test_form.py b/sapl/base/tests/test_form.py index 35f1c82bf..9464fe39e 100644 --- a/sapl/base/tests/test_form.py +++ b/sapl/base/tests/test_form.py @@ -39,3 +39,30 @@ def test_casa_legislativa_form_invalido(): }) assert not form.is_valid() + + +@pytest.mark.django_db(transaction=False) +def test_casa_legislativa_form_informacao_geral_sem_limite(): + # O campo é editado por editor de texto rico, então o valor submetido é + # HTML: as tags não podem consumir um limite de caracteres. + html = ''.join('

%s

' % linha for linha in + ['Câmara Municipal', 'Rua Osvaldo Cruz, 555 - Centro', + 'Expediente das 8h às 12h e das 13h30 às 17h30']) + assert len(html) > 100 + + form = CasaLegislativaForm(data={'codigo': 'codigo', + 'nome': 'nome', + 'sigla': 'sg', + 'endereco': 'endereco', + 'cep': '70000-000', + 'municipio': 'municipio', + 'uf': 'DF', + 'telefone': '33333333', + 'fax': '33333333', + 'endereco_web': '', + 'email': '', + 'informacao_geral': html + }) + + assert form.is_valid(), form.errors + assert form.cleaned_data['informacao_geral'] == html diff --git a/sapl/base/views.py b/sapl/base/views.py index ce3a0a717..2b53aa57c 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -48,10 +48,10 @@ from sapl.parlamentares.models import ( from sapl.protocoloadm.models import (Anexado, Protocolo) from sapl.relatorios.views import (relatorio_estatisticas_acesso_normas) from sapl.sessao.models import (Bancada, SessaoPlenaria) -from sapl.settings import EMAIL_SEND_USER +from sapl.settings import EMAIL_SEND_USER, RATE_LIMITER_RATE from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured, SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, google_recaptcha_configured, - get_client_ip, sapn_is_enabled, is_weak_password) + get_client_ip, sapn_is_enabled, is_weak_password, ratelimit_ip) from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, EstatisticasAcessoNormasForm) from .models import AppConfig, CasaLegislativa @@ -67,10 +67,11 @@ class IndexView(TemplateView): return TemplateView.get(self, request, *args, **kwargs) -@method_decorator(ratelimit(key=lambda group, request: get_client_ip(request), - rate='20/m', +@method_decorator(ratelimit(key=ratelimit_ip, + rate=RATE_LIMITER_RATE, method=ratelimit.UNSAFE, - block=True), name='dispatch') + block=True), + name='dispatch') class LoginSapl(views.LoginView): template_name = 'base/login.html' authentication_form = LoginForm @@ -1399,6 +1400,10 @@ class SaplSearchView(SearchView): return context +@method_decorator(ratelimit(key=ratelimit_ip, + rate=RATE_LIMITER_RATE, + block=True), + name='dispatch') class PesquisarAuditLogView(PermissionRequiredMixin, FilterView): model = AuditLog filterset_class = AuditLogFilterSet diff --git a/sapl/comissoes/forms.py b/sapl/comissoes/forms.py index 76f511ed2..599b9a5b3 100644 --- a/sapl/comissoes/forms.py +++ b/sapl/comissoes/forms.py @@ -558,3 +558,28 @@ class DocumentoAcessorioEditForm(FileFieldCheckMixin, forms.ModelForm): # raise ValidationError(f'Favor anexar arquivo em {nome_arquivo}') return self.cleaned_data + + +class ReuniaoFilterSet(django_filters.FilterSet): + + nome = django_filters.CharFilter( + lookup_expr='icontains', label=_('Nome da Reunião')) + tema = django_filters.CharFilter( + lookup_expr='icontains', label=_('Tema da Reunião')) + + class Meta(FilterOverridesMetaMixin): + model = Reuniao + fields = ['numero', 'nome', 'tema', 'data'] + + def __init__(self, *args, **kwargs): + super(ReuniaoFilterSet, self).__init__(*args, **kwargs) + + row1 = to_row([('numero', 3), ('nome', 5), ('tema', 4)]) + row2 = to_row([('data', 6)]) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Pesquisar Reunião'), + row1, row2, + form_actions(label=_('Pesquisar')))) diff --git a/sapl/comissoes/migrations/0031_comissao_data_ultima_atualizacao.py b/sapl/comissoes/migrations/0031_comissao_data_ultima_atualizacao.py new file mode 100644 index 000000000..e35f98e0a --- /dev/null +++ b/sapl/comissoes/migrations/0031_comissao_data_ultima_atualizacao.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2026-06-26 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('comissoes', '0030_auto_20231007_2149'), + ] + + operations = [ + migrations.AddField( + model_name='comissao', + name='data_ultima_atualizacao', + field=models.DateTimeField(auto_now=True, null=True, verbose_name='Data da Última Atualização'), + ), + ] diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index 2d9a323d3..694fdceaf 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -34,6 +34,9 @@ class Comissao(models.Model): tipo = models.ForeignKey(TipoComissao, on_delete=models.PROTECT, verbose_name=_('Tipo')) + data_ultima_atualizacao = models.DateTimeField( + blank=True, null=True, auto_now=True, + verbose_name=_('Data da Última Atualização')) nome = models.CharField(max_length=100, verbose_name=_('Nome')) sigla = models.CharField(max_length=10, verbose_name=_('Sigla')) data_criacao = models.DateField(verbose_name=_('Data de Criação')) diff --git a/sapl/comissoes/views.py b/sapl/comissoes/views.py index 5d3e450c2..92cde447f 100644 --- a/sapl/comissoes/views.py +++ b/sapl/comissoes/views.py @@ -22,17 +22,22 @@ from sapl.comissoes.forms import (ComissaoForm, ComposicaoForm, ParticipacaoCreateForm, ParticipacaoEditForm, PautaReuniaoFilterSet, PautaReuniaoForm, - PeriodoForm, ReuniaoForm) + PeriodoForm, ReuniaoFilterSet, ReuniaoForm) from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud, PermissionRequiredForAppCrudMixin, RP_DETAIL, RP_LIST) from sapl.materia.models import (MateriaEmTramitacao, MateriaLegislativa, PautaReuniao, Tramitacao) -from sapl.utils import show_results_filter_set +from sapl.utils import show_results_filter_set, ratelimit_ip from .models import (CargoComissao, Comissao, Composicao, DocumentoAcessorio, Participacao, Periodo, Reuniao, TipoComissao) +from ratelimit.decorators import ratelimit +from django.utils.decorators import method_decorator + +from ..settings import RATE_LIMITER_RATE + def pegar_url_composicao(pk): participacao = Participacao.objects.get(id=pk) @@ -211,7 +216,7 @@ class ReuniaoCrud(MasterDetailCrud): public = [RP_LIST, RP_DETAIL, ] class BaseMixin(MasterDetailCrud.BaseMixin): - list_field_names = ['data', 'nome', 'tema', 'upload_ata'] + list_field_names = ['data', 'nome', 'tema', 'upload_pauta', 'upload_ata'] class DetailView(MasterDetailCrud.DetailView): template_name = "comissoes/reuniao_detail.html" @@ -243,6 +248,11 @@ class ReuniaoCrud(MasterDetailCrud): logger = logging.getLogger(__name__) paginate_by = 10 + def get_queryset(self): + qs = super().get_queryset() + self.filterset = ReuniaoFilterSet(self.request.GET, queryset=qs) + return self.filterset.qs + def take_reuniao_pk(self): username = self.request.user.username @@ -271,6 +281,8 @@ class ReuniaoCrud(MasterDetailCrud): context['documentoacessorio_set'] = DocumentoAcessorio.objects.filter( reuniao__pk=context['reuniao_pk'] ).order_by('id') + + context['form'] = self.filterset.form return context class UpdateView(MasterDetailCrud.UpdateView): @@ -333,6 +345,10 @@ class RemovePautaView(PermissionRequiredMixin, CreateView): return HttpResponseRedirect(success_url) +@method_decorator(ratelimit(key=ratelimit_ip, + rate=RATE_LIMITER_RATE, + block=True), + name='dispatch') class AdicionaPautaView(PermissionRequiredMixin, FilterView): filterset_class = PautaReuniaoFilterSet template_name = 'comissoes/pauta.html' diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index 18c24cf1f..c4154ec00 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -987,7 +987,7 @@ class DispositivoEdicaoVigenciaForm(ModelForm): p.pk, _('%s realizada em %s. %s') % ( p.tipo_publicacao, defaultfilters.date( - p.data, "d \d\e F \d\e Y"), + p.data, r"d \d\e F \d\e Y"), str(p.ta))) for p in pubs] dvs = Dispositivo.objects.order_by('ordem').filter( diff --git a/sapl/compilacao/models.py b/sapl/compilacao/models.py index 7b54fcb67..e8a32615e 100644 --- a/sapl/compilacao/models.py +++ b/sapl/compilacao/models.py @@ -287,7 +287,7 @@ class TextoArticulado(TimestampedMixin): return _('%(tipo)s nº %(numero)s, de %(data)s') % { 'tipo': self.tipo_ta, 'numero': numero, - 'data': defaultfilters.date(self.data, "d \d\e F \d\e Y").lower()} + 'data': defaultfilters.date(self.data, r"d \d\e F \d\e Y").lower()} def hash(self): from django.core import serializers @@ -943,7 +943,7 @@ class Publicacao(TimestampedMixin): def __str__(self): return _('%s realizada em %s \n %s') % ( self.tipo_publicacao, - defaultfilters.date(self.data, "d \d\e F \d\e Y"), + defaultfilters.date(self.data, r"d \d\e F \d\e Y"), self.ta) diff --git a/sapl/compilacao/utils.py b/sapl/compilacao/utils.py index aa1793484..c56f618b8 100644 --- a/sapl/compilacao/utils.py +++ b/sapl/compilacao/utils.py @@ -1,5 +1,7 @@ import sys +from django.utils.translation import ugettext_lazy as _ + DISPOSITIVO_SELECT_RELATED = ( 'tipo_dispositivo', 'ta_publicado', @@ -66,3 +68,14 @@ def get_integrations_view_names(): if 'IntegracaoTaView' in str(base): result.append(v) return result + + +def valid_int(value): + if value: + try: + int(value) + except (TypeError, ValueError): + raise ValueError( + _('Valor "%(value)s" não é um número inteiro válido.') % { + 'value': value}) + return value diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 9afbe3201..93a066b3f 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -49,7 +49,7 @@ from sapl.compilacao.models import (STATUS_TA_EDITION, STATUS_TA_PRIVATE, VeiculoPublicacao, Vide) from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, DISPOSITIVO_SELECT_RELATED_EDIT, - get_integrations_view_names) + get_integrations_view_names, valid_int) from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, CrudAux, CrudListView,\ make_pagination from sapl.settings import BASE_DIR @@ -3177,14 +3177,14 @@ class DispositivoSearchFragmentFormView(ListView): result = [] try: - tipo_model = self.request.GET.get('tipo_model', '') + tipo_model = valid_int(self.request.GET.get('tipo_model', '')) limit = int(self.request.GET.get('max_results', 100)) - tipo_ta = self.request.GET.get('tipo_ta', '') + tipo_ta = valid_int(self.request.GET.get('tipo_ta', '')) num_ta = self.request.GET.get('num_ta', '') - ano_ta = self.request.GET.get('ano_ta', '') + ano_ta = valid_int(self.request.GET.get('ano_ta', '')) rotulo = self.request.GET.get('rotulo', '') str_texto = self.request.GET.get('texto', '') - texto = str_texto.split(' ') + texto_valores = str_texto.split(' ') tipo_resultado = self.request.GET.get('tipo_resultado', '') tipo_resultado = '' if tipo_resultado == 'False' else tipo_resultado @@ -3228,20 +3228,24 @@ class DispositivoSearchFragmentFormView(ListView): AND_CONTROLS = '''AND td.dispositivo_de_alteracao = true AND td.dispositivo_de_articulacao = true''' - texto = list(map("d.texto ~* '{}'".format, texto)) + texto = ['d.texto ~* %s'] * len(texto_valores) AND_TEXTO_ROTULO = '' + texto_rotulo_params = [] if str_texto and rotulo: - AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* '{BUSCA_ROTULO}') OR - ({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* '{BUSCA_ROTULO}') + busca_texto = ' AND '.join(texto) + AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* %s) OR + ({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* %s) )'''.format( - BUSCA_TEXTO=' AND '.join(texto), - BUSCA_ROTULO=rotulo + BUSCA_TEXTO=busca_texto, ) + texto_rotulo_params = ( + texto_valores + [rotulo] + texto_valores + [rotulo]) elif str_texto: - AND_TEXTO_ROTULO = ' AND %s' % ' AND '.join(texto) + AND_TEXTO_ROTULO = ' AND ' + ' AND '.join(texto) + texto_rotulo_params = texto_valores elif rotulo: - AND_TEXTO_ROTULO = "AND d.rotulo ~* '{BUSCA_ROTULO}'".format( - BUSCA_ROTULO=rotulo) + AND_TEXTO_ROTULO = "AND d.rotulo ~* %s" + texto_rotulo_params = [rotulo] else: AND_TEXTO_ROTULO = '' @@ -3288,17 +3292,21 @@ class DispositivoSearchFragmentFormView(ListView): AND3_TIPO_TA="AND ta.tipo_ta_id = {}".format( tipo_ta.id) if tipo_ta else '', - AND2_ANO="AND ta.ano = {}".format( - ano_ta) if ano_ta else '', + AND2_ANO="AND ta.ano = %s" if ano_ta else '', - AND1_NUMERO="AND ta.numero ~* '{}'".format( - num_ta) if num_ta else '', + AND1_NUMERO="AND ta.numero ~* %s" if num_ta else '', AND_TEXTO_ROTULO=AND_TEXTO_ROTULO if AND_TEXTO_ROTULO else '', AND_CONTROLS=AND_CONTROLS if AND_CONTROLS else '' ) - result = Dispositivo.objects.raw(sql) + params = texto_rotulo_params.copy() + if num_ta: + params.append(num_ta) + if ano_ta: + params.append(ano_ta) + + result = Dispositivo.objects.raw(sql, params) r = [] ids = set() diff --git a/sapl/crud/base.py b/sapl/crud/base.py index ceaef7580..0966c4bc4 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -26,7 +26,11 @@ from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.crispy_layout_mixin import SaplFormHelper from sapl.rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, RP_LIST) -from sapl.utils import normalize +from sapl.settings import RATE_LIMITER_RATE +from sapl.utils import normalize, ratelimit_ip + +from ratelimit.decorators import ratelimit +from django.utils.decorators import method_decorator logger = logging.getLogger(settings.BASE_DIR.name) @@ -101,7 +105,6 @@ variáveis do crud: class SearchMixin(models.Model): - search = models.TextField(blank=True, default='') logger = logging.getLogger(__name__) @@ -238,7 +241,7 @@ class PermissionRequiredContainerCrudMixin(PermissionRequiredMixin): @property def container_field_set(self): - if hasattr(self, 'crud') and\ + if hasattr(self, 'crud') and \ not hasattr(self.crud, 'container_field_set'): self.crud.container_field_set = '' if hasattr(self, 'crud'): @@ -267,12 +270,11 @@ class CrudBaseMixin(CrispyLayoutFormMixin): obj.public = [] if hasattr(self, 'permission_required') and self.permission_required: - self.permission_required = tuple( ( self.permission(pr) for pr in ( set(self.permission_required) - set(obj.public) - ) + ) ) ) @@ -337,9 +339,9 @@ class CrudBaseMixin(CrispyLayoutFormMixin): if not obj.DetailView.permission_required: return self.resolve_url(ACTION_DETAIL, args=(self.object.id,)) else: - return self.resolve_url(ACTION_DETAIL, args=(self.object.id,))\ + return self.resolve_url(ACTION_DETAIL, args=(self.object.id,)) \ if self.request.user.has_perm( - self.permission(RP_DETAIL)) else '' + self.permission(RP_DETAIL)) else '' @property def update_url(self): @@ -347,9 +349,9 @@ class CrudBaseMixin(CrispyLayoutFormMixin): if not obj.UpdateView.permission_required: return self.resolve_url(ACTION_UPDATE, args=(self.object.id,)) else: - return self.resolve_url(ACTION_UPDATE, args=(self.object.id,))\ + return self.resolve_url(ACTION_UPDATE, args=(self.object.id,)) \ if self.request.user.has_perm( - self.permission(RP_CHANGE)) else '' + self.permission(RP_CHANGE)) else '' @property def delete_url(self): @@ -357,9 +359,9 @@ class CrudBaseMixin(CrispyLayoutFormMixin): if not obj.DeleteView.permission_required: return self.resolve_url(ACTION_DELETE, args=(self.object.id,)) else: - return self.resolve_url(ACTION_DELETE, args=(self.object.id,))\ + return self.resolve_url(ACTION_DELETE, args=(self.object.id,)) \ if self.request.user.has_perm( - self.permission(RP_DELETE)) else '' + self.permission(RP_DELETE)) else '' @property def openapi_url(self): @@ -388,6 +390,10 @@ class CrudBaseMixin(CrispyLayoutFormMixin): return self.model._meta.verbose_name_plural +@method_decorator(ratelimit(key=ratelimit_ip, + rate=RATE_LIMITER_RATE, + block=True), + name='dispatch') class CrudListView(PermissionRequiredContainerCrudMixin, ListView): permission_required = (RP_LIST,) logger = logging.getLogger(__name__) @@ -496,7 +502,7 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView): if m: ss = get_field_display(m, n[-1])[1] ss = ( - ('
' if '