diff options
| author | Jan Tuomi <jans.tuomi@gmail.com> | 2024-02-29 21:10:43 +0200 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2024-12-21 19:43:13 +0200 |
| commit | 5c64529e77f150f8b83930f17e792b2cb8e38a87 (patch) | |
| tree | 89b28f50812fb735dfdaa0bca24764959168d84d | |
| parent | 3661c687f0c54bd9f4e1075aa651bc612d634a13 (diff) | |
Add whole day mode, various improvements
| -rw-r--r-- | app.py | 22 | ||||
| -rw-r--r-- | db.py | 24 | ||||
| -rw-r--r-- | migrations/0003_whole_day_poll.sql | 1 | ||||
| -rw-r--r-- | static/styles.css | 17 | ||||
| -rw-r--r-- | templates/index.html.j2 | 21 | ||||
| -rw-r--r-- | templates/manage.html.j2 | 70 | ||||
| -rw-r--r-- | templates/poll_choice_datetime_range.html.j2 | 24 | ||||
| -rw-r--r-- | templates/poll_info_table.html.j2 | 24 | ||||
| -rw-r--r-- | templates/poll_vote_list.html.j2 | 21 | ||||
| -rw-r--r-- | templates/poll_vote_table.html.j2 | 20 |
10 files changed, 143 insertions, 101 deletions
@@ -37,7 +37,7 @@ def validation_error(message: str): return render_template("error.html.j2", error=message), 400 @app.route("/") -def hello_world(): +def index(): created_poll_codes = [] for k, _ in request.cookies.items(): if k.startswith("diddle_manage_code_"): @@ -71,6 +71,7 @@ def create(): form["description"], form["author_name"], form["author_email"], + "is_whole_day" in form, choices, ) @@ -177,6 +178,7 @@ def update_poll_info(code): form["description"], form["author_name"], form["author_email"], + "is_whole_day" in form, ) return redirect(f"/manage/{code}") @@ -188,16 +190,24 @@ def add_choice(code): return validation_error("Start datetime is required") if "end_datetime" not in form or len(form["end_datetime"]) == 0: return validation_error("End datetime is required") - if form["start_datetime"] >= form["end_datetime"]: + if form["start_datetime"] > form["end_datetime"]: return validation_error("Start datetime must be before end datetime") + start_datetime = form["start_datetime"] + if len(start_datetime) == 10: + start_datetime += "T00:00" + + end_datetime = form["end_datetime"] + if len(end_datetime) == 10: + end_datetime += "T23:59" + db.add_choice_to_poll( code, - form["start_datetime"], - form["end_datetime"], + start_datetime, + end_datetime, ) - return redirect(f"/manage/{code}") + return redirect(f"/manage/{code}?focus_next=1") @app.post("/manage/<code>/delete_choice/<choice_id>") def delete_choice(code, choice_id): @@ -207,7 +217,7 @@ def delete_choice(code, choice_id): db.delete_choice(choice_id) - return redirect(f"/manage/{code}") + return redirect(f"/manage/{code}?focus_next=1") @app.get("/manage/<code>") def manage(code): @@ -70,9 +70,19 @@ class Choice: def end_datetime_notz(self): return self.end_datetime.replace(tzinfo=None) + def start_date_notz(self): + return self.start_datetime.date() + + def end_date_notz(self): + return self.end_datetime.date() + def ends_on_same_day(self): return self.start_datetime.date() == self.end_datetime.date() + def ends_at_same_datetime(self): + return self.start_datetime.date() == self.end_datetime.date() \ + and self.start_datetime.time() == self.end_datetime.time() + @dataclass class Poll: id: str @@ -83,6 +93,7 @@ class Poll: author_email: str | None choices: list[Choice] manage_code: str + is_whole_day: bool def share_url(self): return f"{BASE_URL}/poll/{self.id}" @@ -99,6 +110,7 @@ def tuple_to_poll(poll_t: tuple) -> Poll: author_name=poll_t[4], author_email=poll_t[5], manage_code=poll_t[6], + is_whole_day=poll_t[7], choices=[] ) @@ -159,13 +171,14 @@ def create_poll(title: str, description: str, author_name: str, author_email: str, + is_whole_day: bool, choices: list[Choice]): with db.cursor() as (conn, cur): try: - cur.execute("INSERT INTO polls (title, description, author_name, author_email)" - "VALUES (%s, %s, %s, %s) RETURNING *", - (title, description, author_name, author_email)) + cur.execute("INSERT INTO polls (title, description, author_name, author_email, whole_day)" + "VALUES (%s, %s, %s, %s, %s) RETURNING *", + (title, description, author_name, author_email, is_whole_day)) poll_t = cur.fetchone() if poll_t is None: @@ -228,12 +241,13 @@ def update_poll_info( description, author_name, author_email, + is_whole_day, ) -> None: with db.cursor() as (conn, cur): try: - cur.execute("UPDATE polls SET title = %s, description = %s, author_name = %s, author_email = %s " + cur.execute("UPDATE polls SET title = %s, description = %s, author_name = %s, author_email = %s, whole_day = %s " "WHERE manage_code = %s", - (title, description, author_name, author_email, code)) + (title, description, author_name, author_email, is_whole_day, code)) conn.commit() except Exception as e: conn.rollback() diff --git a/migrations/0003_whole_day_poll.sql b/migrations/0003_whole_day_poll.sql new file mode 100644 index 0000000..e532d0c --- /dev/null +++ b/migrations/0003_whole_day_poll.sql @@ -0,0 +1 @@ +ALTER TABLE polls ADD COLUMN IF NOT EXISTS whole_day BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/static/styles.css b/static/styles.css index 229149c..0e538e8 100644 --- a/static/styles.css +++ b/static/styles.css @@ -43,13 +43,13 @@ input[type="submit"].yellow { } input[type="text"], -input[type="datetime-local"] { +input[type*="date"] { font-size: 16px; color: black; } input[type="text"]:disabled, -input[type="datetime-local"]:disabled { +input[type*="date"]:disabled { color: #666; } @@ -73,6 +73,10 @@ table { padding: 7px 5px; } +.vote-table th { + vertical-align: bottom; +} + .vote-table th span { display: block; } @@ -140,3 +144,12 @@ input[type="checkbox"] { input[type="checkbox"]:disabled { cursor: initial; } + +td.checkbox-field { + text-align: left; +} + +td.checkbox-field input[type="checkbox"] { + flex: initial; + margin-left: 0; +} diff --git a/templates/index.html.j2 b/templates/index.html.j2 index f84b428..db458da 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -3,26 +3,7 @@ {% block content %} <h2>Create new diddle</h2> <form class="poll-form" action="/poll/create" method="post"> - <table> - <tbody> - <tr> - <td><label for="title">Title</label></td> - <td><input type="text" name="title" required></td> - </tr> - <tr> - <td><label for="description">Description (optional)</label></td> - <td><textarea name="description"></textarea></td> - </tr> - <tr> - <td><label for="author_name">Your name</label></td> - <td><input type="text" name="author_name" required></td> - </tr> - <tr> - <td><label for="author_email">Your email (optional)</label></td> - <td><input type="text" name="author_email"></td> - </tr> - </tbody> - </table> + {% include "poll_info_table.html.j2" %} <p>You can add options after submitting.</p> <input class="green" type="submit" value="Create"> diff --git a/templates/manage.html.j2 b/templates/manage.html.j2 index 55a14bd..4a5da8d 100644 --- a/templates/manage.html.j2 +++ b/templates/manage.html.j2 @@ -14,27 +14,8 @@ </p> <form class="poll-form" action="/manage/{{ poll.manage_code }}/update_info" method="post"> - <table> - <tbody> - <tr> - <td><label for="title">Title</label></td> - <td><input type="text" name="title" value="{{ poll.title }}" required></td> - </tr> - <tr> - <td><label for="description">Description (optional)</label></td> - <td><textarea name="description">{{ poll.description }}</textarea></td> - </tr> - <tr> - <td><label for="author_name">Your name</label></td> - <td><input type="text" name="author_name" value="{{ poll.author_name }}" required></td> - </tr> - <tr> - <td><label for="author_email">Your email (optional)</label></td> - <td><input type="text" name="author_email" value="{{ poll.author_email }}"></td> - </tr> - </tbody> - </table> - <br> + {% include "poll_info_table.html.j2" %} + <p></p> <input type="submit" value="Update"> </form> @@ -55,11 +36,17 @@ <form action="/manage/{{ poll.manage_code }}/delete_choice/{{ choice.id }}" method="post"> <tr> <td> - <input type="datetime-local" name="start_datetime_{{ choice.id }}" value="{{ choice.start_datetime_notz() }}" disabled + <input type="{% if poll.is_whole_day %}date{% else %}datetime-local{% endif %}" + name="start_datetime_{{ choice.id }}" + value="{% if poll.is_whole_day %}{{ choice.start_date_notz() }}{% else %}{{ choice.start_datetime_notz() }}{% endif %}" + disabled {% if last_choice_id == choice.id %}id="last-start-datetime"{% endif %}> </td> <td> - <input type="datetime-local" name="end_datetime_{{ choice.id }}" value="{{ choice.end_datetime_notz() }}" disabled + <input type="{% if poll.is_whole_day %}date{% else %}datetime-local{% endif %}" + name="end_datetime_{{ choice.id }}" + value="{% if poll.is_whole_day %}{{ choice.end_date_notz() }}{% else %}{{ choice.end_datetime_notz() }}{% endif %}" + disabled {% if last_choice_id == choice.id %}id="last-end-datetime"{% endif %}> </td> <td> @@ -71,10 +58,10 @@ <form action="/manage/{{ poll.manage_code }}/add_choice" method="post"> <tr> <td> - <input type="datetime-local" name="start_datetime" required> + <input type="{% if poll.is_whole_day %}date{% else %}datetime-local{% endif %}" name="start_datetime" required> </td> <td> - <input type="datetime-local" name="end_datetime" required> + <input type="{% if poll.is_whole_day %}date{% else %}datetime-local{% endif %}" name="end_datetime" required> </td> <td> <input class="green" type="submit" value="Add"> @@ -94,7 +81,9 @@ </div> <script> +const isWholeDay = {% if poll.is_whole_day %}true{% else %}false{% endif %}; const hour = 1000 * 60 * 60; +const day = hour * 24; const startDatetimeInput = document.querySelector('input[name="start_datetime"]'); const endDatetimeInput = document.querySelector('input[name="end_datetime"]'); @@ -103,15 +92,38 @@ const lastStartDatetimeInput = document.getElementById('last-start-datetime'); const lastEndDatetimeInput = document.getElementById('last-end-datetime'); if (lastStartDatetimeInput !== null && startDatetimeInput.valueAsDate === null) { - startDatetimeInput.valueAsDate = new Date(lastStartDatetimeInput.valueAsDate.getTime() + hour); + if (isWholeDay) { + // set the start date to the last end date + 1 day if in whole day mode + startDatetimeInput.valueAsDate = new Date(lastEndDatetimeInput.valueAsDate.getTime() + day); + endDatetimeInput.valueAsDate = new Date(startDatetimeInput.valueAsDate.getTime()); + } else { + // set the start date to the last end date + 1 hour if not in whole day mode + startDatetimeInput.valueAsDate = new Date(lastEndDatetimeInput.valueAsDate.getTime()); + endDatetimeInput.valueAsDate = new Date(startDatetimeInput.valueAsDate.getTime() + hour); + } } -startDatetimeInput.addEventListener('blur', onStartDatetimeBlur); +startDatetimeInput.addEventListener('change', onStartDatetimeChange); -function onStartDatetimeBlur(event) { +function onStartDatetimeChange(event) { const endDatetimeInput = document.querySelector('input[name="end_datetime"]'); if (endDatetimeInput.valueAsDate === null) { - endDatetimeInput.valueAsDate = new Date(event.target.valueAsDate.getTime() + hour); + if (isWholeDay) { + // set the end date to the start date if in whole day mode + endDatetimeInput.valueAsDate = new Date(event.target.valueAsDate.getTime()); + } else { + // set the end date to the start date + 1 hour if not in whole day mode + endDatetimeInput.valueAsDate = new Date(event.target.valueAsDate.getTime() + hour); + } + } +} + +const query = new URLSearchParams(window.location.search); +const focusNext = query.get('focus_next'); +if (focusNext !== null) { + const nextInput = document.querySelector(`input[name="start_datetime"]`); + if (nextInput !== null) { + nextInput.focus(); } } </script> diff --git a/templates/poll_choice_datetime_range.html.j2 b/templates/poll_choice_datetime_range.html.j2 new file mode 100644 index 0000000..ddfa970 --- /dev/null +++ b/templates/poll_choice_datetime_range.html.j2 @@ -0,0 +1,24 @@ +<span> +{{ choice.start_datetime.strftime("%a %d.%m") }} +</span> +<span> + {% if now.year != choice.start_datetime.year %}.{{choice.start_datetime.year}}{% endif %} +</span> +<span> + {% if not poll.is_whole_day %}{{ choice.start_datetime.strftime("%H:%M") }}{% endif %} +</span> +{% if (poll.is_whole_day and not choice.ends_on_same_day()) or (not poll.is_whole_day and not choice.ends_at_same_datetime()) %} +<span>—</span> +{% endif %} + +{% if not choice.ends_on_same_day() %} +<span> + {{ choice.end_datetime.strftime("%a %d.%m") }} +</span> +<span> + {% if now.year != choice.end_datetime.year %}.{{choice.end_datetime.year}}{% endif %} +</span> +{% endif %} +<span> + {% if not poll.is_whole_day %}{{ choice.end_datetime.strftime("%H:%M") }}{% endif %} +</span> diff --git a/templates/poll_info_table.html.j2 b/templates/poll_info_table.html.j2 new file mode 100644 index 0000000..7955ca8 --- /dev/null +++ b/templates/poll_info_table.html.j2 @@ -0,0 +1,24 @@ +<table> + <tbody> + <tr> + <td><label for="title">Title</label></td> + <td><input type="text" name="title" value="{% if poll %}{{ poll.title }}{% endif %}" required></td> + </tr> + <tr> + <td><label for="description">Description (optional)</label></td> + <td><textarea name="description">{% if poll %}{{ poll.description }}{% endif %}</textarea></td> + </tr> + <tr> + <td><label for="author_name">Your name</label></td> + <td><input type="text" name="author_name" value="{% if poll %}{{ poll.author_name }}{% endif %}" required></td> + </tr> + <tr> + <td><label for="author_email">Your email (optional)</label></td> + <td><input type="text" name="author_email" value="{% if poll %}{{ poll.author_email }}{% endif %}"></td> + </tr> + <tr> + <td><label for="is_whole_day">Whole day event?</label></td> + <td class="checkbox-field"><input type="checkbox" name="is_whole_day" {% if poll and poll.is_whole_day %}checked{% endif %}></td> + </tr> + </tbody> +</table> diff --git a/templates/poll_vote_list.html.j2 b/templates/poll_vote_list.html.j2 index c18b3e0..845356b 100644 --- a/templates/poll_vote_list.html.j2 +++ b/templates/poll_vote_list.html.j2 @@ -4,26 +4,7 @@ <label for="choice_{{ choice.id }}"> <input type="checkbox" name="choice_{{ choice.id }}" id="choice_{{ choice.id }}"> <strong> - {{ choice.start_datetime.strftime("%a %d.%m") }} - <span> - {% if now.year != choice.start_datetime.year %}.{{choice.start_datetime.year}}{% endif %} - </span> - <span> - {{ choice.start_datetime.strftime("%H:%M") }} - </span> - <span>—</span> - {% if not choice.ends_on_same_day() %} - <span> - {{ choice.end_datetime.strftime("%a %d.%m") }} - </span> - <span> - {% if now.year != choice.end_datetime.year %}.{{choice.end_datetime.year}}{% endif %} - </span> - {% endif %} - <span> - {{ choice.end_datetime.strftime("%H:%M") }} - </span> - </span> + {% include "poll_choice_datetime_range.html.j2" %} </strong> <span> <i>{{ choice.votes | length }} votes</i> diff --git a/templates/poll_vote_table.html.j2 b/templates/poll_vote_table.html.j2 index cfa336a..97fcac5 100644 --- a/templates/poll_vote_table.html.j2 +++ b/templates/poll_vote_table.html.j2 @@ -3,25 +3,7 @@ <th></th> {% for choice in choices %} <th> - {{ choice.start_datetime.strftime("%a %d.%m") }} - <span> - {% if now.year != choice.start_datetime.year %}.{{choice.start_datetime.year}}{% endif %} - </span> - <span> - {{ choice.start_datetime.strftime("%H:%M") }} - </span> - <span>—</span> - {% if not choice.ends_on_same_day() %} - <span> - {{ choice.end_datetime.strftime("%a %d.%m") }} - </span> - <span> - {% if now.year != choice.end_datetime.year %}.{{choice.end_datetime.year}}{% endif %} - </span> - {% endif %} - <span> - {{ choice.end_datetime.strftime("%H:%M") }} - </span> + {% include "poll_choice_datetime_range.html.j2" %} <span> <i>{{ choice.votes | length }} votes</i> </span> |
