Browse Source

Rate limiter: move scanner probes to nginx, fix NAT IP block for authenticated users, add 404-scan counter; remove dead painel sub-views

- nginx sapl.conf: return 444 for scanner extension probes (.php, .asp,
  .jsp, .env, etc.) before requests reach Gunicorn — zero Python cost
- ratelimit.py: remove check 2b (scanner_probe) — dead code now that
  nginx handles it; remove unused `import os`
- ratelimit.py: authenticated users skip the rl:ip:blocked check (check 2)
  to prevent anonymous NAT traffic from blocking legislative house staff
- ratelimit.py: add _handle_not_found — post-response 404 counter per
  anonymous IP; blocks after RATE_LIMIT_404_THRESHOLD (default 10) hits
  in the anon window, catching path probes without known extensions
- settings.py: replace RATE_LIMIT_SCANNER_EXTENSIONS with
  RATE_LIMIT_404_THRESHOLD; add RL_IP_404S Redis key constant
- painel: remove dead mensagem/parlamentares/votacao templates, views,
  and URL entries — unreachable from any menu or template
- RATE-LIMITER-PLAN.md: update decision flow, mermaid diagram,
  enforcement graduation table, and key schema to reflect all changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rate-limiter-2026
Edward Ribeiro 7 days ago
parent
commit
568c838d08
  1. 8
      docker/config/nginx/sapl.conf
  2. 51
      plan/RATE-LIMITER-PLAN.md
  3. 47
      sapl/middleware/ratelimit.py
  4. 7
      sapl/painel/urls.py
  5. 14
      sapl/painel/views.py
  6. 13
      sapl/settings.py
  7. 120
      sapl/templates/painel/mensagem.html
  8. 128
      sapl/templates/painel/parlamentares.html
  9. 123
      sapl/templates/painel/votacao.html

8
docker/config/nginx/sapl.conf

@ -136,6 +136,14 @@ server {
proxy_pass http://sapl_server; proxy_pass http://sapl_server;
} }
# ----------------------------------------------------------------
# Scanner extension probes (.php, .asp, etc.) — SAPL never serves
# these. Drop the connection before reaching Gunicorn.
# ----------------------------------------------------------------
location ~* \.(php|asp|aspx|jsp|cgi|env|htaccess|htpasswd|bak|sql|sh|bash|py|rb|pl)$ {
return 444;
}
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# General traffic — moderate rate limit. # General traffic — moderate rate limit.
# ---------------------------------------------------------------- # ----------------------------------------------------------------

51
plan/RATE-LIMITER-PLAN.md

@ -176,6 +176,27 @@ sapl-redis-6d9f8b7c4d-xk2lm 1/1 Running 0 30s
## Verify the rate limiter ## Verify the rate limiter
### Canary tenants
Current canary namespaces receiving the `rate-limiter-2026` image:
```
joaopessoa-pb patobranco-pr al-am al-pi al-ro divinopolis-mg
```
Verify image digest, `imagePullPolicy: Always`, and `REDIS_URL` for all six at once:
```bash
# From monitoring_metrics-2025-2026/logs/cluster-prod/
bash check-canary-tenants.sh
```
Expected: all checks green and the same `sha256` digest across all pods.
---
### Functional test
`scripts/test_ratelimiter.py` fires repeated GET requests at a SAPL URL and reports `scripts/test_ratelimiter.py` fires repeated GET requests at a SAPL URL and reports
when the first 429 is returned. when the first 429 is returned.
@ -600,8 +621,9 @@ Decision flow inside `RateLimitMiddleware._evaluate()`:
1. IP in whitelist? → pass (no further checks) 1. IP in whitelist? → pass (no further checks)
1a. UA matches BOT_UA_FRAGMENTS list? → 429 reason=known_ua 1a. UA matches BOT_UA_FRAGMENTS list? → 429 reason=known_ua
1b. UA token hash in rl:bot:ua:blocked SET? → 429 reason=redis_ua 1b. UA token hash in rl:bot:ua:blocked SET? → 429 reason=redis_ua
2. IP in rl:ip:{ip}:blocked? → 429 reason=ip_blocked 2. Anonymous AND IP in rl:ip:{ip}:blocked? → 429 reason=ip_blocked
2b. Path extension in RATE_LIMIT_SCANNER_EXTENSIONS? → SET blocked, 429 reason=scanner_probe (authenticated users skip — they have independent per-user limiting at 3c)
(scanner extension probes are rejected at nginx before reaching Django — see sapl.conf)
3. Authenticated user? 3. Authenticated user?
3a. User in rl:{ns}:user:{uid}:blocked? → 429 reason=user_blocked 3a. User in rl:{ns}:user:{uid}:blocked? → 429 reason=user_blocked
3b. Suspicious headers (no Accept/AL)? → 429 reason=suspicious_headers_auth 3b. Suspicious headers (no Accept/AL)? → 429 reason=suspicious_headers_auth
@ -627,18 +649,17 @@ flowchart TD
C1B -- "yes — token hash in rl:bot:ua:blocked" --> R_RUA([429\nredis_ua]) C1B -- "yes — token hash in rl:bot:ua:blocked" --> R_RUA([429\nredis_ua])
C1B -- no --> C2 C1B -- no --> C2
C2{"IP blocked?"} C2{"Authenticated?"}
C2 -- "yes — rl:ip:IP:blocked exists" --> R_IPB([429\nip_blocked]) C2 -- yes --> C2B
C2 -- no --> C2B C2 -- no --> C2_ANON
C2B{"Scanner extension?\n.php .asp .aspx …"} C2_ANON{"IP blocked?\nrl:ip:IP:blocked"}
C2B -- yes --> SIPB["SET rl:ip:IP:blocked TTL 300 s"] C2_ANON -- yes --> R_IPB([429\nip_blocked])
SIPB --> R_SCN([429\nscanner_probe]) C2_ANON -- no --> C3
C2B -- no --> C3
C3{"Authenticated?"} C3{"Authenticated?"}
C3 -- yes --> C3A C3 -- yes --> C3A
C3 -- no --> C4A C3 -- "no (anonymous)" --> C4A
subgraph AUTH ["Authenticated"] subgraph AUTH ["Authenticated"]
C3A{"User blocked?"} C3A{"User blocked?"}
@ -674,13 +695,14 @@ Roll out to canary pods first; promote check-by-check in order of false-positive
| Order | Check | Reason | Risk | Condition to promote | | Order | Check | Reason | Risk | Condition to promote |
|-------|-------|--------|------|---------------------| |-------|-------|--------|------|---------------------|
| nginx | scanner extensions | `return 444` in `sapl.conf` for `.php`/`.asp`/etc. | Zero | Gunicorn never sees these requests |
| 1st | `known_ua` | Substring in hardcoded `BOT_UA_FRAGMENTS` list | Zero | UA strings are deterministic | | 1st | `known_ua` | Substring in hardcoded `BOT_UA_FRAGMENTS` list | Zero | UA strings are deterministic |
| 2nd | `redis_ua` | Token hash in `rl:bot:ua:blocked` SET | Zero | Keys only set manually by operators | | 2nd | `redis_ua` | Token hash in `rl:bot:ua:blocked` SET | Zero | Keys only set manually by operators |
| 3rd | `ip_blocked` | Marker set by prior proven-bad requests | Zero | Fast-path only, no new blocks created | | 3rd | `ip_blocked` | Marker set by prior proven-bad requests | Zero | Fast-path only, no new blocks created |
| 4th | `scanner_probe` | Path ext in `RATE_LIMIT_SCANNER_EXTENSIONS` | Zero | Django never legitimately serves `.php`/`.asp`/etc. | | 4th | `ip_rate` | Rolling IP counter ≥ 35/min | Low | Threshold calibrated from canary logs |
| 5th | `ip_rate` | Rolling IP counter ≥ 35/min | Low | Threshold calibrated from canary logs | | 5th | `suspicious_headers` | No Accept-Language **and** no Accept | Medium | Confirmed no legitimate clients omit both headers |
| 6th | `suspicious_headers` | No Accept-Language **and** no Accept | Medium | Confirmed no legitimate clients omit both headers | | 6th | `ua_rotation` (ns/window) | NS/IP clock-aligned bucket ≥ 35 | Medium | NAT IP whitelist in place (see Open Questions) |
| 7th | `ua_rotation` (ns/window) | NS/IP clock-aligned bucket ≥ 35 | Medium | NAT IP whitelist in place (see Open Questions) | | 7th | `404_scan` | Anonymous IP accumulates ≥ 10 404s/min | Low | Catches path probes without known extensions |
### Decorator migration ### Decorator migration
@ -836,6 +858,7 @@ Redis PDF caching would solve "high request volume reaching the file layer" —
| 0 | Static file cache (logos) | `static:{ns}:{sha256}` | 3 – 24 h | — | *Future* (requires OpenResty/Lua) | | 0 | Static file cache (logos) | `static:{ns}:{sha256}` | 3 – 24 h | — | *Future* (requires OpenResty/Lua) |
| 0 | File content cache (≤ 360 KB) | `file:{ns}:{sha256}` | 1 h | — | *Future* | | 0 | File content cache (≤ 360 KB) | `file:{ns}:{sha256}` | 1 h | — | *Future* |
| 1 | IP rate-limit counter | `rl:ip:{ip}:reqs` | 60 s | 35 (`RATE_LIMITER_RATE`) | `RL_IP_REQUESTS` | | 1 | IP rate-limit counter | `rl:ip:{ip}:reqs` | 60 s | 35 (`RATE_LIMITER_RATE`) | `RL_IP_REQUESTS` |
| 1 | IP 404 counter | `rl:ip:{ip}:404s` | 60 s | 10 (`RATE_LIMIT_404_THRESHOLD`) | `RL_IP_404S` |
| 1 | IP blocked marker | `rl:ip:{ip}:blocked` | 300 s | — | `RL_IP_BLOCKED` | | 1 | IP blocked marker | `rl:ip:{ip}:blocked` | 300 s | — | `RL_IP_BLOCKED` |
| 1 | User rate-limit counter | `rl:{ns}:user:{uid}:reqs` | 60 s | 120 (`RATE_LIMITER_RATE_AUTHENTICATED`) | `RL_USER_REQUESTS` | | 1 | User rate-limit counter | `rl:{ns}:user:{uid}:reqs` | 60 s | 120 (`RATE_LIMITER_RATE_AUTHENTICATED`) | `RL_USER_REQUESTS` |
| 1 | User blocked marker | `rl:{ns}:user:{uid}:blocked` | 300 s | — | `RL_USER_BLOCKED` | | 1 | User blocked marker | `rl:{ns}:user:{uid}:blocked` | 300 s | — | `RL_USER_BLOCKED` |

47
sapl/middleware/ratelimit.py

@ -4,8 +4,7 @@ RateLimitMiddleware — cross-pod rate limiting backed by shared Redis.
Decision flow (per request): Decision flow (per request):
1. Known bot UA? 429 (Python list substring match) 1. Known bot UA? 429 (Python list substring match)
1b. Redis UA deny list? 429 (runtime SET token hash match, refreshed every 60 s) 1b. Redis UA deny list? 429 (runtime SET token hash match, refreshed every 60 s)
2. IP in blocked set? 429 2. Anonymous AND IP in blocked set? 429 (authenticated users skip have per-user limit at 3c)
2b. Path extension in scanner set? SET RL_IP_BLOCKED, 429
3. Authenticated user? 3. Authenticated user?
a. User blocked? 429 a. User blocked? 429
b. Suspicious hdrs? 429 b. Suspicious hdrs? 429
@ -28,7 +27,6 @@ no per-request lookup is needed or correct.
import hashlib import hashlib
import logging import logging
import os
import re import re
import time import time
from datetime import date from datetime import date
@ -53,6 +51,7 @@ _NAMESPACE = settings.POD_NAMESPACE
RL_IP_REQUESTS = 'rl:ip:{ip}:reqs' RL_IP_REQUESTS = 'rl:ip:{ip}:reqs'
RL_IP_BLOCKED = 'rl:ip:{ip}:blocked' RL_IP_BLOCKED = 'rl:ip:{ip}:blocked'
RL_IP_404S = 'rl:ip:{ip}:404s'
RL_USER_REQUESTS = 'rl:{ns}:user:{uid}:reqs' RL_USER_REQUESTS = 'rl:{ns}:user:{uid}:reqs'
RL_USER_BLOCKED = 'rl:{ns}:user:{uid}:blocked' RL_USER_BLOCKED = 'rl:{ns}:user:{uid}:blocked'
RL_NS_WINDOW = 'rl:{ns}:ip:{ip}:w:{bucket}' RL_NS_WINDOW = 'rl:{ns}:ip:{ip}:w:{bucket}'
@ -212,6 +211,7 @@ class RateLimitMiddleware:
self.auth_threshold, self.auth_window = _parse_rate(settings.RATE_LIMITER_RATE_AUTHENTICATED) self.auth_threshold, self.auth_window = _parse_rate(settings.RATE_LIMITER_RATE_AUTHENTICATED)
self.whitelist = set(settings.RATE_LIMIT_WHITELIST_IPS) self.whitelist = set(settings.RATE_LIMIT_WHITELIST_IPS)
self._rl_cache = caches['ratelimit'] self._rl_cache = caches['ratelimit']
self.not_found_threshold = settings.RATE_LIMIT_404_THRESHOLD
self._bypass_paths = [ self._bypass_paths = [
re.compile(p) for p in getattr(settings, 'RATE_LIMIT_BYPASS_PATHS', []) re.compile(p) for p in getattr(settings, 'RATE_LIMIT_BYPASS_PATHS', [])
] ]
@ -249,7 +249,10 @@ class RateLimitMiddleware:
getattr(getattr(request, 'user', None), 'pk', 'anon'), getattr(getattr(request, 'user', None), 'pk', 'anon'),
_NAMESPACE, _NAMESPACE,
) )
return self.get_response(request) response = self.get_response(request)
if response.status_code == 404:
self._handle_not_found(request, decision['ip'])
return response
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Evaluation # Evaluation
@ -271,17 +274,12 @@ class RateLimitMiddleware:
if self._is_redis_blocked_ua(ua): if self._is_redis_blocked_ua(ua):
return {'action': 'block', 'reason': 'redis_ua', 'ip': ip} return {'action': 'block', 'reason': 'redis_ua', 'ip': ip}
# Check 2: IP already blocked # Check 2: IP already blocked — authenticated users are exempt since they
if self._rl_cache.get(RL_IP_BLOCKED.format(ip=ip)): # have independent per-user limiting at check 3c; IP blocks target anonymous traffic.
user = getattr(request, 'user', None)
if not (user and user.is_authenticated) and self._rl_cache.get(RL_IP_BLOCKED.format(ip=ip)):
return {'action': 'block', 'reason': 'ip_blocked', 'ip': ip} return {'action': 'block', 'reason': 'ip_blocked', 'ip': ip}
# Check 2b: scanner probe (e.g. .php, .asp) — Django never serves these.
ext = os.path.splitext(request.path)[1].lower()
if ext in settings.RATE_LIMIT_SCANNER_EXTENSIONS:
self._rl_cache.set(RL_IP_BLOCKED.format(ip=ip), 1, timeout=self.BLOCK_TTL)
return {'action': 'block', 'reason': 'scanner_probe', 'ip': ip}
user = getattr(request, 'user', None)
if user is not None and user.is_authenticated: if user is not None and user.is_authenticated:
return self._evaluate_authenticated(request, ip) return self._evaluate_authenticated(request, ip)
return self._evaluate_anonymous(request, ip) return self._evaluate_anonymous(request, ip)
@ -336,6 +334,29 @@ class RateLimitMiddleware:
# Helpers — delegate to module-level so media.py can reuse them # Helpers — delegate to module-level so media.py can reuse them
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _handle_not_found(self, request, ip):
"""
Block IPs that accumulate too many 404s in one window catches scanner
probes that use paths without recognised extensions (e.g. /wp-login,
/.git/HEAD, /xmlrpc) and bypass check 2b entirely.
Only anonymous requests are counted; authenticated users have their own
per-user rate limit and may legitimately hit stale bookmarks.
"""
user = getattr(request, 'user', None)
if user and user.is_authenticated:
return
if ip in self.whitelist:
return
count = self._incr_with_ttl(RL_IP_404S.format(ip=ip), ttl=self.anon_window)
if count >= self.not_found_threshold:
self._rl_cache.set(RL_IP_BLOCKED.format(ip=ip), 1, timeout=self.BLOCK_TTL)
logger.warning(
'ratelimit_block layer=django reason=404_scan ip=%s path=%s namespace=%s',
ip, request.path, _NAMESPACE,
extra={'ua': request.META.get('HTTP_USER_AGENT', '')},
)
self._inc_block_metric('404_scan')
def _incr_with_ttl(self, key, ttl): def _incr_with_ttl(self, key, ttl):
return _incr_with_ttl(key, ttl) return _incr_with_ttl(key, ttl)

7
sapl/painel/urls.py

@ -1,8 +1,7 @@
from django.conf.urls import url from django.conf.urls import url
from .apps import AppConfig from .apps import AppConfig
from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view, from .views import (cronometro_painel, get_dados_painel, painel_view,
painel_parlamentar_view, painel_view, painel_votacao_view,
switch_painel, verifica_painel, votante_view) switch_painel, verifica_painel, votante_view)
app_name = AppConfig.name app_name = AppConfig.name
@ -11,12 +10,8 @@ urlpatterns = [
url(r'^painel-principal/(?P<pk>\d+)$', painel_view, url(r'^painel-principal/(?P<pk>\d+)$', painel_view,
name="painel_principal"), name="painel_principal"),
url(r'^painel/(?P<pk>\d+)/dados$', get_dados_painel, name='dados_painel'), url(r'^painel/(?P<pk>\d+)/dados$', get_dados_painel, name='dados_painel'),
url(r'^painel/mensagem$', painel_mensagem_view, name="painel_mensagem"),
url(r'^painel/parlamentar$', painel_parlamentar_view,
name='painel_parlamentar'),
url(r'^painel/switch-painel$', switch_painel, url(r'^painel/switch-painel$', switch_painel,
name="switch_painel"), name="switch_painel"),
url(r'^painel/votacao$', painel_votacao_view, name='painel_votacao'),
url(r'^painel/verifica-painel$', verifica_painel, url(r'^painel/verifica-painel$', verifica_painel,
name="verifica_painel"), name="verifica_painel"),
url(r'^painel/cronometro$', cronometro_painel, name='cronometro_painel'), url(r'^painel/cronometro$', cronometro_painel, name='cronometro_painel'),

14
sapl/painel/views.py

@ -308,20 +308,6 @@ def verifica_painel(request):
return resposta return resposta
@user_passes_test(check_permission)
def painel_mensagem_view(request):
return render(request, 'painel/mensagem.html')
@user_passes_test(check_permission)
def painel_parlamentar_view(request):
return render(request, 'painel/parlamentares.html')
@user_passes_test(check_permission)
def painel_votacao_view(request):
return render(request, 'painel/votacao.html')
@user_passes_test(check_permission) @user_passes_test(check_permission)
def cronometro_painel(request): def cronometro_painel(request):

13
sapl/settings.py

@ -421,16 +421,9 @@ RATE_LIMIT_WHITELIST_IPS = config(
# Lower values pick up new blocked UAs faster; higher values reduce Redis round-trips. # Lower values pick up new blocked UAs faster; higher values reduce Redis round-trips.
RATE_LIMITER_UA_BLOCKLIST_REFRESH = config('RATE_LIMITER_UA_BLOCKLIST_REFRESH', default=60, cast=int) RATE_LIMITER_UA_BLOCKLIST_REFRESH = config('RATE_LIMITER_UA_BLOCKLIST_REFRESH', default=60, cast=int)
# File extensions that indicate a scanner probe (e.g. PHP/ASP app fingerprinting). # Maximum 404 responses from one anonymous IP in one anon window before the IP
# Requests for these extensions are blocked immediately and the IP is written to # is blocked. Catches path-probing scanners that don't use recognised extensions.
# rl:ip:{ip}:blocked for BLOCK_TTL seconds — Django never legitimately serves them. RATE_LIMIT_404_THRESHOLD = config('RATE_LIMIT_404_THRESHOLD', default=10, cast=int)
RATE_LIMIT_SCANNER_EXTENSIONS = frozenset(
config(
'RATE_LIMIT_SCANNER_EXTENSIONS',
default='.php .asp .aspx .jsp .cgi .env',
cast=lambda v: [x.strip() for x in v.split() if x.strip()],
)
)
# Paths exempt from rate limiting at the Django layer. # Paths exempt from rate limiting at the Django layer.
# Regex strings matched against request.path. # Regex strings matched against request.path.

120
sapl/templates/painel/mensagem.html

@ -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>

128
sapl/templates/painel/parlamentares.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>

123
sapl/templates/painel/votacao.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…
Cancel
Save