mirror of https://github.com/interlegis/sapl.git
31 changed files with 467 additions and 313 deletions
@ -1,145 +0,0 @@ |
|||||
# CLAUDE.md |
|
||||
|
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
|
||||
|
|
||||
## Project Overview |
|
||||
|
|
||||
SAPL (Sistema de Apoio ao Processo Legislativo) is a Django-based legislative management system used by Brazilian municipal and state legislative houses. It manages bills, parliamentary sessions, committees, norms, protocols, and related legislative workflows. |
|
||||
|
|
||||
## Commands |
|
||||
|
|
||||
### Development |
|
||||
|
|
||||
```bash |
|
||||
# Run dev server |
|
||||
python manage.py runserver |
|
||||
|
|
||||
# Docker (dev, without bundled DB) |
|
||||
docker-compose -f docker/docker-compose-dev.yml up |
|
||||
|
|
||||
# Docker (dev, with PostgreSQL container) |
|
||||
docker-compose -f docker/docker-compose-dev-db.yml up |
|
||||
``` |
|
||||
|
|
||||
### Database Setup (local PostgreSQL) |
|
||||
|
|
||||
```bash |
|
||||
sudo -u postgres psql -c "CREATE ROLE sapl LOGIN ENCRYPTED PASSWORD 'sapl' NOSUPERUSER INHERIT CREATEDB NOCREATEROLE NOREPLICATION;" |
|
||||
sudo -u postgres psql -c "CREATE DATABASE sapl WITH OWNER=sapl ENCODING='UTF8' LC_COLLATE='pt_BR.UTF-8' LC_CTYPE='pt_BR.UTF-8' CONNECTION LIMIT=-1 TEMPLATE template0;" |
|
||||
python manage.py migrate |
|
||||
``` |
|
||||
|
|
||||
### Testing |
|
||||
|
|
||||
```bash |
|
||||
# All tests (reuses DB by default for speed) |
|
||||
pytest |
|
||||
|
|
||||
# Single test file or test function |
|
||||
pytest sapl/materia/tests/test_materia.py |
|
||||
pytest sapl/materia/tests/test_materia.py::test_function_name |
|
||||
|
|
||||
# Force DB recreation |
|
||||
pytest --create-db |
|
||||
|
|
||||
# With coverage |
|
||||
pytest --cov=sapl |
|
||||
``` |
|
||||
|
|
||||
Tests require `DJANGO_SETTINGS_MODULE=sapl.settings` (set in `pytest.ini`). All tests must be marked with `@pytest.mark.django_db`. The `conftest.py` root fixture provides an `app` fixture (WebTest `DjangoTestApp`). |
|
||||
|
|
||||
### Linting / Formatting |
|
||||
|
|
||||
```bash |
|
||||
flake8 . |
|
||||
isort . |
|
||||
autopep8 --in-place <file.py> |
|
||||
``` |
|
||||
|
|
||||
### Restore Database from Backup |
|
||||
|
|
||||
```bash |
|
||||
./scripts/restore_db.sh -f /path/to/dump |
|
||||
./scripts/restore_db.sh -f /path/to/dump -p 5433 # Docker port |
|
||||
``` |
|
||||
|
|
||||
## Architecture |
|
||||
|
|
||||
### Django Apps |
|
||||
|
|
||||
Apps are under `sapl/` and follow domain boundaries: |
|
||||
|
|
||||
| App | Domain | |
|
||||
|-----|--------| |
|
||||
| `base` | `CasaLegislativa` (legislative house config), `AppConfig`, `Autor` (authorship) | |
|
||||
| `parliamentary` | `Parlamentar`, `Legislatura`, `SessaoLegislativa`, `Coligacao` | |
|
||||
| `materia` | Bills (`MateriaLegislativa`), types, tracking, annexes | |
|
||||
| `norma` | Laws/norms (`NormaJuridica`) and hierarchies | |
|
||||
| `sessao` | Plenary sessions, agenda, attendance, voting | |
|
||||
| `comissoes` | Committees (`Comissao`) and meetings (`Reuniao`) | |
|
||||
| `protocoloadm` | Administrative protocols and document intake | |
|
||||
| `compilacao` | Structured/articulated texts (LexML-like tree structure) | |
|
||||
| `lexml` | LexML XML standard integration | |
|
||||
| `audiencia` | Public hearings | |
|
||||
| `painel` | Real-time session display panel | |
|
||||
| `relatorios` | PDF report generation | |
|
||||
| `api` | REST API entry point (auto-generated ViewSets) | |
|
||||
| `crud` | Generic CRUD base views | |
|
||||
| `rules` | Business rules and permission definitions | |
|
||||
|
|
||||
### REST API |
|
||||
|
|
||||
The API uses a custom `drfautoapi` package (`drfautoapi/drfautoapi.py`) that auto-generates DRF ViewSets, Serializers, and FilterSets from Django models. Authentication is Token + Session. Permissions use a custom `SaplModelPermissions` class that maps HTTP methods to Django model permissions. |
|
||||
|
|
||||
OpenAPI 3.0 docs are generated by drf-spectacular. |
|
||||
|
|
||||
### Caching |
|
||||
|
|
||||
- **Default:** File-based (`/var/tmp/django_cache`) |
|
||||
- **Production:** Redis via django-redis; configured at startup by `configure_redis_cache()` in `sapl/settings.py` |
|
||||
- **Cache key prefix:** `cache:{POD_NAMESPACE}:` (namespace-isolated for multi-tenant k8s) |
|
||||
- **Rate limiter state** is shared via Redis keys |
|
||||
|
|
||||
### Feature Flags |
|
||||
|
|
||||
django-waffle is used for feature flags. Switches (global on/off) can be toggled via: |
|
||||
|
|
||||
```bash |
|
||||
python manage.py waffle_switch <switch_name> on|off |
|
||||
``` |
|
||||
|
|
||||
### Key Environment Variables |
|
||||
|
|
||||
| Variable | Purpose | |
|
||||
|----------|---------| |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | |
|
||||
| `SECRET_KEY` | Django secret key | |
|
||||
| `DEBUG` | Debug mode | |
|
||||
| `REDIS_URL` | Redis host:port | |
|
||||
| `CACHE_BACKEND` | `file` or `redis` | |
|
||||
| `POD_NAMESPACE` | K8s namespace (used in cache key prefix) | |
|
||||
| `USE_SOLR` | Enable Haystack/Solr full-text search | |
|
||||
| `SOLR_URL` / `SOLR_COLLECTION` | Solr connection | |
|
||||
|
|
||||
### Docker Build |
|
||||
|
|
||||
The production build requires a MaxMind GeoLite2-ASN license key (for nginx ASN-based bot blocking): |
|
||||
|
|
||||
```bash |
|
||||
docker build --secret id=maxmind_key,src=.env -f docker/Dockerfile -t sapl:local . |
|
||||
``` |
|
||||
|
|
||||
Optional build args: `WITH_NGINX`, `WITH_GRAPHVIZ`, `WITH_POPPLER`, `WITH_PSQL_CLIENT`. |
|
||||
|
|
||||
### Key File Locations |
|
||||
|
|
||||
| File | Purpose | |
|
||||
|------|---------| |
|
||||
| `sapl/settings.py` | All Django settings, including cache/rate-limit setup | |
|
||||
| `pytest.ini` | Test configuration (DJANGO_SETTINGS_MODULE, addopts) | |
|
||||
| `conftest.py` | Root pytest fixtures | |
|
||||
| `drfautoapi/drfautoapi.py` | Auto-API generation logic | |
|
||||
| `docker/startup_scripts/start.sh` | Container entrypoint (migrations, waffle, gunicorn) | |
|
||||
| `requirements/requirements.txt` | Production deps | |
|
||||
| `requirements/test-requirements.txt` | Test deps | |
|
||||
| `requirements/dev-requirements.txt` | Dev/lint deps | |
|
||||
@ -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,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,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'), |
||||
|
), |
||||
|
] |
||||
Loading…
Reference in new issue