Skip to content

Commit

Permalink
Big update. Add LMS to .ics export
Browse files Browse the repository at this point in the history
  • Loading branch information
depocoder committed Oct 17, 2024
1 parent 6ebf526 commit 55cb200
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 26 deletions.
8 changes: 8 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
This project was created to replace Modeus/Netology calendars


## Features

* Export to .ics calendar format
* Your timezone support (default Moscow)
* Modeus + Netology integration
* LMS support (not required to use)
* Redis cache

## Getting started

1. Install [poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
Expand Down
42 changes: 27 additions & 15 deletions backend/yet_another_calendar/web/api/bulk/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

from yet_another_calendar.settings import settings
from ..netology import views as netology_views
from ..lms import views as lms_views
from ..modeus import views as modeus_views
from ..modeus import schema as modeus_schema
from ..lms import schema as lms_schema
from ..netology import schema as netology_schema
from . import schema

Expand All @@ -22,17 +24,16 @@

def create_ics_event(title: str, starts_at: datetime.datetime, ends_at: datetime.datetime,
lesson_id: Any, description: Optional[str] = None,
webinar_url: Optional[str] = None) -> icalendar.Event:
url: Optional[str] = None) -> icalendar.Event:
event = icalendar.Event()
dt_now = datetime.datetime.now()
event.add('summary', title)
event.add('location', webinar_url)
event.add('location', url if url else 'unknown location')
event.add('dtstart', starts_at)
event.add('dtend', ends_at)
event.add('dtstamp', dt_now)
event.add('uid', lesson_id)
if description:
event.add('description', description)
event.add('DESCRIPTION', description)
return event


Expand All @@ -44,31 +45,36 @@ def export_to_ics(calendar: schema.CalendarResponse) -> Iterable[bytes]:
for netology_lesson in calendar.netology.webinars:
if not netology_lesson.starts_at or not netology_lesson.ends_at:
continue
event = create_ics_event(title=f"Netology: {netology_lesson.block_title}|{netology_lesson.title}",
starts_at=netology_lesson.starts_at,
event = create_ics_event(title=f"Netology: {netology_lesson.block_title}", starts_at=netology_lesson.starts_at,
ends_at=netology_lesson.ends_at, lesson_id=netology_lesson.id,
webinar_url=netology_lesson.webinar_url)
description=netology_lesson.title,
url=netology_lesson.webinar_url)
ics_calendar.add_component(event)
for modeus_lesson in calendar.utmn.modeus_events:
event = create_ics_event(title=f"Modeus: {modeus_lesson.course_name}|{modeus_lesson.name}",
starts_at=modeus_lesson.start_time, ends_at=modeus_lesson.end_time,
lesson_id=modeus_lesson.id,
description=modeus_lesson.description)
event = create_ics_event(title=f"Modeus: {modeus_lesson.course_name}", starts_at=modeus_lesson.start_time,
ends_at=modeus_lesson.end_time, lesson_id=modeus_lesson.id,
description=modeus_lesson.name)
ics_calendar.add_component(event)
for lms_event in calendar.utmn.lms_events:
dt_start = lms_event.dt_end - datetime.timedelta(hours=2)
event = create_ics_event(title=f"LMS: {lms_event.course_name}", starts_at=dt_start, ends_at=lms_event.dt_end,
lesson_id=lms_event.id, description=lms_event.name, url=lms_event.url)
ics_calendar.add_component(event)
yield ics_calendar.to_ical()


async def refresh_events(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
timezone: str,
) -> schema.RefreshedCalendarResponse:
"""Clear events cache."""
cached_json = await get_cached_calendar(body, jwt_token, calendar_id, cookies, timezone)
cached_json = await get_cached_calendar(body, lms_user, jwt_token, calendar_id, cookies, timezone)
cached_calendar = schema.CalendarResponse.model_validate(cached_json)
calendar = await get_calendar(body, jwt_token, calendar_id, cookies, timezone)
calendar = await get_calendar(body, lms_user, jwt_token, calendar_id, cookies, timezone)
changed = cached_calendar.get_hash() != calendar.get_hash()
try:
cache_key = default_key_builder(get_cached_calendar, args=(body, jwt_token, calendar_id, cookies), kwargs={})
Expand All @@ -88,6 +94,7 @@ async def refresh_events(

async def get_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
Expand All @@ -97,23 +104,28 @@ async def get_calendar(
tz = pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise HTTPException(detail="Wrong timezone", status_code=status.HTTP_400_BAD_REQUEST) from None

lms_response = None
async with asyncio.TaskGroup() as tg:
netology_response = tg.create_task(netology_views.get_calendar(body, calendar_id, cookies))
modeus_response = tg.create_task(modeus_views.get_calendar(body, jwt_token))
if lms_user.is_enabled:
lms_response = tg.create_task(lms_views.get_events(lms_user, body))
lms_events = lms_response.result() if lms_response else []
return schema.CalendarResponse.model_validate(
{"netology": netology_response.result(), "utmn": {
"modeus_events": modeus_response.result(),
"lms_events": lms_events,
}},
).change_timezone(tz)


@cache(expire=settings.redis_events_time_live)
async def get_cached_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
timezone: str,
) -> schema.CalendarResponse:
return await get_calendar(body, jwt_token, calendar_id, cookies, timezone)
return await get_calendar(body, lms_user, jwt_token, calendar_id, cookies, timezone)
14 changes: 9 additions & 5 deletions backend/yet_another_calendar/web/api/bulk/schema.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
import hashlib
from typing import Self, Optional
from typing import Self

from pydantic import BaseModel, Field

Expand All @@ -13,7 +13,7 @@ def now_dt_utc() -> datetime.datetime:

class UtmnResponse(BaseModel):
modeus_events: list[modeus_schema.FullEvent]
lms_events: Optional[list[lms_schema.ModuleResponse]] = Field(default=None)
lms_events: list[lms_schema.ModuleResponse]


class BulkResponse(BaseModel):
Expand All @@ -28,9 +28,13 @@ def change_timezone(self, timezone: datetime.tzinfo) -> Self:
webinar.starts_at = webinar.validate_starts_at(webinar.starts_at, timezone)
webinar.ends_at = webinar.validate_ends_at(webinar.ends_at, timezone)

for event in self.utmn.modeus_events:
event.start_time = event.validate_starts_at(event.start_time, timezone)
event.end_time = event.validate_end_time(event.end_time, timezone)
for modeus_event in self.utmn.modeus_events:
modeus_event.start_time = modeus_event.validate_starts_at(modeus_event.start_time, timezone)
modeus_event.end_time = modeus_event.validate_end_time(modeus_event.end_time, timezone)

for lms_event in self.utmn.lms_events:
lms_event.dt_start = lms_event.dt_start.astimezone(timezone)
lms_event.dt_end = lms_event.dt_end.astimezone(timezone)
return self


Expand Down
10 changes: 7 additions & 3 deletions backend/yet_another_calendar/web/api/bulk/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from yet_another_calendar.settings import settings
from ..modeus import schema as modeus_schema
from ..lms import schema as lms_schema
from ..netology import schema as netology_schema
from . import integration, schema

Expand All @@ -18,6 +19,7 @@
@router.post("/events/")
async def get_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
Expand All @@ -27,13 +29,14 @@ async def get_calendar(
Get events from Netology and Modeus, cached.
"""

cached_calendar = await integration.get_cached_calendar(body, jwt_token, calendar_id, cookies, time_zone)
cached_calendar = await integration.get_cached_calendar(body, lms_user, jwt_token, calendar_id, cookies, time_zone)
return schema.CalendarResponse.model_validate(cached_calendar)


@router.post("/refresh_events/")
async def refresh_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
Expand All @@ -43,12 +46,13 @@ async def refresh_calendar(
Refresh events in redis.
"""

return await integration.refresh_events(body, jwt_token, calendar_id, cookies, time_zone)
return await integration.refresh_events(body, lms_user, jwt_token, calendar_id, cookies, time_zone)


@router.post("/export_ics/")
async def export_ics(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
Expand All @@ -57,5 +61,5 @@ async def export_ics(
"""
Export into .ics format
"""
calendar = await integration.get_calendar(body, jwt_token, calendar_id, cookies, time_zone)
calendar = await integration.get_calendar(body, lms_user, jwt_token, calendar_id, cookies, time_zone)
return StreamingResponse(integration.export_to_ics(calendar))
5 changes: 3 additions & 2 deletions backend/yet_another_calendar/web/api/lms/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def get_username(self) -> str:
class User(BaseModel):
id: int
token: str
is_enabled: bool = False


class Course(BaseModel):
Expand Down Expand Up @@ -61,8 +62,8 @@ class Module(BaseModule):


class ModuleResponse(BaseModule):
dt_start: Optional[datetime.datetime]
dt_end: Optional[datetime.datetime]
dt_start: datetime.datetime
dt_end: datetime.datetime
is_completed: bool
course_name: str

Expand Down
2 changes: 1 addition & 1 deletion backend/yet_another_calendar/web/api/lms/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def get_user_info(


@router.post("/events")
async def get_course_info(
async def get_events(
user: schema.User,
body: ModeusTimeBody,
) -> list[schema.ModuleResponse]:
Expand Down

0 comments on commit 55cb200

Please sign in to comment.