management command to update the whole uv guide

This commit is contained in:
imperosol 2025-01-21 15:40:12 +01:00
parent 27e3781653
commit 997641d514
5 changed files with 84 additions and 14 deletions

View File

View File

View File

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

View File

@ -54,11 +54,11 @@ class UtbmFullUvSchema(Schema):
code: str code: str
departement: str = "NA" departement: str = "NA"
libelle: str libelle: str | None
objectifs: str objectifs: str | None
programme: str programme: str | None
acquisition_competences: str acquisition_competences: str | None
acquisition_notions: str acquisition_notions: str | None
langue: str langue: str
code_langue: str code_langue: str
credits_ects: int credits_ects: int

View File

@ -1,5 +1,7 @@
"""Set of functions to interact with the UTBM UV api.""" """Set of functions to interact with the UTBM UV api."""
from typing import Iterator
import requests import requests
from django.conf import settings from django.conf import settings
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -11,7 +13,7 @@ class UtbmApiClient(requests.Session):
"""A wrapper around `requests.Session` to perform requests to the UTBM UV API.""" """A wrapper around `requests.Session` to perform requests to the UTBM UV API."""
BASE_URL = settings.SITH_PEDAGOGY_UTBM_API BASE_URL = settings.SITH_PEDAGOGY_UTBM_API
_cache = {} _cache = {"short_uvs": {}}
@cached_property @cached_property
def current_year(self) -> int: def current_year(self) -> int:
@ -26,8 +28,6 @@ class UtbmApiClient(requests.Session):
"""Get the list of UVs in their short format from the UTBM API""" """Get the list of UVs in their short format from the UTBM API"""
if year is None: if year is None:
year = self.current_year year = self.current_year
if "short_uvs" not in self._cache:
self._cache["short_uvs"] = {}
if lang not in self._cache["short_uvs"]: if lang not in self._cache["short_uvs"]:
self._cache["short_uvs"][lang] = {} self._cache["short_uvs"][lang] = {}
if year not in self._cache["short_uvs"][lang]: if year not in self._cache["short_uvs"][lang]:
@ -37,6 +37,39 @@ class UtbmApiClient(requests.Session):
self._cache["short_uvs"][lang][year] = uvs self._cache["short_uvs"][lang][year] = uvs
return self._cache["short_uvs"][lang][year] return self._cache["short_uvs"][lang][year]
def fetch_uvs(
self, lang: str = "fr", year: int | None = None
) -> Iterator[UvSchema]:
"""Fetch all UVs from the UTBM API, parsed in a format that we can use.
Warning:
We need infos from the full uv schema, and the UTBM UV API
has no route to get all of them at once.
We must do one request per UV (for a total of around 730 UVs),
which takes a lot of time.
Hopefully, there seems to be no rate-limit, so an error
in the middle of the process isn't likely to occur.
"""
if year is None:
year = self.current_year
shorts_uvs = self.fetch_short_uvs(lang, year)
# When UVs are common to multiple branches (like most HUMA)
# the UTBM API duplicates them for every branch.
# We have no way in our db to link a UV to multiple formations,
# so we just create a single UV, which formation is the one
# of the first UV found in the list.
# For example, if we have CC01 (TC), CC01 (IMSI) and CC01 (EDIM),
# we will only keep CC01 (TC).
unique_short_uvs = {}
for uv in shorts_uvs:
if uv.code not in unique_short_uvs:
unique_short_uvs[uv.code] = uv
for uv in unique_short_uvs.values():
uv_url = f"{self.BASE_URL}/uv/{lang}/{year}/{uv.code}/{uv.code_formation}"
response = requests.get(uv_url)
full_uv = UtbmFullUvSchema.model_validate_json(response.content)
yield make_clean_uv(uv, full_uv)
def find_uv(self, lang: str, code: str, year: int | None = None) -> UvSchema | None: def find_uv(self, lang: str, code: str, year: int | None = None) -> UvSchema | None:
"""Find an UV from the UTBM API.""" """Find an UV from the UTBM API."""
# query the UV list # query the UV list
@ -92,9 +125,9 @@ def make_clean_uv(short_uv: UtbmShortUvSchema, full_uv: UtbmFullUvSchema) -> UvS
semester = "CLOSED" semester = "CLOSED"
return UvSchema( return UvSchema(
title=full_uv.libelle, title=full_uv.libelle or "",
code=full_uv.code, code=full_uv.code,
credit_type=short_uv.code_categorie, credit_type=short_uv.code_categorie or "FREE",
semester=semester, semester=semester,
language=short_uv.code_langue.upper(), language=short_uv.code_langue.upper(),
credits=full_uv.credits_ects, credits=full_uv.credits_ects,
@ -105,8 +138,8 @@ def make_clean_uv(short_uv: UtbmShortUvSchema, full_uv: UtbmFullUvSchema) -> UvS
hours_TE=next((i.nbh for i in full_uv.activites if i.code == "TE"), 0) // 60, hours_TE=next((i.nbh for i in full_uv.activites if i.code == "TE"), 0) // 60,
hours_CM=next((i.nbh for i in full_uv.activites if i.code == "CM"), 0) // 60, hours_CM=next((i.nbh for i in full_uv.activites if i.code == "CM"), 0) // 60,
manager=full_uv.respo_automne or full_uv.respo_printemps or "", manager=full_uv.respo_automne or full_uv.respo_printemps or "",
objectives=full_uv.objectifs, objectives=full_uv.objectifs or "",
program=full_uv.programme, program=full_uv.programme or "",
skills=full_uv.acquisition_competences, skills=full_uv.acquisition_competences or "",
key_concepts=full_uv.acquisition_notions, key_concepts=full_uv.acquisition_notions or "",
) )