Merge pull request #1066 from ae-utbm/remove-gcalendar

Remove external calendar
This commit is contained in:
thomas girod 2025-04-06 17:21:33 +02:00 committed by GitHub
commit a78ccbd2cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 2 additions and 134 deletions

View File

@ -1,8 +1,6 @@
from pathlib import Path
from typing import Literal from typing import Literal
from django.conf import settings from django.http import HttpResponse
from django.http import Http404, HttpResponse
from ninja import Query from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.pagination import PageNumberPaginationExtra
@ -18,23 +16,6 @@ from core.views.files import send_raw_file
@api_controller("/calendar") @api_controller("/calendar")
class CalendarController(ControllerBase): class CalendarController(ControllerBase):
CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars"
@route.get("/external.ics", url_name="calendar_external")
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
from the frontend. Google is blocking CORS request in its 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.
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", url_name="calendar_internal") @route.get("/internal.ics", url_name="calendar_internal")
def calendar_internal(self): def calendar_internal(self):
return send_raw_file(IcsCalendar.get_internal()) return send_raw_file(IcsCalendar.get_internal())

View File

@ -1,8 +1,6 @@
from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import final from typing import final
import requests
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.conf import settings from django.conf import settings
from django.db.models import F, QuerySet from django.db.models import F, QuerySet
@ -19,35 +17,8 @@ from core.models import User
@final @final
class IcsCalendar: class IcsCalendar:
_CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars"
_EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics"
_INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.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 = requests.get(
"https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics"
)
if not calendar.ok:
return None
cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True)
with open(cls._EXTERNAL_CALENDAR, "wb") as f:
_ = f.write(calendar.content)
return cls._EXTERNAL_CALENDAR
@classmethod @classmethod
def get_internal(cls) -> Path: def get_internal(cls) -> Path:
if not cls._INTERNAL_CALENDAR.exists(): if not cls._INTERNAL_CALENDAR.exists():

View File

@ -8,7 +8,6 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import iCalendarPlugin from "@fullcalendar/icalendar"; import iCalendarPlugin from "@fullcalendar/icalendar";
import listPlugin from "@fullcalendar/list"; import listPlugin from "@fullcalendar/list";
import { import {
calendarCalendarExternal,
calendarCalendarInternal, calendarCalendarInternal,
calendarCalendarUnpublished, calendarCalendarUnpublished,
newsDeleteNews, newsDeleteNews,
@ -151,11 +150,6 @@ export class IcsCalendar extends inheritHtmlElement("div") {
format: "ics", format: "ics",
className: "internal", className: "internal",
}, },
{
url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`,
format: "ics",
className: "external",
},
{ {
url: `${await makeUrl(calendarCalendarUnpublished)}${cacheInvalidate}`, url: `${await makeUrl(calendarCalendarUnpublished)}${cacheInvalidate}`,
format: "ics", format: "ics",
@ -224,9 +218,6 @@ export class IcsCalendar extends inheritHtmlElement("div") {
}; };
const makePopupTools = (event: EventImpl) => { const makePopupTools = (event: EventImpl) => {
if (event.source.internalEventSource.ui.classNames.includes("external")) {
return null;
}
if (!(this.canDelete || this.canModerate)) { if (!(this.canDelete || this.canModerate)) {
return null; return null;
} }

View File

@ -1,8 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Callable
from unittest.mock import MagicMock, patch
from urllib.parse import quote from urllib.parse import quote
import pytest import pytest
@ -11,7 +9,6 @@ from django.contrib.auth.models import Permission
from django.http import HttpResponse from django.http import HttpResponse
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import now from django.utils.timezone import now
from model_bakery import baker, seq from model_bakery import baker, seq
from pytest_django.asserts import assertNumQueries from pytest_django.asserts import assertNumQueries
@ -41,78 +38,6 @@ def accel_redirect_to_file(response: HttpResponse) -> Path | None:
) )
@pytest.mark.django_db
class TestExternalCalendar:
@pytest.fixture
def mock_request(self):
mock = MagicMock()
with patch("requests.get", mock):
yield mock
@pytest.fixture
def mock_current_time(self):
mock = MagicMock()
original = timezone.now
with patch("django.utils.timezone.now", mock):
yield mock, original
@pytest.fixture(autouse=True)
def clear_cache(self):
IcsCalendar._EXTERNAL_CALENDAR.unlink(missing_ok=True)
def test_fetch_error(self, client: Client, mock_request: MagicMock):
mock_request.return_value = MockResponse(ok=False, value="not allowed")
assert client.get(reverse("api:calendar_external")).status_code == 404
def test_fetch_success(self, client: Client, mock_request: MagicMock):
external_response = MockResponse(ok=True, value="Definitely an ICS")
mock_request.return_value = external_response
response = client.get(reverse("api:calendar_external"))
assert response.status_code == 200
out_file = accel_redirect_to_file(response)
assert out_file is not None
assert out_file.exists()
with open(out_file, "r") as f:
assert f.read() == external_response.value
def test_fetch_caching(
self,
client: Client,
mock_request: MagicMock,
mock_current_time: tuple[MagicMock, Callable[[], datetime]],
):
fake_current_time, original_timezone = mock_current_time
start_time = original_timezone()
fake_current_time.return_value = start_time
external_response = MockResponse(200, "Definitely an ICS")
mock_request.return_value = external_response
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
mock_request.return_value = MockResponse(200, "This should be ignored")
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
mock_request.assert_called_once()
fake_current_time.return_value = start_time + timedelta(hours=1, seconds=1)
external_response = MockResponse(200, "This won't be ignored")
mock_request.return_value = external_response
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
assert mock_request.call_count == 2
@pytest.mark.django_db @pytest.mark.django_db
class TestInternalCalendar: class TestInternalCalendar:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)