mirror of https://github.com/interlegis/sapl.git
Browse Source
Performance-only slice extracted from feat/rate-limiter-2026 (commitfeat/sessao-relatorio-perf2f2a976df), which bundled query-optimization work in with unrelated rate-limiter/caching changes. - customize_link_materia() and the Ordem do Dia / Expediente ListViews now use prefetched querysets instead of per-row queries. - get_etiqueta_protocolos() batch-fetches MateriaLegislativa and DocumentoAdministrativo instead of querying per protocolo in a loop. - New migration redefines the materia_materiaemtramitacao view and adds a concurrent index (tram_materia_id_desc) backing the tramitacao prefetch. - Drops a redundant .distinct() + order_by on a related field in RelatorioMateriasTramitacaoFilterSet.qs. - Removes the unused painel_mensagem/parlamentar/votacao views and templates (also bundled into2f2a976dfby mistake). - Adds CLAUDE.md project documentation. Note: feat/painel-votacao-v2 still imports painel_mensagem_view, painel_parlamentar_view and painel_votacao_view. That branch will need reconciling with this removal when it's rebased onto 3.1.x. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
13 changed files with 709 additions and 597 deletions
@ -0,0 +1,145 @@ |
|||||
|
# 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,62 @@ |
|||||
|
from django.db import migrations, models |
||||
|
|
||||
|
_OLD_VIEW = """ |
||||
|
create or replace view materia_materiaemtramitacao as |
||||
|
select m.id as id, |
||||
|
m.id as materia_id, |
||||
|
t.id as tramitacao_id, |
||||
|
t.unidade_tramitacao_destino_id as unidade_tramitacao_atual_id |
||||
|
from materia_materialegislativa m |
||||
|
inner join materia_tramitacao t on (m.id = t.materia_id) |
||||
|
where t.id = (select max(id) from materia_tramitacao where materia_id = m.id) |
||||
|
order by m.id DESC |
||||
|
""" |
||||
|
|
||||
|
_NEW_VIEW = """ |
||||
|
create or replace view materia_materiaemtramitacao as |
||||
|
select distinct on (m.id) |
||||
|
m.id as id, |
||||
|
m.id as materia_id, |
||||
|
t.id as tramitacao_id, |
||||
|
t.unidade_tramitacao_destino_id as unidade_tramitacao_atual_id |
||||
|
from materia_materialegislativa m |
||||
|
inner join materia_tramitacao t on t.materia_id = m.id |
||||
|
order by m.id desc, t.id desc |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
# CREATE INDEX CONCURRENTLY cannot run inside a transaction. |
||||
|
atomic = False |
||||
|
|
||||
|
dependencies = [ |
||||
|
('materia', '0087_update_viewdb_materiaemtramitacao'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.RunSQL(sql=_NEW_VIEW, reverse_sql=_OLD_VIEW), |
||||
|
migrations.SeparateDatabaseAndState( |
||||
|
database_operations=[ |
||||
|
migrations.RunSQL( |
||||
|
sql=""" |
||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS |
||||
|
tram_materia_id_desc |
||||
|
ON materia_tramitacao (materia_id, id DESC) |
||||
|
""", |
||||
|
reverse_sql=""" |
||||
|
DROP INDEX CONCURRENTLY IF EXISTS |
||||
|
tram_materia_id_desc |
||||
|
""", |
||||
|
), |
||||
|
], |
||||
|
state_operations=[ |
||||
|
migrations.AddIndex( |
||||
|
model_name='tramitacao', |
||||
|
index=models.Index( |
||||
|
fields=['materia', '-id'], |
||||
|
name='tram_materia_id_desc', |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
] |
||||
@ -1,120 +0,0 @@ |
|||||
{% load i18n %} |
|
||||
{% load common_tags %} |
|
||||
{% load render_bundle from webpack_loader %} |
|
||||
{% load webpack_static from webpack_loader %} |
|
||||
|
|
||||
<!DOCTYPE HTML> |
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9" lang="pt-br"> <![endif]--> |
|
||||
<!--[if gt IE 8]><!--> |
|
||||
<html lang="pt-br"> |
|
||||
<!--<![endif]--> |
|
||||
|
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<!-- TODO: does it need this head_title here? --> |
|
||||
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
|
|
||||
{% render_chunk_vendors 'css' %} |
|
||||
{% render_bundle 'global' 'css' %} |
|
||||
{% render_bundle 'painel' 'css' %} |
|
||||
|
|
||||
|
|
||||
<STYLE type="text/css"> |
|
||||
@media screen { |
|
||||
body {font-size: medium; color: white; line-height: 1em; background: black;} |
|
||||
} |
|
||||
</STYLE> |
|
||||
|
|
||||
</head> |
|
||||
<body> |
|
||||
<h1>{{ context.title }}</h1> |
|
||||
<input id="json_url" type="hidden" value="{% url 'sapl.painel:dados_painel' %}"> |
|
||||
<h2>Ajax refresh counter: <span id="counter"></span></h2> |
|
||||
<h3> |
|
||||
<span id="sessao_plenaria"></span><br/><br/> |
|
||||
<span id="sessao_plenaria_data"></span><br/><br/> |
|
||||
<span id="sessao_plenaria_hora_inicio"></span></br><br/> |
|
||||
<h2><span id="relogio"></span></h2></br><br/><br/> |
|
||||
<span id="materia_legislativa_texto"></span><br/> |
|
||||
<span id="observacao_materia"></span> |
|
||||
</h3> |
|
||||
</body> |
|
||||
|
|
||||
{% render_chunk_vendors 'js' %} |
|
||||
{% render_bundle 'global' 'js' %} |
|
||||
{% render_bundle 'painel' 'js' %} |
|
||||
|
|
||||
<script type="text/javascript"> |
|
||||
$(document).ready(function() { |
|
||||
|
|
||||
//TODO: replace by a fancy jQuery clock |
|
||||
function checkTime(i) { |
|
||||
if (i<10) {i = "0" + i}; // add zero in front of numbers < 10 |
|
||||
return i; |
|
||||
} |
|
||||
function startTime() { |
|
||||
var today=new Date(); |
|
||||
var h=today.getHours(); |
|
||||
var m=today.getMinutes(); |
|
||||
var s=today.getSeconds(); |
|
||||
m = checkTime(m); |
|
||||
s = checkTime(s); |
|
||||
$("#relogio").text(h+":"+m+":"+s) |
|
||||
var t = setTimeout(function(){ |
|
||||
startTime() |
|
||||
},500); |
|
||||
} |
|
||||
|
|
||||
startTime(); |
|
||||
|
|
||||
var counter = 1; |
|
||||
(function poll() { |
|
||||
$.ajax({ |
|
||||
url: $("#json_url").val(), |
|
||||
type: "GET", |
|
||||
success: function(data) { |
|
||||
|
|
||||
//TODO: json spitted out is very complex, have to simplify/flat it |
|
||||
//TODO: probably building it by hand on REST side |
|
||||
|
|
||||
console.debug(data) |
|
||||
|
|
||||
var presentes = $("#parlamentares"); |
|
||||
presentes.children().remove(); |
|
||||
|
|
||||
presentes_ordem_dia = data.presentes_ordem_dia |
|
||||
$.each(presentes_ordem_dia, function(index, parlamentar) { |
|
||||
$('<li />', {text: parlamentar.nome + '/' + parlamentar.partido + ' ' + parlamentar.voto }).appendTo(presentes); |
|
||||
}); |
|
||||
|
|
||||
var votacao = $("#votacao") |
|
||||
votacao.children().remove() |
|
||||
votacao.append("<li>Sim: " + data["numero_votos_sim"] + "</li>") |
|
||||
votacao.append("<li>Não: " + data["numero_votos_nao"] + "</li>") |
|
||||
votacao.append("<li>Abstenções: " + data["numero_abstencoes"] + "</li>") |
|
||||
votacao.append("<li>Presentes: " + data["presentes"] + "</li>") |
|
||||
votacao.append("<li>Total votos: " + data["total_votos"] + "</li>") |
|
||||
|
|
||||
$("#sessao_plenaria").text(data["sessao_plenaria"]) |
|
||||
$("#sessao_plenaria_data").text("Data Início: " + data["sessao_plenaria_data"]) |
|
||||
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + data["sessao_plenaria_hora_inicio"]) |
|
||||
|
|
||||
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]) |
|
||||
$("#observacao_materia").text(data["observacao_materia"]) |
|
||||
$("#resultado_votacao").text(data["tipo_resultado"]) |
|
||||
|
|
||||
$("#counter").text(counter); |
|
||||
counter++; |
|
||||
}, |
|
||||
error: function(err) { |
|
||||
console.error(err); |
|
||||
}, |
|
||||
dataType: "json", |
|
||||
//complete: setTimeout(function() {poll()}, 5000), |
|
||||
timeout: 20000 // TODO: decrease |
|
||||
}) |
|
||||
})(); |
|
||||
}); |
|
||||
</script> |
|
||||
</html> |
|
||||
@ -1,128 +0,0 @@ |
|||||
{% load i18n %} |
|
||||
{% load common_tags %} |
|
||||
|
|
||||
{% load render_bundle from webpack_loader %} |
|
||||
{% load webpack_static from webpack_loader %} |
|
||||
|
|
||||
<!DOCTYPE HTML> |
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9" lang="pt-br"> <![endif]--> |
|
||||
<!--[if gt IE 8]><!--> |
|
||||
<html lang="pt-br"> |
|
||||
<!--<![endif]--> |
|
||||
|
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<!-- TODO: does it need this head_title here? --> |
|
||||
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
|
|
||||
{% render_chunk_vendors 'css' %} |
|
||||
{% render_bundle 'global' 'css' %} |
|
||||
{% render_bundle 'painel' 'css' %} |
|
||||
|
|
||||
|
|
||||
|
|
||||
<STYLE type="text/css"> |
|
||||
@media screen { |
|
||||
body {font-size: medium; color: white; line-height: 1em; background: black;} |
|
||||
} |
|
||||
</STYLE> |
|
||||
|
|
||||
</head> |
|
||||
<body> |
|
||||
<h1>{{ context.title }}</h1> |
|
||||
<input id="json_url" type="hidden" value="{% url 'sapl.painel:dados_painel' %}"> |
|
||||
<h3> |
|
||||
<span id="sessao_plenaria"></span><br/><br/> |
|
||||
<span id="sessao_plenaria_data"></span><br/><br/> |
|
||||
<span id="sessao_plenaria_hora_inicio"></span></br><br/> |
|
||||
<h2><span id="relogio"></span></h2> |
|
||||
<table> |
|
||||
<tr> |
|
||||
<td> |
|
||||
<ul id="parlamentares"> |
|
||||
</ul> |
|
||||
</td> |
|
||||
</tr> |
|
||||
</table> |
|
||||
</h3> |
|
||||
</body> |
|
||||
|
|
||||
{% render_chunk_vendors 'js' %} |
|
||||
{% render_bundle 'global' 'js' %} |
|
||||
{% render_bundle 'painel' 'js' %} |
|
||||
|
|
||||
<script type="text/javascript"> |
|
||||
$(document).ready(function() { |
|
||||
|
|
||||
//TODO: replace by a fancy jQuery clock |
|
||||
function checkTime(i) { |
|
||||
if (i<10) {i = "0" + i}; // add zero in front of numbers < 10 |
|
||||
return i; |
|
||||
} |
|
||||
function startTime() { |
|
||||
var today=new Date(); |
|
||||
var h=today.getHours(); |
|
||||
var m=today.getMinutes(); |
|
||||
var s=today.getSeconds(); |
|
||||
m = checkTime(m); |
|
||||
s = checkTime(s); |
|
||||
$("#relogio").text(h+":"+m+":"+s) |
|
||||
var t = setTimeout(function(){ |
|
||||
startTime() |
|
||||
},500); |
|
||||
} |
|
||||
|
|
||||
startTime(); |
|
||||
|
|
||||
var counter = 1; |
|
||||
(function poll() { |
|
||||
$.ajax({ |
|
||||
url: $("#json_url").val(), |
|
||||
type: "GET", |
|
||||
success: function(data) { |
|
||||
|
|
||||
//TODO: json spitted out is very complex, have to simplify/flat it |
|
||||
//TODO: probably building it by hand on REST side |
|
||||
|
|
||||
console.debug(data) |
|
||||
|
|
||||
var presentes = $("#parlamentares"); |
|
||||
presentes.children().remove(); |
|
||||
|
|
||||
presentes_ordem_dia = data.presentes_ordem_dia |
|
||||
$.each(presentes_ordem_dia, function(index, parlamentar) { |
|
||||
$('<li />', {text: parlamentar.nome + '/' + parlamentar.partido }).appendTo(presentes); |
|
||||
/*$('<li />', {text: parlamentar.nome + '/' + parlamentar.partido + ' ' + parlamentar.voto }).appendTo(presentes);*/ |
|
||||
}); |
|
||||
|
|
||||
var votacao = $("#votacao") |
|
||||
votacao.children().remove() |
|
||||
votacao.append("<li>Sim: " + data["numero_votos_sim"] + "</li>") |
|
||||
votacao.append("<li>Não: " + data["numero_votos_nao"] + "</li>") |
|
||||
votacao.append("<li>Abstenções: " + data["numero_abstencoes"] + "</li>") |
|
||||
votacao.append("<li>Presentes: " + data["presentes"] + "</li>") |
|
||||
votacao.append("<li>Total votos: " + data["total_votos"] + "</li>") |
|
||||
|
|
||||
$("#sessao_plenaria").text(data["sessao_plenaria"]) |
|
||||
$("#sessao_plenaria_data").text("Data Início: " + data["sessao_plenaria_data"]) |
|
||||
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + data["sessao_plenaria_hora_inicio"]) |
|
||||
|
|
||||
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]) |
|
||||
$("#observacao_materia").text(data["observacao_materia"]) |
|
||||
$("#resultado_votacao").text(data["tipo_resultado"]) |
|
||||
|
|
||||
$("#counter").text(counter); |
|
||||
counter++; |
|
||||
}, |
|
||||
error: function(err) { |
|
||||
console.error(err); |
|
||||
}, |
|
||||
dataType: "json", |
|
||||
//complete: setTimeout(function() {poll()}, 5000), |
|
||||
timeout: 20000 // TODO: decrease |
|
||||
}) |
|
||||
})(); |
|
||||
}); |
|
||||
</script> |
|
||||
</html> |
|
||||
@ -1,123 +0,0 @@ |
|||||
{% load i18n %} |
|
||||
{% load render_bundle from webpack_loader %} |
|
||||
{% load webpack_static from webpack_loader %} |
|
||||
|
|
||||
<!DOCTYPE HTML> |
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9" lang="pt-br"> <![endif]--> |
|
||||
<!--[if gt IE 8]><!--> |
|
||||
<html lang="pt-br"> |
|
||||
<!--<![endif]--> |
|
||||
|
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<!-- TODO: does it need this head_title here? --> |
|
||||
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
|
|
||||
{% render_chunk_vendors 'css' %} |
|
||||
{% render_bundle 'global' 'css' %} |
|
||||
{% render_bundle 'painel' 'css' %} |
|
||||
|
|
||||
<STYLE type="text/css"> |
|
||||
@media screen { |
|
||||
body {font-size: medium; color: white; line-height: 1em; background: black;} |
|
||||
} |
|
||||
</STYLE> |
|
||||
</head> |
|
||||
<body> |
|
||||
<h1>{{ context.title }}</h1> |
|
||||
<input id="json_url" type="hidden" value="{% url 'sapl.painel:dados_painel' %}"> |
|
||||
<h3> |
|
||||
<span id="sessao_plenaria"></span><br/><br/> |
|
||||
<span id="sessao_plenaria_data"></span><br/><br/> |
|
||||
<span id="sessao_plenaria_hora_inicio"></span></br><br/> |
|
||||
<h2><span id="relogio"></span></h2> |
|
||||
<table> |
|
||||
<tr> |
|
||||
<td> |
|
||||
<ul id="votacao"> |
|
||||
</ul> |
|
||||
</td> |
|
||||
</tr> |
|
||||
</table> |
|
||||
<span id="resultado_votacao"></span><br/> |
|
||||
</h3> |
|
||||
</body> |
|
||||
|
|
||||
{% render_chunk_vendors 'js' %} |
|
||||
{% render_bundle 'global' 'js' %} |
|
||||
{% render_bundle 'painel' 'js' %} |
|
||||
|
|
||||
<script type="text/javascript"> |
|
||||
$(document).ready(function() { |
|
||||
|
|
||||
//TODO: replace by a fancy jQuery clock |
|
||||
function checkTime(i) { |
|
||||
if (i<10) {i = "0" + i}; // add zero in front of numbers < 10 |
|
||||
return i; |
|
||||
} |
|
||||
function startTime() { |
|
||||
var today=new Date(); |
|
||||
var h=today.getHours(); |
|
||||
var m=today.getMinutes(); |
|
||||
var s=today.getSeconds(); |
|
||||
m = checkTime(m); |
|
||||
s = checkTime(s); |
|
||||
$("#relogio").text(h+":"+m+":"+s) |
|
||||
var t = setTimeout(function(){ |
|
||||
startTime() |
|
||||
},500); |
|
||||
} |
|
||||
|
|
||||
startTime(); |
|
||||
|
|
||||
var counter = 1; |
|
||||
(function poll() { |
|
||||
$.ajax({ |
|
||||
url: $("#json_url").val(), |
|
||||
type: "GET", |
|
||||
success: function(data) { |
|
||||
|
|
||||
//TODO: json spitted out is very complex, have to simplify/flat it |
|
||||
//TODO: probably building it by hand on REST side |
|
||||
|
|
||||
console.debug(data) |
|
||||
|
|
||||
var presentes = $("#parlamentares"); |
|
||||
presentes.children().remove(); |
|
||||
|
|
||||
presentes_ordem_dia = data.presentes_ordem_dia |
|
||||
$.each(presentes_ordem_dia, function(index, parlamentar) { |
|
||||
$('<li />', {text: parlamentar.nome + '/' + parlamentar.partido + ' ' + parlamentar.voto }).appendTo(presentes); |
|
||||
}); |
|
||||
|
|
||||
var votacao = $("#votacao") |
|
||||
votacao.children().remove() |
|
||||
votacao.append("<li>Sim: " + data["numero_votos_sim"] + "</li>") |
|
||||
votacao.append("<li>Não: " + data["numero_votos_nao"] + "</li>") |
|
||||
votacao.append("<li>Abstenções: " + data["numero_abstencoes"] + "</li>") |
|
||||
votacao.append("<li>Presentes: " + data["presentes"] + "</li>") |
|
||||
votacao.append("<li>Total votos: " + data["total_votos"] + "</li>") |
|
||||
|
|
||||
$("#sessao_plenaria").text(data["sessao_plenaria"]) |
|
||||
$("#sessao_plenaria_data").text("Data Início: " + data["sessao_plenaria_data"]) |
|
||||
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + data["sessao_plenaria_hora_inicio"]) |
|
||||
|
|
||||
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]) |
|
||||
$("#observacao_materia").text(data["observacao_materia"]) |
|
||||
$("#resultado_votacao").text(data["tipo_resultado"]) |
|
||||
|
|
||||
$("#counter").text(counter); |
|
||||
counter++; |
|
||||
}, |
|
||||
error: function(err) { |
|
||||
console.error(err); |
|
||||
}, |
|
||||
dataType: "json", |
|
||||
//complete: setTimeout(function() {poll()}, 5000), |
|
||||
timeout: 20000 // TODO: decrease |
|
||||
}) |
|
||||
})(); |
|
||||
}); |
|
||||
</script> |
|
||||
</html> |
|
||||
Loading…
Reference in new issue