mirror of https://github.com/interlegis/sapl.git
committed by
GitHub
248 changed files with 4850 additions and 1539 deletions
@ -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 |
|
||||
@ -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] |
|
||||
@ -1,73 +1,123 @@ |
|||||
FROM python:3.9-slim-buster |
# ---------- 1) BUILDER ---------- |
||||
|
FROM python:3.12-slim-bookworm AS builder |
||||
# Setup env |
|
||||
ENV LANG C.UTF-8 |
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \ |
||||
ENV LC_ALL C.UTF-8 |
DEBIAN_FRONTEND=noninteractive \ |
||||
ENV PYTHONDONTWRITEBYTECODE 1 |
VENV_DIR=/opt/venv \ |
||||
ENV PYTHONUNBUFFERED=1 |
PIP_NO_CACHE_DIR=on |
||||
ENV DEBIAN_FRONTEND noninteractive |
|
||||
|
# Dev headers e toolchain só no builder |
||||
ENV BUILD_PACKAGES apt-utils apt-file libpq-dev graphviz-dev build-essential git pkg-config \ |
RUN apt-get update && apt-get install -y --no-install-recommends \ |
||||
python3-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \ |
build-essential git pkg-config \ |
||||
libcairo2-dev software-properties-common python3-setuptools python3-pip |
libpq-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \ |
||||
|
libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev libharfbuzz-dev \ |
||||
## NAO EH PRA TIRAR O vim DA LISTA DE COMANDOS INSTALADOS!!! |
libfreetype6-dev zlib1g-dev \ |
||||
ENV RUN_PACKAGES graphviz python3-lxml python3-magic postgresql-client python3-psycopg2 \ |
&& rm -rf /var/lib/apt/lists/* |
||||
poppler-utils curl jq bash vim python3-venv tzdata nodejs \ |
|
||||
fontconfig ttf-dejavu python nginx |
# Venv independente do sistema |
||||
|
RUN python -m venv "${VENV_DIR}" \ |
||||
RUN mkdir -p /var/interlegis/sapl |
&& "${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/ |
WORKDIR /var/interlegis/sapl/ |
||||
|
|
||||
ADD . /var/interlegis/sapl/ |
# Traga o venv pré-instalado |
||||
|
COPY --from=builder ${VENV_DIR} ${VENV_DIR} |
||||
RUN apt-get update && \ |
|
||||
apt-get upgrade -y && \ |
# Código da aplicação (depois do venv para aproveitar cache) |
||||
apt-get install -y --no-install-recommends $BUILD_PACKAGES $RUN_PACKAGES && \ |
COPY . /var/interlegis/sapl/ |
||||
fc-cache -fv && \ |
|
||||
pip3 install --no-cache-dir --upgrade pip setuptools && \ |
# Nginx (somente se instalado) |
||||
rm -f /etc/nginx/conf.d/* && \ |
RUN if [ "$WITH_NGINX" = "1" ]; then \ |
||||
pip install --no-cache-dir -r /var/interlegis/sapl/requirements/dev-requirements.txt --upgrade setuptools && \ |
rm -f /etc/nginx/conf.d/*; \ |
||||
SUDO_FORCE_REMOVE=yes apt-get purge -y --auto-remove $BUILD_PACKAGES && \ |
cp docker/config/nginx/sapl.conf /etc/nginx/conf.d/sapl.conf; \ |
||||
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* |
cp docker/config/nginx/nginx.conf /etc/nginx/nginx.conf; \ |
||||
|
fi |
||||
WORKDIR /var/interlegis/sapl/ |
|
||||
ADD . /var/interlegis/sapl/ |
# Scripts + gunicorn.conf no diretório da app |
||||
|
RUN install -m 755 docker/startup_scripts/start.sh /var/interlegis/sapl/start.sh \ |
||||
COPY docker/start.sh $HOME |
&& install -m 755 docker/startup_scripts/wait-for-pg.sh /var/interlegis/sapl/wait-for-pg.sh \ |
||||
COPY docker/solr_cli.py $HOME |
&& install -m 755 docker/startup_scripts/wait-for-solr.sh /var/interlegis/sapl/wait-for-solr.sh \ |
||||
COPY docker/wait-for-pg.sh $HOME |
&& install -m 644 docker/startup_scripts/solr_cli.py /var/interlegis/sapl/solr_cli.py \ |
||||
COPY docker/wait-for-solr.sh $HOME |
&& install -m 644 docker/startup_scripts/create_admin.py /var/interlegis/sapl/create_admin.py \ |
||||
COPY docker/create_admin.py $HOME |
&& install -m 644 docker/startup_scripts/genkey.py /var/interlegis/sapl/genkey.py \ |
||||
COPY docker/genkey.py $HOME |
&& install -m 644 docker/startup_scripts/gunicorn.conf.py /var/interlegis/sapl/gunicorn.conf.py |
||||
COPY docker/gunicorn_start.sh $HOME |
|
||||
|
# (Se possível, evite copiar .env no build. Use secrets/variáveis em runtime.) |
||||
COPY docker/config/nginx/sapl.conf /etc/nginx/conf.d |
|
||||
COPY docker/config/nginx/nginx.conf /etc/nginx/nginx.conf |
|
||||
COPY docker/config/env_dockerfile /var/interlegis/sapl/sapl/.env |
COPY docker/config/env_dockerfile /var/interlegis/sapl/sapl/.env |
||||
|
|
||||
RUN python3 manage.py collectstatic --noinput --clear |
# Logs (só se nginx estiver presente) |
||||
|
RUN if [ "$WITH_NGINX" = "1" ]; then \ |
||||
# Remove .env(fake) e sapl.db da imagem |
ln -sf /dev/stdout /var/log/nginx/access.log; \ |
||||
RUN rm -rf /var/interlegis/sapl/sapl/.env && \ |
ln -sf /dev/stderr /var/log/nginx/error.log; \ |
||||
rm -rf /var/interlegis/sapl/sapl.db |
fi \ |
||||
|
&& mkdir -p /var/log/sapl/ \ |
||||
RUN chmod +x /var/interlegis/sapl/start.sh && \ |
&& ln -sf /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log |
||||
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 |
|
||||
|
|
||||
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/"] |
VOLUME ["/var/interlegis/sapl/data", "/var/interlegis/sapl/media", "/var/log/sapl/"] |
||||
|
|
||||
|
ENTRYPOINT ["/usr/bin/tini","--"] |
||||
CMD ["/var/interlegis/sapl/start.sh"] |
CMD ["/var/interlegis/sapl/start.sh"] |
||||
|
|||||
@ -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 |
|
||||
@ -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 |
||||
|
|
||||
@ -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 |
||||
|
|
||||
|
|
||||
@ -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;" |
|
||||
@ -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 |
||||
@ -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 "$@" |
||||
File diff suppressed because it is too large
@ -1,11 +1,11 @@ |
|||||
-r test-requirements.txt |
-r test-requirements.txt |
||||
|
|
||||
autopep8==1.2.4 |
autopep8==2.3.2 |
||||
beautifulsoup4==4.9.1 |
beautifulsoup4==4.13.5 |
||||
django-debug-toolbar==1.11.1 |
django-debug-toolbar==3.2.4 |
||||
ipdb==0.13.3 |
ipdb==0.13.13 |
||||
fancycompleter==0.9.1 |
fancycompleter==0.11.1 |
||||
pdbpp==0.10.3 |
pdbpp==0.11.7 |
||||
pip-review==0.4 |
pip-review==1.3.0 |
||||
pipdeptree==0.10.1 |
pipdeptree==2.28.0 |
||||
pydevd-pycharm~=203.7148.7 |
pydevd-pycharm~=203.7148.7 |
||||
|
|||||
@ -1,11 +1,10 @@ |
|||||
-r requirements.txt |
-r requirements.txt |
||||
coverage==4.4 |
coverage==7.6.1 |
||||
django-webtest==1.9.7 |
django-webtest==1.9.8 |
||||
flake8==2.6.2 |
flake8==7.1.1 |
||||
isort==4.2.5 |
isort==5.13.2 |
||||
model-bakery==1.1.0 |
model-bakery==1.5.0 |
||||
pep8==1.7.0 |
pycodestyle==2.12.1 |
||||
pytest==5.4.3 |
pytest==8.3.3 |
||||
pytest-cov==2.10.0 |
pytest-cov==5.0.0 |
||||
pytest-django==3.8.0 |
WebTest==3.0.6 |
||||
webtest==2.0.21 |
|
||||
|
|||||
@ -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!" |
||||
@ -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'), |
||||
|
), |
||||
|
] |
||||
@ -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'), |
||||
|
), |
||||
|
] |
||||
@ -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'), |
||||
|
), |
||||
|
] |
||||
@ -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'), |
||||
|
), |
||||
|
] |
||||
@ -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'), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,36 @@ |
|||||
|
# core/health.py |
||||
|
import logging |
||||
|
import time |
||||
|
from typing import Tuple, Optional |
||||
|
from django.db import connection |
||||
|
from django.core.cache import cache |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
def check_app() -> Tuple[bool, Optional[str], float]: |
||||
|
t0 = time.monotonic() |
||||
|
return True, None, (time.monotonic() - t0) * 1000 |
||||
|
|
||||
|
|
||||
|
def check_db() -> Tuple[bool, Optional[str], float]: |
||||
|
t0 = time.monotonic() |
||||
|
try: |
||||
|
with connection.cursor() as cur: |
||||
|
cur.execute("SELECT 1") |
||||
|
cur.fetchone() |
||||
|
return True, None, (time.monotonic() - t0) * 1000 |
||||
|
except Exception as e: |
||||
|
logging.error(e) |
||||
|
return False, "An internal error has occurred!", (time.monotonic() - t0) * 1000 |
||||
|
|
||||
|
|
||||
|
def check_cache() -> Tuple[bool, Optional[str], float]: |
||||
|
t0 = time.monotonic() |
||||
|
try: |
||||
|
cache.set("_hc", "1", 5) |
||||
|
ok = cache.get("_hc") == "1" |
||||
|
return ok, None if ok else "Cache get/set failed", (time.monotonic() - t0) * 1000 |
||||
|
except Exception as e: |
||||
|
logging.error(e) |
||||
|
return False, "An internal error has occurred!", (time.monotonic() - t0) * 1000 |
||||
@ -0,0 +1,21 @@ |
|||||
|
# sapl/logging/filters.py |
||||
|
import logging |
||||
|
import contextvars |
||||
|
|
||||
|
_request_id = contextvars.ContextVar("request_id", default="-") |
||||
|
|
||||
|
|
||||
|
def set_request_id(value: str): |
||||
|
_request_id.set(value) |
||||
|
|
||||
|
|
||||
|
def get_request_id() -> str: |
||||
|
return _request_id.get() |
||||
|
|
||||
|
|
||||
|
class RequestIdFilter(logging.Filter): |
||||
|
def filter(self, record: logging.LogRecord) -> bool: |
||||
|
# garante que SEMPRE existe |
||||
|
if not hasattr(record, "request_id"): |
||||
|
record.request_id = get_request_id() |
||||
|
return True |
||||
@ -0,0 +1,385 @@ |
|||||
|
from datetime import date |
||||
|
|
||||
|
import pytest |
||||
|
from django.db import IntegrityError, transaction |
||||
|
from model_bakery import baker |
||||
|
from rest_framework.test import APIClient |
||||
|
|
||||
|
from sapl.base.models import AppConfig as BaseAppConfig |
||||
|
from sapl.materia.models import (MateriaLegislativa, RegimeTramitacao, |
||||
|
TipoMateriaLegislativa) |
||||
|
from sapl.parlamentares.models import Legislatura |
||||
|
|
||||
|
|
||||
|
# --------------------------------------------------------------------------- |
||||
|
# Helpers / fixtures |
||||
|
# --------------------------------------------------------------------------- |
||||
|
|
||||
|
@pytest.fixture |
||||
|
def tipo_materia(db): |
||||
|
"""TipoMateriaLegislativa sem sequencia_numeracao própria (usa global).""" |
||||
|
return baker.make(TipoMateriaLegislativa, descricao='Projeto de Lei', |
||||
|
sigla='PL', sequencia_numeracao='') |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def tipo_materia_anual(db): |
||||
|
return baker.make(TipoMateriaLegislativa, descricao='Requerimento', |
||||
|
sigla='REQ', sequencia_numeracao='A') |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def tipo_materia_unico(db): |
||||
|
return baker.make(TipoMateriaLegislativa, descricao='Indicação', |
||||
|
sigla='IND', sequencia_numeracao='U') |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def tipo_materia_legislatura(db): |
||||
|
return baker.make(TipoMateriaLegislativa, descricao='PEC', |
||||
|
sigla='PEC', sequencia_numeracao='L') |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def regime(db): |
||||
|
return baker.make(RegimeTramitacao, descricao='Normal') |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def app_config(db): |
||||
|
return BaseAppConfig.objects.create(sequencia_numeracao_protocolo='A') |
||||
|
|
||||
|
|
||||
|
def _criar_materia(tipo, numero, ano, regime, **kwargs): |
||||
|
"""Atalho para criar MateriaLegislativa com campos obrigatórios.""" |
||||
|
defaults = dict( |
||||
|
tipo=tipo, |
||||
|
numero=numero, |
||||
|
ano=ano, |
||||
|
data_apresentacao=date(ano, 1, 1), |
||||
|
regime_tramitacao=regime, |
||||
|
ementa='Ementa de teste', |
||||
|
) |
||||
|
defaults.update(kwargs) |
||||
|
return MateriaLegislativa.objects.create(**defaults) |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de get_proximo_numero – numeração por ano (padrão) |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_proximo_numero_primeiro_do_ano(tipo_materia_anual, regime, app_config): |
||||
|
"""Sem matérias existentes, o primeiro número gerado deve ser 1.""" |
||||
|
with transaction.atomic(): |
||||
|
numero, ano = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual, ano=2024) |
||||
|
assert numero == 1 |
||||
|
assert ano == 2024 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_proximo_numero_sequencial(tipo_materia_anual, regime, app_config): |
||||
|
"""Com matérias existentes 1, 2, 3 → próximo deve ser 4.""" |
||||
|
for n in (1, 2, 3): |
||||
|
_criar_materia(tipo_materia_anual, n, 2024, regime) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, ano = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual, ano=2024) |
||||
|
assert numero == 4 |
||||
|
assert ano == 2024 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_proximo_numero_ignora_outro_ano(tipo_materia_anual, regime, app_config): |
||||
|
"""Matérias em anos diferentes não influenciam a sequência.""" |
||||
|
_criar_materia(tipo_materia_anual, 10, 2023, regime, |
||||
|
data_apresentacao=date(2023, 6, 1)) |
||||
|
_criar_materia(tipo_materia_anual, 1, 2024, regime) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, ano = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual, ano=2024) |
||||
|
assert numero == 2 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_proximo_numero_ignora_outro_tipo(tipo_materia_anual, regime, app_config): |
||||
|
"""Matérias de outro tipo não influenciam a sequência.""" |
||||
|
outro_tipo = baker.make(TipoMateriaLegislativa, descricao='Moção', |
||||
|
sigla='MOC', sequencia_numeracao='A') |
||||
|
_criar_materia(outro_tipo, 50, 2024, regime) |
||||
|
_criar_materia(tipo_materia_anual, 3, 2024, regime) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual, ano=2024) |
||||
|
assert numero == 4 |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de get_proximo_numero – numeração única |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_proximo_numero_unico(tipo_materia_unico, regime, app_config): |
||||
|
"""Numeração 'U' ignora o ano – sequência é global por tipo.""" |
||||
|
_criar_materia(tipo_materia_unico, 1, 2022, regime, |
||||
|
data_apresentacao=date(2022, 1, 1)) |
||||
|
_criar_materia(tipo_materia_unico, 2, 2023, regime, |
||||
|
data_apresentacao=date(2023, 1, 1)) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_unico, ano=2024) |
||||
|
assert numero == 3 |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de get_proximo_numero – numeração por legislatura |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_proximo_numero_por_legislatura(tipo_materia_legislatura, regime, |
||||
|
app_config): |
||||
|
"""Numeração 'L' conta apenas matérias dentro da legislatura vigente.""" |
||||
|
baker.make(Legislatura, numero=1, |
||||
|
data_inicio=date(2021, 1, 1), |
||||
|
data_fim=date(2024, 12, 31), |
||||
|
data_eleicao=date(2020, 11, 15)) |
||||
|
|
||||
|
_criar_materia(tipo_materia_legislatura, 1, 2022, regime, |
||||
|
data_apresentacao=date(2022, 3, 1)) |
||||
|
_criar_materia(tipo_materia_legislatura, 2, 2023, regime, |
||||
|
data_apresentacao=date(2023, 5, 1)) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_legislatura, ano=2024) |
||||
|
assert numero == 3 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_proximo_numero_legislatura_inexistente(tipo_materia_legislatura, |
||||
|
regime, app_config): |
||||
|
"""Sem legislatura vigente, número inicia em 1.""" |
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_legislatura, ano=2050) |
||||
|
assert numero == 1 |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de get_proximo_numero – numero_candidato |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_numero_candidato_disponivel(tipo_materia_anual, regime, app_config): |
||||
|
"""Se o número candidato está disponível, deve ser retornado.""" |
||||
|
_criar_materia(tipo_materia_anual, 1, 2024, regime) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual, ano=2024, numero_candidato=5) |
||||
|
assert numero == 5 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_numero_candidato_em_uso_retorna_sequencial(tipo_materia_anual, |
||||
|
regime, app_config): |
||||
|
"""Se o número candidato já existe, retorna o sequencial.""" |
||||
|
_criar_materia(tipo_materia_anual, 5, 2024, regime) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual, ano=2024, numero_candidato=5) |
||||
|
assert numero == 6 |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de get_proximo_numero – resolução de tipo por pk (int/str) |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_tipo_passado_como_int(tipo_materia_anual, app_config): |
||||
|
"""O tipo pode ser passado como int (pk).""" |
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual.pk, ano=2024) |
||||
|
assert numero == 1 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_tipo_passado_como_str(tipo_materia_anual, app_config): |
||||
|
"""O tipo pode ser passado como str representando o pk.""" |
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=str(tipo_materia_anual.pk), ano=2024) |
||||
|
assert numero == 1 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_tipo_inexistente_levanta_excecao(app_config): |
||||
|
"""Pk inexistente deve levantar DoesNotExist.""" |
||||
|
with pytest.raises(TipoMateriaLegislativa.DoesNotExist): |
||||
|
with transaction.atomic(): |
||||
|
MateriaLegislativa.get_proximo_numero(tipo=99999, ano=2024) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_tipo_invalido_levanta_validation_error(app_config): |
||||
|
"""Tipo não conversível para int deve levantar ValidationError.""" |
||||
|
from django.core.exceptions import ValidationError |
||||
|
with pytest.raises(ValidationError): |
||||
|
with transaction.atomic(): |
||||
|
MateriaLegislativa.get_proximo_numero(tipo='abc', ano=2024) |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de get_proximo_numero – ano padrão |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_ano_none_usa_ano_atual(tipo_materia_anual, app_config): |
||||
|
"""Se ano=None, deve usar o ano corrente.""" |
||||
|
from django.utils import timezone |
||||
|
with transaction.atomic(): |
||||
|
_, ano = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia_anual, ano=None) |
||||
|
assert ano == timezone.now().year |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de get_proximo_numero – select_for_update (proteção concorrência) |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_unique_together_protege_duplicata(tipo_materia_anual, regime, |
||||
|
app_config): |
||||
|
"""O unique_together (tipo, numero, ano) impede duplicatas no banco.""" |
||||
|
_criar_materia(tipo_materia_anual, 1, 2024, regime) |
||||
|
with pytest.raises(IntegrityError): |
||||
|
with transaction.atomic(): |
||||
|
_criar_materia(tipo_materia_anual, 1, 2024, regime) |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes de global config – tipo sem sequencia_numeracao usa global |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_tipo_sem_sequencia_usa_config_global(tipo_materia, regime, app_config): |
||||
|
"""Tipo com sequencia_numeracao vazio deve usar a config global ('A').""" |
||||
|
_criar_materia(tipo_materia, 1, 2024, regime) |
||||
|
_criar_materia(tipo_materia, 2, 2024, regime) |
||||
|
_criar_materia(tipo_materia, 10, 2023, regime, |
||||
|
data_apresentacao=date(2023, 1, 1)) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
numero, _ = MateriaLegislativa.get_proximo_numero( |
||||
|
tipo=tipo_materia, ano=2024) |
||||
|
# global default 'A' → sequencial por ano → MAX(2024) = 2 → próximo = 3 |
||||
|
assert numero == 3 |
||||
|
|
||||
|
|
||||
|
# =========================================================================== |
||||
|
# Testes do endpoint API create (MateriaLegislativaViewSet) |
||||
|
# =========================================================================== |
||||
|
|
||||
|
@pytest.fixture |
||||
|
def api_client_autenticado(db): |
||||
|
"""APIClient autenticado como superusuário (tem todas as permissões).""" |
||||
|
from django.contrib.auth import get_user_model |
||||
|
User = get_user_model() |
||||
|
user = User.objects.create_superuser( |
||||
|
username='admin_api', password='secret123', email='a@b.com') |
||||
|
client = APIClient() |
||||
|
client.force_authenticate(user=user) |
||||
|
return client |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_api_create_sem_numero_auto_gera(api_client_autenticado, |
||||
|
tipo_materia_anual, regime, |
||||
|
app_config): |
||||
|
"""POST sem 'numero' deve auto-gerar próximo sequencial.""" |
||||
|
_criar_materia(tipo_materia_anual, 1, 2024, regime) |
||||
|
|
||||
|
response = api_client_autenticado.post( |
||||
|
'/api/materia/materialegislativa/', |
||||
|
{ |
||||
|
'tipo': tipo_materia_anual.pk, |
||||
|
'ano': 2024, |
||||
|
'data_apresentacao': '2024-06-01', |
||||
|
'regime_tramitacao': regime.pk, |
||||
|
'ementa': 'Teste auto-gerar número', |
||||
|
}, |
||||
|
format='json', |
||||
|
) |
||||
|
assert response.status_code == 201 |
||||
|
assert response.data['numero'] == 2 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_api_create_com_numero_respeita_dado(api_client_autenticado, |
||||
|
tipo_materia_anual, regime, |
||||
|
app_config): |
||||
|
"""POST com 'numero' explícito deve criar com o número informado.""" |
||||
|
response = api_client_autenticado.post( |
||||
|
'/api/materia/materialegislativa/', |
||||
|
{ |
||||
|
'tipo': tipo_materia_anual.pk, |
||||
|
'numero': 42, |
||||
|
'ano': 2024, |
||||
|
'data_apresentacao': '2024-06-01', |
||||
|
'regime_tramitacao': regime.pk, |
||||
|
'ementa': 'Número exato', |
||||
|
}, |
||||
|
format='json', |
||||
|
) |
||||
|
assert response.status_code == 201 |
||||
|
assert response.data['numero'] == 42 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_api_create_numero_duplicado_retorna_erro(api_client_autenticado, |
||||
|
tipo_materia_anual, regime, |
||||
|
app_config): |
||||
|
"""POST com número já existente retorna erro. |
||||
|
|
||||
|
O DRF detecta a violação de unique_together via UniqueTogetherValidator |
||||
|
no serializer e retorna 400 antes de chegar ao banco. |
||||
|
""" |
||||
|
_criar_materia(tipo_materia_anual, 42, 2024, regime) |
||||
|
|
||||
|
response = api_client_autenticado.post( |
||||
|
'/api/materia/materialegislativa/', |
||||
|
{ |
||||
|
'tipo': tipo_materia_anual.pk, |
||||
|
'numero': 42, |
||||
|
'ano': 2024, |
||||
|
'data_apresentacao': '2024-06-01', |
||||
|
'regime_tramitacao': regime.pk, |
||||
|
'ementa': 'Duplicata', |
||||
|
}, |
||||
|
format='json', |
||||
|
) |
||||
|
assert response.status_code == 400 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_api_create_sem_tipo_valida_serializer(api_client_autenticado, regime): |
||||
|
"""POST sem 'tipo' deve falhar na validação do serializer (400).""" |
||||
|
response = api_client_autenticado.post( |
||||
|
'/api/materia/materialegislativa/', |
||||
|
{ |
||||
|
'numero': 1, |
||||
|
'ano': 2024, |
||||
|
'data_apresentacao': '2024-06-01', |
||||
|
'regime_tramitacao': regime.pk, |
||||
|
'ementa': 'Sem tipo', |
||||
|
}, |
||||
|
format='json', |
||||
|
) |
||||
|
assert response.status_code == 400 |
||||
@ -0,0 +1,12 @@ |
|||||
|
from prometheus_client import CollectorRegistry, Gauge, generate_latest, CONTENT_TYPE_LATEST |
||||
|
|
||||
|
|
||||
|
def health_registry(check_results: dict) -> bytes: |
||||
|
""" |
||||
|
check_results: {"app": True/False, "db": True/False, ...} |
||||
|
""" |
||||
|
reg = CollectorRegistry() |
||||
|
g = Gauge("app_health", "1 if healthy, 0 if unhealthy", ["component"], registry=reg) |
||||
|
for comp, ok in check_results.items(): |
||||
|
g.labels(component=comp).set(1 if ok else 0) |
||||
|
return generate_latest(reg) |
||||
@ -0,0 +1,24 @@ |
|||||
|
import uuid |
||||
|
from sapl.logging.filters import set_request_id |
||||
|
|
||||
|
HEADER_NAME = "HTTP_X_REQUEST_ID" |
||||
|
RESPONSE_HEADER = "X-Request-ID" |
||||
|
|
||||
|
|
||||
|
def _new_id(): |
||||
|
return uuid.uuid4().hex |
||||
|
|
||||
|
|
||||
|
class RequestIdMiddleware: |
||||
|
def __init__(self, get_response): |
||||
|
self.get_response = get_response |
||||
|
|
||||
|
def __call__(self, request): |
||||
|
# recebe `request_id` do nginx ou do cliente senão cria um |
||||
|
request_id = request.META.get(HEADER_NAME) or _new_id() |
||||
|
request_id = str(request_id)[:64] |
||||
|
request.request_id = request_id |
||||
|
set_request_id(request_id) |
||||
|
response = self.get_response(request) |
||||
|
response[RESPONSE_HEADER] = request_id |
||||
|
return response |
||||
@ -0,0 +1,23 @@ |
|||||
|
# Generated by Django 2.2.28 on 2025-12-01 18:31 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('parlamentares', '0044_adiciona_cargos_mesa'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterField( |
||||
|
model_name='parlamentar', |
||||
|
name='nome_completo', |
||||
|
field=models.CharField(max_length=80, verbose_name='Nome Completo'), |
||||
|
), |
||||
|
migrations.AlterField( |
||||
|
model_name='parlamentar', |
||||
|
name='nome_parlamentar', |
||||
|
field=models.CharField(max_length=80, verbose_name='Nome Parlamentar'), |
||||
|
), |
||||
|
] |
||||
@ -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 = [ |
||||
|
('parlamentares', '0045_auto_20251201_1531'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='parlamentar', |
||||
|
name='data_ultima_atualizacao', |
||||
|
field=models.DateTimeField(auto_now=True, null=True, verbose_name='Data da Última Atualização'), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,28 @@ |
|||||
|
# Generated by Django 2.2.28 on 2026-06-26 15:20 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('protocoloadm', '0045_auto_20240711_1405'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='documentoadministrativo', |
||||
|
name='data_ultima_atualizacao', |
||||
|
field=models.DateTimeField(auto_now=True, null=True, verbose_name='Data da Última Atualização'), |
||||
|
), |
||||
|
migrations.AlterField( |
||||
|
model_name='documentoadministrativo', |
||||
|
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'), |
||||
|
), |
||||
|
migrations.AlterField( |
||||
|
model_name='protocolo', |
||||
|
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 do Protocolo'), |
||||
|
), |
||||
|
] |
||||
@ -1,3 +1,52 @@ |
|||||
# from django.test import TestCase |
import pytest |
||||
|
from model_bakery import baker |
||||
|
|
||||
# Create your tests here. |
from sapl.base.models import CasaLegislativa |
||||
|
from sapl.relatorios.views import get_sessao_plenaria |
||||
|
from sapl.sessao.models import ExpedienteSessao, SessaoPlenaria, TipoExpediente |
||||
|
from sapl.sessao.views import get_expedientes |
||||
|
|
||||
|
|
||||
|
def cria_sessao_com_expedientes(): |
||||
|
"""Cria uma sessão cujos expedientes têm `ordenacao` inversa à ordem |
||||
|
alfabética dos nomes -- o caso que expõe o OSTicket #125461.""" |
||||
|
sessao = baker.make(SessaoPlenaria) |
||||
|
tipo_leitura = baker.make( |
||||
|
TipoExpediente, nome='Leitura e Aprovação da Ata', ordenacao=1) |
||||
|
tipo_grande = baker.make( |
||||
|
TipoExpediente, nome='Grande Expediente', ordenacao=2) |
||||
|
|
||||
|
for tipo in (tipo_grande, tipo_leitura): |
||||
|
baker.make(ExpedienteSessao, sessao_plenaria=sessao, tipo=tipo, |
||||
|
conteudo='<p>Conteúdo de {}.</p>'.format(tipo.nome)) |
||||
|
|
||||
|
return sessao |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_relatorio_sessao_respeita_ordenacao_do_tipo_expediente(): |
||||
|
"""O PDF da Sessão Plenária deve seguir o campo `ordenacao` de |
||||
|
TipoExpediente, e não a ordem alfabética do nome.""" |
||||
|
sessao = cria_sessao_com_expedientes() |
||||
|
casa = baker.make(CasaLegislativa) |
||||
|
user = baker.make('auth.User') |
||||
|
|
||||
|
lst_expedientes = get_sessao_plenaria(sessao, casa, user)[6] |
||||
|
nomes = [e['nom_expediente'] for e in lst_expedientes] |
||||
|
|
||||
|
assert nomes == ['Leitura e Aprovação da Ata', 'Grande Expediente'] |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_relatorio_sessao_tem_mesma_ordem_de_expedientes_do_resumo(): |
||||
|
"""O PDF e o Resumo exibido em tela não podem divergir: foi essa |
||||
|
divergência que originou o OSTicket #125461.""" |
||||
|
sessao = cria_sessao_com_expedientes() |
||||
|
casa = baker.make(CasaLegislativa) |
||||
|
user = baker.make('auth.User') |
||||
|
|
||||
|
do_pdf = [e['nom_expediente'] |
||||
|
for e in get_sessao_plenaria(sessao, casa, user)[6]] |
||||
|
da_tela = [e['tipo'].nome for e in get_expedientes(sessao)['expedientes']] |
||||
|
|
||||
|
assert do_pdf == da_tela |
||||
|
|||||
@ -0,0 +1,40 @@ |
|||||
|
# Generated by Django 2.2.28 on 2026-08-05 16:00 |
||||
|
|
||||
|
from django.db import migrations |
||||
|
|
||||
|
|
||||
|
# Mantém uma única presença por (sessão, parlamentar). Presença é um sim/não, |
||||
|
# então as linhas repetidas não carregam informação adicional: preserva-se a |
||||
|
# de menor id e descartam-se as demais. |
||||
|
REMOVE_DUPLICADAS_SQL = """ |
||||
|
DELETE FROM {tabela} a |
||||
|
USING {tabela} b |
||||
|
WHERE a.sessao_plenaria_id = b.sessao_plenaria_id |
||||
|
AND a.parlamentar_id = b.parlamentar_id |
||||
|
AND a.id > b.id; |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('sessao', '0069_auto_20220919_1705'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.RunSQL( |
||||
|
REMOVE_DUPLICADAS_SQL.format( |
||||
|
tabela='sessao_sessaoplenariapresenca'), |
||||
|
migrations.RunSQL.noop), |
||||
|
migrations.RunSQL( |
||||
|
REMOVE_DUPLICADAS_SQL.format(tabela='sessao_presencaordemdia'), |
||||
|
migrations.RunSQL.noop), |
||||
|
migrations.AlterUniqueTogether( |
||||
|
name='sessaoplenariapresenca', |
||||
|
unique_together={('sessao_plenaria', 'parlamentar')}, |
||||
|
), |
||||
|
migrations.AlterUniqueTogether( |
||||
|
name='presencaordemdia', |
||||
|
unique_together={('sessao_plenaria', 'parlamentar')}, |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"interval": 300, |
||||
|
"requests-per-second": 5, |
||||
|
"burst": 10 |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue