Browse Source

bug fixes

feat/painel-votacao-v2
Edward Ribeiro 3 weeks ago
parent
commit
e2cf321c41
  1. 1
      .python-version
  2. 5
      frontend/src/__apps/leitura-materia/main.js
  3. 7
      frontend/src/__apps/painel/main.js
  4. 2
      frontend/src/__apps/painel/ws/normalizePainel.js
  5. 3
      frontend/src/__apps/painel/ws/painelSocket.js
  6. 5
      frontend/src/__apps/votacao-simbolica/main.js
  7. 8
      frontend/src/__apps/votacao/main.js
  8. 3
      frontend/src/__apps/voto-individual/main.js
  9. 6
      frontend/src/components/leitura-materia/LeituraMateria.vue
  10. 12
      frontend/src/components/votacao-simbolica/VotacaoSimbolica.vue
  11. 12
      frontend/src/components/voto-individual/VotoIndividual.vue
  12. 18
      sapl/audiencia/migrations/0021_auto_20260916_1709.py
  13. 33
      sapl/materia/migrations/0088_auto_20260916_1709.py
  14. 29
      sapl/norma/migrations/0046_auto_20260916_1709.py
  15. 34
      sapl/painel/urls.py
  16. 47
      sapl/painel/views.py
  17. 23
      sapl/protocoloadm/migrations/0046_auto_20260916_1709.py
  18. 152
      sapl/sessao/views.py
  19. 609
      sapl/templates/painel/index.html
  20. 1
      sapl/templates/painel/painel_v2.html
  21. 323
      sapl/templates/painel/voto_individual.html
  22. 3
      sapl/templates/painel/voto_individual_v2.html
  23. 1
      sapl/templates/sessao/painel.html
  24. 14
      sapl/templates/sessao/votacao/nominal.html

1
.python-version

@ -0,0 +1 @@
3.12

5
frontend/src/__apps/leitura-materia/main.js

@ -78,7 +78,10 @@ new Vue({ // eslint-disable-line
try { try {
const data = JSON.parse(message.data) const data = JSON.parse(message.data)
if (data.type === 'data') { if (data.type === 'data') {
this.painelStore().applyData(data) // applyData() espera o payload cru, não o envelope {type,
// payload} (mesmo ajuste de painel/votacao/voto-individual
// main.js).
this.painelStore().applyData(data.payload)
} }
} catch (e) { } catch (e) {
console.error('Leitura materia WS parse error:', e) console.error('Leitura materia WS parse error:', e)

7
frontend/src/__apps/painel/main.js

@ -79,7 +79,7 @@ new Vue({
// Toda a lógica de estado (flags, sessão, parlamentares, matéria, // Toda a lógica de estado (flags, sessão, parlamentares, matéria,
// resultado e votos) vive no store Pinia. O header/footer leem a // resultado e votos) vive no store Pinia. O header/footer leem a
// sessão do store — não há mais escrita via $refs. // sessão do store — não há mais escrita via $refs.
this.painelStore().applyData(data) this.painelStore().applyData(data.payload);
} catch (e) { } catch (e) {
console.error('Error applying painel data', e) console.error('Error applying painel data', e)
} }
@ -106,8 +106,9 @@ new Vue({
this.ws.addEventListener('message', (message) => { this.ws.addEventListener('message', (message) => {
try { try {
const data = JSON.parse(message.data) // console.log(`Data received from ws: ${JSON.stringify(message.data)}`);
const data = JSON.parse(message.data);
console.log(JSON.stringify(data));
if (data.type === 'data') { if (data.type === 'data') {
this.updateState(data); this.updateState(data);
} else if (data.type === 'pong') { } else if (data.type === 'pong') {

2
frontend/src/__apps/painel/ws/normalizePainel.js

@ -75,6 +75,8 @@ export function normalizePainelData (raw) {
nome_parlamentar: asString(o.nome, '') nome_parlamentar: asString(o.nome, '')
})) }))
console.log(d.sessao_iniciada);
return { return {
// sessao_iniciada vem de sessao.iniciada is not False (True ou None // sessao_iniciada vem de sessao.iniciada is not False (True ou None
// contam como "iniciada" — sessões legadas ficaram com None, mesma // contam como "iniciada" — sessões legadas ficaram com None, mesma

3
frontend/src/__apps/painel/ws/painelSocket.js

@ -38,5 +38,4 @@ export function nextBackoffDelay (currentDelayMs) {
} }
export const DISCONNECTED_MESSAGE = export const DISCONNECTED_MESSAGE =
'Conexão em tempo real perdida. Verifique se o Redis e o Daphne (ou ' + 'Conexão em tempo real perdida. Tentando reconectar…'
'"./manage.py runserver") estão em execução. Tentando reconectar…'

5
frontend/src/__apps/votacao-simbolica/main.js

@ -83,7 +83,10 @@ new Vue({ // eslint-disable-line
try { try {
const data = JSON.parse(message.data) const data = JSON.parse(message.data)
if (data.type === 'data') { if (data.type === 'data') {
this.painelStore().applyData(data) // applyData() espera o payload cru, não o envelope {type,
// payload} (mesmo ajuste de painel/votacao/voto-individual
// main.js).
this.painelStore().applyData(data.payload)
} }
} catch (e) { } catch (e) {
console.error('Votacao simbolica WS parse error:', e) console.error('Votacao simbolica WS parse error:', e)

8
frontend/src/__apps/votacao/main.js

@ -201,7 +201,13 @@ const v = new Vue({ // eslint-disable-line
updateState (data) { updateState (data) {
try { try {
// Toda a normalização/aplicação de estado vive no store Pinia. // Toda a normalização/aplicação de estado vive no store Pinia.
this.painelStore().applyData(data) // applyData() espera o payload cru (normalizePainelData() lê
// d.sessao_iniciada etc. diretamente) — não o envelope {type,
// payload} inteiro da mensagem WS, senão sessao_aberta/
// painel_aberto nunca saem de DEFAULT_STATE e a tela não renderiza
// nada. Mesmo ajuste já feito em painel/main.js e voto-individual/
// main.js.
this.painelStore().applyData(data.payload)
} catch (e) { } catch (e) {
console.error('Error updating state:', e) console.error('Error updating state:', e)
} }

3
frontend/src/__apps/voto-individual/main.js

@ -85,7 +85,8 @@ new Vue({
const data = JSON.parse(message.data) const data = JSON.parse(message.data)
if (data.type === 'data') { if (data.type === 'data') {
// Atualiza o display compartilhado da matéria (sessao/materia) // Atualiza o display compartilhado da matéria (sessao/materia)
this.painelStore().applyData(data) console.log(`${JSON.stringify(data.payload)}`);
this.painelStore().applyData(data.payload)
// E dispara a checagem do estado pessoal — sem poll periódico, // E dispara a checagem do estado pessoal — sem poll periódico,
// isto é a única forma de saber que algo mudou. // isto é a única forma de saber que algo mudou.
this.fetchStatus() this.fetchStatus()

6
frontend/src/components/leitura-materia/LeituraMateria.vue

@ -3,13 +3,13 @@
<legend>Leitura de Matéria</legend> <legend>Leitura de Matéria</legend>
<div v-if="error_message" class="alert alert-danger"> <div v-if="error_message" class="alert alert-danger">
[[ error_message ]] {{ error_message }}
</div> </div>
<div> <div>
<b>Matéria:</b> [[ materia.texto ]] <b>Matéria:</b> {{ materia.texto }}
<br /> <br />
<b>Ementa:</b> [[ materia.ementa ]] <b>Ementa:</b> {{ materia.ementa }}
</div> </div>
<br /> <br />

12
frontend/src/components/votacao-simbolica/VotacaoSimbolica.vue

@ -3,17 +3,17 @@
<legend>Votação Simbólica</legend> <legend>Votação Simbólica</legend>
<div v-if="error_message || localError" class="alert alert-danger"> <div v-if="error_message || localError" class="alert alert-danger">
[[ error_message || localError ]] {{ error_message || localError }}
</div> </div>
<div> <div>
<b>Matéria:</b> [[ materia.texto ]] <b>Matéria:</b> {{ materia.texto }}
<br /> <br />
<b>Ementa:</b> [[ materia.ementa ]] <b>Ementa:</b> {{ materia.ementa }}
<br /><br /> <br /><br />
<b>Total presentes:</b> [[ totalPresentes ]] (com presidente) <b>Total presentes:</b> {{ totalPresentes }} (com presidente)
<br /> <br />
<b>Total votantes:</b> [[ totalVotantes ]] (com presidente) <b>Total votantes:</b> {{ totalVotantes }} (com presidente)
</div> </div>
<br /> <br />
@ -42,7 +42,7 @@
<select v-model="resultadoSelected" class="form-control"> <select v-model="resultadoSelected" class="form-control">
<option value="">---------</option> <option value="">---------</option>
<option v-for="tipo in tipos_resultado" :key="tipo.id" :value="tipo.id"> <option v-for="tipo in tipos_resultado" :key="tipo.id" :value="tipo.id">
[[ tipo.nome ]] {{ tipo.nome }}
</option> </option>
</select> </select>
</div> </div>

12
frontend/src/components/voto-individual/VotoIndividual.vue

@ -1,18 +1,18 @@
<template> <template>
<div class="container-fluid text-center py-4"> <div class="container-fluid text-center py-4">
<template v-if="!errorMessage"> <template v-if="!errorMessage">
<h1 class="mb-3">[[ sessaoPlenaria ]]</h1> <h1 class="mb-3">{{ sessaoPlenaria }}</h1>
<h2 class="mb-3"> <h2 class="mb-3">
Voto: Voto:
<span :class="votoColorClass">[[ votoParlamentar ]]</span> <span :class="votoColorClass">{{ votoParlamentar }}</span>
</h2> </h2>
<p v-if="statusMessage" class="text-info">[[ statusMessage ]]</p> <p v-if="statusMessage" class="text-info">{{ statusMessage }}</p>
<div class="my-4"> <div class="my-4">
<h3 class="mb-2">Matéria em Votação</h3> <h3 class="mb-2">Matéria em Votação</h3>
<div class="fs-4">[[ materia.texto ]]</div> <div class="fs-4">{{ materia.texto }}</div>
<div class="fs-6 fst-italic">[[ materia.ementa ]]</div> <div class="fs-6 fst-italic">{{ materia.ementa }}</div>
</div> </div>
<form method="POST" :action="voteActionUrl"> <form method="POST" :action="voteActionUrl">
@ -27,7 +27,7 @@
<template v-else> <template v-else>
<h1 class="my-5">⏸</h1> <h1 class="my-5">⏸</h1>
<h2 class="text-warning">[[ errorMessage ]]</h2> <h2 class="text-warning">{{ errorMessage }}</h2>
</template> </template>
<div class="mt-5"> <div class="mt-5">

18
sapl/audiencia/migrations/0021_auto_20260916_1709.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2026-09-16 20:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audiencia', '0020_auto_20251201_1450'),
]
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'),
),
]

33
sapl/materia/migrations/0088_auto_20260916_1709.py

@ -0,0 +1,33 @@
# Generated by Django 2.2.28 on 2026-09-16 20:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0087_update_viewdb_materiaemtramitacao'),
]
operations = [
migrations.AlterField(
model_name='materialegislativa',
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='materialegislativa',
name='ano_origem_externa',
field=models.PositiveSmallIntegerField(blank=True, 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)], null=True, verbose_name='Ano'),
),
migrations.AlterField(
model_name='numeracao',
name='ano_materia',
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='proposicao',
name='ano',
field=models.PositiveSmallIntegerField(blank=True, 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)], default=None, null=True, verbose_name='Ano'),
),
]

29
sapl/norma/migrations/0046_auto_20260916_1709.py

@ -0,0 +1,29 @@
# Generated by Django 2.2.28 on 2026-09-16 20:09
from django.db import migrations, models
import sapl.norma.models
class Migration(migrations.Migration):
dependencies = [
('norma', '0045_auto_20240711_1405'),
]
operations = [
migrations.AlterField(
model_name='anexonormajuridica',
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='normaestatisticas',
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)], default=sapl.norma.models.get_ano_atual, verbose_name='Ano'),
),
migrations.AlterField(
model_name='normajuridica',
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'),
),
]

34
sapl/painel/urls.py

@ -4,35 +4,27 @@ from django.urls import path
from .apps import AppConfig from .apps import AppConfig
from .views import (cronometro_painel, painel_view, from .views import (cronometro_painel, painel_view,
switch_painel, verifica_painel, votante_status, votante_view, switch_painel, verifica_painel, votante_status, votante_view,
votante_view_v2, websocket_view, vote_controller, vote_controller, cancel_voting, close_voting, votos_status)
cancel_voting, close_voting, votos_status)
app_name = AppConfig.name app_name = AppConfig.name
urlpatterns = [ urlpatterns = [
url(r'^painel-principal/(?P<pk>\d+)$', painel_view,
name="painel_principal"),
url(r'^painel/switch-painel$', switch_painel,
name="switch_painel"),
url(r'^painel/verifica-painel$', verifica_painel,
name="verifica_painel"),
url(r'^painel/cronometro$', cronometro_painel, name='cronometro_painel'),
# url(r'^painel/cronometro$', include(CronometroPainelCrud.get_urls())),
url(r'^voto-individual/$', votante_view, ## Votacao - FRONTEND (Django/VueJS)
name='voto_individual'), path("painel/<int:sessao_id>", painel_view, name='painel_principal'),
url(r'^voto-individual/status$', votante_status, path("painel/voto-individual", votante_view, name='voto_individual'),
name='voto_individual_status'),
url(r'^voto-individual/v2$', votante_view_v2,
name='voto_individual_v2'),
path("v2/painel/<int:controller_id>", websocket_view, name='painel_websocket'), ## Votacao - API
path("v2/painel/controller/<int:controller_id>/vote", path("voto-individual/status", votante_status, name='voto_individual_status'),
path("painel/switch-painel", switch_painel, name="switch_painel"),
path("painel/verifica-painel", verifica_painel, name="verifica_painel"),
path("painel/cronometro", cronometro_painel, name='cronometro_painel'),
path("painel/controller/<int:controller_id>/vote",
vote_controller, name='vote_controller'), vote_controller, name='vote_controller'),
path("v2/painel/controller/<int:controller_id>/cancel", path("painel/controller/<int:controller_id>/cancel",
cancel_voting, name='cancel_voting'), cancel_voting, name='cancel_voting'),
path("v2/painel/controller/<int:controller_id>/close", path("painel/controller/<int:controller_id>/close",
close_voting, name='close_voting'), close_voting, name='close_voting'),
path("v2/painel/controller/<int:controller_id>/votos-status", path("painel/controller/<int:controller_id>/votos-status",
votos_status, name='votos_status'), votos_status, name='votos_status'),
] ]

47
sapl/painel/views.py

@ -133,30 +133,6 @@ def votacao(context, context_vars):
return context, context_vars return context, context_vars
@never_cache
@user_passes_test(check_permission)
def painel_view(request, pk):
logger = logging.getLogger(__name__)
utc_now = timezone.now()
local_now = timezone.localtime(utc_now)
utc_offset = int(local_now.utcoffset().total_seconds() / 60)
server_epoch_ms = int(utc_now.timestamp() * 1000)
logger.info(
"painel_view pk=%s utc_now=%s local_now=%s utc_offset=%s server_epoch_ms=%s",
pk, utc_now, local_now, utc_offset, server_epoch_ms
)
context = {'head_title': str(_('Painel Plenário')),
'sessao_id': pk,
'server_epoch_ms': server_epoch_ms,
'utc_offset': utc_offset,
}
return render(request, 'painel/index.html', context)
def sessao_votacao(context, context_vars): def sessao_votacao(context, context_vars):
pk = context_vars['sessao'].pk pk = context_vars['sessao'].pk
context.update({'sessao_id': pk}) context.update({'sessao_id': pk})
@ -314,12 +290,10 @@ def _save_voto_individual(request, context_vars):
broadcast_dados_painel(request, context_vars['sessao'].id) broadcast_dados_painel(request, context_vars['sessao'].id)
@never_cache @never_cache
@login_required @login_required
@permission_required('parlamentares.can_vote', raise_exception=True) @permission_required('parlamentares.can_vote', raise_exception=True)
def votante_view(request): def votante_view(request):
template_name = 'painel/voto_individual.html'
context, context_vars = _resolve_votante_context(request) context, context_vars = _resolve_votante_context(request)
if request.method == 'POST': if request.method == 'POST':
@ -327,23 +301,11 @@ def votante_view(request):
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('sapl.painel:voto_individual')) reverse('sapl.painel:voto_individual'))
return render(request, template_name, context) return render(request, 'painel/voto_individual_v2.html', context)
@never_cache @never_cache
@login_required @login_required
@permission_required('parlamentares.can_vote', raise_exception=True)
def votante_view_v2(request):
context, context_vars = _resolve_votante_context(request)
if request.method == 'POST':
_save_voto_individual(request, context_vars)
return HttpResponseRedirect(
reverse('sapl.painel:voto_individual_v2'))
return render(request, 'painel/voto_individual_v2.html', context)
@user_passes_test(check_permission) @user_passes_test(check_permission)
def switch_painel(request): def switch_painel(request):
sessao = SessaoPlenaria.objects.get(id=request.POST['pk_sessao']) sessao = SessaoPlenaria.objects.get(id=request.POST['pk_sessao'])
@ -358,6 +320,8 @@ def switch_painel(request):
return JsonResponse({}) return JsonResponse({})
@never_cache
@login_required
@user_passes_test(check_permission) @user_passes_test(check_permission)
def verifica_painel(request): def verifica_painel(request):
sessao = SessaoPlenaria.objects.get(id=request.GET['pk_sessao']) sessao = SessaoPlenaria.objects.get(id=request.GET['pk_sessao'])
@ -698,13 +662,14 @@ def broadcast_dados_painel(request, sessao_id):
@never_cache @never_cache
@user_passes_test(check_permission) @user_passes_test(check_permission)
def websocket_view(request, controller_id): def painel_view(request, sessao_id):
## O controller da sessao WS é o ID da sessao
now = timezone.localtime(timezone.now()) now = timezone.localtime(timezone.now())
utc_offset = now.utcoffset().total_seconds() / 60 utc_offset = now.utcoffset().total_seconds() / 60
context = {'head_title': str(_('Painel Plenário')), context = {'head_title': str(_('Painel Plenário')),
'utc_offset': utc_offset, 'utc_offset': utc_offset,
'enable_live_ws': True, 'enable_live_ws': True,
'controller_id': controller_id, # aka, sessao_plenaria_id 'controller_id': sessao_id, # aka, sessao_plenaria_id
} }
return render(request, "painel/painel_v2.html", context) return render(request, "painel/painel_v2.html", context)

23
sapl/protocoloadm/migrations/0046_auto_20260916_1709.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2026-09-16 20:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0045_auto_20240711_1405'),
]
operations = [
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'),
),
]

152
sapl/sessao/views.py

@ -1,10 +1,10 @@
from collections import OrderedDict
from datetime import datetime
import json import json
import logging import logging
from datetime import datetime
from enum import IntEnum
from re import sub from re import sub
from django.conf import settings import pytz
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import permission_required, user_passes_test from django.contrib.auth.decorators import permission_required, user_passes_test
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -14,8 +14,8 @@ from django.db.models import Max, Q
from django.http import JsonResponse, QueryDict from django.http import JsonResponse, QueryDict
from django.http.response import Http404, HttpResponseRedirect from django.http.response import Http404, HttpResponseRedirect
from django.middleware.csrf import get_token from django.middleware.csrf import get_token
from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.urls.base import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.datastructures import MultiValueDictKeyError from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -29,10 +29,7 @@ from django.views.generic.base import RedirectView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django_filters.views import FilterView from django_filters.views import FilterView
import pytz
from ratelimit.decorators import ratelimit from ratelimit.decorators import ratelimit
from django.utils.decorators import method_decorator
from sapl.base.models import AppConfig as AppsAppConfig from sapl.base.models import AppConfig as AppsAppConfig
from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux, from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
@ -40,13 +37,12 @@ from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
PermissionRequiredForAppCrudMixin, make_pagination) PermissionRequiredForAppCrudMixin, make_pagination)
from sapl.materia.forms import filtra_tramitacao_status from sapl.materia.forms import filtra_tramitacao_status
from sapl.materia.models import (Autoria, TipoMateriaLegislativa, from sapl.materia.models import (Autoria, TipoMateriaLegislativa,
Tramitacao, MateriaEmTramitacao, Numeracao) Tramitacao, MateriaEmTramitacao)
from sapl.materia.views import MateriaLegislativaPesquisaView from sapl.materia.views import MateriaLegislativaPesquisaView
from sapl.painel.views import broadcast_dados_painel from sapl.painel.views import broadcast_dados_painel
from sapl.parlamentares.models import (Filiacao, Legislatura, Mandato, from sapl.parlamentares.models import (Filiacao, Legislatura, Mandato,
Parlamentar, SessaoLegislativa) Parlamentar, SessaoLegislativa)
from sapl.protocoloadm.models import TipoDocumentoAdministrativo, \ from sapl.protocoloadm.models import DocumentoAdministrativo
DocumentoAdministrativo
from sapl.sessao.apps import AppConfig from sapl.sessao.apps import AppConfig
from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm, OrdemExpedienteLeituraForm, \ from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm, OrdemExpedienteLeituraForm, \
CorrespondenciaForm, CorrespondenciaEmLoteFilterSet CorrespondenciaForm, CorrespondenciaEmLoteFilterSet
@ -54,7 +50,6 @@ from sapl.sessao.models import Correspondencia
from sapl.settings import TIME_ZONE, RATE_LIMITER_RATE from sapl.settings import TIME_ZONE, RATE_LIMITER_RATE
from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip, \ from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip, \
MultiFormatOutputMixin, PautaMultiFormatOutputMixin, ratelimit_ip MultiFormatOutputMixin, PautaMultiFormatOutputMixin, ratelimit_ip
from .forms import (AdicionarVariasMateriasFilterSet, AdicionarVariasMateriasForm, BancadaForm, from .forms import (AdicionarVariasMateriasFilterSet, AdicionarVariasMateriasForm, BancadaForm,
ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm,
MesaForm, OradorExpedienteForm, OradorForm, PautaSessaoFilterSet, MesaForm, OradorExpedienteForm, OradorForm, PautaSessaoFilterSet,
@ -77,10 +72,16 @@ TipoResultadoVotacaoCrud = CrudAux.build(
TipoResultadoVotacao, 'tipo_resultado_votacao') TipoResultadoVotacao, 'tipo_resultado_votacao')
TipoRetiradaPautaCrud = CrudAux.build(TipoRetiradaPauta, 'tipo_retirada_pauta') TipoRetiradaPautaCrud = CrudAux.build(TipoRetiradaPauta, 'tipo_retirada_pauta')
# constantes
class TipoVotacao(IntEnum):
# IntEnum, não Enum puro: obj.tipo_votacao vem do banco como int (o
# campo é IntegerField) — um Enum comum nunca é igual a nem convertível
# para o int equivalente (TipoVotacao.NOMINAL == 2 é False), o que
# quebra toda comparação abaixo e todo filtro do ORM
# (tipo_votacao=TipoVotacao.LEITURA). IntEnum resolve os dois.
SIMBOLICA = 1 SIMBOLICA = 1
NOMINAL = 2 NOMINAL = 2
SECRETA = 3 SECRETA = 3 # not supported, by design
LEITURA = 4 LEITURA = 4
@ -247,15 +248,35 @@ def abrir_votacao(request, pk, spk):
# Leitura" (que só aparece depois que a lista recarrega) para # Leitura" (que só aparece depois que a lista recarrega) para
# chegar aonde queria. # chegar aonde queria.
registro_view_names_ordem = { registro_view_names_ordem = {
SIMBOLICA: 'votacaosimbolica', NOMINAL: 'votacaonominal', # Simbólica e Leitura (Ordem do Dia) vão para as telas de
SECRETA: 'votacaosecreta', LEITURA: 'leituraod', # registro v2 (Vue) — mesmo raciocínio do NOMINAL abaixo.
# Secreta fica na legada: não tem tela v2 (VotacaoView/
# votacao.html atende Simbólica e Secreta hoje, mas só
# Simbólica ganhou uma tela v2 dedicada). NOMINAL aqui não é
# usado — tratado à parte logo abaixo (única URL v2 sem
# oid/mid).
TipoVotacao.SIMBOLICA: 'votacaosimbolica_v2', TipoVotacao.NOMINAL: 'votacaonominal',
TipoVotacao.SECRETA: 'votacaosecreta', TipoVotacao.LEITURA: 'leituraod_v2',
} }
registro_view_names_expediente = { registro_view_names_expediente = {
SIMBOLICA: 'votacaosimbolicaexp', NOMINAL: 'votacaonominalexp', TipoVotacao.SIMBOLICA: 'votacaosimbolicaexp', TipoVotacao.NOMINAL: 'votacaonominalexp',
SECRETA: 'votacaosecretaexp', LEITURA: 'leituraexp', TipoVotacao.SECRETA: 'votacaosecretaexp', TipoVotacao.LEITURA: 'leituraexp',
} }
registro_view_names = (registro_view_names_expediente if is_expediente registro_view_names = (registro_view_names_expediente if is_expediente
else registro_view_names_ordem) else registro_view_names_ordem)
if not is_expediente and materia_votacao.tipo_votacao == TipoVotacao.NOMINAL:
# Votação Nominal de Ordem do Dia vai para a tela de registro
# v2 (Vue) em vez da legada — é a única com submissão por
# parlamentar (VotacaoVotos.vue -> vote_controller, salva e
# transmite a cada troca de voto, sem esperar o "Encerrar
# Votação" em lote). Sem equivalente v2 ainda para
# Nominal/Expediente nem para os outros tipos de votação, que
# continuam na tela legada abaixo. votacaonominal_v2 não tem
# oid/mid na URL — a matéria aberta é resolvida via o broadcast
# do WebSocket, não por parâmetro de rota.
success_url = reverse('sapl.sessao:votacaonominal_v2', kwargs={'pk': spk})
else:
registro_view_name = registro_view_names[materia_votacao.tipo_votacao] registro_view_name = registro_view_names[materia_votacao.tipo_votacao]
success_url = reverse('sapl.sessao:' + registro_view_name, kwargs={ success_url = reverse('sapl.sessao:' + registro_view_name, kwargs={
'pk': spk, 'oid': materia_votacao.pk, 'pk': spk, 'oid': materia_votacao.pk,
@ -338,30 +359,30 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
exist_leitura = obj.registroleitura_set.filter( exist_leitura = obj.registroleitura_set.filter(
materia=obj.materia).exists() materia=obj.materia).exists()
if (obj.tipo_votacao != LEITURA and not exist_resultado and not exist_retirada) or \ if (obj.tipo_votacao != TipoVotacao.LEITURA and not exist_resultado and not exist_retirada) or \
(obj.tipo_votacao == LEITURA and not exist_leitura): (obj.tipo_votacao == TipoVotacao.LEITURA and not exist_leitura):
if obj.votacao_aberta: if obj.votacao_aberta:
url = '' url = ''
if is_expediente: if is_expediente:
if obj.tipo_votacao == SIMBOLICA: if obj.tipo_votacao == TipoVotacao.SIMBOLICA:
url = reverse('sapl.sessao:votacaosimbolicaexp', url = reverse('sapl.sessao:votacaosimbolicaexp',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == NOMINAL: elif obj.tipo_votacao == TipoVotacao.NOMINAL:
url = reverse('sapl.sessao:votacaonominalexp', url = reverse('sapl.sessao:votacaonominalexp',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == SECRETA: elif obj.tipo_votacao == TipoVotacao.SECRETA:
url = reverse('sapl.sessao:votacaosecretaexp', url = reverse('sapl.sessao:votacaosecretaexp',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == LEITURA: elif obj.tipo_votacao == TipoVotacao.LEITURA:
url = reverse('sapl.sessao:leituraexp', url = reverse('sapl.sessao:leituraexp',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
@ -369,26 +390,29 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
'mid': obj.materia_id}) 'mid': obj.materia_id})
else: else:
if obj.tipo_votacao == SIMBOLICA: # Ordem do Dia: Simbólica/Nominal/Leitura têm tela de
url = reverse('sapl.sessao:votacaosimbolica', # registro v2 (Vue) — mesmo link que abrir_votacao()
kwargs={ # usa para redirecionar direto após abrir. Secreta
'pk': obj.sessao_plenaria_id, # continua na legada (sem tela v2). votacaonominal_v2
'oid': obj.pk, # não tem oid/mid na URL (matéria resolvida via o
'mid': obj.materia_id}) # broadcast do WebSocket).
elif obj.tipo_votacao == NOMINAL: if obj.tipo_votacao == TipoVotacao.SIMBOLICA:
url = reverse('sapl.sessao:votacaonominal', url = reverse('sapl.sessao:votacaosimbolica_v2',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == SECRETA: elif obj.tipo_votacao == TipoVotacao.NOMINAL:
url = reverse('sapl.sessao:votacaonominal_v2',
kwargs={'pk': obj.sessao_plenaria_id})
elif obj.tipo_votacao == TipoVotacao.SECRETA:
url = reverse('sapl.sessao:votacaosecreta', url = reverse('sapl.sessao:votacaosecreta',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == LEITURA: elif obj.tipo_votacao == TipoVotacao.LEITURA:
url = reverse('sapl.sessao:leituraod', url = reverse('sapl.sessao:leituraod_v2',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
@ -400,15 +424,17 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
page_number = "<input type='hidden' name='page' value='%s' />" % context['page'] page_number = "<input type='hidden' name='page' value='%s' />" % context['page']
if has_permission: if has_permission:
if obj.tipo_votacao != LEITURA: if obj.tipo_votacao != TipoVotacao.LEITURA:
# Votação Nominal é a única que envolve votos individuais # Votação Nominal de Expediente é a única que ainda
# pelos tablets (VOTACAO_NOMINAL em sapl/painel/views.py), e # aponta para VotacaoNominalAbstract (sem tela v2) —
# é a única cuja tela de registro (VotacaoNominalAbstract) # é a única cuja tela de registro sabe tratar POST
# sabe tratar POST sem efeito colateral — por isso só ela é # sem efeito colateral, por isso só ela é convertida
# convertida para POST aqui; Simbólica/Secreta continuam GET. # para POST aqui; Simbólica/Secreta continuam GET, e
# Nominal de Ordem do Dia também (aponta para
# votacaonominal_v2, que só faz GET).
metodo = '' metodo = ''
csrf_input = '' csrf_input = ''
if obj.tipo_votacao == NOMINAL: if is_expediente and obj.tipo_votacao == TipoVotacao.NOMINAL:
metodo = ' method="post"' metodo = ' method="post"'
csrf_input = ( csrf_input = (
'<input type="hidden" name="csrfmiddlewaretoken" value="%s" />' '<input type="hidden" name="csrfmiddlewaretoken" value="%s" />'
@ -453,7 +479,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
url += '&page=' + context['page'] url += '&page=' + context['page']
if has_permission: if has_permission:
if not obj.tipo_votacao == LEITURA: if not obj.tipo_votacao == TipoVotacao.LEITURA:
btn_abrir = ''' btn_abrir = '''
Matéria não votada<br /> Matéria não votada<br />
<a href="%s" <a href="%s"
@ -483,7 +509,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
retirada_observacao)) retirada_observacao))
else: else:
if obj.tipo_votacao == LEITURA: if obj.tipo_votacao == TipoVotacao.LEITURA:
resultado = obj.registroleitura_set.filter( resultado = obj.registroleitura_set.filter(
materia_id=obj.materia_id).last() materia_id=obj.materia_id).last()
resultado_descricao = "Matéria lida" resultado_descricao = "Matéria lida"
@ -497,51 +523,51 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
if has_permission: if has_permission:
url = '' url = ''
if is_expediente: if is_expediente:
if obj.tipo_votacao == SIMBOLICA: if obj.tipo_votacao == TipoVotacao.SIMBOLICA:
url = reverse( url = reverse(
'sapl.sessao:votacaosimbolicaexpedit', 'sapl.sessao:votacaosimbolicaexpedit',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == NOMINAL: elif obj.tipo_votacao == TipoVotacao.NOMINAL:
url = reverse('sapl.sessao:votacaonominalexpedit', url = reverse('sapl.sessao:votacaonominalexpedit',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == SECRETA: elif obj.tipo_votacao == TipoVotacao.SECRETA:
url = reverse('sapl.sessao:votacaosecretaexpedit', url = reverse('sapl.sessao:votacaosecretaexpedit',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == LEITURA: elif obj.tipo_votacao == TipoVotacao.LEITURA:
url = reverse('sapl.sessao:leituraexp', url = reverse('sapl.sessao:leituraexp',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
else: else:
if obj.tipo_votacao == SIMBOLICA: if obj.tipo_votacao == TipoVotacao.SIMBOLICA:
url = reverse('sapl.sessao:votacaosimbolicaedit', url = reverse('sapl.sessao:votacaosimbolicaedit',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == NOMINAL: elif obj.tipo_votacao == TipoVotacao.NOMINAL:
url = reverse('sapl.sessao:votacaonominaledit', url = reverse('sapl.sessao:votacaonominaledit',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == SECRETA: elif obj.tipo_votacao == TipoVotacao.SECRETA:
url = reverse('sapl.sessao:votacaosecretaedit', url = reverse('sapl.sessao:votacaosecretaedit',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) 'mid': obj.materia_id})
elif obj.tipo_votacao == LEITURA: elif obj.tipo_votacao == TipoVotacao.LEITURA:
url = reverse('sapl.sessao:leituraod', url = reverse('sapl.sessao:leituraod',
kwargs={ kwargs={
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
@ -556,7 +582,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
resultado_observacao)) resultado_observacao))
else: else:
if obj.tipo_votacao == NOMINAL: if obj.tipo_votacao == TipoVotacao.NOMINAL:
if is_expediente: if is_expediente:
url = reverse( url = reverse(
'sapl.sessao:votacao_nominal_transparencia', 'sapl.sessao:votacao_nominal_transparencia',
@ -579,7 +605,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente, request=N
resultado_descricao, resultado_descricao,
resultado_observacao)) resultado_observacao))
elif obj.tipo_votacao == SIMBOLICA: elif obj.tipo_votacao == TipoVotacao.SIMBOLICA:
if is_expediente: if is_expediente:
url = reverse( url = reverse(
'sapl.sessao:votacao_simbolica_transparencia', 'sapl.sessao:votacao_simbolica_transparencia',
@ -1560,7 +1586,12 @@ class PainelView(PermissionRequiredForAppCrudMixin, TemplateView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.user.is_anonymous: if request.user.is_anonymous:
self.template_name = 'painel/index.html' # painel/index.html foi removido nesta branch; o telão público
# é servido por painel_v2.html (sapl.painel:painel_principal).
# Ramo inalcançável hoje (has_module_perms é sempre False para
# AnonymousUser, PermissionRequiredForAppCrudMixin barra antes
# de chegar aqui) — mantido por segurança caso isso mude.
self.template_name = 'painel/painel_v2.html'
request.session['discurso'] = 'stop' request.session['discurso'] = 'stop'
request.session['aparte'] = 'stop' request.session['aparte'] = 'stop'
@ -2998,7 +3029,8 @@ class VotacaoView(SessaoPermissionMixin):
'e da ordem de id={}. '.format(materia_id, ordem_id) + str( 'e da ordem de id={}. '.format(materia_id, ordem_id) + str(
e)) e))
if wants_json: if wants_json:
return JsonResponse({"ok": False, "message": "Erro ao salvar o registro de votação."}, status=400) return JsonResponse({"ok": False, "message": "Erro ao salvar o registro de votação."},
status=400)
return self.form_invalid(form) return self.form_invalid(form)
else: else:
ordem = OrdemDia.objects.get(id=ordem_id) ordem = OrdemDia.objects.get(id=ordem_id)
@ -4136,7 +4168,8 @@ class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
'resultado_observacao': resultado_observacao, 'resultado_observacao': resultado_observacao,
'situacao': ultima_tramitacao.status if ultima_tramitacao else _("Não informada"), 'situacao': ultima_tramitacao.status if ultima_tramitacao else _("Não informada"),
'processo': f'{str(numeracao.numero_materia)}/{str(numeracao.ano_materia)}' if numeracao else '-', 'processo': f'{str(numeracao.numero_materia)}/{str(numeracao.ano_materia)}' if numeracao else '-',
'autor': [str(x.autor) for x in Autoria.objects.select_related("autor").filter(materia_id=o.materia_id)], 'autor': [str(x.autor) for x in
Autoria.objects.select_related("autor").filter(materia_id=o.materia_id)],
'turno': get_turno(ultima_tramitacao.turno) if ultima_tramitacao else '', 'turno': get_turno(ultima_tramitacao.turno) if ultima_tramitacao else '',
'periodo': 'ordem dia', 'periodo': 'ordem dia',
}) })
@ -4246,7 +4279,6 @@ class PesquisarSessaoPlenariaView(MultiFormatOutputMixin, FilterView):
return r return r
class PesquisarPautaSessaoView(PesquisarSessaoPlenariaView): class PesquisarPautaSessaoView(PesquisarSessaoPlenariaView):
filterset_class = PautaSessaoFilterSet filterset_class = PautaSessaoFilterSet
template_name = 'sessao/pauta_sessao_filter.html' template_name = 'sessao/pauta_sessao_filter.html'
@ -4619,7 +4651,7 @@ class LeituraEmBloco(PermissionRequiredForAppCrudMixin, ListView):
def get_queryset(self): def get_queryset(self):
return ExpedienteMateria.objects.filter(sessao_plenaria_id=self.kwargs['pk'], return ExpedienteMateria.objects.filter(sessao_plenaria_id=self.kwargs['pk'],
retiradapauta=None, tipo_votacao=LEITURA, registroleitura__materia=None) retiradapauta=None, tipo_votacao=TipoVotacao.LEITURA, registroleitura__materia=None)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -4701,7 +4733,7 @@ class LeituraEmBlocoExpediente(LeituraEmBloco):
def get_queryset(self): def get_queryset(self):
return ExpedienteMateria.objects.filter(sessao_plenaria_id=self.kwargs['pk'], return ExpedienteMateria.objects.filter(sessao_plenaria_id=self.kwargs['pk'],
retiradapauta=None, tipo_votacao=LEITURA, registroleitura__materia=None) retiradapauta=None, tipo_votacao=TipoVotacao.LEITURA, registroleitura__materia=None)
class LeituraEmBlocoOrdemDia(LeituraEmBloco): class LeituraEmBlocoOrdemDia(LeituraEmBloco):
@ -4710,7 +4742,7 @@ class LeituraEmBlocoOrdemDia(LeituraEmBloco):
def get_queryset(self): def get_queryset(self):
return OrdemDia.objects.filter(sessao_plenaria_id=self.kwargs['pk'], return OrdemDia.objects.filter(sessao_plenaria_id=self.kwargs['pk'],
retiradapauta=None, tipo_votacao=LEITURA, registroleitura__materia=None) retiradapauta=None, tipo_votacao=TipoVotacao.LEITURA, registroleitura__materia=None)
class VotacaoEmBlocoExpediente(PermissionRequiredForAppCrudMixin, ListView): class VotacaoEmBlocoExpediente(PermissionRequiredForAppCrudMixin, ListView):

609
sapl/templates/painel/index.html

@ -1,609 +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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- TODO: does it need this head_title here? -->
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title>
{% block webpack_loader_css %}
{% render_chunk_vendors 'css' %}
{% render_bundle 'global' 'css' %}
{% render_bundle 'painel' 'css' %}
{% endblock webpack_loader_css %}
<style type="text/css">
html, body {
max-width: 100%;
overflow-x: hidden;
}
@media screen {
ul, li {
list-style-type: none;
}
}
</style>
</head>
<body class="painel-principal">
<div id="ws-status-banner" style="display:none; position:fixed; top:0; left:0; right:0; z-index:2000; padding:10px 16px; text-align:center; font-weight:bold; color:#fff;"></div>
<audio type="hidden" id="audio" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<div class="d-flex justify-content-center">
<h1 id="sessao_plenaria" class="title text-title"></h1>
</div>
<div class="row ">
<div class="col text-center">
<span id="sessao_plenaria_data" class="text-value"></span>
</div>
<div class="col text-center">
<span id="sessao_plenaria_hora_inicio" class="text-value"></span>
</div>
</div>
<div class="row justify-content-center">
<div class="col-1">
<img src="" id="logo-painel" class="logo-painel" alt=""/>
</div>
</div>
<div class="row justify-content-center">
<h2 class="text-danger"><span id="message"></span></h2>
</div>
<div class="row">
<div class="col text-center"><span class="text-value data-hora" id="date"></span></div>
<div class="col text-center"><span class="text-value data-hora" id="relogio"></span></div>
</div>
<div class="">
<div class="d-flex justify-content-start">
<div class="col-md-4">
<div class="text-center painel">
<h2 class="text-subtitle">Parlamentares</h2>
<span id="parlamentares" class="text-value text-center"></span>
</div>
</div>
<div class="d-flex col-md-8 painels">
<div class="col-md-6 text-center painel" id="aparecer_oradores">
<h2 class="text-subtitle">Oradores</h2>
<span id="orador"></span>
</div>
<div class="col-md-6 text-left painel">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Cronômetros</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', 1)">
A+
</button>
</div>
<div class="text-value" id="box_cronometros">
Discurso: <span id="cronometro_discurso"></span><br>
Aparte: <span id="cronometro_aparte"></span><br>
Questão de Ordem: <span id="cronometro_ordem"></span><br>
Considerações Finais: <span id="cronometro_consideracoes"></span>
</div>
</div>
<div class="col-md-6 text-left painel" id="resultado_votacao_div">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Resultado</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', 1)">
A+
</button>
</div>
<div id="box_votacao">
<span id="votacao" class="text-value"></span>
<span id="resultado_votacao" lass="text-title"></span>
</div>
</div>
<div class="col-md-6 text-center painel" id="obs_materia_div">
<h2 class="text-subtitle" id="mat_em_votacao">Matéria em Votação</h2>
<span id="materia_legislativa_texto" class="text-value"></span>
<br>
<span id="materia_legislativa_ementa" class="text-value"></span>
<br>
<span id="observacao_materia" class="text-value"></span>
</div>
<div class="col-md-6 text-center painel" id="tema_solene_div" style="display: none">
<h2 class="text-subtitle">Tema da Sessão Solene</h2>
<span id="sessao_solene_tema" class="text-value"></span>
</div>
</div>
</div>
</div>
</div>
</body>
{% block webpack_loader_js %}
{% render_chunk_vendors 'js' %}
{% render_bundle 'global' 'js' %}
{% render_bundle 'painel' 'js' %}
{% endblock webpack_loader_js %}
{% block webpack_loader_chunks_js %}
{% endblock webpack_loader_chunks_js %}
<script type="text/javascript">
$(document).ready(function() {
// As constantes decisões sobre a existência ou não do horário de verão,
// assim como que data de início e termino do mesmo, fizeram com que fosse necessário
// substituir a chamada a Date() por um esquema mais elaborado, onde se
// recupera o offset do UTC (-3 GMT, no caso de Brasília) e seta-se
// manualmente. Esta informação vem do servidor, desta forma não ficamos
// na dependência da atualização de browser, pois tanto o Date() em JS
// quanto as libs python (django.utils.timezone, datetime, pytz, etc)
// lêem do tzdata, que precisa ser atualizado toda vez que o governo
// brasileiro modifica alguma coisa relacionada ao horário de verão.
// Recuperando essa informação do servidor só teremos que atualizar as
// libs tzdata (Linux) e pytz (Python) uma vez. Além disso, o uso da
// biblioteca moment.js é recomendada, pois ela trata data e hora
// melhor que o Date() do JS.
//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;
}
const utcOffset = {{ utc_offset }};
const serverEpochMs = {{ server_epoch_ms }};
const pageLoadedAtMs = performance.now();
function startTime() {
const elapsedMs = performance.now() - pageLoadedAtMs;
const now = moment
.utc(serverEpochMs + elapsedMs)
.utcOffset(utcOffset);
$("#date").text(now.format("DD/MM/YY"));
$("#relogio").text(now.format("HH:mm:ss"));
setTimeout(startTime, 500);
}
startTime();
var audioAlertFinish = document.getElementById("audio");
$('#cronometro_discurso').runner({
autostart: false,
countdown: true,
startAt: {{ 'discurso'|cronometro_to_seconds }} * 1000,
stopAt: 0,
milliseconds: false,
format: function(value) {
let h = Math.floor((value/1000) / 3600);
h = checkTime(h);
let m = Math.floor((value/1000) % 3600 / 60);
m = checkTime(m);
let s = Math.floor((value/1000) % 3600 % 60);
s = checkTime(s);
return h.toString() + ":" + m.toString() + ":" + s.toString();
}
}).on('runnerFinish', function(eventObject, info){
audioAlertFinish.play();
});
$('#cronometro_aparte').runner({
autostart: false,
countdown: true,
startAt: {{ 'aparte'|cronometro_to_seconds }} * 1000,
stopAt: 0,
milliseconds: false,
format: function(value) {
let h = Math.floor((value/1000) / 3600);
h = checkTime(h);
let m = Math.floor((value/1000) % 3600 / 60);
m = checkTime(m);
let s = Math.floor((value/1000) % 3600 % 60);
s = checkTime(s);
return h.toString() + ":" + m.toString() + ":" + s.toString();
}
}).on('runnerFinish', function(eventObject, info){
audioAlertFinish.play();
});
$('#cronometro_ordem').runner({
autostart: false,
countdown: true,
startAt: {{ 'ordem'|cronometro_to_seconds }} * 1000,
stopAt: 0,
milliseconds: false,
format: function(value) {
let h = Math.floor((value/1000) / 3600);
h = checkTime(h);
let m = Math.floor((value/1000) % 3600 / 60);
m = checkTime(m);
let s = Math.floor((value/1000) % 3600 % 60);
s = checkTime(s);
return h.toString() + ":" + m.toString() + ":" + s.toString();
}
}).on('runnerFinish', function(eventObject, info){
audioAlertFinish.play();
});
$('#cronometro_consideracoes').runner({
autostart: false,
countdown: true,
startAt: {{ 'consideracoes'|cronometro_to_seconds }} * 1000,
stopAt: 0,
milliseconds: false,
format: function(value) {
let h = Math.floor((value/1000) / 3600);
h = checkTime(h);
let m = Math.floor((value/1000) % 3600 / 60);
m = checkTime(m);
let s = Math.floor((value/1000) % 3600 % 60);
s = checkTime(s);
return h.toString() + ":" + m.toString() + ":" + s.toString();
}
}).on('runnerFinish', function(eventObject, info){
audioAlertFinish.play();
});
var discurso_previous;
var ordem_previous;
var aparte_previous;
var consideracoes_previous;
// Renderiza o payload completo que o WebSocket manda em cada broadcast
// (build_dados_painel() — antes também servido por um endpoint HTTP de
// polling, removido junto com o fallback).
function renderDadosPainel(data) {
if (!data) {
return;
}
$("#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"])
$("#sessao_solene_tema").text(data["tema_solene"])
if (data["status_painel"] == false) {
$("#message").text("PAINEL ENCONTRA-SE FECHADO");
}
else {
$("#message").text("");
}
if (data["sessao_solene"]){
$("#resultado_votacao_div").hide();
$("#obs_materia_div").hide();
$('#tema_solene_div').show();
}
if (data["brasao"] != null)
$("#logo-painel").attr("src", data["brasao"]);
var presentes = $("#parlamentares");
var votacao = $("#votacao");
var oradores = $("#orador")
$("#votacao").text('');
presentes.children().remove();
votacao.children().remove();
oradores.children().remove();
var oradores_list = data["oradores"];
var presentes_list = data["presentes"];
if (data["status_painel"] == true) {
mostrar_voto = data["mostrar_voto"];
presentes.append('<table id="parlamentares_list">');
$.each(presentes_list, function (index, parlamentar) {
if (parlamentar.voto == 'Voto Informado' && mostrar_voto == false){
$('#parlamentares_list').append('<tr><td style="padding-right:20px; color:yellow" >' +
parlamentar.nome +
'</td> <td style="padding-right:20px; color:yellow">' +
parlamentar.partido + '</td> <td style="padding-right:20px; color:yellow">'
+ '</td></tr>')
}
else{
$('#parlamentares_list').append(show_voto(parlamentar))
}
});
presentes.append('</table>')
if (data["oradores"].length > 0){
$('#aparecer_oradores').show();
oradores.append('<table id="oradores_list">');
$.each(oradores_list, function (index, orador) {
$('#oradores_list').append('<tr><td style="padding-right:20px; color:white" >' +
orador.numero + 'º &nbsp' +
orador.nome +'</td></tr>')
});
oradores.append('</table>');
}
else {
$('#aparecer_oradores').hide();
}
}
else{
presentes.append('<span style="color:white" id="parlamentares_list">');
$('#parlamentares_list').append(
'<center>A listagem de parlamentares só aparecerá quando o painel estiver aberto.</center>')
presentes.append('</span>');
oradores.append('<span style="color:white" id="oradores_list">');
$('#oradores_list').append(
'<center>A listagem de oradores só aparecerá quando o painel estiver aberto.</center>')
oradores.append('</span>');
votacao.append('<span id="votacao">');
$("#votacao").append('<center>A votação só aparecerá quando o painel estiver aberto</center>');
votacao.append('</span>');
}
if(data["status_painel"]){
if (data['materia_legislativa_texto']){
var votacao = $("#votacao");
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["num_presentes"] + "</li>");
votacao.append("<li>Total votos: " + data["total_votos"] + "</li>");
}
else{
$("#votacao").append('<center>Não há votação, pois não há nenhuma matéria aberta ou já votada.</center>');
}
}
var discurso_current = data["cronometro_discurso"];
if (!discurso_previous){
discurso_previous = ''
}
if (discurso_current != discurso_previous) {
$('#cronometro_discurso').runner(discurso_current);
discurso_previous = discurso_current;
}
var aparte_current = data["cronometro_aparte"];
if (!aparte_previous){
aparte_previous = ''
}
if (aparte_current != aparte_previous) {
$('#cronometro_aparte').runner(aparte_current);
aparte_previous = aparte_current;
}
var ordem_current = data["cronometro_ordem"];
if (!ordem_previous){
ordem_previous = ''
}
if (ordem_current != ordem_previous) {
$('#cronometro_ordem').runner(ordem_current);
ordem_previous = ordem_current;
}
var consideracoes_current = data["cronometro_consideracoes"];
if (!consideracoes_previous){
consideracoes_previous = ''
}
if (consideracoes_current != consideracoes_previous) {
$('#cronometro_consideracoes').runner(consideracoes_current);
consideracoes_previous = consideracoes_current;
}
if($('#cronometro_discurso').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if($('#cronometro_aparte').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if($('#cronometro_ordem').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if($('#cronometro_consideracoes').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if(data['sessao_finalizada']){
$("#obs_materia_div").hide();
$("#resultado_votacao_div").hide();
}
else if (data['materia_legislativa_texto']){
if (data["status_painel"] == true){
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]);
$("#materia_legislativa_ementa").text(data["materia_legislativa_ementa"]);
}
else{
$("#materia_legislativa_texto").text('A Matéria em votação só aparecerá quando o painel estiver aberto');
}
}
else{
$("#materia_legislativa_texto").text('Não há nenhuma matéria votada ou para votação.');
}
if (data['observacao_materia'] && data["status_painel"] == true){
var texto = data['observacao_materia'];
if(texto.length > 151) {
$("#observacao_materia").text(texto.substr(0, 145).concat('(...)'));
}
else{
$("#observacao_materia").text(texto);
}
}
else{
$("#observacao_materia").text('');
}
if (data['tipo_resultado'] && data['status_painel'] == true){
if(data['tipo_votacao'] != 'Leitura' && !data['sessao_finalizada'] && !data["sessao_solene"]){
$("#resultado_votacao").css("color", "#45919D");
$("#mat_em_votacao").text("Matéria em Votação");
$("#resultado_votacao_div").show();
}
else{
$("#resultado_votacao_div").hide();
$("#mat_em_votacao").text("Matéria em Leitura");
}
console.log(data["tipo_resultado"], data['tipo_votacao']);
$("#resultado_votacao").text(data["tipo_resultado"]);
var resultado_votacao_upper = $("#resultado_votacao").text().toUpperCase();
console.log(resultado_votacao_upper, data['tipo_resultado']);
if (resultado_votacao_upper.search("APROV") != -1){
$("#resultado_votacao").css("color", "#7CFC00");
$("#mat_em_votacao").text("Matéria Votada");
}
else if (resultado_votacao_upper.search("REJEIT") != -1){
$("#resultado_votacao").css("color", "red");
$("#mat_em_votacao").text("Matéria Votada");
}
else if (resultado_votacao_upper.search("LIDA") != -1){
$("#mat_em_votacao").text("Matéria Lida");
}
}
else{
$("#resultado_votacao").text('');
if(data['tipo_votacao'] != 'Leitura')
$("#mat_em_votacao").text("Matéria em Votação");
else{
$("#mat_em_votacao").text("Matéria em Leitura");
}
}
}
// Sem fallback de polling: esta página vive inteiramente do
// WebSocket. PainelConsumer.connect() (sapl/painel/consumers.py)
// manda um snapshot completo assim que a conexão é aceita — então
// tanto a carga inicial quanto toda atualização depois vêm daqui.
// Se a conexão não sobe (Redis/Daphne fora do ar), showWsBanner()
// deixa isso visível na página em vez de deixá-la muda em silêncio.
(function() {
var banner = document.getElementById('ws-status-banner');
function showWsBanner(message, isError) {
if (!banner) return;
banner.textContent = message;
banner.style.display = 'block';
banner.style.background = isError ? '#b00020' : '#b8860b';
}
function hideWsBanner() {
if (!banner) return;
banner.style.display = 'none';
}
// Fechados explicitamente por PainelConsumer.connect() para
// condições que uma nova tentativa não resolve sozinha.
var PERMANENT_CLOSE_MESSAGES = {
4401: 'Sessão expirada ou usuário não autenticado. Faça login novamente e recarregue a página.',
4403: 'Seu usuário não tem permissão para acessar esta tela.',
4404: 'Sessão plenária não encontrada.'
};
var wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var wsUrl = wsProtocol + '//' + window.location.host + '/ws/painel/{{ sessao_id }}/';
var MIN_RECONNECT_DELAY_MS = 1000;
var MAX_RECONNECT_DELAY_MS = 30000;
var reconnectDelayMs = MIN_RECONNECT_DELAY_MS;
var socket = null;
function nextDelay() {
var base = Math.min(reconnectDelayMs * 2, MAX_RECONNECT_DELAY_MS);
// +/-30% de jitter — evita que várias telas conectadas na mesma
// sessão reconectem todas no mesmo instante após uma queda do
// Daphne, batendo em build_dados_painel() ao mesmo tempo.
return Math.round(base * (0.7 + Math.random() * 0.6));
}
function connect() {
socket = new WebSocket(wsUrl);
socket.onopen = function() {
reconnectDelayMs = MIN_RECONNECT_DELAY_MS;
hideWsBanner();
};
socket.onmessage = function(event) {
var message;
try {
message = JSON.parse(event.data);
} catch (e) {
return;
}
if (message && message.type === 'data') {
renderDadosPainel(message.payload);
}
};
socket.onclose = function(event) {
var permanentMessage = PERMANENT_CLOSE_MESSAGES[event.code];
if (permanentMessage) {
showWsBanner(permanentMessage, true);
return;
}
showWsBanner(
'Conexão em tempo real perdida. Verifique se o Redis e o ' +
'Daphne (ou "./manage.py runserver") estão em execução. ' +
'Tentando reconectar…',
false
);
setTimeout(connect, reconnectDelayMs);
reconnectDelayMs = nextDelay();
};
socket.onerror = function() {
socket.close();
};
}
connect();
})();
});
function show_voto(parlamentar) {
color = 'white'
if (parlamentar.voto == "Sim"){
color = 'green'
}
else if (parlamentar.voto == "Não"){
color = 'red'
}
return ('<tr> <td style="padding-right:20px">' +
`<font color="`+color+`">${parlamentar.nome}</font> </td> <td style="padding-right:20px">` +
`<font color="`+color+`">${parlamentar.partido}</font> </td> <td style="padding-right:20px">` +
`<font color="`+color+`">${parlamentar.voto}</font> </td></tr>`)
}
function changeFontSize(elementId, increment) {
let element = document.getElementById(elementId);
if (element) {
let currentSize = parseFloat(window.getComputedStyle(element, null).getPropertyValue('font-size'));
element.style.fontSize = (currentSize + increment) + 'px';
}
}
</script>
</html>

1
sapl/templates/painel/painel_v2.html

@ -23,7 +23,6 @@
{% endblock webpack_loader_css %} {% endblock webpack_loader_css %}
</head> </head>
<body class="min-vh-100 d-flex flex-column p-2"> <body class="min-vh-100 d-flex flex-column p-2">
{% block vue_content %} {% block vue_content %}
<style>[v-cloak]{display:none}</style> <style>[v-cloak]{display:none}</style>

323
sapl/templates/painel/voto_individual.html

@ -1,323 +0,0 @@
{% load i18n %}
{% load common_tags %}
{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}
<!DOCTYPE HTML>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<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">
html, body {
max-width: 100%;
overflow-x: hidden;
}
@media screen {
body {
background: #1c1b1b;
}
ul, li {
list-style-type: none;
}
#date, #relogio, #resultado_votacao, #blocoprincipal, #blocoerro {
font-family: Verdana;
}
#votos
{
text-align:center;
}
#votosim, #votonao, #votoabstencao
{
display:inline;
}
#voltar {
margin-left: 5rem;
}
}
</style>
</head>
<body>
<div id="ws-status-banner" style="display:none; position:fixed; top:0; left:0; right:0; z-index:2000; padding:10px 16px; text-align:center; font-weight:bold; color:#fff;"></div>
{% if not error_message %}
<div id="blocoprincipal" style="display:block">
<h1><b><font color="#4FA64D"><p align="center">{{sessao}}</p></font></b></h1>
<table style="width:90%">
<tr>
<td style="text-align:center"><font color="white" size="4">Data Início: {{data}}</font></td>
<td style="text-align:center"><font color="white" size="4">Hora Início: {{hora}}</font></td>
</tr>
<tr>
<td style="text-align:center"><font color="white" size="6"><span id="date"></span></font></td>
<td style="text-align:center"><font color="white" size="6"><span id="relogio"></span></font></td>
</tr>
</table>
</br>
</br>
<h2>
<font color="white">
<p align="center" style="font-family:Verdana">Voto:
{% if voto_parlamentar == "Sim" %}
<font color="#449d44">{{voto_parlamentar}}</font>
{% elif voto_parlamentar == "Não" %}
<font color="#d9534f">{{voto_parlamentar}}</font>
{% elif voto_parlamentar == "Abstenção" %}
<font color="#eaeaea">{{voto_parlamentar}}</font>
{% endif %}
</p>
</font>
</h2>
{% if status_message %}
<p align="center" style="font-family:Verdana">
<font color="#8fbfe0">{{status_message}}</font>
</p>
{% endif %}
</br>
</br>
<h2><font color="#459170"><p align="center" style="font-family:Verdana">Matéria em Votação</p></font></h2>
<table style="width:75%; border:1px;" align="center">
<tr><th style="text-align:center"><h4><font color="white" size="5">{{materia}}</font></h4></th></tr>
<tr><th style="text-align:center"><h4><font color="white">{{ementa}}</font></h4></th></tr>
<tr><th style="text-align:center"><font color="#45919D"><span id="resultado_votacao"></span></font></th></tr>
</table>
<br /><br />
<form method='POST'>
{% csrf_token %}
<div class="row" align="center">
<div class="col-md-12" id="votos">
<div id="votosim"><input type="submit" class="btn btn-lg btn-success" id="voto" name="voto" type="submit" value="Sim" /></div>
&nbsp;
<div id="votonao"><input type="submit" class="btn btn-lg btn-danger" id="voto" name="voto" type="submit" value="Não"/></div>
&nbsp;
<div id="votoabstencao"><input type="submit" class="btn btn-lg btn-secondary" id="voto" name="voto" type="submit" value="Abstenção"/></div>
</div>
</div>
</br>
</br>
<center><table style="width:30%">
<tr>
<td style="text-align:center"><button type="button" class="btn btn-lg btn-primary" onclick="javascript:window.location.reload(true)">Atualizar</button></td>
<td style="text-align:center"><button type="button" class="btn btn-lg btn-secondary" onclick="javascript:window.close()">Sair</button></td>
</tr>
</table></center>
</form>
</div>
{% else %}
<div id="blocoerro" style="display:block">
<h1><font color="#eaeaea"><p align="center" style="font-family:Verdana">⏸</p></font></h1>
<h2><font color="#f0ad4e"><p align="center" style="font-family:Verdana">{{error_message}}</p></font></h2>
</br>
</br>
</br>
<center><table style="width:30%">
<tr>
<td style="text-align:center"><button type="button" class="btn btn-lg btn-primary" onclick="javascript:window.location.reload(true)">Atualizar</button></td>
<td style="text-align:center"><button type="button" class="btn btn-lg btn-secondary" onclick="javascript:window.close()">Sair</button></td>
</tr>
</table></center>
</div>
{% endif %}
</body>
</html>
{% if not error_message %}
{% render_chunk_vendors 'js' %}
{% render_bundle 'global' 'js' %}
{% render_bundle 'painel' 'js' %}
<script type="text/javascript">
$(window).on('beforeunload', function () {
$("input[type=submit], input[type=button]").prop("disabled", "disabled");
});
//TODO: Este código é necessário?
// $( "#votosim" ).mouseleave(function(){document.location.reload(true);});
// $( "#votonao" ).mouseleave(function(){document.location.reload(true);});
// $( "#votoabstencao" ).mouseleave(function(){document.location.reload(true);});
$(document).on('keyup', (e) => {
var tecla_press = e.keyCode;
switch (tecla_press) {
case 83: // 83 = valor da tecla S
document.querySelectorAll("#votosim input")[0].click();
break;
case 78: //78 = valor da tecla N
document.querySelectorAll("#votonao input")[0].click();
break;
case 65: //65 = valor da tecla A
document.querySelectorAll("#votoabstencao input")[0].click();
break;
case 86: //86 = valor da tecla V
document.querySelectorAll("#voltar button")[0].click();
break;
};
});
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(() => startTime(), 500);
}
$(document).ready(function(){
var d = new Date();
var n = d.toLocaleDateString();
$("#date").text(n);
startTime();
});
</script>
{% endif %}
<script type="text/javascript">
// Sem poll de segurança: esta tela vive do WebSocket. O broadcast
// (build_dados_painel) carrega o estado compartilhado do painel, não o
// status pessoal deste vereador — então cada 'data' recebido apenas
// dispara verificaEstado(), que busca o status pessoal de verdade e
// recarrega a página só se algo relevante mudou.
(function() {
var estadoAtual = {
materiaId: "{% if materia %}{{ materia.id }}{% endif %}",
errorMessage: "{{ error_message|default:''|escapejs }}",
statusMessage: "{{ status_message|default:''|escapejs }}",
votoParlamentar: "{{ voto_parlamentar|default:''|escapejs }}"
};
function verificaEstado() {
fetch("{% url 'sapl.painel:voto_individual_status' %}", {credentials: 'same-origin'})
.then(function(resp) { return resp.json(); })
.then(function(data) {
var novo = {
materiaId: data.materia_id ? String(data.materia_id) : "",
errorMessage: data.error_message || "",
statusMessage: data.status_message || "",
votoParlamentar: data.voto_parlamentar || ""
};
if (novo.materiaId !== estadoAtual.materiaId ||
novo.errorMessage !== estadoAtual.errorMessage ||
novo.statusMessage !== estadoAtual.statusMessage ||
novo.votoParlamentar !== estadoAtual.votoParlamentar) {
document.location.reload();
}
})
.catch(function() { /* sem poll — só tenta de novo no próximo broadcast */ });
}
var banner = document.getElementById('ws-status-banner');
function showWsBanner(message, isError) {
if (!banner) return;
banner.textContent = message;
banner.style.display = 'block';
banner.style.background = isError ? '#b00020' : '#b8860b';
}
function hideWsBanner() {
if (!banner) return;
banner.style.display = 'none';
}
var PERMANENT_CLOSE_MESSAGES = {
4401: 'Sessão expirada ou usuário não autenticado. Faça login novamente e recarregue a página.',
4403: 'Seu usuário não tem permissão para acessar esta tela.',
4404: 'Sessão plenária não encontrada.'
};
// Antes, a conexão WS só era aberta condicionalmente a haver matéria em
// votação: sem isso, a tela nunca abria WebSocket enquanto nenhuma
// matéria estivesse aberta, e dependia inteiramente do poll (agora
// removido) para notar quando uma matéria abrisse. Conecta sempre que
// houver uma sessão (can_vote()/sessao_votacao() em sapl/painel/views.py
// só preenchem sessao_id quando existe uma SessaoPlenaria com votação
// aberta — com ou sem matéria aberta dentro dela) — abrir_votacao já
// dispara broadcast_dados_painel(). Sem sessao_id não há URL de WS
// válida pra montar (nenhuma sessão aberta agora é um estado normal,
// não uma falha de conexão), então nem tenta.
{% if sessao_id %}
(function() {
var wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var wsUrl = wsProtocol + '//' + window.location.host + '/ws/painel/{{ sessao_id }}/';
var MIN_RECONNECT_DELAY_MS = 1000;
var MAX_RECONNECT_DELAY_MS = 30000;
var reconnectDelayMs = MIN_RECONNECT_DELAY_MS;
function nextDelay() {
var base = Math.min(reconnectDelayMs * 2, MAX_RECONNECT_DELAY_MS);
return Math.round(base * (0.7 + Math.random() * 0.6));
}
function connect() {
var socket = new WebSocket(wsUrl);
socket.onopen = function() {
reconnectDelayMs = MIN_RECONNECT_DELAY_MS;
hideWsBanner();
verificaEstado();
};
socket.onmessage = function(event) {
var message;
try {
message = JSON.parse(event.data);
} catch (e) {
return;
}
if (message && message.type === 'data') {
verificaEstado();
}
};
socket.onclose = function(event) {
var permanentMessage = PERMANENT_CLOSE_MESSAGES[event.code];
if (permanentMessage) {
showWsBanner(permanentMessage, true);
return;
}
showWsBanner(
'Conexão em tempo real perdida. Verifique se o Redis e o ' +
'Daphne (ou "./manage.py runserver") estão em execução. ' +
'Tentando reconectar…',
false
);
setTimeout(connect, reconnectDelayMs);
reconnectDelayMs = nextDelay();
};
socket.onerror = function() {
socket.close();
};
}
connect();
})();
{% endif %}
})();
</script>

3
sapl/templates/painel/voto_individual_v2.html

@ -20,7 +20,6 @@
{% render_bundle 'voto-individual' 'css' %} {% render_bundle 'voto-individual' 'css' %}
</head> </head>
<body> <body>
<style>[v-cloak]{display:none}</style> <style>[v-cloak]{display:none}</style>
<input type="hidden" id="csrf-token" value="{{ csrf_token }}"> <input type="hidden" id="csrf-token" value="{{ csrf_token }}">
@ -34,7 +33,7 @@
{% render_bundle 'voto-individual' 'js' %} {% render_bundle 'voto-individual' 'js' %}
<script type="text/javascript"> <script type="text/javascript">
window.VOTO_INDIVIDUAL_URL = "{% url 'sapl.painel:voto_individual_v2' %}"; window.VOTO_INDIVIDUAL_URL = "{% url 'sapl.painel:voto_individual' %}";
window.VOTO_INDIVIDUAL_STATUS_URL = "{% url 'sapl.painel:voto_individual_status' %}"; window.VOTO_INDIVIDUAL_STATUS_URL = "{% url 'sapl.painel:voto_individual_status' %}";
</script> </script>
</body> </body>

1
sapl/templates/sessao/painel.html

@ -370,6 +370,7 @@ $(document).ready(function(){
alert("Erro ao verificar o Painel"); alert("Erro ao verificar o Painel");
}, },
success: function (data) { success: function (data) {
console.log(`verifica painel: ${data}`);
if (data['status']) { if (data['status']) {
botao_abrir.hide(); botao_abrir.hide();
botao_fechar.show(); botao_fechar.show();

14
sapl/templates/sessao/votacao/nominal.html

@ -127,13 +127,17 @@
$("#soma_votos").append("<div class='row'><div class='col-md-12'>Não: " + votos_nao + "</div></div>"); $("#soma_votos").append("<div class='row'><div class='col-md-12'>Não: " + votos_nao + "</div></div>");
$("#soma_votos").append("<div class='row'><div class='col-md-12'>Abstenções: " + votos_abstencao + "</div></div>"); $("#soma_votos").append("<div class='row'><div class='col-md-12'>Abstenções: " + votos_abstencao + "</div></div>");
$("#soma_votos").append("<div class='row'><div class='col-md-12'>Ainda não votaram: " + nao_votou + "</div></div>"); $("#soma_votos").append("<div class='row'><div class='col-md-12'>Ainda não votaram: " + nao_votou + "</div></div>");
var t = setTimeout(function(){
conta_votos()
}, 500);
} }
// Único disparo automático real removido: um setTimeout(conta_votos, 500)
// se re-agendava para sempre, sondando o próprio DOM a cada 500ms — a
// única coisa "polling" nesta tela (não os fetches abaixo, que são
// disparados por evento, não por temporizador). Substituído por dois
// gatilhos por evento: troca manual do operador num <select> (abaixo)
// e chegada de voto por WS (dentro de atualiza_votos_ao_vivo(), que já
// chama conta_votos() quando algo muda) — mais a chamada única no
// carregamento logo abaixo.
conta_votos(); conta_votos();
$(document).on('change', '[name=voto_parlamentar]', conta_votos);
window.onload = conta_votos();
$(window).on('beforeunload', function () { $(window).on('beforeunload', function () {
$("input[type=submit], input[type=button]").prop("disabled", "disabled"); $("input[type=submit], input[type=button]").prop("disabled", "disabled");

Loading…
Cancel
Save