Skip to content

Commit 0a1a8d0

Browse files
committed
Added: ballot bdd to link votes together for 1 voter
1 parent a41901b commit 0a1a8d0

File tree

6 files changed

+132
-21
lines changed

6 files changed

+132
-21
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ uvicorn app.main:app --reload --env-file .env.local
7777

7878
> If you need to alter the database, you can create new migrations using [alembic](https://alembic.sqlalchemy.org/en/latest/index.html).
7979
80+
## Migration:
81+
82+
On bdd structure change:
83+
Try an autogenerated migration:
84+
```
85+
alembic revision --autogenerate -m "change context"
86+
```
87+
88+
To apply it:
89+
```
90+
alembic upgrade head
91+
```
8092

8193
## TODO
8294

app/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ def jws_verify(token: str) -> Mapping[str, t.Any]:
2727
def create_ballot_token(
2828
vote_ids: int | list[int],
2929
election_ref: str,
30+
ballot_id:int
3031
) -> str:
3132
if isinstance(vote_ids, int):
3233
vote_ids = [vote_ids]
3334
vote_ids = sorted(vote_ids)
3435
return jws.sign(
35-
{"votes": vote_ids, "election": election_ref},
36+
{ "votes": vote_ids, "election": election_ref, "ballot": ballot_id },
3637
settings.secret,
3738
algorithm="HS256",
3839
)

app/crud.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,33 @@ def create_invite_tokens(
218218
_check_election_is_not_ended(get_election(db, election_ref))
219219
now = datetime.now()
220220
params = {"date_created": now, "date_modified": now, "election_ref": election_ref}
221-
db_votes = [models.Vote(**params) for _ in range(num_voters * num_candidates)]
222-
db.bulk_save_objects(db_votes, return_defaults=True)
223-
db.commit()
221+
222+
try:
223+
db_ballots = [models.Ballot(election_ref=election_ref) for _ in range(num_voters)]
224+
db.bulk_save_objects(db_ballots, return_defaults=True)
225+
226+
db_votes = []
227+
228+
for ballot in db_ballots:
229+
for _ in range(num_candidates):
230+
db_votes.append(models.Vote(**params, ballot_id=ballot.id))
231+
232+
db.bulk_save_objects(db_votes, return_defaults=True)
233+
db.commit()
234+
except Exception as e:
235+
db.rollback()
236+
raise e
237+
238+
tokens = []
224239
vote_ids = [int(str(v.id)) for v in db_votes]
225-
tokens = [
226-
create_ballot_token(vote_ids[i::num_voters], election_ref)
227-
for i in range(num_voters)
228-
]
240+
241+
for i, ballot in enumerate(db_ballots):
242+
start = i * num_candidates
243+
end = start + num_candidates
244+
tokens.append(
245+
create_ballot_token(vote_ids[start:end], election_ref, int(str(ballot.id)))
246+
)
247+
229248
return tokens
230249

231250

@@ -405,18 +424,29 @@ def create_ballot(db: Session, ballot: schemas.BallotCreate) -> schemas.BallotGe
405424
)
406425
_check_ballot_is_consistent(election, ballot)
407426

408-
# Ideally, we would use RETURNING but it does not work yet for SQLite
409-
db_votes = [
410-
models.Vote(**v.model_dump(), election_ref=ballot.election_ref) for v in ballot.votes
411-
]
412-
db.add_all(db_votes)
413-
db.commit()
414-
for v in db_votes:
415-
db.refresh(v)
427+
try:
428+
db_ballot = models.Ballot(election_ref=ballot.election_ref)
429+
db.add(db_ballot)
430+
db.flush()
431+
432+
# Create votes and associate them with the ballot
433+
db_votes = [
434+
models.Vote(**v.model_dump(), election_ref=ballot.election_ref, ballot_id=db_ballot.id)
435+
for v in ballot.votes
436+
]
437+
db.add_all(db_votes)
438+
db.commit()
439+
db.refresh(db_ballot)
440+
441+
for v in db_votes:
442+
db.refresh(v)
443+
except Exception as e:
444+
db.rollback()
445+
raise e
416446

417447
votes_get = [schemas.VoteGet.model_validate(v) for v in db_votes]
418448
vote_ids = [v.id for v in votes_get]
419-
token = create_ballot_token(vote_ids, ballot.election_ref)
449+
token = create_ballot_token(vote_ids, ballot.election_ref, int(db_ballot.id))
420450
return schemas.BallotGet(votes=votes_get, token=token, election=election)
421451

422452

@@ -511,6 +541,13 @@ def update_ballot(
511541
if len(db_votes) != len(vote_ids):
512542
raise errors.NotFoundError("votes")
513543

544+
# Verify all votes belong to the same ballot
545+
ballot_ids = {int(v.ballot_id) for v in db_votes if v.ballot_id is not None}
546+
547+
if len(ballot_ids) > 1:
548+
raise errors.ForbiddenError("All votes must belong to the same ballot")
549+
550+
# old API does not contains ballot id in the token
514551
election = schemas.ElectionGet.model_validate(db_votes[0].election)
515552

516553
for vote, db_vote in zip(ballot.votes, db_votes):
@@ -521,7 +558,6 @@ def update_ballot(
521558
db.commit()
522559

523560
votes_get = [schemas.VoteGet.model_validate(v) for v in db_votes]
524-
token = create_ballot_token(vote_ids, election_ref)
525561
return schemas.BallotGet(votes=votes_get, token=token, election=election)
526562

527563

app/models.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from sqlalchemy.sql import func
33
from sqlalchemy.orm import relationship
44
from .database import Base
5-
5+
import uuid # Pour pouvoir appeler uuid.uuid4()
6+
from sqlalchemy import UUID
67

78
class Election(Base):
89
__tablename__ = "elections"
@@ -24,6 +25,7 @@ class Election(Base):
2425
grades = relationship("Grade", back_populates="election")
2526
candidates = relationship("Candidate", back_populates="election")
2627
votes = relationship("Vote", back_populates="election")
28+
ballots = relationship("Ballot", back_populates="election")
2729

2830

2931
class Candidate(Base):
@@ -72,3 +74,20 @@ class Vote(Base):
7274

7375
election_ref = Column(String(20), ForeignKey("elections.ref"))
7476
election = relationship("Election", back_populates="votes")
77+
78+
ballot_id = Column(Integer, ForeignKey("ballots.id"), nullable=True)
79+
ballot = relationship("Ballot", back_populates="votes")
80+
81+
82+
class Ballot(Base):
83+
__tablename__ = "ballots"
84+
85+
id = Column(Integer, primary_key=True, index=True)
86+
87+
voter_uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
88+
89+
date_created = Column(DateTime, server_default=func.now())
90+
election_ref = Column(String(20), ForeignKey("elections.ref"))
91+
92+
election = relationship("Election", back_populates="ballots")
93+
votes = relationship("Vote", back_populates="ballot")

app/tests/test_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def test_ballot_token():
4141
"""
4242
vote_ids = list(range(1000))
4343
election_ref = "qwertyuiop"
44-
token = create_ballot_token(vote_ids, election_ref)
44+
token = create_ballot_token(vote_ids, election_ref, 1)
4545
data = jws_verify(token)
46-
assert data == {"votes": vote_ids, "election": election_ref}
46+
assert data == {"votes": vote_ids, "election": election_ref, "ballot": 1}
4747

4848

4949
def test_admin_token():
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Add ballot table and ballot_id to votes
2+
3+
Revision ID: 81b4c6fc826d
4+
Revises: 48bf0bdc1ca1
5+
Create Date: 2025-10-31 17:07:55.504501
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '81b4c6fc826d'
14+
down_revision = '48bf0bdc1ca1'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table('ballots',
22+
sa.Column('id', sa.Integer(), nullable=False),
23+
sa.Column('voter_uuid', sa.UUID(), nullable=True),
24+
sa.Column('date_created', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
25+
sa.Column('election_ref', sa.String(length=20), nullable=True),
26+
sa.ForeignKeyConstraint(['election_ref'], ['elections.ref'], ),
27+
sa.PrimaryKeyConstraint('id')
28+
)
29+
op.create_index(op.f('ix_ballots_id'), 'ballots', ['id'], unique=False)
30+
op.create_index(op.f('ix_ballots_voter_uuid'), 'ballots', ['voter_uuid'], unique=True)
31+
op.add_column('votes', sa.Column('ballot_id', sa.Integer(), nullable=True))
32+
op.create_foreign_key(None, 'votes', 'ballots', ['ballot_id'], ['id'])
33+
# ### end Alembic commands ###
34+
35+
36+
def downgrade() -> None:
37+
# ### commands auto generated by Alembic - please adjust! ###
38+
op.drop_constraint(None, 'votes', type_='foreignkey')
39+
op.drop_column('votes', 'ballot_id')
40+
op.drop_index(op.f('ix_ballots_voter_uuid'), table_name='ballots')
41+
op.drop_index(op.f('ix_ballots_id'), table_name='ballots')
42+
op.drop_table('ballots')
43+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)