Skip to content

Commit 551e73f

Browse files
committed
add test data, fix bugs, refactor
1 parent 8e64f4d commit 551e73f

File tree

7 files changed

+197
-71
lines changed

7 files changed

+197
-71
lines changed

src/alembic/versions/243190df5588_create_election_tables.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
def upgrade() -> None:
2323
op.create_table(
2424
"election",
25-
sa.Column("slug", sa.String(length=32), nullable=False),
26-
sa.Column("name", sa.String(length=32), nullable=False),
25+
sa.Column("slug", sa.String(length=64), nullable=False),
26+
sa.Column("name", sa.String(length=64), nullable=False),
2727
sa.Column("type", sa.String(length=64), default="general_election"),
2828
sa.Column("datetime_start_nominations", sa.DateTime(), nullable=False),
2929
sa.Column("datetime_start_voting", sa.DateTime(), nullable=False),
@@ -35,8 +35,8 @@ def upgrade() -> None:
3535
"election_nominee",
3636
sa.Column("computing_id", sa.String(length=32), nullable=False),
3737
sa.Column("full_name", sa.String(length=64), nullable=False),
38-
sa.Column("facebook", sa.String(length=64), nullable=True),
39-
sa.Column("instagram", sa.String(length=64), nullable=True),
38+
sa.Column("facebook", sa.String(length=128), nullable=True),
39+
sa.Column("instagram", sa.String(length=128), nullable=True),
4040
sa.Column("email", sa.String(length=64), nullable=True),
4141
sa.Column("discord", sa.String(length=32), nullable=True),
4242
sa.Column("discord_id", sa.String(length=32), nullable=True),

src/elections/crud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@ async def update_election(db_session: AsyncSession, new_election: Election) -> b
4646
sqlalchemy
4747
.update(Election)
4848
.where(Election.slug == target_slug)
49-
.values(new_election)
49+
.values(new_election.to_update_dict())
5050
)
5151
return True

src/elections/tables.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717

1818
election_types = ["general_election", "by_election", "council_rep_election"]
1919

20+
MAX_ELECTION_NAME = 64
21+
MAX_ELECTION_SLUG = 64
22+
2023
class Election(Base):
2124
__tablename__ = "election"
2225

2326
# Slugs are unique identifiers
24-
slug = Column(String(32), primary_key=True)
25-
name = Column(String(32), nullable=False)
27+
slug = Column(String(MAX_ELECTION_SLUG), primary_key=True)
28+
name = Column(String(MAX_ELECTION_NAME), nullable=False)
2629
type = Column(String(64), default="general_election")
2730
datetime_start_nominations = Column(DateTime, nullable=False)
2831
datetime_start_voting = Column(DateTime, nullable=False)
@@ -53,15 +56,28 @@ def public_details(self) -> dict:
5356
"datetime_end_voting": self.datetime_end_voting.isoformat(),
5457
}
5558

59+
def to_update_dict(self) -> dict:
60+
return {
61+
"slug": self.slug,
62+
"name": self.name,
63+
"type": self.type,
64+
65+
"datetime_start_nominations": self.datetime_start_nominations,
66+
"datetime_start_voting": self.datetime_start_voting,
67+
"datetime_end_voting": self.datetime_end_voting,
68+
69+
"survey_link": self.survey_link,
70+
}
71+
5672
# Each row represents a nominee of a given election
5773
class Nominee(Base):
5874
__tablename__ = "election_nominee"
5975

6076
# Previously named sfuid
6177
computing_id = Column(String(COMPUTING_ID_LEN), primary_key=True)
6278
full_name = Column(String(64), nullable=False)
63-
facebook = Column(String(64))
64-
instagram = Column(String(64))
79+
facebook = Column(String(128))
80+
instagram = Column(String(128))
6581
email = Column(String(64))
6682
discord = Column(String(DISCORD_NAME_LEN))
6783
discord_id = Column(String(DISCORD_ID_LEN))

src/elections/urls.py

Lines changed: 96 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import elections
1111
from elections.tables import Election, election_types
1212
from permission.types import ElectionOfficer, WebsiteAdmin
13-
from utils.urls import logged_in_or_raise
13+
from utils.urls import is_logged_in
1414

1515
_logger = logging.getLogger(__name__)
1616

@@ -21,20 +21,50 @@
2121

2222
def _slugify(text: str) -> str:
2323
"""Creates a unique slug based on text passed in. Assumes non-unicode text."""
24-
return re.sub(r"[\W_]+", "-", text)
24+
return re.sub(r"[\W_]+", "-", text.replace("/", "").replace("&", ""))
2525

2626
async def _validate_user(
2727
request: Request,
2828
db_session: database.DBSession,
2929
) -> tuple[bool, str, str]:
30-
session_id, computing_id = logged_in_or_raise(request, db_session)
30+
logged_in, session_id, computing_id = await is_logged_in(request, db_session)
31+
if not logged_in:
32+
return False, None, None
33+
3134
has_permission = await ElectionOfficer.has_permission(db_session, computing_id)
3235
if not has_permission:
3336
has_permission = await WebsiteAdmin.has_permission(db_session, computing_id)
37+
3438
return has_permission, session_id, computing_id
3539

3640
# elections ------------------------------------------------------------- #
3741

42+
@router.get(
43+
"/by_name/{name:str}",
44+
description="Retrieves the election data for an election by name"
45+
)
46+
async def get_election(
47+
request: Request,
48+
db_session: database.DBSession,
49+
name: str,
50+
):
51+
election = await elections.crud.get_election(db_session, _slugify(name))
52+
if election is None:
53+
raise HTTPException(
54+
status_code=status.HTTP_400_BAD_REQUEST,
55+
detail=f"election with slug {_slugify(name)} does not exist"
56+
)
57+
elif datetime.now() >= election.datetime_start_voting:
58+
# after the voting period starts, all election data becomes public
59+
return JSONResponse(election.serializable_dict())
60+
61+
is_valid_user, _, _ = await _validate_user(request, db_session)
62+
return JSONResponse(
63+
election.serializable_dict()
64+
if is_valid_user
65+
else election.public_details()
66+
)
67+
3868
@router.post(
3969
"/by_name/{name:str}",
4070
description="Creates an election and places it in the database. Returns election json on success",
@@ -60,59 +90,57 @@ async def create_election(
6090
# TODO: is this header actually required?
6191
headers={"WWW-Authenticate": "Basic"},
6292
)
63-
elif elections.crud.get_election(db_session, _slugify(name)) is not None:
93+
elif len(name) <= elections.tables.MAX_ELECTION_NAME:
94+
raise HTTPException(
95+
status_code=status.HTTP_400_BAD_REQUEST,
96+
detail=f"election name {name} is too long",
97+
)
98+
elif len(_slugify(name)) <= elections.tables.MAX_SLUG_NAME:
99+
raise HTTPException(
100+
status_code=status.HTTP_400_BAD_REQUEST,
101+
detail=f"election slug {_slugify(name)} is too long",
102+
)
103+
elif await elections.crud.get_election(db_session, _slugify(name)) is not None:
64104
# don't overwrite a previous election
65105
raise HTTPException(
66106
status_code=status.HTTP_400_BAD_REQUEST,
67107
detail="would overwrite previous election",
68108
)
109+
elif not (
110+
(datetime_start_nominations <= datetime_start_voting)
111+
and (datetime_start_voting <= datetime_end_voting)
112+
):
113+
raise HTTPException(
114+
status_code=status.HTTP_400_BAD_REQUEST,
115+
detail="dates must be in order from earliest to latest",
116+
)
117+
118+
# TODO: force dates to be in order; here & on the update election endpoint
69119

70120
await elections.crud.create_election(
71121
Election(
72-
_slugify(name),
73-
name,
74-
election_type,
75-
datetime_start_nominations,
76-
datetime_start_voting,
77-
datetime_end_voting,
78-
survey_link
122+
slug = _slugify(name),
123+
name = name,
124+
type = election_type,
125+
datetime_start_nominations = datetime_start_nominations,
126+
datetime_start_voting = datetime_start_voting,
127+
datetime_end_voting = datetime_end_voting,
128+
survey_link = survey_link
79129
),
80130
db_session
81131
)
82132
await db_session.commit()
83133

84-
election = elections.crud.get_election(db_session, _slugify(name))
134+
election = await elections.crud.get_election(db_session, _slugify(name))
85135
return JSONResponse(election.serializable_dict())
86136

87-
@router.delete(
88-
"/by_name/{name:str}",
89-
description="Deletes an election from the database. Returns whether the election exists after deletion."
90-
)
91-
async def delete_election(
92-
request: Request,
93-
db_session: database.DBSession,
94-
name: str
95-
):
96-
is_valid_user, _, _ = await _validate_user(request, db_session)
97-
if not is_valid_user:
98-
raise HTTPException(
99-
status_code=status.HTTP_401_UNAUTHORIZED,
100-
detail="must have election officer permission",
101-
# TODO: is this header actually required?
102-
headers={"WWW-Authenticate": "Basic"},
103-
)
104-
105-
await elections.crud.delete_election(_slugify(name), db_session)
106-
await db_session.commit()
107-
108-
old_election = elections.crud.get_election(db_session, _slugify(name))
109-
return JSONResponse({"exists": old_election is None})
110-
111137
@router.patch(
112138
"/by_name/{name:str}",
113139
description="""
114140
Updates an election in the database.
115-
Note that this don't let you to change the name of an election as it would generate a new slug!
141+
142+
Note that this doesn't let you change the name of an election, unless the new
143+
name produces the same slug.
116144
117145
Returns election json on success.
118146
"""
@@ -135,15 +163,23 @@ async def update_election(
135163
detail="must have election officer or admin permission",
136164
headers={"WWW-Authenticate": "Basic"},
137165
)
166+
elif not (
167+
(datetime_start_nominations <= datetime_start_voting)
168+
and (datetime_start_voting <= datetime_end_voting)
169+
):
170+
raise HTTPException(
171+
status_code=status.HTTP_400_BAD_REQUEST,
172+
detail="dates must be in order from earliest to latest",
173+
)
138174

139175
new_election = Election(
140-
_slugify(name),
141-
name,
142-
election_type,
143-
datetime_start_nominations,
144-
datetime_start_voting,
145-
datetime_end_voting,
146-
survey_link
176+
slug = _slugify(name),
177+
name = name,
178+
type = election_type,
179+
datetime_start_nominations = datetime_start_nominations,
180+
datetime_start_voting = datetime_start_voting,
181+
datetime_end_voting = datetime_end_voting,
182+
survey_link = survey_link
147183
)
148184
success = await elections.crud.update_election(db_session, new_election)
149185
if not success:
@@ -154,31 +190,32 @@ async def update_election(
154190
else:
155191
await db_session.commit()
156192

157-
election = elections.crud.get_election(db_session, _slugify(name))
193+
election = await elections.crud.get_election(db_session, _slugify(name))
158194
return JSONResponse(election.serializable_dict())
159195

160-
@router.get(
196+
@router.delete(
161197
"/by_name/{name:str}",
162-
description="Retrieves the election data for an election by name"
198+
description="Deletes an election from the database. Returns whether the election exists after deletion."
163199
)
164-
async def get_election(
200+
async def delete_election(
165201
request: Request,
166202
db_session: database.DBSession,
167-
name: str,
203+
name: str
168204
):
169-
election = elections.crud.get_election(db_session, _slugify(name))
170-
if election is None:
205+
is_valid_user, _, _ = await _validate_user(request, db_session)
206+
if not is_valid_user:
171207
raise HTTPException(
172-
status_code=status.HTTP_400_BAD_REQUEST,
173-
detail=f"election with slug {_slugify(name)} does not exist"
208+
status_code=status.HTTP_401_UNAUTHORIZED,
209+
detail="must have election officer permission",
210+
# TODO: is this header actually required?
211+
headers={"WWW-Authenticate": "Basic"},
174212
)
175213

176-
is_valid_user, _, _ = await _validate_user(request, db_session)
177-
return JSONResponse(
178-
election.serializable_dict()
179-
if is_valid_user
180-
else election.public_details()
181-
)
214+
await elections.crud.delete_election(_slugify(name), db_session)
215+
await db_session.commit()
216+
217+
old_election = await elections.crud.get_election(db_session, _slugify(name))
218+
return JSONResponse({"exists": old_election is None})
182219

183220
# registration ------------------------------------------------------------- #
184221

0 commit comments

Comments
 (0)