Skip to content

Commit ec6e1ab

Browse files
authored
add route + test (#113)
* add route + test * black * add repeated insert * black
1 parent d25907b commit ec6e1ab

5 files changed

Lines changed: 165 additions & 8 deletions

File tree

calendar_backend/exceptions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33

44
class ObjectNotFound(Exception):
5-
def __init__(self, type: Type, ids: int | list[int]):
6-
super().__init__(f"Objects of type {type.__name__} {ids=} not found")
5+
def __init__(self, type: Type, ids: int | list[int] = [], name: str | None = None):
6+
msg = f"Objects of type {type.__name__} {ids=} not found"
7+
if name:
8+
msg = f"Objects of type {type.__name__} with {name=} not found"
9+
super().__init__(msg)
710

811

912
class NotEnoughCriteria(Exception):

calendar_backend/models/db.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""Database common classes and methods
2-
"""
1+
"""Database common classes and methods"""
32

43
from __future__ import annotations
54

calendar_backend/routes/event/event.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import logging
2-
from datetime import date, timedelta
2+
from datetime import date, datetime, timedelta
33
from typing import Literal
44

55
from auth_lib.fastapi import UnionAuth
6-
from fastapi import APIRouter, Depends, Query
7-
from fastapi.responses import FileResponse
6+
from fastapi import APIRouter, Depends, Query, status
7+
from fastapi.responses import FileResponse, JSONResponse
88
from fastapi_sqlalchemy import db
99
from pydantic import TypeAdapter
1010

1111
from calendar_backend.exceptions import NotEnoughCriteria
1212
from calendar_backend.methods import list_calendar
1313
from calendar_backend.models import Event, Group, Lecturer, Room
1414
from calendar_backend.routes.models import EventGet
15-
from calendar_backend.routes.models.event import EventPatch, EventPost, GetListEvent
15+
from calendar_backend.routes.models.event import (
16+
EventPatch,
17+
EventPatchName,
18+
EventPatchResult,
19+
EventPost,
20+
EventRepeatedPost,
21+
GetListEvent,
22+
)
1623
from calendar_backend.settings import get_settings
1724

1825

@@ -98,6 +105,44 @@ async def create_event(event: EventPost, _=Depends(UnionAuth(scopes=["timetable.
98105
return EventGet.model_validate(event_get)
99106

100107

108+
@router.post("/repeating", response_model=list[EventGet])
109+
async def create_repeating_event(
110+
event: EventRepeatedPost, # _=Depends(UnionAuth(scopes=["timetable.event.create"]))
111+
) -> list[EventGet]:
112+
if event.repeat_timedelta_days <= 0:
113+
return JSONResponse(
114+
status_code=status.HTTP_400_BAD_REQUEST, content={"detail": f"Timedelta must be a positive integer"}
115+
)
116+
if event.repeat_until_ts > event.start_ts + timedelta(days=1095):
117+
return JSONResponse(
118+
status_code=status.HTTP_400_BAD_REQUEST,
119+
content={"detail": "Due to disk utilization limits, events with duration > 3 years is restricted"},
120+
)
121+
events = []
122+
event_dict = event.model_dump()
123+
rooms = [Room.get(room_id, session=db.session) for room_id in event_dict.pop("room_id", [])]
124+
lecturers = [Lecturer.get(lecturer_id, session=db.session) for lecturer_id in event_dict.pop("lecturer_id", [])]
125+
groups = [Group.get(group_id, session=db.session) for group_id in event_dict.pop("group_id", [])]
126+
repeat_timedelta_days = timedelta(days=event.repeat_timedelta_days)
127+
cur_start_ts = event_dict["start_ts"]
128+
cur_end_ts = event_dict["end_ts"]
129+
while cur_start_ts <= event.repeat_until_ts:
130+
event_get = Event.create(
131+
name=event_dict["name"],
132+
start_ts=cur_start_ts,
133+
end_ts=cur_end_ts,
134+
room=rooms,
135+
lecturer=lecturers,
136+
group=groups,
137+
session=db.session,
138+
)
139+
events.append(event_get)
140+
cur_start_ts += repeat_timedelta_days
141+
cur_end_ts += repeat_timedelta_days
142+
adapter = TypeAdapter(list[EventGet])
143+
return adapter.validate_python(events)
144+
145+
101146
@router.post("/bulk", response_model=list[EventGet])
102147
async def create_events(
103148
events: list[EventPost], _=Depends(UnionAuth(scopes=["timetable.event.create"]))
@@ -139,6 +184,17 @@ async def create_events(
139184
return adapter.validate_python(result)
140185

141186

187+
@router.patch("/patch_name", response_model=EventPatchResult, summary="Batch update events by name")
188+
async def patch_event_by_name(
189+
event_inp: EventPatchName, _=Depends(UnionAuth(scopes=["timetable.event.update"]))
190+
) -> EventPatchResult:
191+
updated = (
192+
db.session.query(Event).filter(Event.name == event_inp.old_name).update(values={"name": event_inp.new_name})
193+
)
194+
db.session.commit()
195+
return EventPatchResult(old_name=event_inp.old_name, new_name=event_inp.new_name, updated=updated)
196+
197+
142198
@router.patch("/{id}", response_model=EventGet)
143199
async def patch_event(
144200
id: int, event_inp: EventPatch, _=Depends(UnionAuth(scopes=["timetable.event.update"]))

calendar_backend/routes/models/event.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ def __repr__(self):
1919
)
2020

2121

22+
class EventPatchName(Base):
23+
old_name: str
24+
new_name: str
25+
26+
27+
class EventPatchResult(Base):
28+
old_name: str
29+
new_name: str
30+
updated: int
31+
32+
2233
class EventPost(Base):
2334
name: str
2435
room_id: list[int]
@@ -35,6 +46,25 @@ def __repr__(self):
3546
)
3647

3748

49+
class EventRepeatedPost(Base):
50+
name: str
51+
room_id: list[int]
52+
group_id: list[int]
53+
lecturer_id: list[int]
54+
start_ts: datetime.datetime
55+
end_ts: datetime.datetime
56+
repeat_timedelta_days: int = 7 # set one week by default
57+
repeat_until_ts: datetime.datetime
58+
59+
def __repr__(self):
60+
return (
61+
f"Lesson(name={self.name},\n"
62+
f" room={self.room_id}, group={self.group_id},\n"
63+
f" lecturer={self.lecturer_id}, start_ts={self.start_ts}, end_ts={self.end_ts})\n"
64+
f" repeats every {self.repeat_timedelta_days} days until {repeat_until_ts}\n"
65+
)
66+
67+
3868
class Event(Base):
3969
id: int
4070
name: str

tests/event/event.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,72 @@ def test_delete_from_to(client_auth: TestClient, dbsession: Session, room_factor
331331
for row in (obj1, obj2):
332332
dbsession.delete(row)
333333
dbsession.commit()
334+
335+
336+
def test_update_by_name(client_auth: TestClient, dbsession: Session, room_factory, group_factory, lecturer_factory):
337+
room_path1 = room_factory(client_auth)
338+
group_path1 = group_factory(client_auth)
339+
lecturer_path1 = lecturer_factory(client_auth)
340+
room_path2 = room_factory(client_auth)
341+
group_path2 = group_factory(client_auth)
342+
lecturer_path2 = lecturer_factory(client_auth)
343+
room_id1 = int(room_path1.split("/")[-1])
344+
group_id1 = int(group_path1.split("/")[-1])
345+
lecturer_id1 = int(lecturer_path1.split("/")[-1])
346+
room_id2 = int(room_path2.split("/")[-1])
347+
group_id2 = int(group_path2.split("/")[-1])
348+
lecturer_id2 = int(lecturer_path2.split("/")[-1])
349+
request_obj = [
350+
{
351+
"name": "string",
352+
"room_id": [room_id1],
353+
"group_id": [group_id1],
354+
"lecturer_id": [lecturer_id1],
355+
"start_ts": "2022-08-26T22:32:38.575Z",
356+
"end_ts": "2022-08-26T22:32:38.575Z",
357+
},
358+
{
359+
"name": "string",
360+
"room_id": [room_id2],
361+
"group_id": [group_id2],
362+
"lecturer_id": [lecturer_id2],
363+
"start_ts": "2022-08-26T22:32:38.575Z",
364+
"end_ts": "2022-08-26T22:32:38.575Z",
365+
},
366+
]
367+
response = client_auth.post(f"{RESOURCE}bulk", json=request_obj)
368+
created = response.json()
369+
assert response.status_code == status.HTTP_200_OK, response.json()
370+
name_to_patch = "not_existing_name"
371+
response = client_auth.patch(
372+
f"{RESOURCE}patch_name", json={"old_name": "not_existing_name", "new_name": "some_name"}
373+
)
374+
assert response.status_code == status.HTTP_200_OK, response.json()
375+
assert response.json()["updated"] == 0 # no events w name "not_existing_name"
376+
response = client_auth.patch(f"{RESOURCE}patch_name", json={"old_name": "string", "new_name": "some_name"})
377+
assert response.status_code == status.HTTP_200_OK, response.json()
378+
assert response.json()["updated"] > 0 # at least 2 events w name "string" (due to our post request)
379+
380+
381+
def test_create_repeated_events(
382+
client_auth: TestClient, dbsession: Session, room_factory, group_factory, lecturer_factory
383+
):
384+
room_path1 = room_factory(client_auth)
385+
group_path1 = group_factory(client_auth)
386+
lecturer_path1 = lecturer_factory(client_auth)
387+
room_id1 = int(room_path1.split("/")[-1])
388+
group_id1 = int(group_path1.split("/")[-1])
389+
lecturer_id1 = int(lecturer_path1.split("/")[-1])
390+
request_obj = {
391+
"name": "string",
392+
"room_id": [room_id1],
393+
"group_id": [group_id1],
394+
"lecturer_id": [lecturer_id1],
395+
"start_ts": "2022-08-26T22:32:38.575Z",
396+
"end_ts": "2022-08-26T22:32:38.575Z",
397+
"repeat_timedelta_days": 7,
398+
"repeat_until_ts": "2023-08-26T22:32:38.575Z",
399+
}
400+
response = client_auth.post(f"{RESOURCE}repeating", json=request_obj)
401+
created = response.json()
402+
assert response.status_code == status.HTTP_200_OK, response.json()

0 commit comments

Comments
 (0)