From ef8c2c576043330a7a22d9549284003e57c284bb Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Fri, 1 Mar 2024 13:55:25 +0200 Subject: Add possibility to manage own vote --- app.py | 42 ++++++++++++++++++++++++++++++++++-- db.py | 35 +++++++++++++++++++++++++----- migrations/0004_vote_manage_code.sql | 1 + static/styles.css | 12 ++++++++++- templates/poll.html.j2 | 23 ++++++++++++++------ templates/poll_vote_list.html.j2 | 23 +++++++++++++++++--- templates/poll_vote_table.html.j2 | 19 +++++++++++++--- 7 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 migrations/0004_vote_manage_code.sql diff --git a/app.py b/app.py index bfb075f..f8422a9 100644 --- a/app.py +++ b/app.py @@ -111,6 +111,13 @@ def poll(id): if not validate_uuid(id): return error_page("Invalid poll ID", 400) + prefill_voter_name = request.args.get("prefill_voter_name") + + voter_codes: list[str] = [] + for k, _ in request.cookies.items(): + if k.startswith("diddle_voter_code_"): + voter_codes.append(k.replace("diddle_voter_code_", "")) + poll = db.get_poll(id) if poll is None: return error_page("Poll not found", 404) @@ -127,11 +134,15 @@ def poll(id): voter_names_set: set[str] = set() selections: dict[VoterNameChoiceIdPair, int] = {} + managed_voter_names: dict[str, str] = {} for choice in poll.choices: for vote in choice.votes: selections[(vote.voter_name, choice.id)] = vote.value voter_names_set.add(vote.voter_name) + if vote.manage_code in voter_codes: + managed_voter_names[vote.voter_name] = vote.manage_code + voter_names = list(voter_names_set) voter_names.sort() @@ -140,7 +151,9 @@ def poll(id): poll=poll, selections=selections, choices=poll.choices, + prefill_voter_name=prefill_voter_name, voter_names=voter_names, + managed_voter_names=managed_voter_names, now=datetime.datetime.now(), display_mode=display_mode)) @@ -175,11 +188,36 @@ def vote_poll(id): choice_id = k.replace("choice_", "") selections[choice_id] = 1 - db.vote_poll(id, voter_name, selections) + manage_code = db.vote_poll(id, voter_name, selections) email_client.send_participation_email_if_enabled(poll_id=id, voter_name=voter_name) - return redirect(f"/poll/{id}") + response = make_response( + redirect(f"/poll/{id}") + ) + response.set_cookie(f"diddle_voter_code_{manage_code}", "1", + samesite="Strict", secure=False) + return response + +@app.post("/poll//delete_voter") +def delete_voter(id): + if not validate_uuid(id): + return error_page("Invalid poll ID", 400) + + voter_manage_code = request.form["voter_code"] + + voter_name = db.get_voter_name_by_manage_code(voter_manage_code) + if voter_name is None: + return error_page("Voter not found", 404) + + db.delete_voter(voter_manage_code) + + resp = make_response( + redirect(f"/poll/{id}?prefill_voter_name={voter_name}") + ) + resp.set_cookie(f"diddle_voter_code_{voter_manage_code}", "", expires=0, + samesite="Strict", secure=False) + return resp @app.post("/manage//update_info") def update_poll_info(code): diff --git a/db.py b/db.py index d91692a..5effa5a 100644 --- a/db.py +++ b/db.py @@ -3,6 +3,7 @@ from typing import Literal from dataclasses import dataclass import datetime import psycopg2 +import uuid BASE_URL = os.environ["BASE_URL"] @@ -55,6 +56,7 @@ class Vote: choice_id: str voter_name: str value: int # 0 or 1 + manage_code: str @dataclass class Choice: @@ -138,6 +140,7 @@ def tuple_to_vote(vote_t: tuple) -> Vote: choice_id=vote_t[2], voter_name=vote_t[3], value=vote_t[4], + manage_code=vote_t[5] ) def get_poll(id: str): @@ -205,16 +208,18 @@ def create_poll(title: str, conn.rollback() raise e -def vote_poll(poll_id: str, voter_name: str, selections: dict[str, int]) -> None: +def vote_poll(poll_id: str, voter_name: str, selections: dict[str, int]) -> str: + """Returns the manage code of the vote.""" with db.cursor() as (conn, cur): try: + manage_code = str(uuid.uuid4()) for choice_id in selections: value = selections[choice_id] - cur.execute("INSERT INTO votes (poll_id, voter_name, choice_id, value)" - "VALUES (%s, %s, %s, %s)", - (poll_id, voter_name, choice_id, value)) - + cur.execute("INSERT INTO votes (poll_id, voter_name, choice_id, value, manage_code)" + "VALUES (%s, %s, %s, %s, %s)", + (poll_id, voter_name, choice_id, value, manage_code)) conn.commit() + return manage_code except Exception as e: conn.rollback() raise e @@ -310,6 +315,26 @@ def delete_poll(code: str) -> None: conn.rollback() raise e +def get_voter_name_by_manage_code(voter_manage_code: str) -> str | None: + with db.cursor() as (conn, cur): + try: + cur.execute("SELECT voter_name FROM votes WHERE manage_code = %s", (voter_manage_code,)) + voter_name = cur.fetchone() + conn.commit() + return voter_name[0] if voter_name else None + except Exception as e: + conn.rollback() + raise e + +def delete_voter(voter_manage_code: str) -> None: + with db.cursor() as (conn, cur): + try: + cur.execute("DELETE FROM votes WHERE manage_code = %s", (voter_manage_code,)) + conn.commit() + except Exception as e: + conn.rollback() + raise e + ### Migrations def ensure_migration_table_exists() -> None: diff --git a/migrations/0004_vote_manage_code.sql b/migrations/0004_vote_manage_code.sql new file mode 100644 index 0000000..69ce38f --- /dev/null +++ b/migrations/0004_vote_manage_code.sql @@ -0,0 +1 @@ +ALTER TABLE votes ADD COLUMN manage_code uuid DEFAULT uuid_generate_v4(); diff --git a/static/styles.css b/static/styles.css index fae665b..73feb61 100644 --- a/static/styles.css +++ b/static/styles.css @@ -122,6 +122,10 @@ table { margin: 0; } +.vote-table input[name="voter_name"] { + max-width: 150px; +} + .danger-zone { margin-top: 40px; } @@ -183,4 +187,10 @@ button#share-link-copy { color: black; font-size: 25px; line-height: 16px; -} \ No newline at end of file +} + +input[type="submit"].delete-voter-btn { + margin-left: 5px; + background: none; + padding: 0; +} diff --git a/templates/poll.html.j2 b/templates/poll.html.j2 index e0ede1f..1661b79 100644 --- a/templates/poll.html.j2 +++ b/templates/poll.html.j2 @@ -21,14 +21,23 @@ -
- {% if display_mode == "table" %} - {% include "poll_vote_table.html.j2" %} - {% else %} - {% include "poll_vote_list.html.j2" %} - {% endif %} -
+ +{% if display_mode == "table" %} +{% include "poll_vote_table.html.j2" %} +{% else %} +{% include "poll_vote_list.html.j2" %} {% endif %} + +{% endif %} {% endblock %} diff --git a/templates/poll_vote_list.html.j2 b/templates/poll_vote_list.html.j2 index 9d3a15c..ef09af4 100644 --- a/templates/poll_vote_list.html.j2 +++ b/templates/poll_vote_list.html.j2 @@ -23,8 +23,25 @@
- - - +
+ + + +
+{% if managed_voter_names | length != 0 %} +
+
+

Remove submission(s)

+{% endif %} +
+ {% for voter_name in managed_voter_names %} +
+ + {{ voter_name }} + +
+ {% endfor %} +
\ No newline at end of file diff --git a/templates/poll_vote_table.html.j2 b/templates/poll_vote_table.html.j2 index a7aa434..440f2b7 100644 --- a/templates/poll_vote_table.html.j2 +++ b/templates/poll_vote_table.html.j2 @@ -12,7 +12,17 @@ {% for voter_name in voter_names %} - {{ voter_name }} + + {% if voter_name in managed_voter_names %} +
+ + {{ voter_name }} + +
+ {% else %} + {{ voter_name }} + {% endif %} + {% for choice in choices %} {% if selections[(voter_name, choice.id)] == 1 %} @@ -32,10 +42,13 @@ {% endfor %} +
- + + {% for choice in choices %} @@ -43,7 +56,7 @@ {% endfor %} +
- -- cgit v1.3