mirror of
https://github.com/ae-utbm/sith.git
synced 2025-04-16 02:50:22 +00:00
Merge pull request #1066 from ae-utbm/remove-gcalendar
Remove external calendar
This commit is contained in:
commit
a78ccbd2cc
21
com/api.py
21
com/api.py
@ -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())
|
||||||
|
@ -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():
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user