diff --git a/com/api.py b/com/api.py index 871b0eac..6aec227f 100644 --- a/com/api.py +++ b/com/api.py @@ -1,15 +1,14 @@ -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path -import urllib3 from django.conf import settings -from django.http import HttpResponse +from django.http import Http404 from django.urls import reverse from django.utils import timezone from ics import Calendar, Event from ninja_extra import ControllerBase, api_controller, route -from com.models import NewsDate +from com.models import IcsCalendar, NewsDate from core.views.files import send_raw_file @@ -19,28 +18,18 @@ class CalendarController(ControllerBase): @route.get("/external.ics") def calendar_external(self): - file = self.CACHE_FOLDER / "external.ics" - # Return cached file if updated less than an our ago - if ( - file.exists() - and timezone.make_aware(datetime.fromtimestamp(file.stat().st_mtime)) - + timedelta(hours=1) - > timezone.now() - ): - return send_raw_file(file) + """Return the ICS file of the AE Google Calendar - calendar = urllib3.request( - "GET", - "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", - ) - if calendar.status != 200: - return HttpResponse(status=calendar.status) + 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. - self.CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(file, "wb") as f: - _ = f.write(calendar.data) - - return send_raw_file(file) + This is why we have this backend based solution. + """ + if (calendar := IcsCalendar.get_external()) is not None: + return send_raw_file(calendar) + raise Http404 @route.get("/internal.ics") def calendar_internal(self): diff --git a/com/models.py b/com/models.py index f3076174..6ec3ce53 100644 --- a/com/models.py +++ b/com/models.py @@ -17,11 +17,16 @@ # details. # # You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# this program; if not, write to the Free Software Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA. # # +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,6 +42,39 @@ 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" + + @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 + + class Sith(models.Model): """A one instance class storing all the modifiable infos."""