Move IcsCalendar to it's own file

This commit is contained in:
Antoine Bartuccio 2025-01-04 18:57:31 +01:00
parent 5d0fc38107
commit 1887a2790f
5 changed files with 80 additions and 72 deletions

View File

@ -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.

74
com/calendar.py Normal file
View File

@ -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

View File

@ -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."""

View File

@ -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")

View File

@ -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