@@ -97,9 +97,19 @@ def get_duration_from_string(duration_str: str) -> timedelta | str:
9797 return "Invalid duration format, expected 'days:hours:minutes'"
9898
9999
100+ def get_datetime_from_string (date_str : str ) -> datetime | str :
101+ """Convert a date string in the format 'YYYY-MM-DD' to a datetime object."""
102+ try :
103+ return datetime .strptime (date_str , "%Y-%m-%d" ).replace (
104+ tzinfo = pytz .timezone ("Europe/London" )
105+ )
106+ except ValueError :
107+ return "Invalid date format, expected 'YYYY-MM-DD'"
108+
109+
100110@events_api_bp .route ("/create" , methods = ["POST" ])
101111@is_exec_wrapper
102- def create_event_api () -> tuple [Response , int ]:
112+ def create_event_api () -> tuple [Response , int ]: # noqa: PLR0911
103113 """Create a new event"""
104114 data = request .get_json ()
105115 if not data :
@@ -116,9 +126,16 @@ def create_event_api() -> tuple[Response, int]:
116126 if field not in data :
117127 return jsonify ({"error" : f"Missing required field: { field } " }), 400
118128
119- start_time = pytz .timezone ("Europe/London" ).localize (
120- datetime .fromisoformat (data ["start_time" ])
129+ start_time = get_datetime_from_string (data ["start_time" ])
130+ if isinstance (start_time , str ):
131+ return jsonify ({"error" : start_time }), 400
132+
133+ end_time = (
134+ get_datetime_from_string (data .get ("end_time" )) if data .get ("end_time" ) else None
121135 )
136+ if isinstance (end_time , str ):
137+ return jsonify ({"error" : end_time }), 400
138+
122139 if "duration" in data :
123140 duration = get_duration_from_string (data ["duration" ])
124141 if isinstance (duration , str ):
@@ -137,6 +154,7 @@ def create_event_api() -> tuple[Response, int]:
137154 data .get ("colour" ),
138155 start_time ,
139156 duration ,
157+ end_time ,
140158 data .get ("tags" , []),
141159 )
142160 if isinstance (event , str ):
@@ -156,12 +174,21 @@ def create_event( # noqa: PLR0913
156174 colour : str | None ,
157175 start_time : datetime ,
158176 duration : timedelta | None ,
177+ end_time : datetime | None ,
159178 tags : list [str ],
160179) -> Event | str :
161180 """Create an event"""
162- # convert start_time and calculate end_time
181+ # convert start_time and normalise end_time
163182 start_time = pytz .timezone ("Europe/London" ).localize (start_time )
164- end_time = start_time + duration if duration else None
183+
184+ if end_time is None :
185+ end_time = start_time + duration if duration else None
186+ else :
187+ end_time = pytz .timezone ("Europe/London" ).localize (end_time )
188+ if duration is not None and end_time != start_time + duration :
189+ return "End time does not match the duration"
190+ if end_time and end_time < start_time :
191+ return "End time cannot be before start time"
165192
166193 # create the event object
167194 event = Event (
@@ -195,7 +222,7 @@ def create_event( # noqa: PLR0913
195222 return event
196223
197224
198- def get_week_from_date (date : datetime ) -> Week | None :
225+ def get_week_from_date (date : datetime ) -> Week | None : # noqa: PLR0912
199226 """Get the week from a given date"""
200227
201228 week = Week .query .filter (
@@ -219,12 +246,12 @@ def get_week_from_date(date: datetime) -> Week | None:
219246 ).json ()
220247
221248 for w in warwick_week ["weeks" ]:
222- start_date = datetime . strptime (w ["startDate" ], "%Y-%m-%d" ). replace (
223- tzinfo = pytz . timezone ( "Europe/London" )
224- )
225- end_date = datetime . strptime (w ["endDate" ], "%Y-%m-%d" ). replace (
226- tzinfo = pytz . timezone ( "Europe/London" )
227- )
249+ start_date = get_datetime_from_string (w ["startDate" ])
250+ if isinstance ( start_date , str ):
251+ return None
252+ end_date = get_datetime_from_string (w ["endDate" ])
253+ if isinstance ( end_date , str ):
254+ return None
228255 if start_date .date () <= date .date () <= end_date .date ():
229256 name = w ["name" ]
230257 if "Term" in name :
@@ -250,9 +277,9 @@ def get_week_from_date(date: datetime) -> Week | None:
250277 with Path ("olddates.json" ).open ("r" ) as f :
251278 old_dates = load (f )
252279 for w in old_dates :
253- start_date = datetime . strptime (w ["start_date" ], "%Y-%m-%d" ). replace (
254- tzinfo = pytz . timezone ( "Europe/London" )
255- )
280+ start_date = get_datetime_from_string (w ["startDate" ])
281+ if isinstance ( start_date , str ):
282+ return None
256283 if start_date .date () <= date .date ():
257284 week = Week (
258285 academic_year = year ,
@@ -295,17 +322,18 @@ def create_repeat_event_api() -> tuple[Response, int]: # noqa: PLR0911
295322
296323 start_times = []
297324 for start_time_str in data ["start_times" ]:
298- try :
299- start_time = pytz .timezone ("Europe/London" ).localize (
300- datetime .fromisoformat (start_time_str )
301- )
302- except ValueError :
303- return (
304- jsonify ({"error" : f"Invalid start time format: { start_time_str } " }),
305- 400 ,
306- )
325+ start_time = get_datetime_from_string (start_time_str )
326+ if isinstance (start_time , str ):
327+ return jsonify ({"error" : start_time }), 400
307328 start_times .append (start_time )
308329
330+ end_times = []
331+ for end_time_str in data .get ("end_times" , []):
332+ end_time = get_datetime_from_string (end_time_str )
333+ if isinstance (end_time , str ):
334+ return jsonify ({"error" : end_time }), 400
335+ end_times .append (end_time )
336+
309337 try :
310338 events = create_repeat_event (
311339 data ["name" ],
@@ -317,6 +345,7 @@ def create_repeat_event_api() -> tuple[Response, int]: # noqa: PLR0911
317345 data .get ("colour" ),
318346 start_times ,
319347 duration , # type: ignore
348+ end_times ,
320349 data .get ("tags" , []),
321350 )
322351 if isinstance (events , str ):
@@ -336,11 +365,14 @@ def create_repeat_event( # noqa: PLR0913
336365 colour : str | None ,
337366 start_times : list [datetime ],
338367 duration : timedelta | None ,
368+ end_times : list [datetime ] | None ,
339369 tags : list [str ],
340370) -> list [Event ] | str :
341371 """Create multiple events at once"""
342372 events = [] # the created events
343- for start_time in start_times :
373+ for start_time , end_time in zip (
374+ start_times , end_times or [None ] * len (start_times )
375+ ):
344376 # iterate through start_times and create events
345377 event = create_event (
346378 name ,
@@ -352,6 +384,7 @@ def create_repeat_event( # noqa: PLR0913
352384 colour ,
353385 start_time ,
354386 duration ,
387+ end_time ,
355388 tags ,
356389 )
357390
@@ -388,7 +421,7 @@ def get_week_by_date(date_str: str) -> tuple[Response, int]:
388421
389422@events_api_bp .route ("/<int:event_id>" , methods = ["PATCH" ])
390423@is_exec_wrapper
391- def edit_event (event_id : int ) -> tuple [Response , int ]:
424+ def edit_event (event_id : int ) -> tuple [Response , int ]: # noqa: PLR0911, PLR0912
392425 """Edit an existing event"""
393426 event = Event .query .get (event_id )
394427 if not event :
@@ -408,11 +441,22 @@ def edit_event(event_id: int) -> tuple[Response, int]:
408441 event .start_time = pytz .timezone ("Europe/London" ).localize (
409442 datetime .fromisoformat (data .get ("start_time" , event .start_time .isoformat ()))
410443 )
444+
445+ # update end_time (with duration logic)
446+ if "end_time" in data :
447+ end_time = get_datetime_from_string (data ["end_time" ])
448+ if isinstance (end_time , str ):
449+ return jsonify ({"error" : end_time }), 400
450+ event .end_time = end_time
451+
411452 if "duration" in data :
412453 duration = get_duration_from_string (data ["duration" ])
413454 if isinstance (duration , str ):
414455 return jsonify ({"error" : duration }), 400
415- event .end_time = event .start_time + duration
456+ if event .end_time is None :
457+ event .end_time = event .start_time + duration
458+ elif event .end_time != event .start_time + duration :
459+ return jsonify ({"error" : "End time does not match the duration" }), 400
416460
417461 # update week if start_time has changed
418462 if "start_time" in data :
0 commit comments