Skip to content

Commit 8c83d29

Browse files
committed
added event creation form
1 parent 7c116b3 commit 8c83d29

File tree

8 files changed

+337
-46
lines changed

8 files changed

+337
-46
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# ignore auto-generated css files (compile using sass pls)
22
static/css/main.css
33
# ignore env
4-
.env
4+
.env
5+
# ignore database
6+
volume/

events/api.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from datetime import datetime
2-
31
from flask import Blueprint, Response, jsonify, request
42

53
from auth.auth import valid_api_auth
@@ -165,12 +163,12 @@ def create_event_api() -> tuple[Response, int]: # noqa: PLR0911
165163
in: body
166164
type: string
167165
required: true
168-
description: The start time of the event in 'YYYY-MM-DD' format.
166+
description: The start time of the event in 'YYYY-MM-DDTHH:MM' format.
169167
- name: end_time
170168
in: body
171169
type: string
172170
required: false
173-
description: The end time of the event in 'YYYY-MM-DD' format (if duration also provided must match the duration).
171+
description: The end time of the event in 'YYYY-MM-DDTHH:MM' format (if duration also provided must match the duration).
174172
- name: duration
175173
in: body
176174
type: string
@@ -296,17 +294,17 @@ def create_repeat_event_api() -> tuple[Response, int]: # noqa: PLR0911
296294
type: array
297295
items:
298296
type : string
299-
example : "2023-10-01"
300-
example : "2023-10-08"
297+
example : "2025-06-24T22:05"
298+
example : "2025-06-25T12:05"
301299
required: true
302300
description: A list of start times for the events in 'YYYY-MM-DD' format.
303301
- name: end_times
304302
in: body
305303
type: array
306304
items:
307305
type : string
308-
example : "2023-10-01"
309-
example : "2023-10-08"
306+
example : "2025-06-24T23:05"
307+
example : "2025-06-25T13:05"
310308
required: false
311309
description: A list of end times for the events in 'YYYY-MM-DD' format (if duration also provided must match the duration).
312310
- name: duration
@@ -444,12 +442,12 @@ def edit_event_api(event_id: int) -> tuple[Response, int]:
444442
in: body
445443
type: string
446444
required: false
447-
description: The new start time of the event in 'YYYY-MM-DD' format.
445+
description: The new start time of the event in 'YYYY-MM-DDTHH:MM' format.
448446
- name: end_time
449447
in: body
450448
type: string
451449
required: false
452-
description: The new end time of the event in 'YYYY-MM-DD' format (if duration also provided must match the duration).
450+
description: The new end time of the event in 'YYYY-MM-DDTHH:MM' format (if duration also provided must match the duration).
453451
- name: duration
454452
in: body
455453
type: string
@@ -639,7 +637,7 @@ def get_week_by_date(date_str: str) -> tuple[Response, int]:
639637
in: path
640638
type: string
641639
required: true
642-
description: The date in 'YYYY-MM-DD' format.
640+
description: The date in 'YYYY-MM-DDTHH:MM' format.
643641
security: []
644642
responses:
645643
200:
@@ -651,10 +649,9 @@ def get_week_by_date(date_str: str) -> tuple[Response, int]:
651649
404:
652650
description: Week not found for the given date.
653651
"""
654-
try:
655-
date = datetime.fromisoformat(date_str)
656-
except ValueError:
657-
return jsonify({"error": "Invalid date format"}), 400
652+
date = get_datetime_from_string(date_str)
653+
if isinstance(date, str):
654+
return jsonify({"error": date}), 400
658655

659656
week = Week.query.filter(
660657
(date >= Week.start_date) & (date <= Week.end_date) # type: ignore

events/ui.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from flask import Blueprint, redirect, render_template, request, url_for
2+
from werkzeug.wrappers import Response
3+
4+
from auth.auth import is_exec_wrapper
5+
from events.utils import (
6+
create_event,
7+
get_datetime_from_string,
8+
get_event_by_slug,
9+
get_timedelta_from_string,
10+
validate_colour,
11+
)
12+
13+
events_ui_bp = Blueprint("events_ui", __name__, url_prefix="/events")
14+
15+
16+
@events_ui_bp.route("/create", methods=["GET", "POST"])
17+
@is_exec_wrapper
18+
def create(error: str | None = None) -> str | Response: # noqa: PLR0911
19+
# if getting, return the ui for creating an event
20+
if request.method == "GET":
21+
return render_template(
22+
"events/form.html",
23+
error=error,
24+
action="events_ui.create",
25+
method="POST",
26+
event=None,
27+
)
28+
29+
# if posting, create the event
30+
31+
print("Creating event with data:", request.form)
32+
33+
# parse colour
34+
text_colour = request.form.get("text_colour", None)
35+
color_colour = request.form.get("color_colour", None)
36+
37+
text_colour = text_colour.strip().lower() if text_colour else None
38+
color_colour = color_colour.strip().lower() if color_colour else None
39+
40+
if (colour := validate_colour(text_colour, color_colour)) is not None:
41+
return redirect(
42+
url_for(
43+
"events_ui.create",
44+
error=error,
45+
action="events_ui.create",
46+
method="POST",
47+
event=None,
48+
)
49+
)
50+
51+
colour = color_colour if color_colour else text_colour
52+
53+
# parse dates and duration
54+
start_time = get_datetime_from_string(request.form["start_time"])
55+
if isinstance(start_time, str):
56+
return redirect(
57+
url_for(
58+
"events_ui.create",
59+
error=start_time,
60+
action="events_ui.create",
61+
method="POST",
62+
event=None,
63+
)
64+
)
65+
duration = (
66+
get_timedelta_from_string(request.form["duration"])
67+
if request.form["duration"]
68+
else None
69+
)
70+
if isinstance(duration, str):
71+
return redirect(
72+
url_for(
73+
"events_ui.create",
74+
error=duration,
75+
action="events_ui.create",
76+
method="POST",
77+
event=None,
78+
)
79+
)
80+
end_time = (
81+
get_datetime_from_string(request.form["end_time"])
82+
if request.form["end_time"]
83+
else None
84+
)
85+
if isinstance(end_time, str):
86+
return redirect(
87+
url_for(
88+
"events_ui.create",
89+
error=end_time,
90+
action="events_ui.create",
91+
method="POST",
92+
event=None,
93+
)
94+
)
95+
96+
# parse tags
97+
tags = (
98+
[tag.strip() for tag in request.form["tags"].split(",")]
99+
if request.form["tags"]
100+
else []
101+
)
102+
103+
# attempt to create the event
104+
event = create_event(
105+
request.form["name"],
106+
request.form["description"],
107+
"draft" in request.form,
108+
request.form["location"],
109+
request.form.get("location_url", None),
110+
request.form.get("icon", None),
111+
colour,
112+
start_time,
113+
duration,
114+
end_time,
115+
tags,
116+
)
117+
118+
# if failed, redirect to the create page with an error
119+
if isinstance(event, str):
120+
return redirect(
121+
url_for(
122+
"events_ui.create",
123+
error=event,
124+
action="events_ui.create",
125+
method="POST",
126+
event=None,
127+
)
128+
)
129+
130+
# if successful, redirect to the event page
131+
return redirect(
132+
url_for(
133+
"events_ui.view",
134+
year=event.date.academic_year,
135+
term=event.date.term,
136+
week=event.date.week,
137+
slug=event.slug,
138+
)
139+
)
140+
141+
142+
# TODO: other event management UI
143+
144+
145+
@events_ui_bp.route("/<int:year>/<int:term>/<int:week>/<string:slug>")
146+
def view(year: int, term: int, week: int, slug: str) -> str:
147+
"""View an event by its year, term, week, and slug."""
148+
149+
event = get_event_by_slug(year, term, week, slug)
150+
151+
if event is None:
152+
return "Event not found"
153+
154+
return str(event.to_dict())

events/utils.py

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,20 @@ def get_timedelta_from_string(duration_str: str) -> timedelta | str:
1818

1919

2020
def get_datetime_from_string(date_str: str) -> datetime | str:
21+
"""Convert a date string in the format 'YYYY-MM-DDTHH:MM' to a datetime object."""
22+
try:
23+
return datetime.strptime(date_str, "%Y-%m-%dT%H:%M").astimezone(
24+
pytz.timezone("Europe/London")
25+
)
26+
except ValueError:
27+
return "Invalid date format, expected 'YYYY-MM-DDTHH:MM'"
28+
29+
30+
def get_date_from_string(date_str: str) -> datetime | str:
2131
"""Convert a date string in the format 'YYYY-MM-DD' to a datetime object."""
2232
try:
23-
return datetime.strptime(date_str, "%Y-%m-%d").replace(
24-
tzinfo=pytz.timezone("Europe/London")
33+
return datetime.strptime(date_str, "%Y-%m-%d").astimezone(
34+
pytz.timezone("Europe/London")
2535
)
2636
except ValueError:
2737
return "Invalid date format, expected 'YYYY-MM-DD'"
@@ -41,13 +51,14 @@ def create_event( # noqa: PLR0913
4151
tags: list[str],
4252
) -> Event | str:
4353
"""Create an event"""
54+
4455
# convert start_time and normalise end_time
45-
start_time = pytz.timezone("Europe/London").localize(start_time)
56+
start_time = start_time.astimezone(pytz.timezone("Europe/London"))
4657

4758
if end_time is None:
4859
end_time = start_time + duration if duration else None
4960
else:
50-
end_time = pytz.timezone("Europe/London").localize(end_time)
61+
end_time = end_time.astimezone(pytz.timezone("Europe/London"))
5162
if duration is not None and end_time != start_time + duration:
5263
return "End time does not match the duration"
5364
if end_time and end_time < start_time:
@@ -71,10 +82,14 @@ def create_event( # noqa: PLR0913
7182

7283
# validate the event
7384
if error := event.validate():
85+
db.session.rollback()
7486
return error
7587

76-
# attach week to the event
77-
event.date = get_week_from_date(start_time) # type: ignore
88+
# create week from the start_time
89+
week = get_week_from_date(start_time)
90+
if week is None:
91+
db.session.rollback()
92+
return "Unable to find or create a week for the event date"
7893

7994
# attach tags to the event
8095
# check all tags exist, create if not
@@ -116,10 +131,10 @@ def get_week_from_date(date: datetime) -> Week | None: # noqa: PLR0912
116131
).json()
117132

118133
for w in warwick_week["weeks"]:
119-
start_date = get_datetime_from_string(w["startDate"])
134+
start_date = get_date_from_string(w["start"])
120135
if isinstance(start_date, str):
121136
return None
122-
end_date = get_datetime_from_string(w["endDate"])
137+
end_date = get_date_from_string(w["end"])
123138
if isinstance(end_date, str):
124139
return None
125140
if start_date.date() <= date.date() <= end_date.date():
@@ -147,7 +162,7 @@ def get_week_from_date(date: datetime) -> Week | None: # noqa: PLR0912
147162
with Path("olddates.json").open("r") as f:
148163
old_dates = load(f)
149164
for w in old_dates:
150-
start_date = get_datetime_from_string(w["startDate"])
165+
start_date = get_date_from_string(w["date"])
151166
if isinstance(start_date, str):
152167
return None
153168
if start_date.date() <= date.date():
@@ -295,12 +310,12 @@ def edit_event( # noqa: PLR0913
295310
event.icon = icon.lower() if icon else event.icon
296311
event.colour = colour if colour else event.colour
297312
event.start_time = (
298-
pytz.timezone("Europe/London").localize(start_time)
313+
start_time.astimezone(pytz.timezone("Europe/London"))
299314
if start_time
300315
else event.start_time
301316
)
302317
event.end_time = (
303-
pytz.timezone("Europe/London").localize(end_time)
318+
end_time.astimezone(pytz.timezone("Europe/London"))
304319
if end_time
305320
else event.end_time
306321
)
@@ -350,3 +365,43 @@ def delete_event(event_id: int) -> bool | str:
350365
clean_weeks()
351366
clean_tags()
352367
return True
368+
369+
370+
def validate_colour(text_colour: str | None, hex_colour: str | None) -> str | None:
371+
"""Validate the colour input"""
372+
373+
# check if the colours match
374+
if text_colour == hex_colour:
375+
return None
376+
377+
# if either colour is None, validation succeeds
378+
if text_colour is None or hex_colour is None:
379+
return None
380+
381+
# attemp to convert text_colour to hex
382+
text_colour = get_hex_from_name(text_colour)
383+
if text_colour is None:
384+
return "Invalid colour name"
385+
386+
# check if the converted text_colour matches the hex_colour
387+
if text_colour.lower() != hex_colour.lower():
388+
return "Colour name does not match hex code"
389+
390+
return None
391+
392+
393+
def get_hex_from_name(name: str) -> str | None:
394+
"""Get the hex colour from a name"""
395+
with Path("colours.json").open("r") as f:
396+
colours = load(f)
397+
return colours.get(name.lower(), None) # type: ignore
398+
399+
400+
def get_name_from_hex(hex_colour: str) -> str | None:
401+
"""Get the name of a colour from its hex code"""
402+
with Path("colours.json").open("r") as f:
403+
colours = load(f)
404+
for name, hex_code in colours.items():
405+
if hex_code.lower() == hex_colour.lower():
406+
return name
407+
return None

0 commit comments

Comments
 (0)