diff --git a/com/api.py b/com/api.py index 63a0e680..9a5b1398 100644 --- a/com/api.py +++ b/com/api.py @@ -4,7 +4,7 @@ from django.conf import settings from django.http import Http404 from ninja_extra import ControllerBase, api_controller, route -from com.models import IcsCalendar +from com.calendar import IcsCalendar from core.views.files import send_raw_file @@ -16,7 +16,7 @@ class CalendarController(ControllerBase): def calendar_external(self): """Return the ICS file of the AE Google Calendar - Because of Google's cors rules, we can't "just" do a request to google ics + Because of Google's cors rules, we can't just do a request to google ics from the frontend. Google is blocking CORS request in it's responses headers. The only way to do it from the frontend is to use Google Calendar API with an API key This is not especially desirable as your API key is going to be provided to the frontend. diff --git a/com/calendar.py b/com/calendar.py new file mode 100644 index 00000000..52cb25b9 --- /dev/null +++ b/com/calendar.py @@ -0,0 +1,74 @@ +from datetime import datetime, timedelta +from pathlib import Path +from typing import final + +import urllib3 +from django.conf import settings +from django.urls import reverse +from django.utils import timezone +from ics import Calendar, Event + +from com.models import NewsDate + + +@final +class IcsCalendar: + _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" + _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics" + _INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics" + + @classmethod + def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None: + if ( + cls._EXTERNAL_CALENDAR.exists() + and timezone.make_aware( + datetime.fromtimestamp(cls._EXTERNAL_CALENDAR.stat().st_mtime) + ) + + expiration + > timezone.now() + ): + return cls._EXTERNAL_CALENDAR + return cls.make_external() + + @classmethod + def make_external(cls) -> Path | None: + calendar = urllib3.request( + "GET", + "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", + ) + if calendar.status != 200: + return None + + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._EXTERNAL_CALENDAR, "wb") as f: + _ = f.write(calendar.data) + return cls._EXTERNAL_CALENDAR + + @classmethod + def get_internal(cls) -> Path: + if not cls._INTERNAL_CALENDAR.exists(): + return cls.make_internal() + return cls._INTERNAL_CALENDAR + + @classmethod + def make_internal(cls) -> Path: + # Updated through a post_save signal on News in com.signals + calendar = Calendar() + for news_date in NewsDate.objects.filter( + news__is_moderated=True, + end_date__gte=timezone.now() + - (timedelta(days=30) * 60), # Roughly get the last 6 months + ).prefetch_related("news"): + event = Event( + name=news_date.news.title, + begin=news_date.start_date, + end=news_date.end_date, + url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), + ) + calendar.events.add(event) + + # Create a file so we can offload the download to the reverse proxy if available + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._INTERNAL_CALENDAR, "wb") as f: + _ = f.write(calendar.serialize().encode("utf-8")) + return cls._INTERNAL_CALENDAR diff --git a/com/models.py b/com/models.py index c7042a38..633c7671 100644 --- a/com/models.py +++ b/com/models.py @@ -22,11 +22,7 @@ # # -from datetime import datetime, timedelta -from pathlib import Path -from typing import final -import urllib3 from django.conf import settings from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives @@ -37,75 +33,11 @@ from django.templatetags.static import static from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from ics import Calendar, Event from club.models import Club from core.models import Notification, Preferences, User -@final -class IcsCalendar: - _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" - _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics" - _INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics" - - @classmethod - def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None: - if ( - cls._EXTERNAL_CALENDAR.exists() - and timezone.make_aware( - datetime.fromtimestamp(cls._EXTERNAL_CALENDAR.stat().st_mtime) - ) - + expiration - > timezone.now() - ): - return cls._EXTERNAL_CALENDAR - return cls.make_external() - - @classmethod - def make_external(cls) -> Path | None: - calendar = urllib3.request( - "GET", - "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", - ) - if calendar.status != 200: - return None - - cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(cls._EXTERNAL_CALENDAR, "wb") as f: - _ = f.write(calendar.data) - return cls._EXTERNAL_CALENDAR - - @classmethod - def get_internal(cls) -> Path: - if not cls._INTERNAL_CALENDAR.exists(): - return cls.make_internal() - return cls._INTERNAL_CALENDAR - - @classmethod - def make_internal(cls) -> Path: - # Updated through a post_save signal on News in com.signals - calendar = Calendar() - for news_date in NewsDate.objects.filter( - news__is_moderated=True, - end_date__gte=timezone.now() - - (timedelta(days=30) * 60), # Roughly get the last 6 months - ).prefetch_related("news"): - event = Event( - name=news_date.news.title, - begin=news_date.start_date, - end=news_date.end_date, - url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), - ) - calendar.events.add(event) - - # Create a file so we can offload the download to the reverse proxy if available - cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(cls._INTERNAL_CALENDAR, "wb") as f: - _ = f.write(calendar.serialize().encode("utf-8")) - return cls._INTERNAL_CALENDAR - - class Sith(models.Model): """A one instance class storing all the modifiable infos.""" diff --git a/com/signals.py b/com/signals.py index b67a4131..ea004ad8 100644 --- a/com/signals.py +++ b/com/signals.py @@ -1,7 +1,8 @@ from django.db.models.base import post_save from django.dispatch import receiver -from com.models import IcsCalendar, News +from com.calendar import IcsCalendar +from com.models import News @receiver(post_save, sender=News, dispatch_uid="update_internal_ics") diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 486f23cd..3ed1025d 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -46,7 +46,8 @@ from accounting.models import ( SimplifiedAccountingType, ) from club.models import Club, Membership -from com.models import IcsCalendar, News, NewsDate, Sith, Weekmail +from com.calendar import IcsCalendar +from com.models import News, NewsDate, Sith, Weekmail from core.models import Group, Page, PageRev, SithFile, User from core.utils import resize_image from counter.models import Counter, Product, ProductType, StudentCard