diff --git a/com/calendar.py b/com/calendar.py index 9003d6de..f3c612e1 100644 --- a/com/calendar.py +++ b/com/calendar.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from pathlib import Path from typing import final -import urllib3 +import requests from dateutil.relativedelta import relativedelta from django.conf import settings from django.urls import reverse @@ -35,16 +35,15 @@ class IcsCalendar: @classmethod def make_external(cls) -> Path | None: - calendar = urllib3.request( - "GET", - "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", + calendar = requests.get( + "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics" ) - if calendar.status != 200: + 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.data) + _ = f.write(calendar.content) return cls._EXTERNAL_CALENDAR @classmethod diff --git a/com/tests/test_api.py b/com/tests/test_api.py index f131052e..c84d8448 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -16,11 +16,11 @@ from com.calendar import IcsCalendar @dataclass class MockResponse: - status: int + ok: bool value: str @property - def data(self): + def content(self): return self.value.encode("utf8") @@ -38,7 +38,7 @@ class TestExternalCalendar: @pytest.fixture def mock_request(self): mock = MagicMock() - with patch("urllib3.request", mock): + with patch("requests.get", mock): yield mock @pytest.fixture @@ -52,15 +52,12 @@ class TestExternalCalendar: def clear_cache(self): IcsCalendar._EXTERNAL_CALENDAR.unlink(missing_ok=True) - @pytest.mark.parametrize("error_code", [403, 404, 500]) - def test_fetch_error( - self, client: Client, mock_request: MagicMock, error_code: int - ): - mock_request.return_value = MockResponse(error_code, "not allowed") + 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(200, "Definitely an ICS") + 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 diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 0cfef902..10fe885d 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -935,10 +935,6 @@ msgstr "rôle" msgid "description" msgstr "description" -#: club/models.py -msgid "past member" -msgstr "ancien membre" - #: club/models.py msgid "Email address" msgstr "Adresse email" @@ -3308,8 +3304,8 @@ msgstr "Nom d'utilisateur, email, ou numéro de compte AE" #: core/views/forms.py msgid "" -"Profile: you need to be visible on the picture, in order to be recognized " -"(e.g. by the barmen)" +"Profile: you need to be visible on the picture, in order to be recognized (e." +"g. by the barmen)" msgstr "" "Photo de profil: vous devez être visible sur la photo afin d'être reconnu " "(par exemple par les barmen)" @@ -3919,8 +3915,8 @@ msgstr "" #: counter/templates/counter/mails/account_dump.jinja msgid "If you think this was a mistake, please mail us at ae@utbm.fr." msgstr "" -"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à " -"ae@utbm.fr." +"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à ae@utbm." +"fr." #: counter/templates/counter/mails/account_dump.jinja msgid "" @@ -4948,6 +4944,10 @@ msgstr "Département" msgid "Credit type" msgstr "Type de crédit" +#: pedagogy/templates/pedagogy/guide.jinja +msgid "closed uv" +msgstr "uv fermée" + #: pedagogy/templates/pedagogy/macros.jinja msgid " not rated " msgstr "non noté" @@ -5990,3 +5990,6 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée." #, python-format msgid "Maximum characters: %(max_length)s" msgstr "Nombre de caractères max: %(max_length)s" + +#~ msgid "past member" +#~ msgstr "ancien membre" diff --git a/pedagogy/api.py b/pedagogy/api.py index e8d34351..9ad0c3f6 100644 --- a/pedagogy/api.py +++ b/pedagogy/api.py @@ -10,13 +10,13 @@ from ninja_extra.pagination import PageNumberPaginationExtra, PaginatedResponseS from core.auth.api_permissions import HasPerm from pedagogy.models import UV from pedagogy.schemas import SimpleUvSchema, UvFilterSchema, UvSchema -from pedagogy.utbm_api import find_uv +from pedagogy.utbm_api import UtbmApiClient @api_controller("/uv") class UvController(ControllerBase): @route.get( - "/{year}/{code}", + "/{code}", permissions=[ # this route will almost always be called in the context # of a UV creation/edition @@ -26,10 +26,14 @@ class UvController(ControllerBase): response=UvSchema, ) def fetch_from_utbm_api( - self, year: Annotated[int, Ge(2010)], code: str, lang: Query[str] = "fr" + self, + code: str, + lang: Query[str] = "fr", + year: Query[Annotated[int, Ge(2010)] | None] = None, ): """Fetch UV data from the UTBM API and returns it after some parsing.""" - res = find_uv(lang, year, code) + with UtbmApiClient() as client: + res = client.find_uv(lang, code, year) if res is None: raise NotFound return res @@ -42,4 +46,4 @@ class UvController(ControllerBase): ) @paginate(PageNumberPaginationExtra, page_size=100) def fetch_uv_list(self, search: Query[UvFilterSchema]): - return search.filter(UV.objects.values()) + return search.filter(UV.objects.order_by("code").values()) diff --git a/pedagogy/management/__init__.py b/pedagogy/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pedagogy/management/commands/__init__.py b/pedagogy/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pedagogy/management/commands/update_uv_guide.py b/pedagogy/management/commands/update_uv_guide.py new file mode 100644 index 00000000..cf525a1f --- /dev/null +++ b/pedagogy/management/commands/update_uv_guide.py @@ -0,0 +1,37 @@ +from django.conf import settings +from django.core.management import BaseCommand + +from core.models import User +from pedagogy.models import UV +from pedagogy.schemas import UvSchema +from pedagogy.utbm_api import UtbmApiClient + + +class Command(BaseCommand): + help = "Update the UV guide" + + def handle(self, *args, **options): + seen_uvs: set[int] = set() + root_user = User.objects.get(pk=settings.SITH_ROOT_USER_ID) + with UtbmApiClient() as client: + self.stdout.write( + "Fetching UVs from the UTBM API.\n" + "This may take a few minutes to complete." + ) + for uv in client.fetch_uvs(): + db_uv = UV.objects.filter(code=uv.code).first() + if db_uv is None: + db_uv = UV(code=uv.code, author=root_user) + fields = list(UvSchema.model_fields.keys()) + fields.remove("id") + fields.remove("code") + for field in fields: + setattr(db_uv, field, getattr(uv, field)) + db_uv.save() + # if it's a creation, django will set the id when saving, + # so at this point, a db_uv will always have an id + seen_uvs.add(db_uv.id) + # UVs that are in database but have not been returned by the API + # are considered as closed UEs + UV.objects.exclude(id__in=seen_uvs).update(semester="CLOSED") + self.stdout.write(self.style.SUCCESS("UV guide updated successfully")) diff --git a/pedagogy/schemas.py b/pedagogy/schemas.py index 716e9a3c..cbcd9157 100644 --- a/pedagogy/schemas.py +++ b/pedagogy/schemas.py @@ -54,11 +54,11 @@ class UtbmFullUvSchema(Schema): code: str departement: str = "NA" - libelle: str - objectifs: str - programme: str - acquisition_competences: str - acquisition_notions: str + libelle: str | None + objectifs: str | None + programme: str | None + acquisition_competences: str | None + acquisition_notions: str | None langue: str code_langue: str credits_ects: int diff --git a/pedagogy/static/pedagogy/css/pedagogy.scss b/pedagogy/static/pedagogy/css/pedagogy.scss index a4ebb370..51656615 100644 --- a/pedagogy/static/pedagogy/css/pedagogy.scss +++ b/pedagogy/static/pedagogy/css/pedagogy.scss @@ -47,11 +47,14 @@ $large-devices: 992px; } } - #dynamic_view { + #uv-list { font-size: 1.1em; overflow-wrap: break-word; - + .closed td.title { + color: lighten($black-color, 10%); + font-style: italic; + } td { text-align: center; border: none; diff --git a/pedagogy/templates/pedagogy/guide.jinja b/pedagogy/templates/pedagogy/guide.jinja index 79b66c24..460fdcc5 100644 --- a/pedagogy/templates/pedagogy/guide.jinja +++ b/pedagogy/templates/pedagogy/guide.jinja @@ -85,7 +85,7 @@ - +
@@ -102,11 +102,17 @@ {% endif %} - +
{% trans %}UV{% endtrans %}