From ec9beee59e1efc5e054b60375776f30347221bcf Mon Sep 17 00:00:00 2001
From: Extroias <91105220+Extroias@users.noreply.github.com>
Date: Fri, 7 Aug 2026 00:15:40 -0300
Subject: [PATCH 01/19] feat:translation on some enums to facilitate frontend
use
---
backend/app/session/enums.py | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/backend/app/session/enums.py b/backend/app/session/enums.py
index aaa0116..59c0a4c 100644
--- a/backend/app/session/enums.py
+++ b/backend/app/session/enums.py
@@ -61,33 +61,33 @@ class ChairEvents(StrEnum):
# --- Additional Info ---
class DebateTypes(StrEnum):
- SPEAKERS_LIST = "Speakers List"
- MODERATED_DEBATE = "Moderated Debate" # During this type, the queue to speak should not be automatic
- UNMODERATED_DEBATE = "Unmoderated Debate"
+ SPEAKERS_LIST = "Lista de Discursos"
+ MODERATED_DEBATE = "Debate Moderado" # During this type, the queue to speak should not be automatic
+ UNMODERATED_DEBATE = "Debate não Moderado"
class Motions(StrEnum):
CHANGE_DEBATE_TYPE = "Mudar Tipo de Debate"
- POSTPONE_SESSION = "Adiaamento de Sessão"
- REOPEN_SESSION = "Reabrir Sessão"
+ POSTPONE_SESSION = "Adiamento de Sessão"
+ REOPEN_SESSION = "Reabertura de Sessão"
TOUR_DE_TABLE = "Tour de Table"
END_DEBATE = "Encerramento de Debate" # TODO: map this out since "motion to close debate" means clear GSL and go to voting procedures in modeldiplomat and can also mean the same as "motion to move into voting procedures"
VOTE_AMENDMENT = "Votação de Emenda" # TODO: check the way this is used, since amendments MUST be voted if they're present during VOTING_PROCEDURES
VOTE_BY_ROLL_CALL = "Votação por Chamada" # TODO: check the way this is used
CLOSE_SPEAKERS_LIST = "Fechamento da Lista de Discursos"
- REOPEN_SPEAKERS_LIST = "Reabrir a Lista de Discursos"
- SPLIT_PROPOSAL = "Divisão de Proposta"
- INTRODUCE_RESOLUTION_PROPOSAL = "Introdução de Proposta de Resolução"
- INTRODUCE_AMENDMENT_PROPOSAL = "Introdução de Proposta de Emenda"
+ REOPEN_SPEAKERS_LIST = "Reabertura de Lista de Discursos"
+ SPLIT_PROPOSAL = "Divisão da Proposta"
+ INTRODUCE_RESOLUTION_PROPOSAL = "Introdução da Proposta de Resolução"
+ INTRODUCE_AMENDMENT_PROPOSAL = "Introdução da Proposta de Emenda"
CHANGE_TOPIC = "Mudança de Tópico"
- QUORUM = "Quórum"
+ QUORUM = "Contagem de Quórum"
CUSTOM_MOTION = "" # not implemented
class Questions(StrEnum):
- ORDER = "Order"
- QUESTION = "Question"
- PERSONAL_PRIVILEGE = "Personal Privilege"
+ ORDER = "Ordem"
+ QUESTION = "Questão"
+ PERSONAL_PRIVILEGE = "Privilégio Pessoal"
class RollCallChoice(StrEnum):
From 23012cfc4de004018a1360a2d59d5069c51edf0c Mon Sep 17 00:00:00 2001
From: Extroias <91105220+Extroias@users.noreply.github.com>
Date: Fri, 7 Aug 2026 00:17:15 -0300
Subject: [PATCH 02/19] fix: removed redundant delegate from the delegates
payload
---
backend/app/session/schemas.py | 2 --
frontend/src/schemas/types.gen.ts | 39 +++++++++++--------------------
2 files changed, 13 insertions(+), 28 deletions(-)
diff --git a/backend/app/session/schemas.py b/backend/app/session/schemas.py
index 239a93c..7c1ef50 100644
--- a/backend/app/session/schemas.py
+++ b/backend/app/session/schemas.py
@@ -18,7 +18,6 @@ class SessionCreationSchema(BaseModel):
# TODO: refactor this to only reflect the payload received by delegates, with MotionModel being a separated entity
class DelegateMotionPayload(BaseModel):
type: enums.Motions
- delegate: int
debate_type: enums.DebateTypes | None = None
total_duration_minutes: int | None = None
@@ -30,7 +29,6 @@ class DelegateMotionPayload(BaseModel):
class DelegateQuestionPayload(BaseModel):
type: enums.Questions
- delegate: int
details: str | None = None
diff --git a/frontend/src/schemas/types.gen.ts b/frontend/src/schemas/types.gen.ts
index 6723ddf..9631fb1 100644
--- a/frontend/src/schemas/types.gen.ts
+++ b/frontend/src/schemas/types.gen.ts
@@ -271,9 +271,9 @@ export type DebateContext = {
* DebateTypes
*/
export const DebateTypes = {
- SPEAKERS_LIST: 'Speakers List',
- MODERATED_DEBATE: 'Moderated Debate',
- UNMODERATED_DEBATE: 'Unmoderated Debate'
+ LISTA_DE_DISCURSOS: 'Lista de Discursos',
+ DEBATE_MODERADO: 'Debate Moderado',
+ DEBATE_NÃO_MODERADO: 'Debate não Moderado'
} as const;
/**
@@ -304,10 +304,6 @@ export type DelegateEvents = typeof DelegateEvents[keyof typeof DelegateEvents];
*/
export type DelegateMotionPayload = {
type: Motions;
- /**
- * Delegate
- */
- delegate: number;
debate_type?: DebateTypes | null;
/**
* Total Duration Minutes
@@ -332,10 +328,6 @@ export type DelegateMotionPayload = {
*/
export type DelegateQuestionPayload = {
type: Questions;
- /**
- * Delegate
- */
- delegate: number;
/**
* Details
*/
@@ -581,19 +573,19 @@ export type MotionContext = {
*/
export const Motions = {
MUDAR_TIPO_DE_DEBATE: 'Mudar Tipo de Debate',
- ADIAAMENTO_DE_SESSÃO: 'Adiaamento de Sessão',
- REABRIR_SESSÃO: 'Reabrir Sessão',
+ ADIAMENTO_DE_SESSÃO: 'Adiamento de Sessão',
+ REABERTURA_DE_SESSÃO: 'Reabertura de Sessão',
TOUR_DE_TABLE: 'Tour de Table',
ENCERRAMENTO_DE_DEBATE: 'Encerramento de Debate',
VOTAÇÃO_DE_EMENDA: 'Votação de Emenda',
VOTAÇÃO_POR_CHAMADA: 'Votação por Chamada',
FECHAMENTO_DA_LISTA_DE_DISCURSOS: 'Fechamento da Lista de Discursos',
- REABRIR_A_LISTA_DE_DISCURSOS: 'Reabrir a Lista de Discursos',
- DIVISÃO_DE_PROPOSTA: 'Divisão de Proposta',
- INTRODUÇÃO_DE_PROPOSTA_DE_RESOLUÇÃO: 'Introdução de Proposta de Resolução',
- INTRODUÇÃO_DE_PROPOSTA_DE_EMENDA: 'Introdução de Proposta de Emenda',
+ REABERTURA_DE_LISTA_DE_DISCURSOS: 'Reabertura de Lista de Discursos',
+ DIVISÃO_DA_PROPOSTA: 'Divisão da Proposta',
+ INTRODUÇÃO_DA_PROPOSTA_DE_RESOLUÇÃO: 'Introdução da Proposta de Resolução',
+ INTRODUÇÃO_DA_PROPOSTA_DE_EMENDA: 'Introdução da Proposta de Emenda',
MUDANÇA_DE_TÓPICO: 'Mudança de Tópico',
- QUÓRUM: 'Quórum',
+ CONTAGEM_DE_QUÓRUM: 'Contagem de Quórum',
'': ''
} as const;
@@ -651,9 +643,9 @@ export type QuestionContext = {
* Questions
*/
export const Questions = {
- ORDER: 'Order',
- QUESTION: 'Question',
- PERSONAL_PRIVILEGE: 'Personal Privilege'
+ ORDEM: 'Ordem',
+ QUESTÃO: 'Questão',
+ PRIVILÉGIO_PESSOAL: 'Privilégio Pessoal'
} as const;
/**
@@ -996,11 +988,6 @@ export type VotingContext = {
voting_registry?: {
[key: string]: VotingChoice;
};
- majority: MajorityTypes;
- /**
- * Veto Power
- */
- veto_power: boolean;
};
/**
From 1ed354fc2f746ec8a92bdf9b236b3d4831f7e758 Mon Sep 17 00:00:00 2001
From: Extroias <91105220+Extroias@users.noreply.github.com>
Date: Fri, 7 Aug 2026 00:17:44 -0300
Subject: [PATCH 03/19] fix: as operator ignores type safety, replaced by
satisfies
---
frontend/src/components/session/Agenda.tsx | 8 ++++----
.../session/bottom-bar-buttons/SessionButton.tsx | 6 +++---
.../session/bottom-bar-buttons/VoteButton.tsx | 4 ++--
frontend/src/components/session/delegation-map.tsx | 10 +++++-----
frontend/src/components/session/manual-quorum.tsx | 2 +-
frontend/src/components/session/speaker-list.tsx | 2 +-
frontend/src/components/session/timer.tsx | 4 ++--
frontend/src/components/session/voting-popup.tsx | 8 ++++----
8 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/frontend/src/components/session/Agenda.tsx b/frontend/src/components/session/Agenda.tsx
index a5ca987..a33278d 100644
--- a/frontend/src/components/session/Agenda.tsx
+++ b/frontend/src/components/session/Agenda.tsx
@@ -84,7 +84,7 @@ export default function Agenda() {
sendMessage(
- {type:ChairEvents.MARK_AGENDA_ITEM_EVENT, payload:{index: index,indiscussion:true}} as MarkAgendaItemEvent
+ {type:ChairEvents.MARK_AGENDA_ITEM_EVENT, payload:{index: index,indiscussion:true}} satisfies MarkAgendaItemEvent
)}>
@@ -96,7 +96,7 @@ export default function Agenda() {
sendMessage(
- {type:ChairEvents.MARK_AGENDA_ITEM_EVENT, payload:{index:index,discussed:true}} as MarkAgendaItemEvent
+ {type:ChairEvents.MARK_AGENDA_ITEM_EVENT, payload:{index:index,discussed:true}} satisfies MarkAgendaItemEvent
)}>
@@ -108,7 +108,7 @@ export default function Agenda() {
sendMessage(
- {type:ChairEvents.DELETE_AGENDA_ITEM_EVENT, payload:{index: index}} as DeleteAgendaItemEvent
+ {type:ChairEvents.DELETE_AGENDA_ITEM_EVENT, payload:{index: index}} satisfies DeleteAgendaItemEvent
)}>
@@ -145,7 +145,7 @@ export default function Agenda() {
() => {
if(numinput.current && topicinput.current) {
sendMessage({type:ChairEvents.SET_AGENDA_ITEM_EVENT,
- payload:{index:numinput.current.value, topic: topicinput.current.value}} as SetAgendaItemEvent)
+ payload:{index:numinput.current.value, topic: topicinput.current.value}} satisfies SetAgendaItemEvent)
numinput.current.value = ""
topicinput.current.value = ""
}
diff --git a/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx b/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx
index 448661c..8db02ea 100644
--- a/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx
+++ b/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx
@@ -51,13 +51,13 @@ export default function TestButton() {
{currentState === States.SETUP_ROOM &&
- sendMessage({ type: ChairEvents.OPEN_SESSION_EVENT, payload: {} } as OpenSessionEvent)}>Abrir Sessao e iniciar Quórum }
+ sendMessage({ type: ChairEvents.OPEN_SESSION_EVENT, payload: {} } satisfies OpenSessionEvent)}>Abrir Sessao e iniciar Quórum }
{currentState === States.ROLL_CALL &&
- sendMessage({ type: ChairEvents.CLOSE_ROLL_CALL_EVENT, payload: {} } as CloseRollCallEvent)}>
+ sendMessage({ type: ChairEvents.CLOSE_ROLL_CALL_EVENT, payload: {} } satisfies CloseRollCallEvent)}>
Fechar Quórum
}
{currentState !== States.SETUP_ROOM && currentState !== States.ROLL_CALL &&
- sendMessage({ type: ChairEvents.CLOSE_SESSION_EVENT, payload: {} } as CloseSessionEvent)}>
+ sendMessage({ type: ChairEvents.CLOSE_SESSION_EVENT, payload: {} } satisfies CloseSessionEvent)}>
Fechar Sessão
}
diff --git a/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx b/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx
index 2721088..49ca724 100644
--- a/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx
+++ b/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx
@@ -60,8 +60,8 @@ export default function VoteButton() {
- {sendMessage({type: ChairEvents.OPEN_INFORMAL_VOTING_EVENT, payload:{title: title.current?.value, veto_power: veto.current?.checked, majority: majority.current?.value}} as OpenInformalVotingEvent)}} className="w-full bg-green-800 text-white hover:bg-green-700">Iniciar Votação
- {sendMessage({type: ChairEvents.CLOSE_INFORMAL_VOTING_EVENT, payload:{} } as CloseInformalVotingEvent)}} className="w-full bg-red-800 text-white hover:bg-red-700">Fechar Votação
+ {sendMessage({type: ChairEvents.OPEN_INFORMAL_VOTING_EVENT, payload:{title: title.current?.value, veto_power: veto.current?.checked as boolean, majority: (majority.current?.value) as MajorityTypes}} satisfies OpenInformalVotingEvent)}} className="w-full bg-green-800 text-white hover:bg-green-700">Iniciar Votação
+ {sendMessage({type: ChairEvents.CLOSE_INFORMAL_VOTING_EVENT, payload:{} } satisfies CloseInformalVotingEvent)}} className="w-full bg-red-800 text-white hover:bg-red-700">Fechar Votação
diff --git a/frontend/src/components/session/delegation-map.tsx b/frontend/src/components/session/delegation-map.tsx
index 7d1e901..c4bfbc6 100644
--- a/frontend/src/components/session/delegation-map.tsx
+++ b/frontend/src/components/session/delegation-map.tsx
@@ -160,10 +160,10 @@ export default function DelegationMap({
Ações sobre a Delegação
- sendMessage({ type: ChairEvents.INSERT_QUEUE_EVENT, payload: { target: delegation.id } } as ChairInsertQueueEvent)}>
+ sendMessage({ type: ChairEvents.INSERT_QUEUE_EVENT, payload: { target: delegation.id } } satisfies ChairInsertQueueEvent)}>
Colocar na Lista de Discursos
- sendMessage({ type: ChairEvents.SPEAKER_EVENT, payload: { speaker_id: delegation.id } } as SpeakerEvent)}>
+ sendMessage({ type: ChairEvents.SPEAKER_EVENT, payload: { speaker_id: delegation.id } } satisfies SpeakerEvent)}>
Dar a palavra
@@ -173,13 +173,13 @@ export default function DelegationMap({
Mudar Presença
- sendMessage({ type: ChairEvents.MARK_ROLL_CALL_EVENT, payload: { delegation_id: delegation.id, choice: RollCallChoice.PRESENT_AND_VOTING } } as MarkRollCallEvent)}>
+ sendMessage({ type: ChairEvents.MARK_ROLL_CALL_EVENT, payload: { delegation_id: delegation.id, choice: RollCallChoice.PRESENT_AND_VOTING } } satisfies MarkRollCallEvent)}>
Presente Votante
- sendMessage({ type: ChairEvents.MARK_ROLL_CALL_EVENT, payload: { delegation_id: delegation.id, choice: RollCallChoice.PRESENT } } as MarkRollCallEvent)}>
+ sendMessage({ type: ChairEvents.MARK_ROLL_CALL_EVENT, payload: { delegation_id: delegation.id, choice: RollCallChoice.PRESENT } } satisfies MarkRollCallEvent)}>
Presente
- sendMessage({ type: ChairEvents.MARK_ROLL_CALL_EVENT, payload: { delegation_id: delegation.id, choice: RollCallChoice.ABSENT } } as MarkRollCallEvent)}>
+ sendMessage({ type: ChairEvents.MARK_ROLL_CALL_EVENT, payload: { delegation_id: delegation.id, choice: RollCallChoice.ABSENT } } satisfies MarkRollCallEvent)}>
Ausente
diff --git a/frontend/src/components/session/manual-quorum.tsx b/frontend/src/components/session/manual-quorum.tsx
index 0fa3589..5c15ff4 100644
--- a/frontend/src/components/session/manual-quorum.tsx
+++ b/frontend/src/components/session/manual-quorum.tsx
@@ -95,7 +95,7 @@ export default function ManualQuorum() {
{
- sendMessage({ type: ChairEvents.MARK_ROLL_CALL_BULK_EVENT, payload: { Rollcalls: tempQuorum } } as MarkRollCallBulkEvent)
+ sendMessage({ type: ChairEvents.MARK_ROLL_CALL_BULK_EVENT, payload: { Rollcalls: tempQuorum } } satisfies MarkRollCallBulkEvent)
}}>
Aceitar e Salvar Quorum
diff --git a/frontend/src/components/session/speaker-list.tsx b/frontend/src/components/session/speaker-list.tsx
index 33f4eac..6be1327 100644
--- a/frontend/src/components/session/speaker-list.tsx
+++ b/frontend/src/components/session/speaker-list.tsx
@@ -92,7 +92,7 @@ export default function SpeakerList() {
variant="outline"
className="flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap"
disabled={waitingCount === 0}
- onClick={() => sendMessage({ type: ChairEvents.SPEAKER_EVENT, payload: {} } as SpeakerEvent)}
+ onClick={() => sendMessage({ type: ChairEvents.SPEAKER_EVENT, payload: {} } satisfies SpeakerEvent)}
>
Proximo
Proximo Orador
diff --git a/frontend/src/components/session/timer.tsx b/frontend/src/components/session/timer.tsx
index a09c441..6693a67 100644
--- a/frontend/src/components/session/timer.tsx
+++ b/frontend/src/components/session/timer.tsx
@@ -73,10 +73,10 @@ const [Seconds, setRemainingSeconds] = useState(0);
{isChair && (
-
{sendMessage({type: ChairEvents.TOGGLE_TIMER_EVENT, payload: {}} as ToggleTimerEvent);}}>
+ {sendMessage({type: ChairEvents.TOGGLE_TIMER_EVENT, payload: {}} satisfies ToggleTimerEvent);}}>
{timerIsRunning ? : }
- sendMessage({type: ChairEvents.INCREASE_TIMER_EVENT, payload: { seconds: 5 }} as IncreaseTimerEvent)}> 5s
+ sendMessage({type: ChairEvents.INCREASE_TIMER_EVENT, payload: { seconds: 5 }} satisfies IncreaseTimerEvent)}> 5s
)}
diff --git a/frontend/src/components/session/voting-popup.tsx b/frontend/src/components/session/voting-popup.tsx
index a8038b2..b1fd9ad 100644
--- a/frontend/src/components/session/voting-popup.tsx
+++ b/frontend/src/components/session/voting-popup.tsx
@@ -49,7 +49,7 @@ export default function VotingPopup() {
{isRollCall1
? "Primeira rodada: Sua delegação pode votar com ou sem direitos. Caso deseje pular, podera votar apos todos, porem sem a possibilidade de pedir direitos"
- : "Segunda rodada: apenas as delegações que pularam na primeira rodada podem votar, sem direitos."}
+ : "Segunda rodada: apenas satisfies delegações que pularam na primeira rodada podem votar, sem direitos."}
)}
@@ -77,11 +77,11 @@ export default function VotingPopup() {
{setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} as CastVoteEvent)}}
+ () => {setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
className="flex-1 bg-green-800 text-white hover:bg-green-700">A Favor
{setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} as CastVoteEvent)}}
+ () => {setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
disabled={isRollCall && !canAbstain}
className="flex-1 bg-gray-700 text-white hover:bg-gray-600 disabled:cursor-not-allowed disabled:opacity-50"
>
@@ -92,7 +92,7 @@ export default function VotingPopup() {
Pular
)}
- {setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} as CastVoteEvent)}}
+ {setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
className="flex-1 bg-red-800 text-white hover:bg-red-700">Contra
From 5b5bdb488f8f4c3d500be97ff5a05786e36cd4bd Mon Sep 17 00:00:00 2001
From: Extroias <91105220+Extroias@users.noreply.github.com>
Date: Fri, 7 Aug 2026 00:18:02 -0300
Subject: [PATCH 04/19] feat:motions button now (mostly) works
---
.../bottom-bar-buttons/MotionsButton.tsx | 126 ++++++++++--------
1 file changed, 70 insertions(+), 56 deletions(-)
diff --git a/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx b/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx
index b1661ce..5de6b28 100644
--- a/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx
+++ b/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx
@@ -39,53 +39,42 @@ import {
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { useCommitteeStore } from "@/store/useCommitteeStore"
-import { States } from "@/schemas/types.gen"
+import { MajorityTypes, States } from "@/schemas/types.gen"
import { useSession } from "@/context/SessionContext"
import { SessionRoles } from "@/schemas/types.gen"
+import {
+ Motions,
+ Questions,
+ DebateTypes,
+ DelegateEvents,
+ type SubmitMotionEvent,
+ type SubmitQuestionEvent,
+ type DelegateQuestionPayload,
+ type DelegateMotionPayload } from "@/schemas/types.gen"
+import { sendMessage } from "@/context/SessionContext"
-const motions = [
- "Moção para Adiamento de Sessão",
- "Moção para Reabertura de Sessão",
- "Moção para Mudar Tipo de Debate",
- "Moção para Tour de Table",
- "Moção para Encerramento de Debate",
- "Moção para Votação de Emenda",
- "Moção para Fechamento de Lista de Discursos",
- "Moção para Reabertura de Lista de Discursos",
- "Moção para Divisão da Proposta",
- "Moção para Introdução da Proposta de Resolução",
- "Moção para Introdução de Proposta de Emenda",
- "Moção para Votação por Chamada",
- "Moção para contagem de Quórum",
-] as const
-
-const points = [
- "Questão de Privilégio Pessoal",
- "Questão de Ordem",
- "Questão de Dúvida",
-] as const
-
-const motionRequiredMajority: Record = {
- "Moção para Adiamento de Sessão": "Maioria simples",
- "Moção para Reabertura de Sessão": "Maioria simples",
- "Moção para Mudar Tipo de Debate": "Maioria simples",
- "Moção para Tour de Table": "Maioria simples",
- "Moção para Encerramento de Debate": "Maioria qualificada",
- "Moção para Votação de Emenda": "Maioria qualificada",
- "Moção para Fechamento de Lista de Discursos": "Maioria simples",
- "Moção para Reabertura de Lista de Discursos": "Maioria simples",
- "Moção para Divisão da Proposta": "Maioria simples",
- "Moção para Introdução da Proposta de Resolução": "Maioria simples",
- "Moção para Introdução de Proposta de Emenda": "Maioria simples",
- "Moção para Votação por Chamada": "Maioria simples",
- "Moção para contagem de Quórum": "Maioria simples",
+const motionRequiredMajority: Record = {
+ [Motions.ADIAMENTO_DE_SESSÃO]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.REABERTURA_DE_SESSÃO]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.MUDAR_TIPO_DE_DEBATE]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.TOUR_DE_TABLE]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.ENCERRAMENTO_DE_DEBATE]: MajorityTypes.MAIORIA_QUALIFICADA,
+ [Motions.VOTAÇÃO_DE_EMENDA]: MajorityTypes.MAIORIA_QUALIFICADA,
+ [Motions.FECHAMENTO_DA_LISTA_DE_DISCURSOS]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.REABERTURA_DE_LISTA_DE_DISCURSOS]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.DIVISÃO_DA_PROPOSTA]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.INTRODUÇÃO_DA_PROPOSTA_DE_RESOLUÇÃO]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.INTRODUÇÃO_DA_PROPOSTA_DE_EMENDA]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.VOTAÇÃO_POR_CHAMADA]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.MUDANÇA_DE_TÓPICO]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions.CONTAGEM_DE_QUÓRUM]: MajorityTypes.MAIORIA_SIMPLES,
+ [Motions[""]]: "",
}
type MotionKind = "moção" | "questão"
-type DebateKind = "moderado" | "não moderado" | "lista de discursos" | ""
function QuestionsMotionsList(type: MotionKind) {
- return type === "moção" ? motions : points
+ return Object.values(type === "moção" ? Motions : Questions)
}
export default function TestButton() {
@@ -95,8 +84,9 @@ export default function TestButton() {
const currentState = useCommitteeStore((state) => state.current_state)
const [motionKind, setMotionKind] = useState("moção")
- const [selectedMotion, setSelectedMotion] = useState("")
- const [debateKindChange, setDebateKind] = useState("")
+ const [selectedMotion, setSelectedMotion] = useState("")
+ const [selectedQuestion, setSelectedQuestion] = useState("")
+ const [debateKindChange, setDebateKind] = useState("")
const [unmoderatedMinutes, setUnmoderatedMinutes] = useState("")
const [speechCount, setSpeechCount] = useState("")
const [minutesPerSpeech, setMinutesPerSpeech] = useState("")
@@ -105,14 +95,33 @@ export default function TestButton() {
const [answerText, setAnswerText] = useState("")
const motionOptions = QuestionsMotionsList(motionKind)
- const showDebateKindField = selectedMotion === "Moção para Mudar Tipo de Debate"
- const showUnmoderatedField = showDebateKindField && debateKindChange === "não moderado"
- const showModeratedFields = showDebateKindField && debateKindChange === "moderado"
+ const showDebateKindField = selectedMotion === Motions.MUDAR_TIPO_DE_DEBATE
+ const showUnmoderatedField = showDebateKindField && debateKindChange === DebateTypes.DEBATE_NÃO_MODERADO
+ const showModeratedFields = showDebateKindField && debateKindChange === DebateTypes.DEBATE_MODERADO
const showMotionDecision = motionKind === "moção" && selectedMotion.length > 0
const selectedMotionMajority = motionRequiredMajority[selectedMotion] ?? "Maioria não definida"
+ const motionBody : DelegateMotionPayload = {
+ type: selectedMotion,
+ ...(unmoderatedMinutes !== "" && {total_duration_minutes: Number(unmoderatedMinutes)}),
+ ...(minutesPerSpeech !== "" && {per_speaker_seconds: Number(minutesPerSpeech)}), //TODO: Fix inconsitency in minutes / seconds
+ ...(debateKindChange !== "" && {debate_type: debateKindChange}),
+ ...(minutesPerSpeech !== "" && {per_speaker_seconds: Number(minutesPerSpeech)}),
+ //TODO: add change topic
+ }
+
+ const questionBody : DelegateQuestionPayload | null =
+ selectedQuestion === "" ? null :
+ {
+ type: selectedQuestion,
+ details: questionText
+ }
+
+
+
const resetMotionFields = () => {
setSelectedMotion("")
+ setSelectedQuestion("")
setDebateKind("")
setUnmoderatedMinutes("")
setSpeechCount("")
@@ -199,8 +208,9 @@ export default function TestButton() {
{
- setSelectedMotion(value)
+ onValueChange={(value : any) => {
+ if(motionKind === "moção") setSelectedMotion(value)
+ else if(motionKind === "questão") setSelectedQuestion(value)
setDebateKind("")
setUnmoderatedMinutes("")
setSpeechCount("")
@@ -215,11 +225,13 @@ export default function TestButton() {
- {motionOptions.map((item) => (
+ {motionOptions.map((item) =>
+ {
+ if(item !== "") return (
{item}
- ))}
+ )})}
@@ -228,15 +240,15 @@ export default function TestButton() {
{showDebateKindField && (
Para qual tipo de debate?
- setDebateKind(value as DebateKind)}>
+ setDebateKind(value as DebateTypes)}>
- Lista de Discursos
- Debate moderado
- Debate não moderado
+ Lista de Discursos
+ Debate moderado
+ Debate não moderado
@@ -305,7 +317,7 @@ export default function TestButton() {
)}
{showMotionDecision && !isChair && (
-
+ {sendMessage({type:DelegateEvents.SUBMIT_MOTION_EVENT, payload: motionBody} satisfies SubmitMotionEvent)}} className="flex-1 bg-green-800 text-white hover:bg-green-700" type="button">
Enviar moção
@@ -321,6 +333,7 @@ export default function TestButton() {
onChange={(event) => setQuestionText(event.target.value)}
/>
+ { isChair && (
Digite a resposta
setAnswerText(event.target.value)}
/>
-
-
- Registrar questão e resposta
+ )
+ }
+ {if(questionBody)sendMessage({type:DelegateEvents.SUBMIT_QUESTION_EVENT, payload: questionBody} satisfies SubmitQuestionEvent)}} className="w-full bg-green-800 text-white hover:bg-green-700" type="button">
+ Registrar questão{isChair && " e resposta"}
>
)}
From 494968e913a1256ea3fc67d5ea65f450fed3afde Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Fri, 7 Aug 2026 12:59:37 -0300
Subject: [PATCH 05/19] revert(backend): revert VotingContext removal of
Majority Type
---
backend/app/session/enums.py | 3 +++
backend/app/session/models.py | 18 +++++++++++++-----
backend/app/tests/session/test_engine.py | 16 ++++++++--------
3 files changed, 24 insertions(+), 13 deletions(-)
diff --git a/backend/app/session/enums.py b/backend/app/session/enums.py
index 59c0a4c..c4d6c24 100644
--- a/backend/app/session/enums.py
+++ b/backend/app/session/enums.py
@@ -112,6 +112,9 @@ class VotingChoice(StrEnum):
FAVOUR = "Favour"
AGAINST = "Against"
ABSTAIN = "Abstain"
+ YES_WITH_RIGHTS = "Yes With Rights"
+ NO_WITH_RIGHTS = "No With Rights"
+ PASS = "Pass"
class VotingType(StrEnum):
diff --git a/backend/app/session/models.py b/backend/app/session/models.py
index e539e16..29e747c 100644
--- a/backend/app/session/models.py
+++ b/backend/app/session/models.py
@@ -59,19 +59,27 @@ class QuestionContext(BaseModel):
class VotingContext(BaseModel):
target_type: enums.VotingType
- motion_in_vote: MotionContext | None = None
title: str | None = None
return_state: enums.States
voting_registry: dict[int, enums.VotingChoice] = {}
+ majority: enums.MajorityTypes | None = None
+
+ motion_in_vote: MotionContext | None = None
+ # resolution_in_vote: ResolutionContext
+ allow_veto_power: bool = False
+
+ def is_choice_allowed(self, choice: enums.VotingChoice) -> bool:
+ if self.target_type == enums.VotingType.PROCEDURAL:
+ return choice in (enums.VotingChoice.FAVOUR, enums.VotingChoice.AGAINST)
+
+ return True
class DebateContext(BaseModel):
debate_type: enums.DebateTypes
return_state: enums.States
- total_duration_seconds: int | None = None # TODO: check if this is needed
- total_speeches: int | None = (
- None # Check if we use total duration or this for calculating overall time, it can also go overtime
- )
+ total_duration_seconds: int | None = None # check if its needed
+ total_speeches: int | None = None
per_speaker_seconds: int | None = None
expires_at: datetime | None = None
topic: str | None = None
diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py
index 295b7e6..21f4b70 100644
--- a/backend/app/tests/session/test_engine.py
+++ b/backend/app/tests/session/test_engine.py
@@ -13,7 +13,7 @@ def open_gsl_state(session_state: md.SessionLiveState) -> md.SessionLiveState:
@pytest.fixture
-def voting_state(session_state: md.SessionLiveState) -> md.SessionLiveState:
+def informal_voting_state(session_state: md.SessionLiveState) -> md.SessionLiveState:
session_state.current_state = enums.States.VOTING_EXECUTION
session_state.voting = md.VotingContext(
target_type=enums.VotingType.INFORMAL,
@@ -368,11 +368,11 @@ def test_delegate_cannot_leave_queue_when_not_queued(
def test_delegate_can_cast_vote(
engine: eng.SessionEngine,
- voting_state: md.SessionLiveState,
+ informal_voting_state: md.SessionLiveState,
cast_vote_event: sch.CastVoteEvent,
delegate_actor: md.SessionActor,
) -> None:
- state = engine.dispatch(voting_state, cast_vote_event, delegate_actor)
+ state = engine.dispatch(informal_voting_state, cast_vote_event, delegate_actor)
assert state.voting is not None
assert state.voting.voting_registry == {0: enums.VotingChoice.FAVOUR}
@@ -380,14 +380,14 @@ def test_delegate_can_cast_vote(
def test_delegate_cannot_cast_vote_twice(
engine: eng.SessionEngine,
- voting_state: md.SessionLiveState,
+ informal_voting_state: md.SessionLiveState,
cast_vote_event: sch.CastVoteEvent,
delegate_actor: md.SessionActor,
) -> None:
- engine.dispatch(voting_state, cast_vote_event, delegate_actor)
+ engine.dispatch(informal_voting_state, cast_vote_event, delegate_actor)
with pytest.raises(eng.InvalidProceduralMove, match="Already cast vote"):
- engine.dispatch(voting_state, cast_vote_event, delegate_actor)
+ engine.dispatch(informal_voting_state, cast_vote_event, delegate_actor)
def test_delegate_cannot_cast_vote_without_voting_context(
@@ -522,11 +522,11 @@ def test_delegate_cannot_open_informal_voting(
def test_chair_can_close_informal_voting(
engine: eng.SessionEngine,
- voting_state: md.SessionLiveState,
+ informal_voting_state: md.SessionLiveState,
close_informal_voting_event: sch.CloseInformalVotingEvent,
chair_actor: md.SessionActor,
) -> None:
- state = engine.dispatch(voting_state, close_informal_voting_event, chair_actor)
+ state = engine.dispatch(informal_voting_state, close_informal_voting_event, chair_actor)
assert state.current_state == enums.States.OPEN_GSL
assert state.voting is None
From 3a6cd810a634e5e78584c7de91d6c8caca64d215 Mon Sep 17 00:00:00 2001
From: Extroias <91105220+Extroias@users.noreply.github.com>
Date: Sun, 9 Aug 2026 01:31:28 -0300
Subject: [PATCH 06/19] feat:motions list now functional
---
.../src/components/session/motions-list.tsx | 34 ++++++-------------
frontend/src/schemas/types.gen.ts | 12 +++++--
2 files changed, 21 insertions(+), 25 deletions(-)
diff --git a/frontend/src/components/session/motions-list.tsx b/frontend/src/components/session/motions-list.tsx
index b81608c..d217a36 100644
--- a/frontend/src/components/session/motions-list.tsx
+++ b/frontend/src/components/session/motions-list.tsx
@@ -14,25 +14,15 @@ import { Badge } from "@/components/ui/badge"
import Flags from "@/components/ui/flags"
import { useSession } from "@/context/SessionContext"
import { SessionRoles } from "@/schemas/types.gen"
+import { useCommitteeStore } from "@/store/useCommitteeStore"
-export type Motion = {
- id: string
- timestamp: string
- title: string
- proposer: string
- proposerCode: string
- priority: number
-}
-
-type MotionsListProps = {
- motions: Motion[]
-}
-
-
-export default function MotionsList({ motions }: MotionsListProps) {
+export default function MotionsList() {
const {role} = useSession()
const isChair = role===SessionRoles.CHAIR
+ const motions = useCommitteeStore((state)=>state.submitted_motions)!
+ const delegations = useCommitteeStore((state)=>state.delegations)
+
const toMinutes = (time: string): number => {
const [hours, minutes] = time.split(":").map(Number)
if (Number.isNaN(hours) || Number.isNaN(minutes)) {
@@ -42,11 +32,9 @@ export default function MotionsList({ motions }: MotionsListProps) {
}
const sortedMotions = [...motions].sort((a, b) => {
- if (b.priority !== a.priority) {
- return b.priority - a.priority
- }
+ return b.priority! - a.priority!
- return toMinutes(a.timestamp) - toMinutes(b.timestamp)
+ //return toMinutes(a.timestamp) - toMinutes(b.timestamp)
})
const queueCount = sortedMotions.length
@@ -60,14 +48,14 @@ export default function MotionsList({ motions }: MotionsListProps) {
-
-
{motion.timestamp}
+ {"00:00"}
- {motion.title}
+ {motion.type}
-
- {motion.proposer}
+
+ {delegations[motion.delegate_id!].name}
diff --git a/frontend/src/schemas/types.gen.ts b/frontend/src/schemas/types.gen.ts
index 9631fb1..855745c 100644
--- a/frontend/src/schemas/types.gen.ts
+++ b/frontend/src/schemas/types.gen.ts
@@ -963,7 +963,10 @@ export type ValidationError = {
export const VotingChoice = {
FAVOUR: 'Favour',
AGAINST: 'Against',
- ABSTAIN: 'Abstain'
+ ABSTAIN: 'Abstain',
+ YES_WITH_RIGHTS: 'Yes With Rights',
+ NO_WITH_RIGHTS: 'No With Rights',
+ PASS: 'Pass'
} as const;
/**
@@ -976,7 +979,6 @@ export type VotingChoice = typeof VotingChoice[keyof typeof VotingChoice];
*/
export type VotingContext = {
target_type: VotingType;
- motion_in_vote?: MotionContext | null;
/**
* Title
*/
@@ -988,6 +990,12 @@ export type VotingContext = {
voting_registry?: {
[key: string]: VotingChoice;
};
+ majority?: MajorityTypes | null;
+ motion_in_vote?: MotionContext | null;
+ /**
+ * Allow Veto Power
+ */
+ allow_veto_power?: boolean;
};
/**
From 86de2c7563e1b0112de892cb810feb09237f4fab Mon Sep 17 00:00:00 2001
From: Extroias <91105220+Extroias@users.noreply.github.com>
Date: Sun, 9 Aug 2026 16:38:00 -0300
Subject: [PATCH 07/19] fix/feat: minor fixes and integration on speaker-list
and voting popup
---
.../src/components/session/speaker-list.tsx | 10 ++++++----
.../src/components/session/voting-popup.tsx | 18 +++++++++++-------
2 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/frontend/src/components/session/speaker-list.tsx b/frontend/src/components/session/speaker-list.tsx
index 6be1327..b07c3b5 100644
--- a/frontend/src/components/session/speaker-list.tsx
+++ b/frontend/src/components/session/speaker-list.tsx
@@ -16,20 +16,21 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip"
import { sendMessage } from "@/context/SessionContext"
-import { type SpeakerEvent, ChairEvents } from "@/schemas/types.gen"
+import { type JoinQueueEvent, type SpeakerEvent, ChairEvents, DelegateEvents } from "@/schemas/types.gen"
import { useSession } from "@/context/SessionContext"
import { SessionRoles } from "@/schemas/types.gen"
-const isAlredyInQueue = true // Replace with actual logic to determine if the user is already in the queue
//TODO determine if queue is open, if not obscure the button and show a message that the queue is closed
export default function SpeakerList() {
- const {role} = useSession()
+ const {role, representation_id} = useSession()
const isChair = role===SessionRoles.CHAIR
const gslQueue = useCommitteeStore((state) => state.gsl_queue ?? [])
const currentSpeaker = useCommitteeStore((state) => state.current_speaker)
const delegationsById = useCommitteeStore((state) => state.delegations)
+
+ const alreadyInQueue = representation_id ? gslQueue.includes(representation_id) : false
const queuedDelegations = gslQueue.flatMap((delegationId) => {
const delegation = delegationsById[String(delegationId)]
return delegation ? [delegation] : []
@@ -79,7 +80,8 @@ export default function SpeakerList() {
sendMessage({type:DelegateEvents.JOIN_QUEUE_EVENT, payload:{}} satisfies JoinQueueEvent)}
>
Se colocar na lista de oradores
diff --git a/frontend/src/components/session/voting-popup.tsx b/frontend/src/components/session/voting-popup.tsx
index b1fd9ad..814b633 100644
--- a/frontend/src/components/session/voting-popup.tsx
+++ b/frontend/src/components/session/voting-popup.tsx
@@ -16,8 +16,8 @@ import {
} from "@/components/ui/field"
import { useState } from "react"
import { useCommitteeStore } from "@/store/useCommitteeStore"
-import { sendMessage } from "@/context/SessionContext"
-import { DelegateEvents, VotingChoice, type CastVoteEvent } from "@/schemas/types.gen"
+import { sendMessage, useSession } from "@/context/SessionContext"
+import { DelegateEvents, SessionRoles, VotingChoice, type CastVoteEvent } from "@/schemas/types.gen"
type VoteType = "rollCall1" | "rollCall2" | "procedural" | "informal"
@@ -27,6 +27,9 @@ const voteType : VoteType = "procedural"
export default function VotingPopup() {
+ const {role, representation_id} = useSession()
+ const isChair = role == SessionRoles.CHAIR
+
//TODO: Implement rollcall voting
const isRollCall1 = voteType === "rollCall1"
const isRollCall2 = voteType === "rollCall2"
@@ -35,11 +38,12 @@ export default function VotingPopup() {
//TEMPORARY^^^^
const voting = useCommitteeStore((state) => state.voting ?? null)
+ const voted = voting && representation_id ? representation_id in voting.voting_registry! : true
const voteTitle = voting?.title
- const [voted, setVoted] = useState(false)
+ const [clicked, setClicked] = useState(false)
return (
-
+
Votação
@@ -77,11 +81,11 @@ export default function VotingPopup() {
{setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
+ () => {setClicked(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
className="flex-1 bg-green-800 text-white hover:bg-green-700">A Favor
{setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
+ () => {setClicked(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
disabled={isRollCall && !canAbstain}
className="flex-1 bg-gray-700 text-white hover:bg-gray-600 disabled:cursor-not-allowed disabled:opacity-50"
>
@@ -92,7 +96,7 @@ export default function VotingPopup() {
Pular
)}
- {setVoted(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
+ {setClicked(true); sendMessage({type:DelegateEvents.CAST_VOTE_EVENT, payload: {vote:VotingChoice.FAVOUR}} satisfies CastVoteEvent)}}
className="flex-1 bg-red-800 text-white hover:bg-red-700">Contra
From 78c2a9bbd099977f6655491c9afb780ed560aa3b Mon Sep 17 00:00:00 2001
From: Extroias <91105220+Extroias@users.noreply.github.com>
Date: Sun, 9 Aug 2026 16:38:27 -0300
Subject: [PATCH 08/19] feat:time-stamp on motions list, and now you can
deny/accept them
---
backend/app/session/engine.py | 1 +
backend/app/session/models.py | 1 +
.../src/components/session/motions-list.tsx | 28 +++++++++----------
frontend/src/schemas/types.gen.ts | 4 +++
4 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index e1705cc..f0b1207 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -278,6 +278,7 @@ def handle_submit_motion(
id=generate_next_motion_id(state),
priority=get_motion_priority(payload.type),
type=payload.type,
+ timestamp=datetime.now(UTC),
delegate_id=actor.delegation.id if actor.delegation is not None else None,
total_duration_minutes=payload.total_duration_minutes,
per_speaker_seconds=payload.per_speaker_seconds,
diff --git a/backend/app/session/models.py b/backend/app/session/models.py
index 29e747c..d92c888 100644
--- a/backend/app/session/models.py
+++ b/backend/app/session/models.py
@@ -40,6 +40,7 @@ class MotionContext(BaseModel):
priority: int = 0
type: enums.Motions
delegate_id: int | None = None
+ timestamp: datetime
debate_type: enums.DebateTypes | None = None
total_duration_minutes: int | None = None
diff --git a/frontend/src/components/session/motions-list.tsx b/frontend/src/components/session/motions-list.tsx
index d217a36..4b6f7b9 100644
--- a/frontend/src/components/session/motions-list.tsx
+++ b/frontend/src/components/session/motions-list.tsx
@@ -12,10 +12,11 @@ import {
} from "@/components/ui/item"
import { Badge } from "@/components/ui/badge"
import Flags from "@/components/ui/flags"
-import { useSession } from "@/context/SessionContext"
-import { SessionRoles } from "@/schemas/types.gen"
+import { sendMessage,useSession } from "@/context/SessionContext"
+import { SessionRoles, ChairEvents, type ResolveMotionEvent} from "@/schemas/types.gen"
import { useCommitteeStore } from "@/store/useCommitteeStore"
+
export default function MotionsList() {
const {role} = useSession()
const isChair = role===SessionRoles.CHAIR
@@ -23,18 +24,10 @@ export default function MotionsList() {
const motions = useCommitteeStore((state)=>state.submitted_motions)!
const delegations = useCommitteeStore((state)=>state.delegations)
- const toMinutes = (time: string): number => {
- const [hours, minutes] = time.split(":").map(Number)
- if (Number.isNaN(hours) || Number.isNaN(minutes)) {
- return Number.MAX_SAFE_INTEGER
- }
- return hours * 60 + minutes
- }
-
const sortedMotions = [...motions].sort((a, b) => {
- return b.priority! - a.priority!
+ if(b.priority !== a.priority) return b.priority! - a.priority!
- //return toMinutes(a.timestamp) - toMinutes(b.timestamp)
+ return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
})
const queueCount = sortedMotions.length
@@ -48,7 +41,10 @@ export default function MotionsList() {
-
-
{"00:00"}
+ {new Date(motion.timestamp).toLocaleTimeString('pt-BR', {
+ hour: '2-digit',
+ minute: '2-digit'
+})}
@@ -61,8 +57,10 @@ export default function MotionsList() {
{isChair && (
- Acatar
- Rejeitar
+ sendMessage({type:ChairEvents.RESOLVE_MOTION_EVENT, payload:{motion_id:motion.id!, action:true }} satisfies ResolveMotionEvent)}>Acatar
+ sendMessage({type:ChairEvents.RESOLVE_MOTION_EVENT, payload:{motion_id:motion.id!, action:false }} satisfies ResolveMotionEvent)}>Rejeitar
)}
diff --git a/frontend/src/schemas/types.gen.ts b/frontend/src/schemas/types.gen.ts
index 855745c..3ea3f68 100644
--- a/frontend/src/schemas/types.gen.ts
+++ b/frontend/src/schemas/types.gen.ts
@@ -549,6 +549,10 @@ export type MotionContext = {
* Delegate Id
*/
delegate_id?: number | null;
+ /**
+ * Timestamp
+ */
+ timestamp: string;
debate_type?: DebateTypes | null;
/**
* Total Duration Minutes
From ba18c8c7ddfb105dae7ed6bac9fa614b8b587be1 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 20:36:20 -0300
Subject: [PATCH 09/19] refactor(backend): split MotionEvent into Delegate and
Chair events
---
backend/app/session/engine.py | 101 ++++++++++++++++-------
backend/app/session/enums.py | 8 ++
backend/app/session/models.py | 11 +--
backend/app/session/schemas.py | 26 ++++--
backend/app/tests/session/test_engine.py | 46 +++++++++--
5 files changed, 144 insertions(+), 48 deletions(-)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index f0b1207..0d502c0 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -98,19 +98,10 @@ def generate_next_question_id(state: SessionLiveState) -> int:
return state._question_id_counter
-# TODO: map more things to be needed here
def validate_motion_payload(
- payload: schemas.DelegateMotionPayload, state: SessionLiveState
+ payload: schemas.MotionPayload, state: SessionLiveState
) -> None:
- """Should validate motion payload and correct it before submitting"""
- # can correct things
- if payload.target_topic is None:
- payload.target_topic = (
- state.agenda_topics[state.active_topic_index][0]
- if state.active_topic_index is not None
- and 0 <= state.active_topic_index < len(state.agenda_topics)
- else None
- )
+ """Should validate motion payload before submitting"""
# can also raise error if there are missing fields
if (
@@ -120,11 +111,6 @@ def validate_motion_payload(
raise InvalidProceduralMove("Cannot submit motion without speaking time")
-def validate_question_payload(
- payload: schemas.DelegateQuestionPayload, state: SessionLiveState
-) -> None: ...
-
-
def count_present_delegations(state: SessionLiveState) -> int:
"""Count total present delegations.
A delegation is considered present (even if AFK)
@@ -142,7 +128,8 @@ def count_present_delegations(state: SessionLiveState) -> int:
)
-def tally_votes(voting: VotingContext, total_presents: int) -> bool:
+def needs_simple_majority_type(motion: enums.Motions) -> bool:
+ """Simple map for majority type. Used when creating VotingContext and tallying votes"""
qualified_motions = (
Motions.POSTPONE_SESSION,
Motions.CHANGE_DEBATE_TYPE,
@@ -150,7 +137,14 @@ def tally_votes(voting: VotingContext, total_presents: int) -> bool:
Motions.CLOSE_SPEAKERS_LIST,
Motions.SPLIT_PROPOSAL,
)
- """Helper for computing votes.
+
+ if motion not in qualified_motions:
+ return True
+ return False
+
+
+def tally_votes(voting: VotingContext, total_presents: int) -> bool:
+ """Helper for computing votes.
Unless motion is explicitly requiring qualified majority,
will use simple majority (also counts for informal votes)"""
if total_presents == 0:
@@ -170,12 +164,10 @@ def tally_votes(voting: VotingContext, total_presents: int) -> bool:
if motion is None:
return in_favor_count >= simple
- # Use qualified majority for "important" motions
if (
- motion.type in qualified_motions
- and in_favor_count >= qualified
- or motion.type not in qualified_motions
+ needs_simple_majority_type(motion.type)
and in_favor_count >= simple
+ or in_favor_count >= qualified
):
return True
@@ -251,10 +243,12 @@ def require_chair(actor: SessionActor) -> None:
# -------------- HANDLERS --------------
-def handle_submit_motion(
+def handle_delegate_submit_motion(
state: SessionLiveState, event: schemas.SubmitMotionEvent, actor: SessionActor
) -> SessionLiveState:
"""Handles/Maps all possible states through a motion"""
+ delegate = require_delegate(actor)
+
# Extract payload (as DelegateMotionSchema)
payload = event.payload
current_state = state.current_state
@@ -269,17 +263,15 @@ def handle_submit_motion(
):
raise InvalidProceduralMove("Submitting motions during caucuses is disabled")
- # if actor.delegation is None and actor.role != SessionRole.CHAIR:
- # raise InvalidProceduralMove("Delegation context is missing")
-
validate_motion_payload(payload, state)
context = MotionContext(
id=generate_next_motion_id(state),
- priority=get_motion_priority(payload.type),
+ priority=get_motion_priority(payload.type) or 1,
type=payload.type,
timestamp=datetime.now(UTC),
delegate_id=actor.delegation.id if actor.delegation is not None else None,
+ debate_type=payload.debate_type,
total_duration_minutes=payload.total_duration_minutes,
per_speaker_seconds=payload.per_speaker_seconds,
target_topic=payload.target_topic,
@@ -293,15 +285,13 @@ def handle_submit_motion(
def handle_submit_question(
state: SessionLiveState, event: schemas.SubmitQuestionEvent, actor: SessionActor
) -> SessionLiveState:
- # TODO: change this so Chair can also catalog motions for delegations
require_delegate(actor)
payload = event.payload
- validate_question_payload(payload, state)
context = QuestionContext(
id=generate_next_question_id(state),
- priority=get_question_priority(payload.type),
+ priority=get_question_priority(payload.type) or 1,
type=payload.type,
delegate_id=actor.delegation.id, # type:ignore since require_delegate assumes actor delegate is not none
details=payload.details,
@@ -658,12 +648,19 @@ def handle_resolve_motion(
if motion is None:
raise InvalidProceduralMove("Motion not found")
+ majority_type = (
+ enums.MajorityTypes.SIMPLE
+ if needs_simple_majority_type(motion.type)
+ else enums.MajorityTypes.QUALIFIED
+ )
+
if payload.action:
state.voting = VotingContext(
target_type=enums.VotingType.PROCEDURAL,
motion_in_vote=motion,
return_state=state.current_state,
voting_registry={},
+ majority=majority_type,
)
state.current_state = States.VOTING_EXECUTION
@@ -673,6 +670,46 @@ def handle_resolve_motion(
return state
+def handle_chair_submit_motion(
+ state: SessionLiveState, event: schemas.LogMotionEvent, actor: SessionActor
+) -> SessionLiveState:
+ require_chair(actor)
+
+ payload = event.payload
+ validate_motion_payload(payload=payload, state=state)
+
+ # create motion context
+ context = MotionContext(
+ id=generate_next_motion_id(state),
+ priority=get_motion_priority(payload.type) or 1,
+ type=payload.type,
+ delegate_id=payload.representation_id,
+ debate_type=payload.debate_type,
+ total_duration_minutes=payload.total_duration_minutes,
+ per_speaker_seconds=payload.per_speaker_seconds,
+ target_topic=payload.target_topic,
+ details=payload.details,
+ )
+
+ majority_type = (
+ enums.MajorityTypes.SIMPLE
+ if needs_simple_majority_type(payload.type)
+ else enums.MajorityTypes.QUALIFIED
+ )
+
+ # set state to be in voting execution
+ state.voting = VotingContext(
+ target_type=enums.VotingType.PROCEDURAL,
+ motion_in_vote=context,
+ return_state=state.current_state,
+ voting_registry={},
+ majority=majority_type,
+ )
+
+ state.current_state = States.VOTING_EXECUTION
+ return state
+
+
def handle_set_agenda(
state: SessionLiveState, event: schemas.SetAgendaEvent, actor: SessionActor
) -> SessionLiveState: ...
@@ -814,9 +851,8 @@ def handle_insert_queue(
SessionLiveState, # Return type
]
-# TODO: check if list has all events, since it's generated by codex
EVENT_HANDLERS: dict[DelegateEvents | ChairEvents, EventHandler] = {
- DelegateEvents.SUBMIT_MOTION: handle_submit_motion,
+ DelegateEvents.SUBMIT_MOTION: handle_delegate_submit_motion,
DelegateEvents.SUBMIT_QUESTION: handle_submit_question,
DelegateEvents.JOIN_QUEUE: handle_join_queue,
DelegateEvents.LEAVE_QUEUE: handle_leave_queue,
@@ -829,6 +865,7 @@ def handle_insert_queue(
ChairEvents.CLOSE_INFORMAL_VOTING: handle_close_informal_voting,
ChairEvents.CLOSE_PROCEDURAL_VOTING: handle_close_procedural_voting,
ChairEvents.RESOLVE_MOTION: handle_resolve_motion,
+ ChairEvents.LOG_MOTION: handle_chair_submit_motion,
ChairEvents.SET_AGENDA: handle_set_agenda,
ChairEvents.SET_AGENDA_ITEM: handle_set_agenda_item,
ChairEvents.MARK_AGENDA_ITEM: handle_mark_agenda_item,
diff --git a/backend/app/session/enums.py b/backend/app/session/enums.py
index c4d6c24..baf1321 100644
--- a/backend/app/session/enums.py
+++ b/backend/app/session/enums.py
@@ -40,6 +40,7 @@ class ChairEvents(StrEnum):
INCREASE_TIMER = "IncreaseTimerEvent"
OPEN_INFORMAL_VOTING = "OpenInformalVotingEvent"
RESOLVE_MOTION = "ResolveMotionEvent"
+ LOG_MOTION = "LogMotionEvent"
CLOSE_PROCEDURAL_VOTING = "CloseProceduralVotingEvent"
CLOSE_INFORMAL_VOTING = "CloseInformalVotingEvent"
@@ -121,3 +122,10 @@ class VotingType(StrEnum):
INFORMAL = "Informal"
PROCEDURAL = "Procedural"
SUBSTANTIVE = "Substantive"
+
+
+class MotionDecision(StrEnum):
+ """Tracks decision for LogMotionEvent for chair"""
+
+ ACCEPT = "Accept"
+ DENY = "Deny"
diff --git a/backend/app/session/models.py b/backend/app/session/models.py
index d92c888..4939518 100644
--- a/backend/app/session/models.py
+++ b/backend/app/session/models.py
@@ -64,22 +64,23 @@ class VotingContext(BaseModel):
return_state: enums.States
voting_registry: dict[int, enums.VotingChoice] = {}
majority: enums.MajorityTypes | None = None
-
+
motion_in_vote: MotionContext | None = None
# resolution_in_vote: ResolutionContext
allow_veto_power: bool = False
-
+
def is_choice_allowed(self, choice: enums.VotingChoice) -> bool:
- if self.target_type == enums.VotingType.PROCEDURAL:
+ if self.target_type == enums.VotingType.PROCEDURAL:
return choice in (enums.VotingChoice.FAVOUR, enums.VotingChoice.AGAINST)
-
+
return True
+
class DebateContext(BaseModel):
debate_type: enums.DebateTypes
return_state: enums.States
- total_duration_seconds: int | None = None # check if its needed
+ total_duration_seconds: int | None = None # check if its needed
total_speeches: int | None = None
per_speaker_seconds: int | None = None
expires_at: datetime | None = None
diff --git a/backend/app/session/schemas.py b/backend/app/session/schemas.py
index 7c1ef50..8b0b5a8 100644
--- a/backend/app/session/schemas.py
+++ b/backend/app/session/schemas.py
@@ -14,19 +14,22 @@ class SessionCreationSchema(BaseModel):
name: str | None = None
-# --- Delegate Payloads ---
-# TODO: refactor this to only reflect the payload received by delegates, with MotionModel being a separated entity
-class DelegateMotionPayload(BaseModel):
+class MotionPayload(BaseModel):
+ """General motion payload. Used on Delegate and Chair payloads"""
+
type: enums.Motions
debate_type: enums.DebateTypes | None = None
-
total_duration_minutes: int | None = None
per_speaker_seconds: int | None = None
target_topic: str | None = None
-
details: str | None = None
+# --- Delegate Payloads ---
+class DelegateMotionPayload(MotionPayload):
+ pass
+
+
class DelegateQuestionPayload(BaseModel):
type: enums.Questions
details: str | None = None
@@ -75,6 +78,13 @@ class AnswerRollCallEvent(BaseModel):
# --- Chair Payloads ---
+class ChairMotionPayload(MotionPayload):
+ """Extended payload for motions. Used to log motions"""
+
+ representation_id: int
+ decision: enums.MotionDecision
+
+
class ChairIncreaseTimerPayload(BaseModel):
seconds: int = 5
@@ -140,6 +150,11 @@ class MarkRollCallBulkPayload(BaseModel):
# --- Chair Events ---
+class LogMotionEvent(BaseModel):
+ type: Literal[enums.ChairEvents.LOG_MOTION]
+ payload: ChairMotionPayload
+
+
class OpenSessionEvent(BaseModel):
type: Literal[enums.ChairEvents.OPEN_SESSION]
payload: EmptyPayload
@@ -238,6 +253,7 @@ class DeleteAgendaItemEvent(BaseModel):
| AnswerRollCallEvent
| JoinQueueEvent
| LeaveQueueEvent
+ | LogMotionEvent
| OpenSessionEvent
| CloseSessionEvent
| IncreaseTimerEvent
diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py
index 21f4b70..ab058bf 100644
--- a/backend/app/tests/session/test_engine.py
+++ b/backend/app/tests/session/test_engine.py
@@ -31,7 +31,6 @@ def submit_debate_motion_event(
type=enums.DelegateEvents.SUBMIT_MOTION,
payload=sch.DelegateMotionPayload(
type=enums.Motions.CHANGE_DEBATE_TYPE,
- delegate=delegate_actor.delegation.id, # type: ignore[union-attr]
debate_type=enums.DebateTypes.MODERATED_DEBATE,
total_duration_minutes=10,
per_speaker_seconds=60,
@@ -39,6 +38,21 @@ def submit_debate_motion_event(
)
+@pytest.fixture
+def log_motion_event(chair_actor: md.SessionActor) -> sch.LogMotionEvent:
+ return sch.LogMotionEvent(
+ type=enums.ChairEvents.LOG_MOTION,
+ payload=sch.ChairMotionPayload(
+ type=enums.Motions.CHANGE_DEBATE_TYPE,
+ debate_type=enums.DebateTypes.MODERATED_DEBATE,
+ total_duration_minutes=10,
+ per_speaker_seconds=60,
+ representation_id=1,
+ decision=enums.MotionDecision.ACCEPT,
+ ),
+ )
+
+
@pytest.fixture
def submit_question_event(delegate_actor: md.SessionActor) -> sch.SubmitQuestionEvent:
return sch.SubmitQuestionEvent(
@@ -272,10 +286,28 @@ def test_delegate_cannot_submit_motion_outside_allowed_phase(
engine.dispatch(session_state, submit_debate_motion_event, delegate_actor)
-@pytest.mark.xfail(
- strict=True,
- reason="handle_submit_motion does not currently reject chair actors.",
-)
+def test_delegate_cannot_submit_chair_motion(
+ engine: eng.SessionEngine,
+ session_state: md.SessionLiveState,
+ log_motion_event: sch.LogMotionEvent,
+ delegate_actor: md.SessionActor,
+) -> None:
+ with pytest.raises(eng.InvalidProceduralMove, match="Chair role required"):
+ engine.dispatch(session_state, log_motion_event, delegate_actor)
+
+
+def test_chair_can_log_motion(
+ engine: eng.SessionEngine,
+ session_state: md.SessionLiveState,
+ log_motion_event: sch.LogMotionEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ state = engine.dispatch(session_state, log_motion_event, chair_actor)
+ assert state.current_state == enums.States.VOTING_EXECUTION
+ assert state.voting is not None and state.voting.motion_in_vote is not None
+ assert state.voting.motion_in_vote.type == log_motion_event.payload.type
+
+
def test_chair_cannot_submit_delegate_motion(
engine: eng.SessionEngine,
open_gsl_state: md.SessionLiveState,
@@ -526,7 +558,9 @@ def test_chair_can_close_informal_voting(
close_informal_voting_event: sch.CloseInformalVotingEvent,
chair_actor: md.SessionActor,
) -> None:
- state = engine.dispatch(informal_voting_state, close_informal_voting_event, chair_actor)
+ state = engine.dispatch(
+ informal_voting_state, close_informal_voting_event, chair_actor
+ )
assert state.current_state == enums.States.OPEN_GSL
assert state.voting is None
From 3209848a489045f78b48eacfaef0a42711402c2f Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 20:39:38 -0300
Subject: [PATCH 10/19] docs: update events docs
---
docs/realtime/events-and-payloads.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/realtime/events-and-payloads.md b/docs/realtime/events-and-payloads.md
index b339380..63d4ca7 100644
--- a/docs/realtime/events-and-payloads.md
+++ b/docs/realtime/events-and-payloads.md
@@ -69,6 +69,10 @@ Privilege`.
- Moves an allowed session to `Finished` and clears active debate and timer
data.
+- `LogMotionEvent`
+ - Payload: same variables from `SubmitMotionEvent` plus `representation_id` and `decision` (`Accept` or `DENY`)
+ - Records the decision from chair for a specific motion, not sent in the system by any delegation
+
- `ToggleTimerEvent`
- Payload: optional `toggle` (defaults to `true`).
- Starts or pauses the timer. The current implementation toggles based on
From 1b7ca3b1e9c316ddae08f784f4fc210471b42fc5 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 20:55:39 -0300
Subject: [PATCH 11/19] refactor(backend): extract passed motion application
---
backend/app/session/engine.py | 196 +++++++++++++++++-----------------
1 file changed, 99 insertions(+), 97 deletions(-)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index 0d502c0..4b7d5c2 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -501,6 +501,104 @@ def handle_close_informal_voting(
return state
+def apply_passed_motion(state: SessionLiveState, motion: MotionContext) -> None:
+ """Apply a passed procedural motion to the live session state in place."""
+ next_state = state.current_state # as fallback
+ state.current_speaker = None
+ state.timer_is_running = False
+ state.timer_expiration = None
+
+ # 1st block: change of debate motions
+ if motion.type == Motions.CHANGE_DEBATE_TYPE and motion.debate_type is not None:
+ state.caucus_list = []
+ state.current_speaker = None
+ duration_seconds = (
+ (motion.total_duration_minutes * 60)
+ if motion.total_duration_minutes is not None
+ else 600
+ ) # defaults to 10 minutes as fallback
+
+ match motion.debate_type:
+ case DebateTypes.MODERATED_DEBATE:
+ next_state = States.MODERATED_CAUCUS
+ state.debate = DebateContext(
+ debate_type=DebateTypes.MODERATED_DEBATE,
+ return_state=state.current_state,
+ total_duration_seconds=duration_seconds,
+ per_speaker_seconds=motion.per_speaker_seconds,
+ expires_at=datetime.now(UTC) + timedelta(seconds=duration_seconds),
+ )
+ reset_timer(
+ state,
+ motion.per_speaker_seconds
+ if motion.per_speaker_seconds is not None
+ else 60,
+ )
+
+ case DebateTypes.UNMODERATED_DEBATE:
+ next_state = States.UNMODERATED_CAUCUS
+ state.debate = DebateContext(
+ debate_type=DebateTypes.UNMODERATED_DEBATE,
+ return_state=state.current_state,
+ total_duration_seconds=duration_seconds,
+ per_speaker_seconds=None,
+ expires_at=datetime.now(UTC) + timedelta(seconds=duration_seconds),
+ )
+ reset_timer(state) # should not display per_speaker timer
+
+ case DebateTypes.SPEAKERS_LIST:
+ next_state = States.OPEN_GSL
+ state.debate = None
+ reset_timer(state, state.gsl_default_time_seconds)
+
+ case _:
+ raise InvalidProceduralMove("Undefined debate type")
+
+ match motion.type:
+ case Motions.POSTPONE_SESSION:
+ # TODO: create a type of force_to_database function here? or query if it's a postpone session on service.py
+ pass
+ case Motions.REOPEN_SESSION:
+ # TODO: same as above
+ pass
+ case Motions.TOUR_DE_TABLE:
+ # note: seems like belongs to debate type
+ pass
+ case Motions.END_DEBATE:
+ # clean gsl list
+ state.gsl_queue = []
+ state.debate = None
+ reset_timer(state)
+ next_state = States.VOTING_PROCEDURES # or VOTING_PREPARATION
+
+ case Motions.VOTE_AMENDMENT:
+ # note: seems more like an informal consultation
+ pass
+ case Motions.VOTE_BY_ROLL_CALL:
+ # will define the VotingContext for resolutions
+ pass
+ case Motions.CLOSE_SPEAKERS_LIST:
+ next_state = States.CLOSED_GSL
+
+ case Motions.REOPEN_SPEAKERS_LIST:
+ next_state = States.OPEN_GSL
+
+ case Motions.SPLIT_PROPOSAL:
+ # note: seems more like an informal consultation
+ pass
+ case Motions.CHANGE_TOPIC:
+ # note: seems more like an informal consultation
+ pass
+ case Motions.QUORUM:
+ state.roll_call = RollCallContext(registry={})
+ next_state = States.ROLL_CALL
+ case _:
+ raise InvalidProceduralMove("Undefined motion type")
+
+ # additional case: if we went from GSL to something, save gsl structures
+ state.current_state = next_state
+
+
def handle_close_procedural_voting(
state: SessionLiveState,
event: schemas.CloseProceduralVotingEvent,
@@ -525,104 +623,8 @@ def handle_close_procedural_voting(
present = count_present_delegations(state)
passed = tally_votes(state.voting, present)
- # TODO: pass everything here into a helper "apply_passed_motion" and "apply_change_debate"
if passed:
- next_state = state.current_state # as fallback
- state.current_speaker = None
- state.timer_is_running = False
- state.timer_expiration = None
-
- # 1st block: change of debate motions
- if motion.type == Motions.CHANGE_DEBATE_TYPE and motion.debate_type is not None:
- state.caucus_list = []
- state.current_speaker = None
- duration_seconds = (
- (motion.total_duration_minutes * 60)
- if motion.total_duration_minutes is not None
- else 600
- ) # defaults to 10 minutes as fallback
-
- match motion.debate_type:
- case DebateTypes.MODERATED_DEBATE:
- next_state = States.MODERATED_CAUCUS
- state.debate = DebateContext(
- debate_type=DebateTypes.MODERATED_DEBATE,
- return_state=state.current_state,
- total_duration_seconds=duration_seconds,
- per_speaker_seconds=motion.per_speaker_seconds,
- expires_at=datetime.now(UTC)
- + timedelta(seconds=duration_seconds),
- )
- reset_timer(
- state,
- motion.per_speaker_seconds
- if motion.per_speaker_seconds is not None
- else 60,
- )
-
- case DebateTypes.UNMODERATED_DEBATE:
- next_state = States.UNMODERATED_CAUCUS
- state.debate = DebateContext(
- debate_type=DebateTypes.UNMODERATED_DEBATE,
- return_state=state.current_state,
- total_duration_seconds=duration_seconds,
- per_speaker_seconds=None,
- expires_at=datetime.now(UTC)
- + timedelta(seconds=duration_seconds),
- )
- reset_timer(state) # should not display per_speaker timer
-
- case DebateTypes.SPEAKERS_LIST:
- next_state = States.OPEN_GSL
- state.debate = None
- reset_timer(state, state.gsl_default_time_seconds)
-
- case _:
- raise InvalidProceduralMove("Undefined debate type")
-
- match motion.type:
- case Motions.POSTPONE_SESSION:
- # TODO: create a type of force_to_database function here? or query if it's a postpone session on service.py
- pass
- case Motions.REOPEN_SESSION:
- # TODO: same as above
- pass
- case Motions.TOUR_DE_TABLE:
- # note: seems like belongs to debate type
- pass
- case Motions.END_DEBATE:
- # clean gsl list
- state.gsl_queue = []
- state.debate = None
- reset_timer(state)
- next_state = States.VOTING_PROCEDURES # or VOTING_PREPARATION
-
- case Motions.VOTE_AMENDMENT:
- # note: seems more like an informal consultation
- pass
- case Motions.VOTE_BY_ROLL_CALL:
- # will define the VotingContext for resolutions
- pass
- case Motions.CLOSE_SPEAKERS_LIST:
- next_state = States.CLOSED_GSL
-
- case Motions.REOPEN_SPEAKERS_LIST:
- next_state = States.OPEN_GSL
-
- case Motions.SPLIT_PROPOSAL:
- # note: seems more like an informal consultation
- pass
- case Motions.CHANGE_TOPIC:
- # note: seems more like an informal consultation
- pass
- case Motions.QUORUM:
- state.roll_call = RollCallContext(registry={})
- next_state = States.ROLL_CALL
- case _:
- raise InvalidProceduralMove("Undefined motion type")
-
- # additional case: if we went from GSL to something, save gsl structures
- state.current_state = next_state
+ apply_passed_motion(state, motion)
else:
# motion failed, so return to last state
state.current_state = state.voting.return_state
From 490f3bb6936345a0dbf69e0ccc50bc6f5dfca5d7 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 20:56:41 -0300
Subject: [PATCH 12/19] fix(backend): timestamp chair logged motions
---
backend/app/session/engine.py | 1 +
backend/app/tests/session/test_engine.py | 4 ++++
2 files changed, 5 insertions(+)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index 4b7d5c2..727e5a3 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -685,6 +685,7 @@ def handle_chair_submit_motion(
id=generate_next_motion_id(state),
priority=get_motion_priority(payload.type) or 1,
type=payload.type,
+ timestamp=datetime.now(UTC),
delegate_id=payload.representation_id,
debate_type=payload.debate_type,
total_duration_minutes=payload.total_duration_minutes,
diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py
index ab058bf..6762ee1 100644
--- a/backend/app/tests/session/test_engine.py
+++ b/backend/app/tests/session/test_engine.py
@@ -1,3 +1,5 @@
+from datetime import UTC, datetime
+
import pytest
import app.session.engine as eng
@@ -151,6 +153,7 @@ def close_speakers_list_motion(
id=1,
priority=1,
type=enums.Motions.CLOSE_SPEAKERS_LIST,
+ timestamp=datetime.now(UTC),
delegate_id=delegate_actor.delegation.id, # type: ignore[union-attr]
)
@@ -161,6 +164,7 @@ def reopen_speakers_list_motion(delegate_actor: md.SessionActor) -> md.MotionCon
id=1,
priority=1,
type=enums.Motions.REOPEN_SPEAKERS_LIST,
+ timestamp=datetime.now(UTC),
delegate_id=delegate_actor.delegation.id, # type: ignore[union-attr]
)
From c6de05164fa3e93bb6647865830818684d73db9f Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 20:57:30 -0300
Subject: [PATCH 13/19] refactor(backend): remove unused variable
---
backend/app/session/engine.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index 727e5a3..f8c8f9f 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -247,7 +247,7 @@ def handle_delegate_submit_motion(
state: SessionLiveState, event: schemas.SubmitMotionEvent, actor: SessionActor
) -> SessionLiveState:
"""Handles/Maps all possible states through a motion"""
- delegate = require_delegate(actor)
+ require_delegate(actor)
# Extract payload (as DelegateMotionSchema)
payload = event.payload
From 38707bd5b0081aab3596e4fdff1efe415c8ebd62 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 21:59:07 -0300
Subject: [PATCH 14/19] feat(backend): add finish caucus event
Co-authored-by: Codex
---
backend/app/session/engine.py | 21 +++++++++
backend/app/session/enums.py | 1 +
backend/app/session/schemas.py | 6 +++
backend/app/tests/session/test_engine.py | 58 ++++++++++++++++++++++++
4 files changed, 86 insertions(+)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index f8c8f9f..eb3a095 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -634,6 +634,26 @@ def handle_close_procedural_voting(
return state
+def handle_finish_caucus(
+ state: SessionLiveState, event: schemas.FinishCaucusEvent, actor: SessionActor
+) -> SessionLiveState:
+ require_chair(actor)
+
+ if state.debate is None or state.current_state not in {
+ States.MODERATED_CAUCUS,
+ States.UNMODERATED_CAUCUS,
+ }:
+ raise InvalidProceduralMove("No active caucus")
+
+ return_state = state.debate.return_state
+ state.current_speaker = None
+ state.caucus_list = []
+ state.debate = None
+ reset_timer(state)
+ state.current_state = return_state
+ return state
+
+
# handles setting state into VOTING_EXECUTION or rejecting the motion
def handle_resolve_motion(
state: SessionLiveState, event: schemas.ResolveMotionEvent, actor: SessionActor
@@ -867,6 +887,7 @@ def handle_insert_queue(
ChairEvents.OPEN_INFORMAL_VOTING: handle_open_informal_voting,
ChairEvents.CLOSE_INFORMAL_VOTING: handle_close_informal_voting,
ChairEvents.CLOSE_PROCEDURAL_VOTING: handle_close_procedural_voting,
+ ChairEvents.FINISH_CAUCUS: handle_finish_caucus,
ChairEvents.RESOLVE_MOTION: handle_resolve_motion,
ChairEvents.LOG_MOTION: handle_chair_submit_motion,
ChairEvents.SET_AGENDA: handle_set_agenda,
diff --git a/backend/app/session/enums.py b/backend/app/session/enums.py
index baf1321..7afe84c 100644
--- a/backend/app/session/enums.py
+++ b/backend/app/session/enums.py
@@ -43,6 +43,7 @@ class ChairEvents(StrEnum):
LOG_MOTION = "LogMotionEvent"
CLOSE_PROCEDURAL_VOTING = "CloseProceduralVotingEvent"
CLOSE_INFORMAL_VOTING = "CloseInformalVotingEvent"
+ FINISH_CAUCUS = "FinishCaucusEvent"
# Disruptive events (i.e manual override events)
MANUAL_PHASE_SET = "SetPhaseEvent"
diff --git a/backend/app/session/schemas.py b/backend/app/session/schemas.py
index 8b0b5a8..bfc02c2 100644
--- a/backend/app/session/schemas.py
+++ b/backend/app/session/schemas.py
@@ -210,6 +210,11 @@ class CloseProceduralVotingEvent(BaseModel):
payload: EmptyPayload
+class FinishCaucusEvent(BaseModel):
+ type: Literal[enums.ChairEvents.FINISH_CAUCUS]
+ payload: EmptyPayload
+
+
class MarkRollCallEvent(BaseModel):
type: Literal[enums.ChairEvents.MARK_ROLLCALL]
payload: MarkRollCallPayload
@@ -261,6 +266,7 @@ class DeleteAgendaItemEvent(BaseModel):
| OpenInformalVotingEvent
| CloseProceduralVotingEvent
| CloseInformalVotingEvent
+ | FinishCaucusEvent
| ResolveMotionEvent
| SpeakerEvent
| SetAgendaEvent
diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py
index 6762ee1..349d779 100644
--- a/backend/app/tests/session/test_engine.py
+++ b/backend/app/tests/session/test_engine.py
@@ -145,6 +145,14 @@ def close_procedural_voting_event() -> sch.CloseProceduralVotingEvent:
)
+@pytest.fixture
+def finish_caucus_event() -> sch.FinishCaucusEvent:
+ return sch.FinishCaucusEvent(
+ type=enums.ChairEvents.FINISH_CAUCUS,
+ payload=sch.EmptyPayload(),
+ )
+
+
@pytest.fixture
def close_speakers_list_motion(
delegate_actor: md.SessionActor,
@@ -700,6 +708,56 @@ def test_delegate_cannot_close_procedural_vote(
)
+def test_chair_can_finish_caucus_and_restore_original_gsl_state(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ finish_caucus_event: sch.FinishCaucusEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.current_state = enums.States.MODERATED_CAUCUS
+ open_gsl_state.current_speaker = 0
+ open_gsl_state.caucus_list = [0, 1]
+ open_gsl_state.timer_is_running = True
+ open_gsl_state.timer_remaining_seconds = 30
+ open_gsl_state.debate = md.DebateContext(
+ debate_type=enums.DebateTypes.MODERATED_DEBATE,
+ return_state=enums.States.OPEN_GSL,
+ total_duration_seconds=600,
+ per_speaker_seconds=60,
+ expires_at=datetime.now(UTC),
+ )
+
+ state = engine.dispatch(open_gsl_state, finish_caucus_event, chair_actor)
+
+ assert state.current_state == enums.States.OPEN_GSL
+ assert state.debate is None
+ assert state.current_speaker is None
+ assert state.caucus_list == []
+ assert state.timer_is_running is False
+ assert state.timer_expiration is None
+ assert state.timer_remaining_seconds == 0
+
+
+def test_delegate_cannot_finish_caucus(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ finish_caucus_event: sch.FinishCaucusEvent,
+ delegate_actor: md.SessionActor,
+) -> None:
+ with pytest.raises(eng.InvalidProceduralMove, match="Chair role required"):
+ engine.dispatch(open_gsl_state, finish_caucus_event, delegate_actor)
+
+
+def test_chair_cannot_finish_without_active_caucus(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ finish_caucus_event: sch.FinishCaucusEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ with pytest.raises(eng.InvalidProceduralMove, match="No active caucus"):
+ engine.dispatch(open_gsl_state, finish_caucus_event, chair_actor)
+
+
def test_tally_votes_correctly_marks_success_simple(
reopen_speakers_list_motion: md.MotionContext,
) -> None:
From 3bb396145d119a98f7c277183b23a27c6e29fd58 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 22:39:40 -0300
Subject: [PATCH 15/19] feat(backend): add debate-aware speaker controls
---
backend/app/session/engine.py | 121 +++++++++-----
backend/app/session/enums.py | 6 +-
backend/app/session/models.py | 2 -
backend/app/session/schemas.py | 36 +++--
backend/app/tests/session/test_engine.py | 191 +++++++++++++++++++----
5 files changed, 271 insertions(+), 85 deletions(-)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index eb3a095..51240d6 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -373,7 +373,7 @@ def handle_open_session(
raise InvalidProceduralMove("Session can only be opened from setup")
state.current_state = States.ROLL_CALL
- state.roll_call = RollCallContext(registry={}, current_delegation=None)
+ state.roll_call = RollCallContext(registry={})
state.voting_choice = {}
state.gsl_queue = []
state.current_speaker = None
@@ -501,9 +501,13 @@ def handle_close_informal_voting(
return state
-def apply_passed_motion(state: SessionLiveState, motion: MotionContext) -> None:
+def apply_passed_motion(
+ state: SessionLiveState,
+ motion: MotionContext,
+ return_state: States
+) -> None:
"""Apply a passed procedural motion to the live session state in place."""
- next_state = state.current_state # as fallback
+ next_state = return_state # as fallback
state.current_speaker = None
state.timer_is_running = False
state.timer_expiration = None
@@ -523,7 +527,7 @@ def apply_passed_motion(state: SessionLiveState, motion: MotionContext) -> None:
next_state = States.MODERATED_CAUCUS
state.debate = DebateContext(
debate_type=DebateTypes.MODERATED_DEBATE,
- return_state=state.current_state,
+ return_state=return_state,
total_duration_seconds=duration_seconds,
per_speaker_seconds=motion.per_speaker_seconds,
expires_at=datetime.now(UTC) + timedelta(seconds=duration_seconds),
@@ -539,7 +543,7 @@ def apply_passed_motion(state: SessionLiveState, motion: MotionContext) -> None:
next_state = States.UNMODERATED_CAUCUS
state.debate = DebateContext(
debate_type=DebateTypes.UNMODERATED_DEBATE,
- return_state=state.current_state,
+ return_state=return_state,
total_duration_seconds=duration_seconds,
per_speaker_seconds=None,
expires_at=datetime.now(UTC) + timedelta(seconds=duration_seconds),
@@ -556,14 +560,13 @@ def apply_passed_motion(state: SessionLiveState, motion: MotionContext) -> None:
match motion.type:
case Motions.POSTPONE_SESSION:
- # TODO: create a type of force_to_database function here? or query if it's a postpone session on service.py
pass
case Motions.REOPEN_SESSION:
- # TODO: same as above
pass
case Motions.TOUR_DE_TABLE:
- # note: seems like belongs to debate type
- pass
+ next_state = States.TOUR_DE_TABLE
+ state.caucus_list = [del_id for del_id, choice in state.roll_call.registry.items() if choice in (enums.RollCallChoice.PRESENT, enums.RollCallChoice.PRESENT_AND_VOTING)]
+
case Motions.END_DEBATE:
# clean gsl list
state.gsl_queue = []
@@ -624,7 +627,7 @@ def handle_close_procedural_voting(
passed = tally_votes(state.voting, present)
if passed:
- apply_passed_motion(state, motion)
+ apply_passed_motion(state, motion, return_state=state.voting.return_state)
else:
# motion failed, so return to last state
state.current_state = state.voting.return_state
@@ -779,24 +782,81 @@ def handle_manual_phase_set(
) -> SessionLiveState: ...
-def handle_choose_speaker(
- state: SessionLiveState, event: schemas.SpeakerEvent, actor: SessionActor
+def handle_next_speaker(
+ state: SessionLiveState, event: schemas.NextSpeakerEvent, actor: SessionActor
) -> SessionLiveState:
require_chair(actor)
- seconds = event.payload.seconds or get_default_speaker_seconds(state)
- if (
- event.payload.speaker_id is None
- and state.current_state in (States.OPEN_GSL, States.CLOSED_GSL)
- and state.gsl_queue
- ):
+ if state.current_state in {States.OPEN_GSL, States.CLOSED_GSL}:
+ if not state.gsl_queue:
+ state.current_speaker = None
+ reset_timer(state)
+ return state
state.current_speaker = state.gsl_queue.pop(0)
+ reset_timer(state, state.gsl_default_time_seconds)
+ return state
+
+ if state.current_state == States.TOUR_DE_TABLE:
+ if not state.caucus_list:
+ state.current_speaker = None
+ reset_timer(state)
+ return state
+ state.current_speaker = state.caucus_list.pop(0)
+ reset_timer(state, state.gsl_default_time_seconds)
+ return state
+
+ if state.current_state == States.MODERATED_CAUCUS:
+ raise InvalidProceduralMove("Chair must grant floor during moderated caucus")
+
+ raise InvalidProceduralMove("Cannot advance speaker right now")
+
+
+def handle_add_gsl_speaker(
+ state: SessionLiveState, event: schemas.AddGslSpeakerEvent, actor: SessionActor
+) -> SessionLiveState:
+ require_chair(actor)
+
+ if state.current_state not in {States.OPEN_GSL, States.CLOSED_GSL}:
+ raise InvalidProceduralMove("Can only add speakers to the GSL")
+
+ representation_id = event.payload.representation_id
+ if representation_id not in state.delegations:
+ raise InvalidProceduralMove("Representation not found")
+ if representation_id in state.gsl_queue:
+ raise InvalidProceduralMove("Representation already in GSL queue")
+
+ state.gsl_queue.append(representation_id)
+ return state
+
+
+def handle_grant_floor(
+ state: SessionLiveState, event: schemas.GrantFloorEvent, actor: SessionActor
+) -> SessionLiveState:
+ require_chair(actor)
+
+ representation_id = event.payload.representation_id
+ if representation_id not in state.delegations:
+ raise InvalidProceduralMove("Representation not found")
+
+ if state.current_state in {States.OPEN_GSL, States.CLOSED_GSL}:
+ if representation_id in state.gsl_queue:
+ state.gsl_queue.remove(representation_id)
+ seconds = event.payload.seconds or state.gsl_default_time_seconds
+ elif state.current_state == States.MODERATED_CAUCUS:
+ if state.debate is None:
+ raise InvalidProceduralMove("No active moderated caucus")
+ seconds = event.payload.seconds or state.debate.per_speaker_seconds or 60
+ elif state.current_state == States.TOUR_DE_TABLE:
+ if representation_id in state.caucus_list:
+ state.caucus_list.remove(representation_id)
+ seconds = event.payload.seconds or state.gsl_default_time_seconds
+ elif state.current_state == States.UNMODERATED_CAUCUS:
+ raise InvalidProceduralMove("Cannot grant floor during unmoderated caucus")
else:
- state.current_speaker = event.payload.speaker_id
+ raise InvalidProceduralMove("Cannot grant floor right now")
- state.timer_is_running = False
- state.timer_expiration = None # will be calculated when timer is toggled
- state.timer_remaining_seconds = seconds or 60 # default to something
+ state.current_speaker = representation_id
+ reset_timer(state, seconds)
return state
@@ -856,18 +916,6 @@ def handle_close_roll_call(
return state
-def handle_insert_queue(
- state: SessionLiveState, event: schemas.ChairInsertQueueEvent, actor: SessionActor
-) -> SessionLiveState:
- require_chair(actor)
- del_id: int = event.payload.target
- delegate = state.delegations.get(del_id)
- if delegate is None:
- raise InvalidProceduralMove("Delegate not found")
- state.gsl_queue.append(delegate.id)
- return state
-
-
# Signature for events/handlers, uses legacy(ish) 3.11 TypeAlias
EventHandler: TypeAlias = Callable[
[SessionLiveState, Any, SessionActor], # overall signature
@@ -896,11 +944,12 @@ def handle_insert_queue(
ChairEvents.DELETE_AGENDA_ITEM: handle_delete_agenda_item,
ChairEvents.MANUAL_PHASE_SET: handle_manual_phase_set,
ChairEvents.CLOSE_SESSION: handle_close_session,
- ChairEvents.CHOOSE_SPEAKER: handle_choose_speaker,
+ ChairEvents.NEXT_SPEAKER: handle_next_speaker,
+ ChairEvents.ADD_GSL_SPEAKER: handle_add_gsl_speaker,
+ ChairEvents.GRANT_FLOOR: handle_grant_floor,
ChairEvents.MARK_ROLLCALL: handle_mark_roll_call,
ChairEvents.MARK_ROLLCALL_BULK: handle_mark_roll_call_bulk,
ChairEvents.CLOSE_ROLLCALL: handle_close_roll_call,
- ChairEvents.INSERT_QUEUE: handle_insert_queue,
}
diff --git a/backend/app/session/enums.py b/backend/app/session/enums.py
index 7afe84c..fd51869 100644
--- a/backend/app/session/enums.py
+++ b/backend/app/session/enums.py
@@ -19,6 +19,7 @@ class States(StrEnum):
# States based on motions, resolutions, etc
MODERATED_CAUCUS = "Moderated Caucus"
UNMODERATED_CAUCUS = "Unmoderated Caucus"
+ TOUR_DE_TABLE = "Tour de Table"
VOTING_EXECUTION = "Voting Execution" # this handles either "motion to moderated caucus" or "motion to voting procedures", for example
BETWEEN_DEBATES = "Between Debates"
@@ -54,11 +55,12 @@ class ChairEvents(StrEnum):
MARK_AGENDA_ITEM = "MarkAgendaItemEvent"
DELETE_AGENDA_ITEM = "DeleteAgendaItemEvent"
SET_AGENDA = "SetAgenda"
- CHOOSE_SPEAKER = "SpeakerEvent"
+ NEXT_SPEAKER = "NextSpeakerEvent"
+ ADD_GSL_SPEAKER = "AddGslSpeakerEvent"
+ GRANT_FLOOR = "GrantFloorEvent"
MARK_ROLLCALL = "MarkRollCallEvent"
MARK_ROLLCALL_BULK = "MarkRollCallBulkEvent"
CLOSE_ROLLCALL = "CloseRollCallEvent"
- INSERT_QUEUE = "InsertQueueEvent"
# --- Additional Info ---
diff --git a/backend/app/session/models.py b/backend/app/session/models.py
index 4939518..83cac5b 100644
--- a/backend/app/session/models.py
+++ b/backend/app/session/models.py
@@ -89,7 +89,6 @@ class DebateContext(BaseModel):
class RollCallContext(BaseModel):
registry: dict[int, enums.RollCallChoice] = {} # Delegation Id as key
- current_delegation: int | None = None # perhaps not needed
class AgendaItem(BaseModel):
@@ -121,7 +120,6 @@ class SessionLiveState(BaseModel):
gsl_default_time_seconds: int = 60
# Caucus variables
- # TODO: how to add a popup placard that fades away after some moment in frontend? related to CHOOSE_SPEAKER
caucus_list: list[
int
] = [] # special list that is only used during moderated caucus, has different semantic functionality than gsl queue
diff --git a/backend/app/session/schemas.py b/backend/app/session/schemas.py
index bfc02c2..79bc205 100644
--- a/backend/app/session/schemas.py
+++ b/backend/app/session/schemas.py
@@ -105,9 +105,9 @@ class ChairResolveMotionPayload(BaseModel):
action: bool
-class ChairForceSpeakerPayload(BaseModel):
- speaker_id: int | None = None # if none, will pass onto next speaker
- seconds: int | None = None # if none, will be based on the current seconds
+class GrantFloorPayload(BaseModel):
+ representation_id: int
+ seconds: int | None = Field(default=None, ge=1)
class ChairSetAgendaPayload(BaseModel):
@@ -118,8 +118,8 @@ class ChairSetPhasePayload(BaseModel):
target_phase: enums.States
-class ChairInsertQueuePayload(BaseModel):
- target: int # Delegate Id
+class AddGslSpeakerPayload(BaseModel):
+ representation_id: int
class SetAgendaItemPayload(BaseModel):
@@ -185,9 +185,19 @@ class ResolveMotionEvent(BaseModel):
payload: ChairResolveMotionPayload
-class SpeakerEvent(BaseModel):
- type: Literal[enums.ChairEvents.CHOOSE_SPEAKER]
- payload: ChairForceSpeakerPayload
+class NextSpeakerEvent(BaseModel):
+ type: Literal[enums.ChairEvents.NEXT_SPEAKER]
+ payload: EmptyPayload
+
+
+class AddGslSpeakerEvent(BaseModel):
+ type: Literal[enums.ChairEvents.ADD_GSL_SPEAKER]
+ payload: AddGslSpeakerPayload
+
+
+class GrantFloorEvent(BaseModel):
+ type: Literal[enums.ChairEvents.GRANT_FLOOR]
+ payload: GrantFloorPayload
class SetAgendaEvent(BaseModel):
@@ -230,11 +240,6 @@ class CloseRollCallEvent(BaseModel):
payload: EmptyPayload
-class ChairInsertQueueEvent(BaseModel):
- type: Literal[enums.ChairEvents.INSERT_QUEUE]
- payload: ChairInsertQueuePayload
-
-
class MarkAgendaItemEvent(BaseModel):
type: Literal[enums.ChairEvents.MARK_AGENDA_ITEM]
payload: MarkAgendaItemPayload
@@ -268,7 +273,9 @@ class DeleteAgendaItemEvent(BaseModel):
| CloseInformalVotingEvent
| FinishCaucusEvent
| ResolveMotionEvent
- | SpeakerEvent
+ | NextSpeakerEvent
+ | AddGslSpeakerEvent
+ | GrantFloorEvent
| SetAgendaEvent
| SetAgendaItemEvent
| MarkAgendaItemEvent
@@ -276,7 +283,6 @@ class DeleteAgendaItemEvent(BaseModel):
| SetPhaseEvent
| MarkRollCallEvent
| CloseRollCallEvent
- | ChairInsertQueueEvent
| MarkRollCallBulkEvent,
Field(discriminator="type"),
]
diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py
index 349d779..b392460 100644
--- a/backend/app/tests/session/test_engine.py
+++ b/backend/app/tests/session/test_engine.py
@@ -201,10 +201,26 @@ def resolve_motion_event() -> sch.ResolveMotionEvent:
@pytest.fixture
-def choose_speaker_event() -> sch.SpeakerEvent:
- return sch.SpeakerEvent(
- type=enums.ChairEvents.CHOOSE_SPEAKER,
- payload=sch.ChairForceSpeakerPayload(speaker_id=1, seconds=45),
+def next_speaker_event() -> sch.NextSpeakerEvent:
+ return sch.NextSpeakerEvent(
+ type=enums.ChairEvents.NEXT_SPEAKER,
+ payload=sch.EmptyPayload(),
+ )
+
+
+@pytest.fixture
+def add_gsl_speaker_event() -> sch.AddGslSpeakerEvent:
+ return sch.AddGslSpeakerEvent(
+ type=enums.ChairEvents.ADD_GSL_SPEAKER,
+ payload=sch.AddGslSpeakerPayload(representation_id=1),
+ )
+
+
+@pytest.fixture
+def grant_floor_event() -> sch.GrantFloorEvent:
+ return sch.GrantFloorEvent(
+ type=enums.ChairEvents.GRANT_FLOOR,
+ payload=sch.GrantFloorPayload(representation_id=1, seconds=45),
)
@@ -232,14 +248,6 @@ def mark_roll_call_bulk_event() -> sch.MarkRollCallBulkEvent:
)
-@pytest.fixture
-def insert_queue_event() -> sch.ChairInsertQueueEvent:
- return sch.ChairInsertQueueEvent(
- type=enums.ChairEvents.INSERT_QUEUE,
- payload=sch.ChairInsertQueuePayload(target=0),
- )
-
-
@pytest.fixture
def open_session_event() -> sch.OpenSessionEvent:
return sch.OpenSessionEvent(
@@ -797,28 +805,162 @@ def test_tally_votes_correctly_marks_fail_majority(
assert not res
-def test_chair_can_choose_speaker(
+def test_chair_can_advance_gsl_speaker(
engine: eng.SessionEngine,
open_gsl_state: md.SessionLiveState,
- choose_speaker_event: sch.SpeakerEvent,
+ next_speaker_event: sch.NextSpeakerEvent,
chair_actor: md.SessionActor,
) -> None:
- state = engine.dispatch(open_gsl_state, choose_speaker_event, chair_actor)
+ open_gsl_state.gsl_queue = [1, 2]
+
+ state = engine.dispatch(open_gsl_state, next_speaker_event, chair_actor)
assert state.current_speaker == 1
+ assert state.gsl_queue == [2]
assert state.timer_is_running is False
assert state.timer_expiration is None
- assert state.timer_remaining_seconds == 45
+ assert state.timer_remaining_seconds == state.gsl_default_time_seconds
+
+
+def test_chair_can_advance_tour_de_table_speaker(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ next_speaker_event: sch.NextSpeakerEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.current_state = enums.States.TOUR_DE_TABLE
+ open_gsl_state.caucus_list = [1, 2]
+ state = engine.dispatch(open_gsl_state, next_speaker_event, chair_actor)
-def test_delegate_cannot_choose_speaker(
+ assert state.current_speaker == 1
+ assert state.caucus_list == [2]
+ assert state.timer_remaining_seconds == state.gsl_default_time_seconds
+
+
+def test_next_speaker_clears_current_speaker_when_gsl_empty(
engine: eng.SessionEngine,
open_gsl_state: md.SessionLiveState,
- choose_speaker_event: sch.SpeakerEvent,
+ next_speaker_event: sch.NextSpeakerEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.current_speaker = 1
+ open_gsl_state.timer_is_running = True
+
+ state = engine.dispatch(open_gsl_state, next_speaker_event, chair_actor)
+
+ assert state.current_speaker is None
+ assert state.timer_remaining_seconds == 0
+ assert state.timer_is_running is False
+
+
+def test_next_speaker_rejects_moderated_caucus(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ next_speaker_event: sch.NextSpeakerEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.current_state = enums.States.MODERATED_CAUCUS
+
+ with pytest.raises(eng.InvalidProceduralMove, match="must grant floor"):
+ engine.dispatch(open_gsl_state, next_speaker_event, chair_actor)
+
+
+def test_delegate_cannot_advance_speaker(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ next_speaker_event: sch.NextSpeakerEvent,
delegate_actor: md.SessionActor,
) -> None:
with pytest.raises(eng.InvalidProceduralMove, match="Chair role required"):
- engine.dispatch(open_gsl_state, choose_speaker_event, delegate_actor)
+ engine.dispatch(open_gsl_state, next_speaker_event, delegate_actor)
+
+
+def test_chair_can_add_gsl_speaker(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ add_gsl_speaker_event: sch.AddGslSpeakerEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ state = engine.dispatch(open_gsl_state, add_gsl_speaker_event, chair_actor)
+
+ assert state.gsl_queue == [1]
+
+
+def test_add_gsl_speaker_rejects_duplicates(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ add_gsl_speaker_event: sch.AddGslSpeakerEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.gsl_queue = [1]
+
+ with pytest.raises(eng.InvalidProceduralMove, match="already in GSL queue"):
+ engine.dispatch(open_gsl_state, add_gsl_speaker_event, chair_actor)
+
+
+def test_chair_can_grant_floor_and_remove_gsl_queue_entry(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ grant_floor_event: sch.GrantFloorEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.gsl_queue = [0, 1, 2]
+
+ state = engine.dispatch(open_gsl_state, grant_floor_event, chair_actor)
+
+ assert state.current_speaker == 1
+ assert state.gsl_queue == [0, 2]
+ assert state.timer_remaining_seconds == 45
+
+
+def test_chair_can_grant_floor_and_remove_tour_de_table_entry(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ grant_floor_event: sch.GrantFloorEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.current_state = enums.States.TOUR_DE_TABLE
+ open_gsl_state.caucus_list = [0, 1, 2]
+
+ state = engine.dispatch(open_gsl_state, grant_floor_event, chair_actor)
+
+ assert state.current_speaker == 1
+ assert state.caucus_list == [0, 2]
+ assert state.timer_remaining_seconds == 45
+
+
+def test_chair_can_grant_floor_in_moderated_caucus(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ grant_floor_event: sch.GrantFloorEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.current_state = enums.States.MODERATED_CAUCUS
+ open_gsl_state.debate = md.DebateContext(
+ debate_type=enums.DebateTypes.MODERATED_DEBATE,
+ return_state=enums.States.OPEN_GSL,
+ per_speaker_seconds=60,
+ )
+ open_gsl_state.caucus_list = [0, 1]
+
+ state = engine.dispatch(open_gsl_state, grant_floor_event, chair_actor)
+
+ assert state.current_speaker == 1
+ assert state.caucus_list == [0, 1]
+ assert state.timer_remaining_seconds == 45
+
+
+def test_grant_floor_rejects_unmoderated_caucus(
+ engine: eng.SessionEngine,
+ open_gsl_state: md.SessionLiveState,
+ grant_floor_event: sch.GrantFloorEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ open_gsl_state.current_state = enums.States.UNMODERATED_CAUCUS
+
+ with pytest.raises(eng.InvalidProceduralMove, match="unmoderated caucus"):
+ engine.dispatch(open_gsl_state, grant_floor_event, chair_actor)
def test_chair_can_mark_roll_call(
@@ -890,17 +1032,6 @@ def test_chair_cannot_mark_roll_call_bulk_nonexistent_delegations(
engine.dispatch(session_state, event, chair_actor)
-def test_chair_insert_queue_uses_delegation_id(
- engine: eng.SessionEngine,
- open_gsl_state: md.SessionLiveState,
- insert_queue_event: sch.ChairInsertQueueEvent,
- chair_actor: md.SessionActor,
-) -> None:
- state = engine.dispatch(open_gsl_state, insert_queue_event, chair_actor)
-
- assert state.gsl_queue == [0]
-
-
def test_chair_open_session_starts_roll_call(
engine: eng.SessionEngine,
session_state: md.SessionLiveState,
From 7431db97ad47003d663500861ce19919c9479b4e Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 22:41:02 -0300
Subject: [PATCH 16/19] docs: document debate speaker events
---
docs/realtime/events-and-payloads.md | 33 ++++++++++++++++++++--------
1 file changed, 24 insertions(+), 9 deletions(-)
diff --git a/docs/realtime/events-and-payloads.md b/docs/realtime/events-and-payloads.md
index 63d4ca7..69d9924 100644
--- a/docs/realtime/events-and-payloads.md
+++ b/docs/realtime/events-and-payloads.md
@@ -101,11 +101,30 @@ Privilege`.
- Tallies the current procedural vote and applies its result or returns to
the earlier phase.
-- `SpeakerEvent`
- - Payload: optional `speaker_id` and `seconds`.
- - Sets a current speaker and the timer duration. Although the schema allows
- an omitted `speaker_id`, the current handler requires an existing
- delegation ID.
+- `FinishCaucusEvent`
+ - Payload: `{}`.
+ - Ends an active moderated or unmoderated caucus. Clears its speaker, timer,
+ and caucus list, then returns to the GSL state stored when the caucus was
+ opened.
+
+- `NextSpeakerEvent`
+ - Payload: `{}`.
+ - In Open or Closed GSL, pops the next `representation_id` from `gsl_queue`
+ into `current_speaker`. In Tour de Table, does the same with `caucus_list`.
+ An empty list clears the current speaker and timer. It is not valid for a
+ moderated or unmoderated caucus.
+
+- `AddGslSpeakerEvent`
+ - Payload: `representation_id`.
+ - Adds a valid representation to the GSL queue during Open or Closed GSL.
+ Duplicate queue entries are rejected.
+
+- `GrantFloorEvent`
+ - Payload: `representation_id` and optional positive `seconds`.
+ - Makes a valid representation the current speaker in GSL, moderated
+ caucus, or Tour de Table. In GSL and Tour de Table, removes that
+ representation from its pending queue/list first. In moderated caucus, it
+ leaves `caucus_list` unchanged. It is not valid during unmoderated caucus.
- `MarkRollCallEvent`
- Payload: `delegation_id` and `choice` (`Present`, `Present and Voting`, or
@@ -122,10 +141,6 @@ Privilege`.
- Marks unrecorded delegations absent, creates voting eligibility, and moves
to Open GSL.
-- `InsertQueueEvent`
- - Payload: `target`.
- - Adds the target delegation to the GSL queue.
-
- `SetAgendaEvent`
- Payload: `agenda`, a list of strings.
- Declared in the schema, but its handler is not implemented. Do not use it
From c0100e865a501bb2317cd9f5a5e63c9d39ac75e9 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 22:44:37 -0300
Subject: [PATCH 17/19] style: format files
---
backend/app/session/engine.py | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index 51240d6..ad8336b 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -502,9 +502,7 @@ def handle_close_informal_voting(
def apply_passed_motion(
- state: SessionLiveState,
- motion: MotionContext,
- return_state: States
+ state: SessionLiveState, motion: MotionContext, return_state: States
) -> None:
"""Apply a passed procedural motion to the live session state in place."""
next_state = return_state # as fallback
@@ -565,7 +563,15 @@ def apply_passed_motion(
pass
case Motions.TOUR_DE_TABLE:
next_state = States.TOUR_DE_TABLE
- state.caucus_list = [del_id for del_id, choice in state.roll_call.registry.items() if choice in (enums.RollCallChoice.PRESENT, enums.RollCallChoice.PRESENT_AND_VOTING)]
+ state.caucus_list = [
+ del_id
+ for del_id, choice in state.roll_call.registry.items()
+ if choice
+ in (
+ enums.RollCallChoice.PRESENT,
+ enums.RollCallChoice.PRESENT_AND_VOTING,
+ )
+ ]
case Motions.END_DEBATE:
# clean gsl list
From 3d843801521e063b5d4fffb1dfcb092e2accd626 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Sun, 9 Aug 2026 22:58:42 -0300
Subject: [PATCH 18/19] fix(backend): restore state after quorum roll call
Co-authored-by: Codex
---
backend/app/session/engine.py | 6 +-
backend/app/session/models.py | 1 +
backend/app/tests/session/test_engine.py | 77 ++++++++++++++++++++++++
3 files changed, 81 insertions(+), 3 deletions(-)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index ad8336b..db58185 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -599,7 +599,7 @@ def apply_passed_motion(
# note: seems more like an informal consultation
pass
case Motions.QUORUM:
- state.roll_call = RollCallContext(registry={})
+ state.roll_call = RollCallContext(registry={}, return_state=return_state)
next_state = States.ROLL_CALL
case _:
raise InvalidProceduralMove("Undefined motion type")
@@ -910,8 +910,8 @@ def handle_close_roll_call(
for delegation_id in state.delegations:
state.roll_call.registry.setdefault(delegation_id, RollCallChoice.ABSENT)
- # may also empty roll call if needed, to avoid loading stale values
- state.current_state = States.OPEN_GSL
+ # Initial roll call enters Open GSL; quorum roll calls restore their source state.
+ state.current_state = state.roll_call.return_state or States.OPEN_GSL
state.voting_choice = {
delegation_id: RollCallChoice.PRESENT_AND_VOTING
if choice == RollCallChoice.PRESENT_AND_VOTING
diff --git a/backend/app/session/models.py b/backend/app/session/models.py
index 83cac5b..340d49c 100644
--- a/backend/app/session/models.py
+++ b/backend/app/session/models.py
@@ -89,6 +89,7 @@ class DebateContext(BaseModel):
class RollCallContext(BaseModel):
registry: dict[int, enums.RollCallChoice] = {} # Delegation Id as key
+ return_state: enums.States | None = None
class AgendaItem(BaseModel):
diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py
index b392460..a0d1933 100644
--- a/backend/app/tests/session/test_engine.py
+++ b/backend/app/tests/session/test_engine.py
@@ -487,6 +487,83 @@ def test_chair_can_close_roll_call(
}
+def test_quorum_roll_call_restores_closed_gsl(
+ engine: eng.SessionEngine,
+ session_state: md.SessionLiveState,
+ close_procedural_voting_event: sch.CloseProceduralVotingEvent,
+ close_roll_call_event: sch.CloseRollCallEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ session_state.current_state = enums.States.VOTING_EXECUTION
+ session_state.voting_choice = {
+ 0: enums.RollCallChoice.PRESENT,
+ 1: enums.RollCallChoice.PRESENT,
+ 2: enums.RollCallChoice.PRESENT,
+ }
+ session_state.voting = md.VotingContext(
+ target_type=enums.VotingType.PROCEDURAL,
+ return_state=enums.States.CLOSED_GSL,
+ motion_in_vote=md.MotionContext(
+ id=1,
+ type=enums.Motions.QUORUM,
+ timestamp=datetime.now(UTC),
+ ),
+ voting_registry={
+ 0: enums.VotingChoice.FAVOUR,
+ 1: enums.VotingChoice.FAVOUR,
+ },
+ )
+
+ state = engine.dispatch(session_state, close_procedural_voting_event, chair_actor)
+
+ assert state.current_state == enums.States.ROLL_CALL
+ assert state.roll_call.return_state == enums.States.CLOSED_GSL
+
+ state = engine.dispatch(state, close_roll_call_event, chair_actor)
+
+ assert state.current_state == enums.States.CLOSED_GSL
+
+
+def test_quorum_roll_call_restores_moderated_caucus(
+ engine: eng.SessionEngine,
+ session_state: md.SessionLiveState,
+ close_procedural_voting_event: sch.CloseProceduralVotingEvent,
+ close_roll_call_event: sch.CloseRollCallEvent,
+ chair_actor: md.SessionActor,
+) -> None:
+ debate = md.DebateContext(
+ debate_type=enums.DebateTypes.MODERATED_DEBATE,
+ return_state=enums.States.OPEN_GSL,
+ per_speaker_seconds=60,
+ )
+ session_state.current_state = enums.States.VOTING_EXECUTION
+ session_state.debate = debate
+ session_state.voting_choice = {
+ 0: enums.RollCallChoice.PRESENT,
+ 1: enums.RollCallChoice.PRESENT,
+ 2: enums.RollCallChoice.PRESENT,
+ }
+ session_state.voting = md.VotingContext(
+ target_type=enums.VotingType.PROCEDURAL,
+ return_state=enums.States.MODERATED_CAUCUS,
+ motion_in_vote=md.MotionContext(
+ id=1,
+ type=enums.Motions.QUORUM,
+ timestamp=datetime.now(UTC),
+ ),
+ voting_registry={
+ 0: enums.VotingChoice.FAVOUR,
+ 1: enums.VotingChoice.FAVOUR,
+ },
+ )
+
+ state = engine.dispatch(session_state, close_procedural_voting_event, chair_actor)
+ state = engine.dispatch(state, close_roll_call_event, chair_actor)
+
+ assert state.current_state == enums.States.MODERATED_CAUCUS
+ assert state.debate == debate
+
+
def test_delegate_cannot_close_roll_call(
engine: eng.SessionEngine,
session_state: md.SessionLiveState,
From 418de8306a412b1817308e7437f566597da494a8 Mon Sep 17 00:00:00 2001
From: wate <110754234+r0liveir@users.noreply.github.com>
Date: Mon, 10 Aug 2026 14:57:31 -0300
Subject: [PATCH 19/19] refactor(backend): remove deprecated voting choice
Co-authored-by: Codex
---
AGENTS.md | 1 +
backend/app/session/engine.py | 7 +++----
backend/app/session/models.py | 2 --
backend/app/tests/session/test_engine.py | 10 +++++-----
sonar-project.properties | 2 +-
5 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 8aee52b..4a4e81d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,3 +15,4 @@
- Treat `representation_id` as the delegation identity everywhere.
- For cross-boundary changes, verify the backend tests and the frontend build/typecheck. Report pre-existing failures separately.
- Inspect security advisories before changing dependencies. Do not run `npm audit fix --force` without approval and a review of the resulting version changes.
+- For any commit done by an agent, include the agent as coauthor (for example, Codex should append `-m "Co-authored-by: Codex "`)
diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py
index db58185..02fe754 100644
--- a/backend/app/session/engine.py
+++ b/backend/app/session/engine.py
@@ -115,13 +115,13 @@ def count_present_delegations(state: SessionLiveState) -> int:
"""Count total present delegations.
A delegation is considered present (even if AFK)
if it's Roll Call Choice is Present / Present and Voting"""
- if state.voting_choice is None:
+ if not state.roll_call.registry:
return 0
return len(
[
True
- for _, vote in state.voting_choice.items()
+ for _, vote in state.roll_call.registry.items()
if vote == enums.RollCallChoice.PRESENT
or vote == enums.RollCallChoice.PRESENT_AND_VOTING
]
@@ -374,7 +374,6 @@ def handle_open_session(
state.current_state = States.ROLL_CALL
state.roll_call = RollCallContext(registry={})
- state.voting_choice = {}
state.gsl_queue = []
state.current_speaker = None
state.debate = None
@@ -912,7 +911,7 @@ def handle_close_roll_call(
# Initial roll call enters Open GSL; quorum roll calls restore their source state.
state.current_state = state.roll_call.return_state or States.OPEN_GSL
- state.voting_choice = {
+ state.roll_call.registry = {
delegation_id: RollCallChoice.PRESENT_AND_VOTING
if choice == RollCallChoice.PRESENT_AND_VOTING
else RollCallChoice.PRESENT
diff --git a/backend/app/session/models.py b/backend/app/session/models.py
index 340d49c..0e86ab4 100644
--- a/backend/app/session/models.py
+++ b/backend/app/session/models.py
@@ -143,8 +143,6 @@ class SessionLiveState(BaseModel):
voting: VotingContext | None = None
# present delegations with voting choice
- voting_choice: dict[int, enums.RollCallChoice] | None = None # DelegationId as key
-
roll_call: RollCallContext # Not None, even if registry is empty
# Additional config
diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py
index a0d1933..18afbfd 100644
--- a/backend/app/tests/session/test_engine.py
+++ b/backend/app/tests/session/test_engine.py
@@ -481,7 +481,7 @@ def test_chair_can_close_roll_call(
state = engine.dispatch(session_state, close_roll_call_event, chair_actor)
assert state.current_state == enums.States.OPEN_GSL
- assert state.voting_choice == {
+ assert state.roll_call.registry == {
1: enums.RollCallChoice.PRESENT,
2: enums.RollCallChoice.PRESENT_AND_VOTING,
}
@@ -495,7 +495,7 @@ def test_quorum_roll_call_restores_closed_gsl(
chair_actor: md.SessionActor,
) -> None:
session_state.current_state = enums.States.VOTING_EXECUTION
- session_state.voting_choice = {
+ session_state.roll_call.registry = {
0: enums.RollCallChoice.PRESENT,
1: enums.RollCallChoice.PRESENT,
2: enums.RollCallChoice.PRESENT,
@@ -538,7 +538,7 @@ def test_quorum_roll_call_restores_moderated_caucus(
)
session_state.current_state = enums.States.VOTING_EXECUTION
session_state.debate = debate
- session_state.voting_choice = {
+ session_state.roll_call.registry = {
0: enums.RollCallChoice.PRESENT,
1: enums.RollCallChoice.PRESENT,
2: enums.RollCallChoice.PRESENT,
@@ -729,7 +729,7 @@ def test_chair_can_close_passed_procedural_vote(
close_procedural_voting_event: sch.CloseProceduralVotingEvent,
chair_actor: md.SessionActor,
) -> None:
- procedural_voting_state.voting_choice = {
+ procedural_voting_state.roll_call.registry = {
0: enums.RollCallChoice.PRESENT,
1: enums.RollCallChoice.PRESENT,
2: enums.RollCallChoice.PRESENT,
@@ -757,7 +757,7 @@ def test_chair_can_close_failed_procedural_vote(
close_procedural_voting_event: sch.CloseProceduralVotingEvent,
chair_actor: md.SessionActor,
) -> None:
- procedural_voting_state.voting_choice = {
+ procedural_voting_state.roll_call.registry = {
0: enums.RollCallChoice.PRESENT,
1: enums.RollCallChoice.PRESENT,
2: enums.RollCallChoice.PRESENT,
diff --git a/sonar-project.properties b/sonar-project.properties
index 02e03c5..d633df7 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -12,6 +12,6 @@ sonar.python.version=3.11
sonar.sources=backend/app,frontend/src
sonar.tests=backend/app/tests
-sonar.exclusions=backend/app/tests/**
+sonar.exclusions=backend/app/tests/**, frontend/src/components/ui/**
sonar.coverage.exclusions=frontend/src/**