From cd2ffda5bb86c7fe64d164bb04098440117d29a5 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 6 Sep 2026 00:04:20 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D1=83=D1=87=D0=BA=D0=B0=20/event/ical=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D1=83=D1=8E=D1=89=D0=B0=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=20list=5Fcalendar.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- calendar_backend/methods/list_calendar.py | 78 ++++++++++++++++++++++- calendar_backend/routes/event/event.py | 9 ++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/calendar_backend/methods/list_calendar.py b/calendar_backend/methods/list_calendar.py index e97b3c13..c312f258 100644 --- a/calendar_backend/methods/list_calendar.py +++ b/calendar_backend/methods/list_calendar.py @@ -4,14 +4,15 @@ import time from datetime import date as date_ from datetime import datetime +from typing import List import pytz -from fastapi import HTTPException +from fastapi import HTTPException, UploadFile, File from fastapi.responses import FileResponse from icalendar import Calendar, Event, vText from sqlalchemy.orm import Session -from calendar_backend.models import Group +from calendar_backend.models import Group, Event as DB_Event, Room, Lecturer from calendar_backend.settings import get_settings from . import utils @@ -20,6 +21,79 @@ settings = get_settings() logger = logging.getLogger(__name__) +def _get_list_from_ical_obj(element, field: str) -> List: + items = element.get(field) + if not items: + return [] + elif isinstance(items, list): + return [int(i) for i in items] + elif "," in (str_items := str(items)) : + return [int(i) for i in items.split(",")] + else: + return [int(items)] + + + +async def create_event_from_icalendar(dbsession: Session, file: UploadFile = File(...)) -> List: + extension = file.filename.split(".")[-1] + available_exts = ["ical", "ics"] + if extension not in available_exts: + raise HTTPException(status_code=422, detail="Не поддерживаемый фармат файла!") + + raw_file: bytes = file.read() + str_file: str = raw_file.decode("utf-8") + cal_obj = Calendar.from_ical(str_file) + events = [] + result = [] + for element in cal_obj.walk(): + data = {} + data["name"] = element.get("summary") + data["start_ts"] = element.get("dtstart") + data["end_ts"] = element.get("dtend") + group_ids_field, lecturer_ids_field, room_ids_field = "X-FF-GROUP-IDS", "X-FF-LECTURER-IDS", "X-FF-ROOM-IDS" + data["group_ids"] = _get_list_from_ical_obj(element, group_ids_field) + data["lecturer_ids"] = _get_list_from_ical_obj(element, lecturer_ids_field) + data["room_ids"] = _get_list_from_ical_obj(element, room_ids_field) + # решено, что group_id обязателен + if not data.get("group_ids"): + raise HTTPException(status_code=403, detail="Невозможно создать событие без группы!") + events.append(data) + + for data in events: + existing_events_query = ( + + DB_Event.get_all(session=dbsession) + .filter(DB_Event.name == data.get("name")) + .filter(DB_Event.start_ts == data.get("start_ts")) + .filter(DB_Event.end_ts == data.get("end_ts")) + ) + is_unique = True + for existing_event in existing_events_query.all(): + if ( + {column.id for column in existing_event.group} == set(data["group_id"]) + and {column.id for column in existing_event.room} == set(data["room_id"]) + and {column.id for column in existing_event.lecturer} == set(data["lecturer_id"]) + ): + is_unique = False + + if is_unique: + rooms = [Room.get(room_id, session=dbsession) for room_id in data.pop("room_id", [])] + lecturers = [ + Lecturer.get(lecturer_id, session=dbsession) for lecturer_id in data.pop("lecturer_id", []) + ] + groups = [Group.get(group_id, session=dbsession) for group_id in data.pop("group_id", [])] + result.append( + DB_Event.create( + **data, + room=rooms, + lecturer=lecturers, + group=groups, + session=dbsession, + ) + ) + dbsession.commit() + return result + async def get_user_calendar(group_id: int, session: Session, start_date: date_, end_date: date_) -> Calendar: """ diff --git a/calendar_backend/routes/event/event.py b/calendar_backend/routes/event/event.py index 1af85121..25dc1469 100644 --- a/calendar_backend/routes/event/event.py +++ b/calendar_backend/routes/event/event.py @@ -3,7 +3,7 @@ from typing import Literal from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends, Query, status +from fastapi import APIRouter, Depends, Query, status, File, UploadFile from fastapi.responses import FileResponse, JSONResponse from fastapi_sqlalchemy import db from pydantic import TypeAdapter @@ -33,6 +33,13 @@ async def get_event_by_id(id: int) -> EventGet: return EventGet.model_validate(Event.get(id, session=db.session)) +@router.post("/ical", response_model=EventPost) +async def create_events_from_3dparty(file: UploadFile = File(...)): + """Создает события на основе .ical или .ics файлов полученных из 3'd party источников.""" + events = await list_calendar.create_event_from_icalendar(db.session, file) + return [EventGet.model_validate(event) for event in events] + + async def _get_timetable(start: date, end: date, group_id, lecturer_id, room_id, detail, limit, offset): if bool(group_id) + bool(lecturer_id) + bool(room_id) != 1: raise NotEnoughCriteria("Exactly one argument group_id, lecturer_id or room_id required") From 792ba6ca0c92cf04f142468c6ef4bae8aeeb1681 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 6 Sep 2026 00:18:23 +0300 Subject: [PATCH 2/2] small fix and code style --- calendar_backend/methods/list_calendar.py | 26 +++++++++++------------ calendar_backend/routes/event/event.py | 4 ++-- tests/conftest.py | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/calendar_backend/methods/list_calendar.py b/calendar_backend/methods/list_calendar.py index c312f258..8bcc6f23 100644 --- a/calendar_backend/methods/list_calendar.py +++ b/calendar_backend/methods/list_calendar.py @@ -7,12 +7,13 @@ from typing import List import pytz -from fastapi import HTTPException, UploadFile, File +from fastapi import File, HTTPException, UploadFile from fastapi.responses import FileResponse from icalendar import Calendar, Event, vText from sqlalchemy.orm import Session -from calendar_backend.models import Group, Event as DB_Event, Room, Lecturer +from calendar_backend.models import Event as DB_Event +from calendar_backend.models import Group, Lecturer, Room from calendar_backend.settings import get_settings from . import utils @@ -21,19 +22,19 @@ settings = get_settings() logger = logging.getLogger(__name__) + def _get_list_from_ical_obj(element, field: str) -> List: items = element.get(field) if not items: return [] elif isinstance(items, list): return [int(i) for i in items] - elif "," in (str_items := str(items)) : + elif "," in (str_items := str(items)): return [int(i) for i in items.split(",")] else: return [int(items)] - async def create_event_from_icalendar(dbsession: Session, file: UploadFile = File(...)) -> List: extension = file.filename.split(".")[-1] available_exts = ["ical", "ics"] @@ -58,15 +59,14 @@ async def create_event_from_icalendar(dbsession: Session, file: UploadFile = Fil if not data.get("group_ids"): raise HTTPException(status_code=403, detail="Невозможно создать событие без группы!") events.append(data) - + for data in events: existing_events_query = ( - - DB_Event.get_all(session=dbsession) - .filter(DB_Event.name == data.get("name")) - .filter(DB_Event.start_ts == data.get("start_ts")) - .filter(DB_Event.end_ts == data.get("end_ts")) - ) + DB_Event.get_all(session=dbsession) + .filter(DB_Event.name == data.get("name")) + .filter(DB_Event.start_ts == data.get("start_ts")) + .filter(DB_Event.end_ts == data.get("end_ts")) + ) is_unique = True for existing_event in existing_events_query.all(): if ( @@ -78,9 +78,7 @@ async def create_event_from_icalendar(dbsession: Session, file: UploadFile = Fil if is_unique: rooms = [Room.get(room_id, session=dbsession) for room_id in data.pop("room_id", [])] - lecturers = [ - Lecturer.get(lecturer_id, session=dbsession) for lecturer_id in data.pop("lecturer_id", []) - ] + lecturers = [Lecturer.get(lecturer_id, session=dbsession) for lecturer_id in data.pop("lecturer_id", [])] groups = [Group.get(group_id, session=dbsession) for group_id in data.pop("group_id", [])] result.append( DB_Event.create( diff --git a/calendar_backend/routes/event/event.py b/calendar_backend/routes/event/event.py index 25dc1469..0337c959 100644 --- a/calendar_backend/routes/event/event.py +++ b/calendar_backend/routes/event/event.py @@ -3,7 +3,7 @@ from typing import Literal from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends, Query, status, File, UploadFile +from fastapi import APIRouter, Depends, File, Query, UploadFile, status from fastapi.responses import FileResponse, JSONResponse from fastapi_sqlalchemy import db from pydantic import TypeAdapter @@ -37,7 +37,7 @@ async def get_event_by_id(id: int) -> EventGet: async def create_events_from_3dparty(file: UploadFile = File(...)): """Создает события на основе .ical или .ics файлов полученных из 3'd party источников.""" events = await list_calendar.create_event_from_icalendar(db.session, file) - return [EventGet.model_validate(event) for event in events] + return [EventGet.model_validate(event) for event in events] async def _get_timetable(start: date, end: date, group_id, lecturer_id, room_id, detail, limit, offset): diff --git a/tests/conftest.py b/tests/conftest.py index 096339a1..82ae1c4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ def client(): @pytest.fixture() def client_auth(mocker: MockerFixture): - user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') + user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__', autospec=True) user_mock.return_value = { "session_scopes": [{"id": 0, "name": "string", "comment": "string"}], "user_scopes": [{"id": 0, "name": "string", "comment": "string"}],