mirror of https://github.com/interlegis/sapl.git
35 changed files with 2206 additions and 14572 deletions
@ -0,0 +1,3 @@ |
|||
docker run --rm -p 6379:6379 redis:7-alpine redis-server --save "" --appendonly no |
|||
|
|||
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application |
|||
@ -0,0 +1,38 @@ |
|||
import './scss/painel.scss' |
|||
import Vue from 'vue' |
|||
import { FormSelectPlugin } from 'bootstrap-vue' |
|||
import axios from 'axios' |
|||
|
|||
//TODO: incluir painel-controle dentro da app de painel, colocando rotas diferentes
|
|||
|
|||
axios.defaults.xsrfCookieName = 'csrftoken' |
|||
axios.defaults.xsrfHeaderName = 'X-CSRFToken' |
|||
|
|||
Vue.use(FormSelectPlugin) |
|||
|
|||
console.log('painel controle main.js carregado') |
|||
|
|||
const v = new Vue({ // eslint-disable-line
|
|||
delimiters: ['[[', ']]'], |
|||
el: '#painel-controle', |
|||
data () { |
|||
return { |
|||
sessao_plenaria: "74ª Sessão Ordinária da 1ª Sessão Legislativa da 18ª Legislatura", |
|||
message: "", |
|||
} |
|||
}, |
|||
|
|||
watch: {}, |
|||
|
|||
computed: { |
|||
|
|||
}, |
|||
|
|||
created () {}, |
|||
|
|||
methods: {}, |
|||
|
|||
mounted () { |
|||
console.log("Painel controle app mounted!") |
|||
} |
|||
}) |
|||
@ -0,0 +1,43 @@ |
|||
|
|||
.painel-principal { |
|||
background: #1c1b1b; |
|||
font-family: Verdana; |
|||
font-size: x-large; |
|||
.text-title { |
|||
color: #4fa64d; |
|||
margin: 0.5rem; |
|||
font-weight: bold; |
|||
} |
|||
.text-subtitle { |
|||
color: #459170; |
|||
font-weight: bold; |
|||
} |
|||
.data-hora { |
|||
font-size: 180%; |
|||
} |
|||
|
|||
.text-value { |
|||
color: white; |
|||
} |
|||
|
|||
.logo-painel { |
|||
max-width: 100%; |
|||
} |
|||
.painels { |
|||
flex-wrap: wrap; |
|||
} |
|||
.painel{ |
|||
margin-top: 1rem; |
|||
table { |
|||
width: 100%; |
|||
} |
|||
h2 { |
|||
margin-bottom: 0.5rem; |
|||
} |
|||
#votacao, #oradores_list { |
|||
text-align: left; |
|||
display: inline-block; |
|||
margin-bottom: 1rem; |
|||
} |
|||
} |
|||
} |
|||
@ -1 +1,295 @@ |
|||
import './scss/painel.scss' |
|||
|
|||
// main.js (Vue 2)
|
|||
import Vue from 'vue' |
|||
import Vuex from 'vuex' |
|||
import { mapState } from 'vuex'; |
|||
import { mapMutations } from 'vuex' |
|||
|
|||
import Cronometro from '../../components/painel/Cronometro.vue' |
|||
import CronometroList from '../../components/painel/CronometroList.vue' |
|||
import PainelHeader from '../../components/painel/PainelHeader.vue' |
|||
import PainelParlamentares from '../../components/painel/PainelParlamentares.vue' |
|||
import PainelOradores from '../../components/painel/PainelOradores.vue' |
|||
import PainelMateria from '../../components/painel/PainelMateria.vue' |
|||
import PainelResultado from '../../components/painel/PainelResultado.vue' |
|||
import alarm from '../../assets/audio/ring.mp3' |
|||
|
|||
// register components
|
|||
Vue.component('painel-cronometro', Cronometro) |
|||
Vue.component('painel-cronometro-list', CronometroList) |
|||
Vue.component('painel-header', PainelHeader) |
|||
Vue.component('painel-parlamentares', PainelParlamentares) |
|||
Vue.component('painel-oradores', PainelOradores) |
|||
Vue.component('painel-materia', PainelMateria) |
|||
Vue.component('painel-resultado', PainelResultado) |
|||
|
|||
// global store
|
|||
Vue.use(Vuex) |
|||
const store = new Vuex.Store({ |
|||
state: { |
|||
sessao_aberta: false, |
|||
painel_aberto: false, |
|||
mostrar_voto: false, |
|||
sessao: {}, |
|||
parlamentares: [], |
|||
oradores: [], |
|||
materia: {}, |
|||
resultado: {}, |
|||
message: '', |
|||
}, |
|||
mutations: { |
|||
sessaoStatus(state, value) { |
|||
state.sessao_aberta = value; |
|||
}, |
|||
painelStatus(state, value) { |
|||
state.painel_aberto = value; |
|||
}, |
|||
setParlamentares(state, parlamentares) { |
|||
state.parlamentares = parlamentares; |
|||
}, |
|||
updateParlamentares(state, votos_parlamentares) { |
|||
if (votos_parlamentares) { |
|||
state.parlamentares.forEach((p)=>{ |
|||
if (p.parlamentar_id in votos_parlamentares) { |
|||
p.voto = votos_parlamentares[p.parlamentar_id].voto |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
setOradores(state, oradores) { |
|||
state.oradores = oradores; |
|||
}, |
|||
setMateria(state, materia) { |
|||
state.materia = materia; |
|||
}, |
|||
setResultado(state, resultado) { |
|||
state.resultado = resultado; |
|||
}, |
|||
setMessage(state, message) { |
|||
state.message = message; |
|||
}, |
|||
setMostrarVoto(state, mostrar_voto) { |
|||
state.mostrar_voto = mostrar_voto; |
|||
} |
|||
}, |
|||
actions: {}, |
|||
getters: {} |
|||
}) |
|||
|
|||
const BACKOFF = 500 |
|||
const PING_INTERVAL = 30000 // 30s
|
|||
|
|||
new Vue({ |
|||
store, |
|||
el: '#painel', |
|||
delimiters: ['[[', ']]'], |
|||
data() { |
|||
return { |
|||
controllerId: null, |
|||
ws: null, |
|||
isOpen: false, |
|||
error: null, |
|||
pingTimer: null, |
|||
reconnectTimer: null, |
|||
} |
|||
}, |
|||
mounted() { |
|||
console.log('Painel principal mounted!') |
|||
// $el is guaranteed here
|
|||
const el = this.$el |
|||
// prefer data-attr; fallback to global if you set it
|
|||
this.controllerId = el.dataset.controllerId || window.controllerId |
|||
console.log(`ControllerId: ${this.controllerId}`) |
|||
this.connectWS() |
|||
|
|||
}, |
|||
computed: { |
|||
...mapState(["painel_aberto", "sessao_aberta"]), |
|||
canRender () { |
|||
return this.sessao_aberta && this.painel_aberto; |
|||
}, |
|||
}, |
|||
methods: { |
|||
...mapMutations(['sessaoStatus', 'painelStatus','setParlamentares', |
|||
'updateParlamentares', 'setOradores', 'setMateria', |
|||
'setResultado', 'setMessage', 'setMostrarVoto']), |
|||
wsURL() { |
|||
const proto = location.protocol === 'https:' ? 'wss' : 'ws' |
|||
return `${proto}://${location.host}/ws/painel/${this.controllerId}/` |
|||
}, |
|||
handleStopWatchEvent(data) { |
|||
console.log("Received a stopwatch update event"); |
|||
//TODO: check if action is valid
|
|||
//TODO: check if stopwatch ID is valid
|
|||
|
|||
const stopwatch = this.$refs[data.id] |
|||
//TODO: stopwatch has to buzz at 00:30 AND 00:00
|
|||
if (data.action == "start" || data.action == "stop") { |
|||
console.log(`Stopwatch Event: ${data}`) |
|||
stopwatch.handleStartStop(); |
|||
} else if (data.action == "reset") { |
|||
stopwatch.handleReset(); |
|||
} else if (data.action == "set") { |
|||
//TODO: check if time is passed and valid
|
|||
stopwatch.initialTime = stopwatch.time |
|||
stopwatch.time = stopwatch.time |
|||
} else { |
|||
console.log(`Invalid stopwatch action ${data.action}`); |
|||
return |
|||
} |
|||
|
|||
// TODO: check if WebSocket is open and handle submission errors
|
|||
// TODO: save stopwatch state on localStorage?
|
|||
// Return updated stopwatch status
|
|||
// "stopwatch": {
|
|||
// "type": "stopwatch.state",
|
|||
// "id": "sw:main",
|
|||
// "status": "running",
|
|||
// "started_at_ms": 1699990000123,
|
|||
// "elapsed_ms": 5320
|
|||
// }
|
|||
// TODO: change started_at and elapsed to milliseconds
|
|||
this.ws.send(JSON.stringify({type:'stopwatch.state', |
|||
id: stopwatch.id, |
|||
status: stopwatch.isRunning ? "running" : "stopped", |
|||
started_at_s: stopwatch.initialTime, |
|||
elapsed_s: stopwatch.initialTime - stopwatch.time, |
|||
})) |
|||
|
|||
}, |
|||
updateState(data) { |
|||
try { |
|||
// CONFIG GERAIS
|
|||
this.sessaoStatus(data.sessao_aberta); |
|||
this.painelStatus(data.painel_aberto); |
|||
this.setMostrarVoto(data.mostrar_voto); |
|||
|
|||
// PARLAMENTARES
|
|||
if (data.parlamentares) { |
|||
// pre-popula para Vuex capturar mudanca de estado de 'voto'
|
|||
data.parlamentares.forEach(p => p.voto = ''); |
|||
this.setParlamentares(data.parlamentares); |
|||
} |
|||
|
|||
// HEADER DO PAINEL
|
|||
// SESSAO_PLENARIA
|
|||
//TODO: group in a single SessaoPlenaria object
|
|||
const headerInstance = this.$refs.painelHeader; |
|||
//TODO: setup as child's props?
|
|||
headerInstance.sessao_plenaria = data.sessao.sessao_plenaria |
|||
headerInstance.sessao_plenaria_data = data.sessao.sessao_plenaria_data |
|||
headerInstance.sessao_plenaria_hora_inicio = data.sessao.sessao_plenaria_hora_inicio |
|||
headerInstance.brasao = data.sessao.brasao |
|||
|
|||
if (data.message) { |
|||
this.setMessage(data.message); |
|||
} |
|||
|
|||
// ORADORES
|
|||
if (data.oradores) { |
|||
this.setOradores(data.oradores); |
|||
} |
|||
|
|||
// MATERIA
|
|||
if (data.materia) { |
|||
this.setMateria(data.materia); |
|||
} |
|||
|
|||
// RESULTADO
|
|||
if (data.materia.resultado) { |
|||
this.setResultado(data.materia.resultado); |
|||
} |
|||
|
|||
if (data.materia.resultado.votos_parlamentares) { |
|||
this.updateParlamentares(data.materia.resultado.votos_parlamentares); |
|||
} |
|||
} catch (e) { |
|||
console.error('Error', e); |
|||
} |
|||
}, |
|||
connectWS() { |
|||
const url = this.wsURL() |
|||
this.ws = new WebSocket(url) |
|||
|
|||
this.ws.addEventListener('open', () => { |
|||
this.isOpen = true; |
|||
this.error = null |
|||
|
|||
console.log(`✅ WebSocket connected to ${url}`); |
|||
|
|||
// send an initial message to the server
|
|||
this.ws.send(JSON.stringify({ type: "echo", message: "Client connected" })); |
|||
|
|||
// ping keep-alive timer
|
|||
this.pingTimer = setInterval(() => { |
|||
const ping = JSON.stringify({ type: "ping", ts: Date.now()}); |
|||
console.log(`Sending ping ${ping}`) |
|||
// TODO: check if this.ws object is still usable!!!
|
|||
this.ws.send(ping); |
|||
}, PING_INTERVAL); |
|||
}) |
|||
|
|||
this.ws.addEventListener('message', (message) => { |
|||
try { |
|||
const data = JSON.parse(message.data) |
|||
console.debug(`${JSON.stringify(data)}`) |
|||
|
|||
if (data.type === 'data') { |
|||
this.updateState(data); |
|||
} else if (data.type == 'echo') { |
|||
console.log(`Received ack from server: ${JSON.stringify(data)}`); |
|||
} else if (data.type == 'ping') { |
|||
console.log(`Received ping from server: ${JSON.stringify(data)}`); |
|||
} else if (data.type == 'stopwatch.update') { |
|||
this.handleStopWatchEvent(data); |
|||
} |
|||
} catch (e) { |
|||
console.error('WS parse error:', e); |
|||
} |
|||
}) |
|||
|
|||
this.ws.addEventListener('close', (e) => { |
|||
console.log("❌ WebSocket closed:", e); |
|||
this.isOpen = false; |
|||
this.reconnectTimer = setTimeout(() => this.connectWS(), BACKOFF); |
|||
}) |
|||
|
|||
this.ws.addEventListener('error', (e) => { |
|||
this.error = e |
|||
console.error('❌ WebSocket error:', e) |
|||
}) |
|||
}, |
|||
closeWS() { |
|||
try { |
|||
console.log(`⚠️ Closing Websocket connection: ${this.wsURL}`); |
|||
this.ws && this.ws.close() |
|||
} catch (_) { |
|||
console.log("Error closing WS connection"); |
|||
} |
|||
}, |
|||
beforeDestroy() { |
|||
// Clear the interval before the component is destroyed
|
|||
if (this.pingTimer) { |
|||
clearInterval(this.pingTimer); |
|||
console.log('pingTimer Interval cleared'); |
|||
} |
|||
|
|||
if (this.reconnectTimer) { |
|||
clearTimeout(this.reconnectTimer); |
|||
console.log('reconnectTimer cleared'); |
|||
} |
|||
|
|||
this.closeWS(); |
|||
}, |
|||
changeFontSize(value) { |
|||
for (var name in this.$refs){ |
|||
if (name.startsWith("sw")) { |
|||
const cronometro = this.$refs[name] |
|||
cronometro.changeFontSize(value) |
|||
} |
|||
} |
|||
}, |
|||
startStopwatch() { if (this.isOpen) this.ws.send(JSON.stringify({ type:'notify', stopwatch:'start' })) }, |
|||
} |
|||
}) |
|||
|
|||
@ -0,0 +1,238 @@ |
|||
import './scss/votacao.scss' |
|||
import Vue from 'vue' |
|||
import { FormSelectPlugin } from 'bootstrap-vue' |
|||
import axios from 'axios' |
|||
|
|||
axios.defaults.xsrfCookieName = 'csrftoken' |
|||
axios.defaults.xsrfHeaderName = 'X-CSRFToken' |
|||
|
|||
Vue.use(FormSelectPlugin) |
|||
|
|||
console.log('votacao main.js carregado') |
|||
|
|||
const v = new Vue({ // eslint-disable-line
|
|||
delimiters: ['[[', ']]'], |
|||
el: '#votacao', |
|||
data () { |
|||
return { |
|||
votacao_aberta: true, |
|||
edit_votes: true, |
|||
disable_resultado: false, |
|||
resultado_selected: "", |
|||
observacoes: "", |
|||
error_message: "", |
|||
tipo_votacao: 2, |
|||
tipo_votacao_descricao: "Votação Nominal", |
|||
materia: "Projeto de Lei Ordinária nº 3 de 2025", |
|||
ementa: "Institui no Município de Pato Branco o Projeto Chá de Fralda Social. ", |
|||
parlamentares: [ |
|||
{ |
|||
"parlamentar_id": 197, |
|||
"nome_parlamentar": "Alexandre Zoche", |
|||
"filiacao": "PRD" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 196, |
|||
"nome_parlamentar": "Anne Cristine Gomes da Silva Cavali", |
|||
"filiacao": "PSD" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 194, |
|||
"nome_parlamentar": "Diogo Domingos Grando", |
|||
"filiacao": "PRD" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 186, |
|||
"nome_parlamentar": "Eduardo Albani Dala Costa", |
|||
"filiacao": "Republicanos" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 3, |
|||
"nome_parlamentar": "Fabricio Preis de Mello", |
|||
"filiacao": "PL" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 6, |
|||
"nome_parlamentar": "Joecir Bernardi", |
|||
"filiacao": "PSD" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 187, |
|||
"nome_parlamentar": "Lindomar Rodrigo Brandão", |
|||
"filiacao": "PP" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 195, |
|||
"nome_parlamentar": "Rafael Foss", |
|||
"filiacao": "União" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 11, |
|||
"nome_parlamentar": "Rodrigo José Correia", |
|||
"filiacao": "União" |
|||
}, |
|||
{ |
|||
"parlamentar_id": 192, |
|||
"nome_parlamentar": "Thania Maria Caminski Gehlen", |
|||
"filiacao": "PP" |
|||
} |
|||
], |
|||
//TODO: check if votos_parlamentares is null
|
|||
votos_parlamentares: { |
|||
186: { |
|||
"voto": "Não", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 186, |
|||
"parlamentar_nome": "Eduardo Albani Dala Costa" |
|||
}, |
|||
195: { |
|||
"voto": "Não", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 195, |
|||
"parlamentar_nome": "Rafael Foss" |
|||
}, |
|||
196: { |
|||
"voto": "Não", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 196, |
|||
"parlamentar_nome": "Anne Cristine Gomes da Silva Cavali" |
|||
}, |
|||
3: { |
|||
"voto": "Sim", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 3, |
|||
"parlamentar_nome": "Fabricio Preis de Mello" |
|||
}, |
|||
11: { |
|||
"voto": "Não", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 11, |
|||
"parlamentar_nome": "Rodrigo José Correia" |
|||
}, |
|||
194: { |
|||
"voto": "Não", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 194, |
|||
"parlamentar_nome": "Diogo Domingos Grando" |
|||
}, |
|||
197: { |
|||
"voto": "Não", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 197, |
|||
"parlamentar_nome": "Alexandre Zoche" |
|||
}, |
|||
6: { |
|||
"voto": "Não", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 6, |
|||
"parlamentar_nome": "Joecir Bernardi" |
|||
}, |
|||
187: { |
|||
"voto": "Abstenção", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 187, |
|||
"parlamentar_nome": "Lindomar Rodrigo Brandão" |
|||
}, |
|||
192: { |
|||
"voto": "Sim", |
|||
"materia_id": 31919, |
|||
"parlamentar_id": 192, |
|||
"parlamentar_nome": "Thania Maria Caminski Gehlen" |
|||
} |
|||
}, |
|||
options: [ |
|||
{ text: 'Sim', value: 'voto_sim' }, |
|||
{ text: 'Não', value: 'voto_nao' }, |
|||
{ text: 'Abstenção', value: 'abstencao' }, |
|||
{ text: 'Não Votou', value: 'nao_votou' }, |
|||
], |
|||
tipos_resultados: [ |
|||
{ |
|||
"id": 13, |
|||
"nome": "Aprovada a retirada de pauta" |
|||
}, |
|||
{ |
|||
"id": 10, |
|||
"nome": "Aprovada por dois terços" |
|||
}, |
|||
{ |
|||
"id": 2, |
|||
"nome": "Aprovada por maioria absoluta" |
|||
}, |
|||
{ |
|||
"id": 1, |
|||
"nome": "Aprovada por maioria simples - conforme o art. 37 do RI o presidente não vota" |
|||
}, |
|||
{ |
|||
"id": 8, |
|||
"nome": "Aprovada por maioria simples - conforme o art. 37 do RI o presidente votou pelo desempate" |
|||
}, |
|||
{ |
|||
"id": 15, |
|||
"nome": "Aprovada." |
|||
}, |
|||
{ |
|||
"id": 7, |
|||
"nome": "Empate - conforme o art. 37 do RI o presidente vota para desempate" |
|||
}, |
|||
{ |
|||
"id": 16, |
|||
"nome": "IMPROCEDENTE" |
|||
}, |
|||
{ |
|||
"id": 12, |
|||
"nome": "Leitura em Plenário" |
|||
}, |
|||
{ |
|||
"id": 17, |
|||
"nome": "PROCEDENTE" |
|||
}, |
|||
{ |
|||
"id": 11, |
|||
"nome": "Prejudicada" |
|||
}, |
|||
{ |
|||
"id": 5, |
|||
"nome": "Rejeitada" |
|||
}, |
|||
{ |
|||
"id": 14, |
|||
"nome": "Rejeitada a retirada de pauta" |
|||
}, |
|||
{ |
|||
"id": 9, |
|||
"nome": "Rejeitada por maioria simples - conforme o art. 37 do RI o presidente votou pelo desempate" |
|||
} |
|||
], |
|||
} |
|||
}, |
|||
|
|||
watch: {}, |
|||
|
|||
computed: { |
|||
total_votos() { |
|||
// TODO: use number index instead of string ("sim", "não") as keys.
|
|||
var groupedVotes = Map.groupBy(Object.values(this.votos_parlamentares), ({ voto }) => voto ) |
|||
// initialize total_votos
|
|||
total_votos = [ |
|||
{"tipo": "Sim", "total": 0}, |
|||
{"tipo": "Não", "total": 0}, |
|||
{"tipo": "Abstenção", "total": 0}, |
|||
{"tipo": "Não Votou", total: 0} |
|||
] |
|||
for (const [key, value] of groupedVotes.entries()) { |
|||
const index = total_votos.findIndex(item => item.tipo === key); |
|||
total_votos[index].total = value.length |
|||
} |
|||
return total_votos |
|||
} |
|||
}, |
|||
|
|||
created () {}, |
|||
|
|||
methods: {}, |
|||
|
|||
mounted () { |
|||
console.log("Votacao app mounted!") |
|||
} |
|||
}) |
|||
@ -0,0 +1,99 @@ |
|||
<template> |
|||
<div> |
|||
<audio |
|||
ref="player" |
|||
:src="audioSrc" |
|||
preload="auto" |
|||
></audio> |
|||
<span ref="time">{{ title }}: {{ formatTime(time) }}<br/></span> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'Cronometro', |
|||
props: ['id', 'title'], |
|||
data() { |
|||
return { |
|||
time: 300, |
|||
isRunning: false, |
|||
initialTime: 300, |
|||
intervalId: null, |
|||
audioSrc: require('@/assets/audio/ring.mp3'), |
|||
} |
|||
}, |
|||
mounted() { |
|||
console.log('Cronometro mounted'); |
|||
console.log(this.audioSrc); |
|||
this.$emit('child-mounted'); // Emit a custom event |
|||
}, |
|||
methods: { |
|||
changeFontSize(value) { |
|||
const el = this.$refs.time; |
|||
if (!el) return; |
|||
let fontSize = window.getComputedStyle(el).fontSize; |
|||
fontSize = parseFloat(fontSize); // safely convert "16px" → 16 |
|||
el.style.fontSize = (fontSize + value) + 'px'; |
|||
}, |
|||
handleStartStop() { |
|||
this.isRunning = !this.isRunning; |
|||
|
|||
if (this.isRunning) { |
|||
this.intervalId = setInterval(() => { |
|||
if (this.time > 0) { |
|||
this.time--; |
|||
// play buzz at 00:00:30 |
|||
if (this.time == 30000) { |
|||
this.playSound(); |
|||
} |
|||
} else { |
|||
this.isRunning = false; |
|||
clearInterval(this.intervalId); |
|||
this.playSound(); |
|||
} |
|||
}, 1000); |
|||
} else { |
|||
clearInterval(this.intervalId); |
|||
} |
|||
}, |
|||
|
|||
handleReset() { |
|||
this.isRunning = false; |
|||
clearInterval(this.intervalId); |
|||
this.time = this.initialTime; |
|||
}, |
|||
|
|||
playSound() { |
|||
const audio = this.$refs.player |
|||
if (!audio) return |
|||
|
|||
const playPromise = audio.play() |
|||
}, |
|||
|
|||
formatTime(seconds) { |
|||
const hrs = Math.floor(seconds / 3600); |
|||
const mins = Math.floor((seconds % 3600) / 60); |
|||
const secs = seconds % 60; |
|||
|
|||
if (hrs > 0) { |
|||
return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
|||
} |
|||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
|||
} |
|||
}, |
|||
watch: { |
|||
initialTime(newVal) { |
|||
if (!this.isRunning) { |
|||
this.time = newVal; |
|||
} |
|||
} |
|||
}, |
|||
beforeDestroy() { |
|||
clearInterval(this.intervalId); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* Add your own styles here */ |
|||
</style> |
|||
@ -0,0 +1,51 @@ |
|||
.<template> |
|||
<div class="col-md-6 text-left painel" v-if="canRender"> |
|||
<div class="d-flex align-items-left justify-content-left mb-2"> |
|||
<h2 class="text-subtitle mb-0">Cronômetros</h2> |
|||
</div> |
|||
<div class="text-value" id="box_cronometros"> |
|||
<Cronometro v-for="(title, idx) in titles" :key="idx" :title="title" :ref="'childRef_' + idx" @child-mounted="handleChildMounted"/> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { ref, onMounted } from 'vue'; |
|||
import { mapState } from 'vuex'; |
|||
import Cronometro from './Cronometro.vue'; |
|||
|
|||
export default { |
|||
name: 'CronometroList', |
|||
components: { |
|||
Cronometro, |
|||
}, |
|||
data() { |
|||
return { |
|||
titles: ["Discurso", "Aparte", "Questão de Ordem", "Considerações Finais"], |
|||
itemRefs: ref([]), // An array to store the refs |
|||
} |
|||
}, |
|||
mounted() { |
|||
console.log('CronometroList mounted'); |
|||
}, |
|||
computed: { |
|||
canRender () { |
|||
return this.sessao_aberta && this.painel_aberto; |
|||
}, |
|||
...mapState(["painel_aberto", "sessao_aberta"]) |
|||
}, |
|||
methods: { |
|||
handleStartStop() { |
|||
console.log("start/stop stopwatch"); |
|||
//console.log(this.$refs.itemRefs); |
|||
}, |
|||
handleChildMounted() { |
|||
console.log('ChildComponent has finished mounting in the parent!'); |
|||
// Perform actions in the parent that depend on the child being fully mounted |
|||
const childId = 0; |
|||
const childComponent = this.$refs['childRef_' + childId]; |
|||
childComponent[0].handleStartStop(); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,72 @@ |
|||
<template v-if="sessao_aberta"> |
|||
<div> |
|||
<div class="d-flex justify-content-center"> |
|||
<h1 id="sessao_plenaria" class="title text-title">{{ sessao_plenaria }} </h1> |
|||
</div> |
|||
<div class="row "> |
|||
<div class="col text-center"> |
|||
<span id="sessao_plenaria_data" class="text-value"> Data Início: {{ sessao_plenaria_data }} </span> |
|||
</div> |
|||
<div class="col text-center"> |
|||
<span id="sessao_plenaria_hora_inicio" class="text-value"> Hora Início: {{ sessao_plenaria_hora_inicio }} </span> |
|||
</div> |
|||
</div> |
|||
<div class="row justify-content-center"> |
|||
<div class="col-1"> |
|||
<img v-bind:src="brasao" id="logo-painel" class="logo-painel" alt=""/> |
|||
</div> |
|||
</div> |
|||
<div class="row justify-content-center"> |
|||
<h2 class="text-danger"><span id="message">{{ message }}</span></h2> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col text-center"><span class="text-value data-hora" id="date">{{ data_atual }}</span></div> |
|||
<div class="col text-center"><span class="text-value data-hora" id="relogio">{{ relogio }}</span></div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from 'vuex'; |
|||
export default { |
|||
name: 'PainelHeader', |
|||
props: { |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
sessao_plenaria: "Sessao Plenaria Teste", |
|||
sessao_plenaria_data: "22/10/2025", |
|||
sessao_plenaria_hora_inicio: "13:30", |
|||
brasao: "", |
|||
data_atual: "", |
|||
relogio: "", |
|||
currentDateTimeId: null, // stores the id returned by setInterval() |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
startCurrentDateTime() { |
|||
this.data_atual = moment().utcOffset(-3).format("DD/MM/YY"); // RECOVER UTC OFFSET!!!! |
|||
this.currentDateTimeId = setInterval(() => { |
|||
this.relogio = moment.utc().utcOffset(-3).format("HH:mm:ss"); |
|||
}, 500); |
|||
}, |
|||
beforeDestroy() { |
|||
// Clear the interval before the component is destroyed |
|||
if (this.currentDateTimeId) { |
|||
clearInterval(this.currentDateTimeId); |
|||
console.log('currentDateTimeId Interval cleared.'); |
|||
} |
|||
}, |
|||
}, |
|||
computed: { |
|||
...mapState(["sessao_aberta", "painel_aberto", "message", "mostrar_voto"]) |
|||
}, |
|||
mounted() { |
|||
console.log('PainelHeader component mounted'); |
|||
this.startCurrentDateTime(); |
|||
} |
|||
} |
|||
</script> |
|||
@ -0,0 +1,42 @@ |
|||
<template> |
|||
<div class="col-md-6 text-center painel" id="obs_materia_div" v-if="canRender"> |
|||
<h2 class="text-subtitle" id="mat_em_votacao">Matéria em Votação</h2> |
|||
<span id="materia_legislativa_texto" class="text-value">{{ materia.texto }}</span> |
|||
<br> |
|||
<span id="materia_legislativa_ementa" class="text-value">{{ materia.ementa }} </span> |
|||
<br> |
|||
<span id="observacao_materia" class="text-value">{{ materia.observacao }}</span> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from 'vuex'; |
|||
export default { |
|||
name: 'PainelMateria', |
|||
data() { |
|||
return { |
|||
/* |
|||
materia: { |
|||
texto: '', |
|||
ementa: '', |
|||
observacao: '', |
|||
} |
|||
*/ |
|||
}; |
|||
}, |
|||
mounted() { |
|||
console.log('PainelMateria mounted'); |
|||
}, |
|||
beforeDestroy() {}, |
|||
computed: { |
|||
canRender () { |
|||
return this.sessao_aberta && this.painel_aberto; |
|||
}, |
|||
...mapState(["painel_aberto", "sessao_aberta", "materia"]) |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* Optional styling */ |
|||
</style> |
|||
@ -0,0 +1,36 @@ |
|||
<template> |
|||
<div class="col-md-6 text-center painel" id="aparecer_oradores" v-if="canRender"> |
|||
<h2 class="text-subtitle">Oradores</h2> |
|||
<table id="oradores_list"> |
|||
<tr v-for="o in oradores" :key="o.ordem_pronunciamento"><td style="padding-right:20px; color:white"> |
|||
{{ o.ordem_pronunciamento }}º   {{ o.nome_parlamentar }}</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from 'vuex'; |
|||
export default { |
|||
name: 'PainelOradores', |
|||
data() { |
|||
return { |
|||
// oradores: [], |
|||
}; |
|||
}, |
|||
mounted() { |
|||
console.log('PainelOradores mounted'); |
|||
}, |
|||
beforeDestroy() {}, |
|||
computed: { |
|||
canRender () { |
|||
return this.sessao_aberta && this.painel_aberto; |
|||
}, |
|||
...mapState(["painel_aberto", "sessao_aberta", "oradores"]) |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* Optional styling */ |
|||
</style> |
|||
@ -0,0 +1,41 @@ |
|||
<template> |
|||
<div class="col-md-4" v-if="canRender"> |
|||
<div class="text-center painel"> |
|||
<h2 class="text-subtitle">Parlamentares</h2> |
|||
<span id="parlamentares" class="text-value text-center"></span> |
|||
<table id="parlamentares_list"> |
|||
<tr v-for="p in parlamentares" :key="p.parlamentar_id" class="text-value text-center"> |
|||
<td style="padding-right:20px; color:yellow" > {{ p.nome_parlamentar }}</td> |
|||
<td style="padding-right:20px; color:yellow"> {{ p.filiacao }}</td> |
|||
<td style="padding-right:20px; color:yellow" v-if="mostrar_voto">{{ p.voto }}</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from 'vuex'; |
|||
export default { |
|||
name: 'PainelParlamentares', |
|||
data() { |
|||
return { |
|||
//parlamentares: [], |
|||
}; |
|||
}, |
|||
mounted() { |
|||
console.log('PainelParlamentares mounted'); |
|||
}, |
|||
beforeDestroy() {}, |
|||
computed: { |
|||
canRender () { |
|||
return this.sessao_aberta && this.painel_aberto; |
|||
}, |
|||
...mapState(["painel_aberto", "sessao_aberta", "parlamentares", "mostrar_voto"]) |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* Optional styling */ |
|||
</style> |
|||
@ -0,0 +1,69 @@ |
|||
<template> |
|||
<div class="col-md-6 text-left painel" id="resultado_votacao_div" v-if="canRender"> |
|||
<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" v-on:click="changeFontSize(-1)"> |
|||
A- |
|||
</button> |
|||
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(1)"> |
|||
A+ |
|||
</button> |
|||
</div> |
|||
<div ref="votacao" id="box_votacao"> |
|||
<div id="votacao" class="text-value"> |
|||
<li>Sim: {{ resultado.numero_votos.votos_sim }}</li> |
|||
<li>Não: {{ resultado.numero_votos.votos_nao }}</li> |
|||
<li>Abstenções: {{ resultado.numero_votos.abstencoes }}</li> |
|||
<li>Presentes: {{ resultado.numero_votos.num_presentes }}</li> |
|||
<li>Total votos: {{ resultado.numero_votos.total_votos }}</li> |
|||
</div> |
|||
<div id="resultado_votacao" class="text-title">{{ resultado.resultado_votacao }}</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from 'vuex'; |
|||
export default { |
|||
name: 'PainelResultado', |
|||
data() { |
|||
return { |
|||
/* |
|||
resultado: { |
|||
numero_votos: { |
|||
votos_sim: 0, |
|||
votos_nao: 0, |
|||
abstencoes: 0, |
|||
total_votos: 0, |
|||
num_presentes: 0, |
|||
}, |
|||
resultado_votacao: '', |
|||
} |
|||
*/ |
|||
}; |
|||
}, |
|||
mounted() { |
|||
console.log('PainelResultado mounted'); |
|||
}, |
|||
beforeDestroy() {}, |
|||
computed: { |
|||
canRender () { |
|||
return this.sessao_aberta && this.painel_aberto; |
|||
}, |
|||
...mapState(["painel_aberto", "sessao_aberta", "resultado"]) |
|||
}, |
|||
methods: { |
|||
changeFontSize(value) { |
|||
const el = this.$refs.votacao; |
|||
if (!el) return; |
|||
let fontSize = window.getComputedStyle(el).fontSize; |
|||
fontSize = parseFloat(fontSize); // safely convert "16px" → 16 |
|||
el.style.fontSize = (fontSize + value) + 'px'; |
|||
}, |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* Optional styling */ |
|||
</style> |
|||
@ -0,0 +1,11 @@ |
|||
<template> |
|||
<div class="VotacaoNominal"> |
|||
<pre v-text="$attrs"/> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'Votacaonominal' |
|||
}; |
|||
</script> |
|||
File diff suppressed because it is too large
@ -0,0 +1,21 @@ |
|||
# sapl/asgi.py |
|||
import os |
|||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sapl.settings") |
|||
|
|||
import django |
|||
django.setup() |
|||
|
|||
from channels.routing import ProtocolTypeRouter, URLRouter |
|||
from channels.auth import AuthMiddlewareStack |
|||
from channels.http import AsgiHandler # Django 2.2 uses AsgiHandler, not django.core.asgi |
|||
from django.urls import path |
|||
|
|||
from sapl.painel.consumers import PainelConsumer, HealthConsumer |
|||
|
|||
application = ProtocolTypeRouter({ |
|||
"http": AsgiHandler(), |
|||
"websocket": AuthMiddlewareStack(URLRouter([ |
|||
# path("ws/painel/", PainelConsumer.as_asgi()), |
|||
path("ws/painel/<int:controller_id>/", PainelConsumer.as_asgi()), |
|||
])), |
|||
}) |
|||
@ -0,0 +1,54 @@ |
|||
|
|||
# Websockets |
|||
|
|||
Rodar o container antes de iniciar o SAPL: |
|||
|
|||
```commandline |
|||
sudo docker run --rm -p 6379:6379 redis:7-alpine redis-server --save "" --appendonly no |
|||
``` |
|||
|
|||
Executar o SAPL |
|||
|
|||
Instalar dependências do Websockets e Redis: |
|||
|
|||
```commandline |
|||
pip install -r requirements/dev-requirements.txt |
|||
``` |
|||
|
|||
Abrir um terminal e rodar `yarn` para servir as páginas VueJS: |
|||
|
|||
```commandline |
|||
yarn serve |
|||
``` |
|||
|
|||
Executar o SAPL (duas formas): |
|||
|
|||
DAPHNE: |
|||
|
|||
Em outro terminal, no diretório raiz, execute como Daphne abaixo: |
|||
```commandline |
|||
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application |
|||
``` |
|||
|
|||
Daphne é excelente para debugar a parte de WebSockets, pois contém melhores mensagens de erro e log. |
|||
O runserver geralmente só vai dar crash ou falhar ao enviar as mensagens via WebSocket. |
|||
|
|||
**MAS atenção: Daphne não faz reload automático após mudanças na página!** |
|||
|
|||
Para isso é que parar e reiniciar o Daphne ou usar `.manage.py runserver` |
|||
|
|||
|
|||
RUNSERVER: |
|||
Em outro terminal, no diretório raiz, execute como Daphne abaixo para debugar os websockets (melhores mensagens de log) |
|||
```commandline |
|||
./manage.py runserver |
|||
``` |
|||
|
|||
Logar no SAPL e acessar a página `http://localhost:8000/painel/v2` |
|||
|
|||
|
|||
Ferramentas: |
|||
|
|||
`wscat`: permite fazer chamadas a websockets via CLI, mas para acessar o WS endpoint do painel precisa estar autenticado. |
|||
|
|||
**Redis Insight:** GUI que é tipo um pgAdmin para o Redis; |
|||
@ -0,0 +1,201 @@ |
|||
import json |
|||
import json |
|||
import logging |
|||
import time |
|||
|
|||
from channels.db import database_sync_to_async |
|||
from channels.generic.websocket import AsyncWebsocketConsumer, AsyncJsonWebsocketConsumer |
|||
|
|||
from sapl.base.models import CasaLegislativa, AppConfig |
|||
from sapl.sessao.models import SessaoPlenaria, SessaoPresencasView, \ |
|||
SessaoOradoresView, SessaoMateriasVotacoesView |
|||
|
|||
|
|||
def get_materia_votacao(votacao): |
|||
return { |
|||
# TOO UGLY! FIX THIS if-else |
|||
"materia_id": votacao.materia.id if votacao and votacao.materia else "", |
|||
"texto": str(votacao.materia) if votacao and votacao.materia else "", |
|||
"ementa": votacao.materia.ementa if votacao and votacao.materia and votacao.materia.ementa else "", |
|||
"resultado": { |
|||
"resultado_votacao": votacao.resultado_votacao if votacao and votacao.resultado_votacao else "", |
|||
"resultado": votacao.resultado if votacao and votacao.resultado else "", |
|||
"numero_votos": votacao.numero_votos if votacao and votacao.numero_votos else {}, |
|||
"votos_parlamentares": votacao.votos_parlamentares if votacao and votacao.votos_parlamentares else [], |
|||
}, |
|||
} |
|||
|
|||
|
|||
def get_dados_painel(sessao_plenaria_id: int) -> dict: |
|||
app_config = AppConfig.objects.first() |
|||
sessao = SessaoPlenaria.objects.get(id=sessao_plenaria_id) |
|||
casa = CasaLegislativa.objects.first() |
|||
brasao = casa.logotipo.url \ |
|||
if app_config.mostrar_brasao_painel else None |
|||
|
|||
# Painel |
|||
# TODO: recuperar outra matéria quando não existir nenhuma materia_votacao aberta! |
|||
materia_votacao = SessaoMateriasVotacoesView.objects. \ |
|||
filter(sessao_plenaria_id=sessao_plenaria_id, votacao_aberta=True).first() |
|||
|
|||
if not materia_votacao: |
|||
return { |
|||
"type": "data", |
|||
"sessao_aberta": sessao.iniciada and not sessao.finalizada, |
|||
"painel_aberto": sessao.painel_aberto, |
|||
"mostrar_voto": app_config.mostrar_voto, |
|||
"message": "PAINEL ENCONTRA-SE FECHADO" if not sessao.painel_aberto else "", |
|||
"sessao": { |
|||
"sessao_plenaria_id": sessao.id, |
|||
"brasao": brasao, |
|||
"sessao_plenaria": str(sessao), |
|||
"sessao_plenaria_data": sessao.data_inicio.strftime("%d/%m/%Y"), |
|||
"sessao_plenaria_hora_inicio": sessao.hora_inicio, |
|||
"sessao_solene": sessao.tipo.nome == "Solene", |
|||
"sessao_finalizada": sessao.finalizada, |
|||
"tema_solene": sessao.tema_solene, |
|||
}, |
|||
"message": "Nenhuma matéria aberta para votação!", |
|||
} |
|||
|
|||
presentes = SessaoPresencasView.objects.filter(sessao_plenaria_id=sessao_plenaria_id, |
|||
etapa_sessao=materia_votacao.etapa_sessao, |
|||
ativo=True).values_list( |
|||
'parlamentar_id', |
|||
'nome_parlamentar', |
|||
'filiacao', ) |
|||
parlamentares = [dict(zip(['parlamentar_id', 'nome_parlamentar', 'filiacao'], p)) for p in presentes] |
|||
if materia_votacao and materia_votacao.numero_votos: |
|||
materia_votacao.numero_votos.update({"num_presentes": len(parlamentares)}) |
|||
|
|||
oradores = SessaoOradoresView.objects.filter(sessao_plenaria_id=sessao_plenaria_id, |
|||
etapa_sessao=materia_votacao.etapa_sessao).values_list( |
|||
'ordem_pronunciamento', |
|||
'nome_parlamentar', |
|||
) |
|||
oradores = [dict(zip(['ordem_pronunciamento', 'nome_parlamentar'], o)) for o in oradores] |
|||
|
|||
# TODO: recover stopwatch state from DB/Cache |
|||
stopwatch = { |
|||
"type": "stopwatch.state", |
|||
"id": "sw:main", |
|||
"status": "running", # "running" | "paused" | "stopped" |
|||
"started_at_ms": 1699990000123, # epoch ms when (re)started |
|||
"elapsed_ms": 5320 |
|||
} |
|||
|
|||
dados_sessao = { |
|||
"type": "data", |
|||
"sessao_aberta": sessao.iniciada and not sessao.finalizada, |
|||
"painel_aberto": sessao.painel_aberto, |
|||
"mostrar_voto": app_config.mostrar_voto, |
|||
"message": "PAINEL ENCONTRA-SE FECHADO" if not sessao.painel_aberto else "", |
|||
"sessao": { |
|||
"sessao_plenaria_id": sessao.id, |
|||
"brasao": brasao, |
|||
"sessao_plenaria": str(sessao), |
|||
"sessao_plenaria_data": sessao.data_inicio.strftime("%d/%m/%Y"), |
|||
"sessao_plenaria_hora_inicio": sessao.hora_inicio, |
|||
"sessao_solene": sessao.tipo.nome == "Solene", |
|||
"sessao_finalizada": sessao.finalizada, |
|||
"tema_solene": sessao.tema_solene, |
|||
}, |
|||
"parlamentares": parlamentares, |
|||
"oradores": oradores, |
|||
"materia": get_materia_votacao(materia_votacao), |
|||
"stopwatch": [stopwatch], # TODO: array of stopwatches |
|||
} |
|||
|
|||
print(json.dumps(dados_sessao, indent=4)) |
|||
return dados_sessao |
|||
|
|||
|
|||
class PainelConsumer(AsyncJsonWebsocketConsumer): |
|||
logger = logging.getLogger(__name__) |
|||
|
|||
# def __init__(self): |
|||
# self.group = set() |
|||
# self.controller_id = None |
|||
|
|||
async def connect(self): |
|||
# TODO: transformar prints em log messages |
|||
user = self.scope.get("user") |
|||
controller_id = self.scope["url_route"]["kwargs"]["controller_id"] |
|||
print(f"user: {user}, controller_id: {controller_id}") |
|||
|
|||
if not (user and user.is_authenticated): |
|||
self.logger.info(f"{user} is not authenticated user!") |
|||
await self.close(code=4401) # explicit, graceful close |
|||
return |
|||
|
|||
if not await self.user_can_view(user.id, controller_id): |
|||
await self.close(code=4403) |
|||
return |
|||
|
|||
self.controller_id = controller_id |
|||
self.group = f"controller_{controller_id}" |
|||
print(self.group) |
|||
await self.channel_layer.group_add(self.group, self.channel_name) |
|||
|
|||
await self.accept() |
|||
# await self.send_json({"type": "data", |
|||
# "text": "Connection established!"}) |
|||
|
|||
print("SENDING bootstrap DATA DO CONSUMER ") |
|||
print(get_dados_painel(controller_id)) |
|||
await self.send_json(get_dados_painel(controller_id)) |
|||
|
|||
async def disconnect(self, code): |
|||
if hasattr(self, "group"): |
|||
await self.channel_layer.group_discard(self.group, self.channel_name) |
|||
|
|||
# Called by server via channel_layer.group_send |
|||
# async def notify(self, event): |
|||
# # event: {"type": "notify", "data": {...}} |
|||
# await self.send_json(event) |
|||
|
|||
async def receive(self, text_data=None, bytes_data=None): |
|||
data = json.loads(text_data or "{}") |
|||
print("Received from client:", data) # TODO: turn into log messages |
|||
msg_type = data.get("type") |
|||
if msg_type == "ping": |
|||
print("PING") |
|||
await self.send_json({"type": "ping", "ts": time.time()}) |
|||
return |
|||
elif msg_type == "echo": |
|||
await self.send_json(data) |
|||
return |
|||
elif msg_type == "stopwatch.state": |
|||
print(data) |
|||
return |
|||
await self.send_json({"type": "error", "message": "Misformed message"}) |
|||
return |
|||
|
|||
async def stopwatch_update(self, event): |
|||
await self.send_json(event) |
|||
return |
|||
|
|||
@database_sync_to_async |
|||
def user_can_view(self, user_id, controller_id) -> bool: |
|||
# Replace with your ACL check (ORM must be in a sync wrapper) |
|||
# return Controller.objects.filter(id=controller_id, owners__id=user_id).exists() |
|||
return True |
|||
|
|||
|
|||
class HealthConsumer(AsyncWebsocketConsumer): |
|||
""" |
|||
WebSockets consumer that doesn"t require authentication to |
|||
debug with wscat (wscat -c ws://127.0.0.1:8000/ws/painel/) |
|||
""" |
|||
logger = logging.getLogger(__name__) |
|||
|
|||
async def connect(self): |
|||
try: |
|||
await self.accept() |
|||
await self.send(json.dumps({"ok": True})) |
|||
except Exception as e: |
|||
self.logger.exception("connect failed: %s", e) |
|||
# Let Channels close with 1011 if we got here |
|||
|
|||
async def receive(self, text_data=None, bytes_data=None): |
|||
await self.send(text_data or "") |
|||
@ -0,0 +1,265 @@ |
|||
# Generated by Django 2.2.28 on 2025-11-17 17:55 |
|||
|
|||
from django.db import migrations |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
dependencies = [ |
|||
('sessao', '0069_auto_20220919_1705'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.RunSQL( |
|||
""" |
|||
CREATE OR REPLACE VIEW sessao_presencas_view AS |
|||
-- |
|||
-- PRESENCAS DE PARLAMENTARES COM FILIACAO (SE HOUVER) |
|||
-- MANDATO DEVE ESTAR ATIVO NA LEGISLATIURA |
|||
-- PARLAMENTAR DEVE ESTAR ATIVO |
|||
-- |
|||
-- EXPEDIENTE |
|||
SELECT |
|||
p.id, |
|||
presenca.sessao_plenaria_id, |
|||
'expediente' AS etapa_sessao, |
|||
p.id as parlamentar_id, |
|||
p.nome_parlamentar as nome_parlamentar, |
|||
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao, |
|||
p.ativo |
|||
FROM sessao_sessaoplenariapresenca AS presenca |
|||
JOIN sessao_sessaoplenaria AS sp |
|||
ON presenca.sessao_plenaria_id = sp.id |
|||
JOIN parlamentares_parlamentar AS p |
|||
ON presenca.parlamentar_id = p.id |
|||
JOIN parlamentares_mandato AS m |
|||
ON p.id = m.parlamentar_id |
|||
AND m.legislatura_id = sp.legislatura_id |
|||
AND m.data_inicio_mandato <= sp.data_inicio |
|||
AND (m.data_fim_mandato IS NULL OR m.data_fim_mandato >= sp.data_inicio) |
|||
-- recupera uma filiacao partidaria, se existir |
|||
LEFT JOIN LATERAL ( |
|||
SELECT pa.sigla |
|||
FROM parlamentares_filiacao f |
|||
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
|||
WHERE f.parlamentar_id = p.id |
|||
AND f.data <= sp.data_inicio |
|||
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
|||
ORDER BY f.data DESC |
|||
LIMIT 1 |
|||
) AS af ON TRUE |
|||
WHERE p.ativo = TRUE |
|||
|
|||
UNION ALL |
|||
|
|||
-- ORDEM DO DIA |
|||
SELECT |
|||
p.id, |
|||
presenca.sessao_plenaria_id, |
|||
'ordemdia' AS etapa_sessao, |
|||
p.id as parlamentar_id, |
|||
p.nome_parlamentar as nome_parlamentar, |
|||
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao, |
|||
p.ativo |
|||
FROM sessao_presencaordemdia AS presenca |
|||
JOIN sessao_sessaoplenaria AS sp |
|||
ON presenca.sessao_plenaria_id = sp.id |
|||
JOIN parlamentares_parlamentar AS p |
|||
ON presenca.parlamentar_id = p.id |
|||
JOIN parlamentares_mandato AS m |
|||
ON p.id = m.parlamentar_id |
|||
AND m.legislatura_id = sp.legislatura_id |
|||
AND m.data_inicio_mandato <= sp.data_inicio |
|||
AND (m.data_fim_mandato IS NULL OR m.data_fim_mandato >= sp.data_inicio) |
|||
-- recupera uma filiacao partidaria, se existir |
|||
LEFT JOIN LATERAL ( |
|||
SELECT pa.sigla |
|||
FROM parlamentares_filiacao f |
|||
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
|||
WHERE f.parlamentar_id = p.id |
|||
AND f.data <= sp.data_inicio |
|||
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
|||
ORDER BY f.data DESC |
|||
LIMIT 1 |
|||
) AS af ON TRUE |
|||
WHERE p.ativo = TRUE |
|||
ORDER BY sessao_plenaria_id, etapa_sessao, nome_parlamentar |
|||
""" |
|||
), |
|||
migrations.RunSQL( |
|||
""" |
|||
-- |
|||
-- ORADORES DE ORDEMDIA/EXPEDIENTE |
|||
-- * PARLAMENTARES ATIVOS |
|||
-- |
|||
CREATE OR REPLACE VIEW sessao_oradores_view AS |
|||
SELECT od.id, |
|||
sessao_plenaria_id, |
|||
'ordemdia' as etapa_sessao, |
|||
numero_ordem as ordem_pronunciamento, |
|||
parlamentar_id, |
|||
nome_parlamentar, |
|||
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao |
|||
FROM sessao_oradorordemdia od |
|||
JOIN sessao_sessaoplenaria sp ON (od.sessao_plenaria_id = sp.id) |
|||
JOIN parlamentares_parlamentar p ON (od.parlamentar_id = p.id) |
|||
-- recupera uma filiacao partidaria, se existir |
|||
LEFT JOIN LATERAL ( |
|||
SELECT pa.sigla |
|||
FROM parlamentares_filiacao f |
|||
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
|||
WHERE f.parlamentar_id = p.id |
|||
AND f.data <= sp.data_inicio |
|||
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
|||
ORDER BY f.data DESC |
|||
LIMIT 1 |
|||
) AS af ON TRUE |
|||
AND p.ativo = TRUE |
|||
UNION ALL |
|||
SELECT ex.id, |
|||
sessao_plenaria_id, |
|||
'expediente' as etapa_sessao, |
|||
numero_ordem as ordem_pronunciamento, |
|||
parlamentar_id, |
|||
nome_parlamentar, |
|||
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao |
|||
FROM sessao_oradorexpediente ex |
|||
JOIN sessao_sessaoplenaria sp ON (ex.sessao_plenaria_id = sp.id) |
|||
JOIN parlamentares_parlamentar p on (ex.parlamentar_id = p.id) |
|||
-- recupera uma filiacao partidaria, se existir |
|||
LEFT JOIN LATERAL ( |
|||
SELECT pa.sigla |
|||
FROM parlamentares_filiacao f |
|||
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
|||
WHERE f.parlamentar_id = p.id |
|||
AND f.data <= sp.data_inicio |
|||
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
|||
ORDER BY f.data DESC |
|||
LIMIT 1 |
|||
) AS af ON TRUE |
|||
AND p.ativo = TRUE |
|||
ORDER BY sessao_plenaria_id, etapa_sessao, ordem_pronunciamento |
|||
""" |
|||
), |
|||
migrations.RunSQL( |
|||
""" |
|||
CREATE OR REPLACE VIEW sessao_materias_votacoes_view AS |
|||
-- |
|||
-- Votacao/Leitura de Materias |
|||
-- |
|||
-- EXPEDIENTE |
|||
WITH votacao_materias AS ( |
|||
SELECT em.sessao_plenaria_id, |
|||
em.id id, |
|||
'expediente' etapa_sessao, |
|||
em.numero_ordem, |
|||
em.materia_id, |
|||
tm.descricao||' nº '||ml.numero||' de '||ml.ano as materia_texto, |
|||
ml.ementa materia_ementa, |
|||
tipo_votacao, |
|||
CASE tipo_votacao |
|||
WHEN 1 THEN 'Simbólica' |
|||
WHEN 2 THEN 'Nominal' |
|||
WHEN 3 THEN 'Secreta' |
|||
WHEN 4 THEN 'Leitura' |
|||
ELSE '' |
|||
END as tipo_votacao_descricao, |
|||
resultado_votacao, |
|||
em.resultado, |
|||
numero_votos, |
|||
votos_parlamentares, |
|||
votacao_aberta |
|||
FROM sessao_expedientemateria em |
|||
JOIN materia_materialegislativa ml ON (em.materia_id = ml.id) |
|||
JOIN materia_tipomaterialegislativa tm ON (ml.tipo_id = tm.id) |
|||
LEFT JOIN sessao_registroleitura rl on (rl.expediente_id = em.id) |
|||
LEFT JOIN LATERAL ( |
|||
SELECT jsonb_build_object( |
|||
'votos_sim', coalesce(rv.numero_votos_sim, 0), |
|||
'votos_nao', coalesce(rv.numero_votos_nao, 0), |
|||
'abstencoes', coalesce(rv.numero_abstencoes, 0), |
|||
'total_votos', coalesce(rv.numero_votos_sim, 0) + coalesce(rv.numero_votos_nao, 0) + coalesce(rv.numero_abstencoes, 0) |
|||
) as numero_votos, |
|||
trv.nome resultado_votacao |
|||
FROM sessao_registrovotacao rv |
|||
JOIN sessao_tiporesultadovotacao trv on (rv.tipo_resultado_votacao_id = trv.id) |
|||
WHERE rv.expediente_id = em.id AND tipo_votacao != 4) rv ON TRUE |
|||
LEFT JOIN LATERAL ( |
|||
SELECT em.sessao_plenaria_id, |
|||
em.numero_ordem, |
|||
jsonb_agg(jsonb_build_object( |
|||
vp.parlamentar_id, |
|||
jsonb_build_object( |
|||
'materia_id', em.materia_id, |
|||
'parlamentar_id', vp.parlamentar_id, |
|||
'parlamentar_nome', p.nome_parlamentar, |
|||
'voto', vp.voto |
|||
)) ORDER BY p.nome_parlamentar) as votos_parlamentares |
|||
FROM sessao_votoparlamentar vp |
|||
JOIN parlamentares_parlamentar p ON (vp.parlamentar_id = p.id) |
|||
WHERE vp.expediente_id = em.id AND em.tipo_votacao != 4 |
|||
GROUP BY em.sessao_plenaria_id, em.numero_ordem |
|||
ORDER BY em.sessao_plenaria_id, em.numero_ordem |
|||
) vp ON TRUE |
|||
|
|||
UNION ALL |
|||
|
|||
-- ORDEM DIA |
|||
SELECT od.sessao_plenaria_id, |
|||
od.id id, |
|||
'ordemdia' etapa_sessao, |
|||
od.numero_ordem, |
|||
od.materia_id, |
|||
tm.descricao||' nº '||ml.numero||' de '||ml.ano as materia_texto, |
|||
ml.ementa, |
|||
tipo_votacao, |
|||
CASE tipo_votacao |
|||
WHEN 1 THEN 'Simbólica' |
|||
WHEN 2 THEN 'Nominal' |
|||
WHEN 3 THEN 'Secreta' |
|||
WHEN 4 THEN 'Leitura' |
|||
ELSE '' |
|||
END as tipo_votacao_descricao, |
|||
resultado_votacao, |
|||
od.resultado, |
|||
numero_votos, |
|||
votos_parlamentares, |
|||
votacao_aberta |
|||
FROM sessao_ordemdia od |
|||
JOIN materia_materialegislativa ml ON (od.materia_id = ml.id) |
|||
JOIN materia_tipomaterialegislativa tm ON (ml.tipo_id = tm.id) |
|||
LEFT JOIN sessao_registroleitura rl on (od.id = rl.expediente_id) |
|||
LEFT JOIN LATERAL ( |
|||
SELECT jsonb_build_object( |
|||
'votos_sim', coalesce(rv.numero_votos_sim, 0), |
|||
'votos_nao', coalesce(rv.numero_votos_nao, 0), |
|||
'abstencoes', coalesce(rv.numero_abstencoes, 0), |
|||
'total_votos', coalesce(rv.numero_votos_sim, 0) + coalesce(rv.numero_votos_nao, 0) + coalesce(rv.numero_abstencoes, 0) |
|||
) as numero_votos, |
|||
trv.nome resultado_votacao |
|||
FROM sessao_registrovotacao rv |
|||
JOIN sessao_tiporesultadovotacao trv on (rv.tipo_resultado_votacao_id = trv.id) |
|||
WHERE rv.expediente_id = od.id AND tipo_votacao != 4) rv ON TRUE |
|||
LEFT JOIN LATERAL ( |
|||
SELECT od.sessao_plenaria_id, |
|||
od.numero_ordem, |
|||
jsonb_agg(jsonb_build_object( |
|||
vp.parlamentar_id, |
|||
jsonb_build_object( |
|||
'materia_id', od.materia_id, |
|||
'parlamentar_id', vp.parlamentar_id, |
|||
'parlamentar_nome', p.nome_parlamentar, |
|||
'voto', vp.voto |
|||
)) ORDER BY p.nome_parlamentar) as votos_parlamentares |
|||
FROM sessao_votoparlamentar vp |
|||
JOIN parlamentares_parlamentar p ON (vp.parlamentar_id = p.id) |
|||
WHERE vp.expediente_id = od.id AND od.tipo_votacao != 4 |
|||
GROUP BY od.sessao_plenaria_id, od.numero_ordem |
|||
ORDER BY od.sessao_plenaria_id, od.numero_ordem |
|||
) vp ON TRUE |
|||
) |
|||
SELECT * |
|||
FROM votacao_materias |
|||
ORDER BY sessao_plenaria_id, etapa_sessao, numero_ordem |
|||
""" |
|||
) |
|||
] |
|||
@ -0,0 +1,98 @@ |
|||
{% 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> |
|||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> |
|||
|
|||
{% 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"> |
|||
|
|||
{% block vue_content %} |
|||
|
|||
<style>[v-cloak]{display:none}</style> |
|||
<div id="painel" v-cloak class="col text-center" data-controller-id="{{ controller_id }}"> |
|||
|
|||
<painel-header ref="painelHeader"></painel-header> |
|||
|
|||
<div class="row justify-content-center"> |
|||
|
|||
<div class="d-flex justify-content-start"> |
|||
<painel-parlamentares ref="parlamentares"></painel-parlamentares> |
|||
|
|||
<div class="d-flex col-md-8 painels"> |
|||
<painel-oradores ref="oradores"></painel-oradores> |
|||
|
|||
<div class="col-md-6 text-left painel" v-if="canRender"> |
|||
<div class="d-flex align-items-left justify-content-left mb-2"> |
|||
<h2 class="text-subtitle mb-0">Cronômetros</h2> |
|||
</div> |
|||
<div class="text-value" id="box_cronometros"> |
|||
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(-1)"> |
|||
A- |
|||
</button> |
|||
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(1)"> |
|||
A+ |
|||
</button> |
|||
<painel-cronometro ref="sw:discurso" id="sw:discurso" title="Discurso"></painel-cronometro> |
|||
<painel-cronometro ref="sw:aparte" id="sw:aparte" title="Aparte"></painel-cronometro> |
|||
<painel-cronometro ref="sw:questao" id="sw:questao" title="Questão de Ordem"></painel-cronometro> |
|||
<painel-cronometro ref="sw:consideracoes" id="sw:consideracoes" title="Considerações Finais"></painel-cronometro> |
|||
</div> |
|||
</div> |
|||
<!-- <painel-cronometro-list ref="cronometro_list"></painel-cronometro-list>--> |
|||
|
|||
<painel-resultado ref="resultado"></painel-resultado> |
|||
|
|||
<painel-materia ref="materia"></painel-materia> |
|||
|
|||
<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> |
|||
|
|||
{% endblock %} |
|||
</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 %} |
|||
</html> |
|||
@ -0,0 +1,106 @@ |
|||
{% extends "crud/detail.html" %} |
|||
{% load i18n %} |
|||
{% load staticfiles %} |
|||
{% load common_tags %} |
|||
{% load render_bundle from webpack_loader %} |
|||
{% load webpack_static from webpack_loader %} |
|||
|
|||
{% block actions %} {% endblock %} |
|||
|
|||
{% block title %} |
|||
{% endblock %} |
|||
|
|||
{% block vue_content %} |
|||
<div id="painel-controle"> |
|||
<h1 class="page-header"> |
|||
Painel Eletrônico <small>([[ sessao_plenaria ]])</small> |
|||
</h1> |
|||
[[ message ]] |
|||
<div class="row"> |
|||
<div class="col-md-6"><a href="" onclick="window.open('{% url 'sapl.painel:painel_principal' pk %}','Comprovante','width=800, height=800, scrollbars=yes'); return false;" class="btn btn-primary btn-sm active">Iniciar painel completo</a></div> |
|||
<div class="col-md-3"><button onclick="switch_painel(true)" id="id_abrir_painel" class="btn btn-primary btn-sm active" style="display: none">Abrir Painel</button></div> |
|||
<div class="col-md-3"><button onclick="switch_painel(false)" id="id_fechar_painel" class="btn btn-danger btn-sm active" style="display: none;">Fechar Painel</button></div> |
|||
</div> |
|||
<br /> |
|||
|
|||
<h1>Operação do Painel Eletrônico</h1> |
|||
<h2><span id="relogio"></span></h2> |
|||
<br /> |
|||
<div class="row"> |
|||
<div class="col-md-12 mb-2"><h3>Cronômetro do Discurso</h3></div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-2"><input size="2" id="discurso" name="discurso" value="" readyonly="true" class="form-control"></div> |
|||
</div> |
|||
<br /> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-6"><button type="button" id="discursoStart" class="btn btn-success">Iniciar</button></div> |
|||
<div class="col-md-6"><button type="button" id="discursoReset" class="btn btn-success">Reiniciar</button></div> |
|||
</div> |
|||
|
|||
<br /><br > |
|||
<div class="row"> |
|||
<div class="col-md-12 mb-2"><h3>Cronômetro do Aparte</h3></div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-2"><input size="2" id="aparte" name="aparte" value="" readyonly="true" class="form-control"></div> |
|||
</div> |
|||
<br /> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-6"><button type="button" id="aparteStart" class="btn btn-success">Iniciar</button></div> |
|||
<div class="col-md-6"><button type="button" id="aparteReset" class="btn btn-success" class="btn btn-success">Reiniciar</button></div> |
|||
</div> |
|||
|
|||
<br /><br > |
|||
<div class="row"> |
|||
<div class="col-md-12 mb-2"><h3>Cronômetro da Questão de Ordem </h3></div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-2"><input size="2" id="ordem" name="ordem" value="" readyonly="true" class="form-control"></div> |
|||
</div> |
|||
<br /> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-6"><button type="button" id="ordemStart" class="btn btn-success">Iniciar</button></div> |
|||
<div class="col-md-6"><button type="button" id="ordemReset" class="btn btn-success">Reiniciar</button></div> |
|||
</div> |
|||
|
|||
<br/> |
|||
<br/> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-12 mb-2"><h3>Cronômetro de Considerações Finais</h3></div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-4"><input size="2" id="consideracoes" name="consideracoes" value="" readyonly="true" class="form-control"></div> |
|||
</div> |
|||
<br /> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-6"><button type="button" id="consideracoesStart" class="btn btn-success">Iniciar</button></div> |
|||
<div class="col-md-6"><button type="button" id="consideracoesReset" class="btn btn-success">Reiniciar</button></div> |
|||
</div> |
|||
<br /><br > |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-6"><button type="button" id="sinalSonoro" class="btn btn-success" onclick="document.getElementById('audio').play();">Sinal Sonoro</button></div> |
|||
</div> |
|||
</div> |
|||
{% endblock vue_content %} |
|||
|
|||
{% block webpack_loader_css %} |
|||
{{ block.super }} |
|||
{% render_bundle 'votacao' 'css' %} |
|||
{% endblock %} |
|||
|
|||
{% block webpack_loader_js %} |
|||
{% render_chunk_vendors 'js' %} |
|||
{% render_bundle 'global' 'js' %} |
|||
{% render_bundle 'painel-controle' 'js' %} |
|||
{% endblock %} |
|||
@ -0,0 +1,78 @@ |
|||
{% extends "crud/detail.html" %} |
|||
{% load i18n %} |
|||
{% load crispy_forms_tags cropping%} |
|||
{% load common_tags %} |
|||
{% load render_bundle from webpack_loader %} |
|||
{% load webpack_static from webpack_loader %} |
|||
|
|||
{% block vue_content %} |
|||
<style>[v-cloak]{display:none}</style> |
|||
<div id="votacao"> |
|||
<fieldset class="form-group"> |
|||
<legend>[[ tipo_votacao_descricao ]]</legend> |
|||
[[ error_message ]] |
|||
|
|||
<!-- Component 1 --> |
|||
<div> |
|||
<b>Matéria:</b> [[ materia ]] |
|||
<br/> |
|||
<b>Ementa:</b> [[ ementa ]] |
|||
<br/> |
|||
</div> |
|||
<br/> |
|||
<!-- Votacao Nominal --> |
|||
<fieldset class="form-group" v-if="tipo_votacao == 1"> |
|||
<legend><strong>Votos</strong></legend> |
|||
<div class="row" v-for="p in parlamentares"> |
|||
<div class="col-md-4" id="nome_parlamentar">[[ p.nome_parlamentar ]]</div> |
|||
<div class="col-md-2" id="filiacao">[[ p.filiacao ]]</div> |
|||
<div class="col-md-4" v-if="edit_votes"> |
|||
<span v-for="o in options" :key="o.value"> |
|||
<input type="radio" :id="o.value" :value="o.text" :name="p.parlamentar_id" v-model="votos_parlamentares[p.parlamentar_id].voto"/> |
|||
<label :for="o.value"> [[ o.text ]]</label> |
|||
|
|||
</span> |
|||
</div> |
|||
<div v-else> |
|||
[[ votos_parlamentares[p.parlamentar_id].voto ]] |
|||
</div> |
|||
</div> |
|||
<br/> |
|||
<br/> |
|||
</fieldset> |
|||
<legend><strong>Total de votos:</strong></legend> |
|||
<div id="total_votos" v-for="voto in total_votos"> |
|||
<div class='row'><div class='col-md-12'>[[ voto.tipo ]]: [[ voto.total ]]</div></div> |
|||
</div> |
|||
<br/> |
|||
<!-- component --> |
|||
<div class="col-md-6"> |
|||
<b>Resultado da Votação: </b> |
|||
<select v-model="resultado_selected" class="select form-control" :disabled="disable_resultado" > |
|||
<option :key="t.id" :value="t.id" v-for="t in tipos_resultados">[[ t.nome ]]</option> |
|||
</select> |
|||
</div> |
|||
<!-- component --> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
Observações: |
|||
<textarea v-model="observacoes" cols="10" rows="10" class="form-control"></textarea> |
|||
</div> |
|||
</div> |
|||
<div class="alert alert-info alert-dismissible " role="alert" v-else> |
|||
<div>Não existe nenhum parlamentar presente para que a votação ocorra.</div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
{% endblock vue_content %} |
|||
|
|||
{% block webpack_loader_css %} |
|||
{{ block.super }} |
|||
{% render_bundle 'votacao' 'css' %} |
|||
{% endblock %} |
|||
|
|||
{% block webpack_loader_js %} |
|||
{% render_chunk_vendors 'js' %} |
|||
{% render_bundle 'global' 'js' %} |
|||
{% render_bundle 'votacao' 'js' %} |
|||
{% endblock %} |
|||
@ -0,0 +1,6 @@ |
|||
# DEBUG |
|||
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application |
|||
|
|||
# Redis-CLI |
|||
redis-cli -h localhost -p 6379 |
|||
|
|||
Loading…
Reference in new issue