Skip to content
This repository has been archived by the owner on Dec 15, 2024. It is now read-only.

Commit

Permalink
feat: share selection; ics for sport and selection
Browse files Browse the repository at this point in the history
  • Loading branch information
dantetemplar committed Nov 23, 2024
1 parent e7cb6ac commit dac6a59
Show file tree
Hide file tree
Showing 12 changed files with 282 additions and 12 deletions.
6 changes: 0 additions & 6 deletions backend/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ repos:
entry: poetry run python ./scripts/generate_settings_schema.py
pass_filenames: false
files: ^src/config_schema.py$
- id: poetry-check
name: poetry check
entry: poetry check
language: system
pass_filenames: false
files: "pyproject.toml"
- id: alembic-auto-migrations
name: generate alembic auto migrations files
language: system
Expand Down
56 changes: 55 additions & 1 deletion backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fastapi = "^0.115.0"
fastapi-swagger = "^0.2.3"
gunicorn = "^23.0.0"
httpx = "^0.27.2"
icalendar = "^6.1.0"
itsdangerous = "^2.2.0"
passlib = "^1.7.4"
pre-commit = "^3.8.0"
Expand Down
9 changes: 9 additions & 0 deletions backend/src/modules/events/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from src.modules.events.schemas import Filters, Pagination, Sort
from src.storages.mongo.events import Event
from src.storages.mongo.selection import Selection


# noinspection PyMethodMayBeStatic
Expand Down Expand Up @@ -114,5 +115,13 @@ async def read_with_filters(
# Return results
return await query.to_list()

async def create_selection(self, filters: Filters, sort: Sort):
selection = Selection(filters=filters, sort=sort)
await selection.insert()
return selection

async def read_selection(self, id_: PydanticObjectId) -> Selection | None:
return await Selection.get(id_)


events_repository: EventsRepository = EventsRepository()
78 changes: 77 additions & 1 deletion backend/src/modules/events/routes.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from datetime import datetime
from datetime import datetime, timedelta

import icalendar
from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from starlette.responses import Response

from src.api.exceptions import IncorrectCredentialsException
from src.modules.events.repository import events_repository
from src.modules.events.schemas import DateFilter, Filters, Pagination, Sort
from src.modules.ics_utils import get_base_calendar
from src.modules.sports.repository import sports_repository
from src.storages.mongo.events import Event
from src.storages.mongo.selection import Selection

router = APIRouter(
prefix="/events",
Expand Down Expand Up @@ -188,3 +192,75 @@ async def get_all_filters_disciplines() -> list[DisciplinesFilterVariants]:
)
for sport in sports
]


@router.post("/search/share", responses={200: {"description": "Share selection"}})
async def share_selection(filters: Filters, sort: Sort) -> Selection:
"""
Share selection. Use this for .ics too.
"""
selection = await events_repository.create_selection(filters, sort)
return selection


@router.get(
"/seach/share/{selection_id}",
responses={200: {"description": "Get selection"}, 404: {"description": "Selection not found"}},
)
async def get_selection(selection_id: PydanticObjectId) -> Selection:
"""
Get selection.
"""
selection = await events_repository.read_selection(selection_id)
if selection is None:
raise HTTPException(status_code=404, detail="Selection not found")

return selection


@router.get(
"/search/share/{selection_id}.ics",
response_class=Response,
responses={
200: {"description": "Get selection in .ics format"},
404: {"description": "Selection not found"},
},
)
async def get_selection_ics(selection_id: PydanticObjectId):
selection = await events_repository.read_selection(selection_id)
if selection is None:
raise HTTPException(status_code=404, detail="Selection not found")

date_filter = DateFilter()
# week before and month after
date_filter.start_date = datetime.now() - timedelta(days=7)
date_filter.end_date = datetime.now() + timedelta(days=30)
selection.filters.date = date_filter

events = await events_repository.read_with_filters(
filters=selection.filters,
sort=selection.sort,
pagination=Pagination(page_size=1000, page_no=1),
)
calendar = get_base_calendar()
calendar["x-wr-calname"] = "Подборка Спортивных Событий"

for event in events:
uid = f"{str(event.id)}@innohassle.ru"

vevent = icalendar.Event()
vevent.add("uid", uid)

vevent.add("summary", f"{event.sport}: {event.title}")
vevent.add("dtstart", icalendar.vDate(event.start_date))
vevent.add("dtend", icalendar.vDate(event.end_date))
vevent.add("description", event.description)
if event.location:
vevent.add("location", "\n".join([str(loc) for loc in event.location]))
calendar.add_component(vevent)

return Response(
content=calendar.to_ical(),
media_type="text/calendar",
headers={"Content-Disposition": 'attachment; filename="schedule.ics"'},
)
36 changes: 36 additions & 0 deletions backend/src/modules/ics_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import datetime

import icalendar

TIMEZONE = "Europe/Moscow"


def get_base_calendar() -> icalendar.Calendar:
"""
Get base calendar with default properties (version, prodid, etc.)
:return: base calendar
:rtype: icalendar.Calendar
"""

calendar = icalendar.Calendar(
prodid="-//one-zero-eight//Календарь Спорта РФ",
version="2.0",
method="PUBLISH",
)

calendar["x-wr-caldesc"] = "Календарь Спорта РФ"
calendar["x-wr-timezone"] = TIMEZONE
#
# add timezone
timezone = icalendar.Timezone(tzid=TIMEZONE)
timezone["x-lic-location"] = TIMEZONE
# add standard timezone
standard = icalendar.TimezoneStandard()
standard.add("tzoffsetfrom", datetime.timedelta(hours=3))
standard.add("tzoffsetto", datetime.timedelta(hours=3))
standard.add("tzname", "MSK")
standard.add("dtstart", datetime.datetime(1970, 1, 1))
timezone.add_component(standard)
calendar.add_component(timezone)

return calendar
4 changes: 3 additions & 1 deletion backend/src/modules/sports/repository.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
__all__ = ["sports_repository"]

from beanie import PydanticObjectId

from src.storages.mongo.sports import Sport


# noinspection PyMethodMayBeStatic
class SportsRepository:
async def read_one(self, id: str) -> Sport | None:
async def read_one(self, id: PydanticObjectId) -> Sport | None:
return await Sport.get(id)

async def read_all(self) -> list[Sport] | None:
Expand Down
55 changes: 53 additions & 2 deletions backend/src/modules/sports/routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from fastapi import APIRouter
import icalendar
from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException
from starlette.responses import Response

from src.api.exceptions import IncorrectCredentialsException
from src.modules.events.repository import events_repository
from src.modules.events.schemas import DisciplineFilter, Filters, Order, Pagination, Sort
from src.modules.ics_utils import get_base_calendar
from src.modules.sports.repository import sports_repository
from src.storages.mongo import Sport

Expand All @@ -22,7 +28,7 @@ async def get_all_sports() -> list[Sport]:


@router.get("/{id}", responses={200: {"description": "Info about sport"}})
async def get_sport(id: str) -> Sport:
async def get_sport(id: PydanticObjectId) -> Sport:
"""
Get info about one sport.
"""
Expand All @@ -43,3 +49,48 @@ async def update_descriptions(name_x_descriptions: dict[str, str]) -> None:
Update descriptions of sports.
"""
await sports_repository.update_decriptions(name_x_descriptions)


@router.get(
"/{id}/.ics",
response_class=Response,
responses={
200: {"description": "Get sport's events in .ics format"},
404: {"description": "Sport not found"},
},
)
async def get_sport_ics(id: PydanticObjectId) -> Response:
sport = await sports_repository.read_one(id)
if sport is None:
raise HTTPException(status_code=404, detail="Sport not found")

filters = Filters(discipline=[DisciplineFilter(sport=sport.sport)])

events = await events_repository.read_with_filters(
filters=filters,
sort=Sort(date=Order.asc),
pagination=Pagination(page_size=1000, page_no=1),
)

calendar = get_base_calendar()
calendar["x-wr-calname"] = f"Подборка Спортивных Событий ({sport.sport})"

for event in events:
uid = f"{str(event.id)}@innohassle.ru"

vevent = icalendar.Event()
vevent.add("uid", uid)

vevent.add("summary", f"{event.title}")
vevent.add("dtstart", icalendar.vDate(event.start_date))
vevent.add("dtend", icalendar.vDate(event.end_date))
vevent.add("description", f"{event.sport}\n\n{event.title}")
if event.location:
vevent.add("location", "\n".join([str(loc) for loc in event.location]))
calendar.add_component(vevent)

return Response(
content=calendar.to_ical(),
media_type="text/calendar",
headers={"Content-Disposition": 'attachment; filename="sport.ics"'},
)
3 changes: 2 additions & 1 deletion backend/src/storages/mongo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

from src.storages.mongo.events import Event
from src.storages.mongo.notifies import Notification
from src.storages.mongo.selection import Selection
from src.storages.mongo.sports import Sport
from src.storages.mongo.users import User

document_models = cast(
list[type[Document] | type[View] | str],
[User, Event, Sport, Notification],
[User, Event, Sport, Notification, Selection],
)
8 changes: 8 additions & 0 deletions backend/src/storages/mongo/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ class EventLocation(BaseSchema):
city: str | None = None
"Название города"

def __str__(self):
s = self.country
if self.region:
s += f", {self.region}"
if self.city:
s += f", {self.city}"
return s


class EventSchema(BaseSchema):
ekp_id: int
Expand Down
14 changes: 14 additions & 0 deletions backend/src/storages/mongo/selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from src.modules.events.schemas import Filters, Sort
from src.pydantic_base import BaseSchema
from src.storages.mongo.__base__ import CustomDocument


class SelectionSchema(BaseSchema):
filters: Filters
"Filter for the selection."
sort: Sort = Sort()
"Sort for the selection."


class Selection(SelectionSchema, CustomDocument):
pass
Loading

0 comments on commit dac6a59

Please sign in to comment.