mirror of
https://github.com/ae-utbm/sith.git
synced 2026-07-23 15:01:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53e454a04a |
@@ -886,28 +886,6 @@ class TestBarmanConnection(TestCase):
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_logout_then_login(self):
|
||||
"""Test that the login is successful if it is after a previous logout.
|
||||
|
||||
This is a regression test for #1440
|
||||
"""
|
||||
self.client.post( # login
|
||||
self.login_url, {"username": self.barman.username, "password": "plop"}
|
||||
)
|
||||
self.client.post( # logout
|
||||
reverse("counter:logout", kwargs={"counter_id": self.counter.id}),
|
||||
data={"user_id": self.barman.id},
|
||||
)
|
||||
response = self.client.post( # and re-login
|
||||
self.login_url, {"username": self.barman.username, "password": "plop"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.headers["HX-Redirect"] == self.detail_url
|
||||
response = self.client.get(
|
||||
self.detail_url, {"username": self.barman.username, "password": "plop"}
|
||||
)
|
||||
assert self.barman in response.wsgi_request.barmen
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_barman_timeout(client: Client):
|
||||
|
||||
@@ -61,7 +61,6 @@ class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView):
|
||||
user = form.get_user()
|
||||
perm = self.object.permanencies.create(user=user, start=timezone.now())
|
||||
self.request.session.setdefault(SESSION_PERMANENCES_KEY, []).append(perm.id)
|
||||
self.request.session.modified = True
|
||||
self.success_url = reverse(
|
||||
"counter:details", kwargs={"counter_id": self.object.id}
|
||||
)
|
||||
|
||||
@@ -148,6 +148,56 @@
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<section>
|
||||
<div class="category-header">
|
||||
<h3 class="margin-bottom">{% trans %}Eurockéennes 2025 partnership{% endtrans %}</h3>
|
||||
{% if user.is_subscribed %}
|
||||
<div id="eurock-partner" style="
|
||||
min-height: 600px;
|
||||
background-color: lightgrey;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
">
|
||||
<p style="text-align: center;">
|
||||
{% trans trimmed %}
|
||||
Our partner uses Weezevent to sell tickets.
|
||||
Weezevent may collect user info according to
|
||||
its own privacy policy.
|
||||
By clicking the accept button you consent to
|
||||
their terms of services.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
|
||||
<a href="https://weezevent.com/fr/politique-de-confidentialite/">{% trans %}Privacy policy{% endtrans %}</a>
|
||||
|
||||
<button
|
||||
hx-get="{{ url("eboutic:eurock") }}"
|
||||
hx-target="#eurock-partner"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="click, load[document.cookie.includes('weezevent_accept=true')]"
|
||||
@htmx:after-request="document.cookie = 'weezevent_accept=true'"
|
||||
>{% trans %}Accept{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{%- trans trimmed %}
|
||||
You must be subscribed to benefit from the partnership with the Eurockéennes.
|
||||
{% endtrans -%}
|
||||
</p>
|
||||
<p>
|
||||
{%- trans trimmed %}
|
||||
This partnership offers a discount of up to 33%
|
||||
on tickets for Friday, Saturday and Sunday,
|
||||
as well as the 3-day package from Friday to Sunday.
|
||||
{% endtrans -%}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% for prices in categories %}
|
||||
{% set category = prices[0].product.product_type %}
|
||||
<section>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<a title="Logiciel billetterie en ligne"
|
||||
href="https://www.weezevent.com?c=sys_widget"
|
||||
class="weezevent-widget-integration"
|
||||
target="_blank"
|
||||
data-src="https://widget.weezevent.com/ticket/8aaba226-f7a3-4192-a64e-72ff8f5b35b7?id_evenement=1419869&locale=fr-FR&code=28747"
|
||||
data-width="650"
|
||||
data-height="600"
|
||||
data-resize="1"
|
||||
data-nopb="0"
|
||||
data-type="neo"
|
||||
data-width_auto="1"
|
||||
data-noscroll="0"
|
||||
data-id="1419869">
|
||||
Billetterie Weezevent
|
||||
</a>
|
||||
<script type="text/javascript" src="https://widget.weezevent.com/weez.js" async defer></script>
|
||||
@@ -31,6 +31,7 @@ from eboutic.views import (
|
||||
EbouticMainView,
|
||||
EbouticPayWithSith,
|
||||
EtransactionAutoAnswer,
|
||||
EurockPartnerFragment,
|
||||
payment_result,
|
||||
)
|
||||
|
||||
@@ -50,4 +51,5 @@ urlpatterns = [
|
||||
EtransactionAutoAnswer.as_view(),
|
||||
name="etransation_autoanswer",
|
||||
),
|
||||
path("eurock/", EurockPartnerFragment.as_view(), name="eurock"),
|
||||
]
|
||||
|
||||
+6
-2
@@ -43,11 +43,11 @@ from django.utils.formats import localize
|
||||
from django.utils.timezone import localtime
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_GET
|
||||
from django.views.generic import DetailView, FormView, UpdateView, View
|
||||
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
|
||||
from django.views.generic.edit import SingleObjectMixin
|
||||
from django_countries.fields import Country
|
||||
|
||||
from core.auth.mixins import CanViewMixin
|
||||
from core.auth.mixins import CanViewMixin, IsSubscriberMixin
|
||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||
from counter.forms import BaseBasketForm, BasketItemForm, BillingInfoForm
|
||||
from counter.models import (
|
||||
@@ -359,3 +359,7 @@ class EtransactionAutoAnswer(View):
|
||||
return HttpResponse(
|
||||
"Payment failed with error: " + request.GET["Error"], status=202
|
||||
)
|
||||
|
||||
|
||||
class EurockPartnerFragment(IsSubscriberMixin, TemplateView):
|
||||
template_name = "eboutic/eurock_fragment.jinja"
|
||||
|
||||
@@ -4465,6 +4465,47 @@ msgstr ""
|
||||
msgid "this page"
|
||||
msgstr "cette page"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "Eurockéennes 2025 partnership"
|
||||
msgstr "Partenariat Eurockéennes 2025"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid ""
|
||||
"Our partner uses Weezevent to sell tickets. Weezevent may collect user info "
|
||||
"according to its own privacy policy. By clicking the accept button you "
|
||||
"consent to their terms of services."
|
||||
msgstr ""
|
||||
"Notre partenaire utilises Wezevent pour vendre ses billets. Weezevent peut "
|
||||
"collecter des informations utilisateur conformément à sa propre politique de "
|
||||
"confidentialité. En cliquant sur le bouton d'acceptation vous consentez à "
|
||||
"leurs termes de service."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "Privacy policy"
|
||||
msgstr "Politique de confidentialité"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
#: trombi/templates/trombi/comment_moderation.jinja
|
||||
msgid "Accept"
|
||||
msgstr "Accepter"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid ""
|
||||
"You must be subscribed to benefit from the partnership with the Eurockéennes."
|
||||
msgstr ""
|
||||
"Vous devez être cotisant pour bénéficier du partenariat avec les "
|
||||
"Eurockéennes."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This partnership offers a discount of up to 33%% on tickets for Friday, "
|
||||
"Saturday and Sunday, as well as the 3-day package from Friday to Sunday."
|
||||
msgstr ""
|
||||
"Ce partenariat permet de profiter d'une réduction jusqu'à 33%% sur les "
|
||||
"billets du vendredi, du samedi et du dimanche, ainsi qu'au forfait 3 jours, "
|
||||
"du vendredi au dimanche."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "Product sold out"
|
||||
msgstr "Produit épuisé"
|
||||
@@ -5908,10 +5949,6 @@ msgstr "fin"
|
||||
msgid "Moderate Trombi comments"
|
||||
msgstr "Modérer les commentaires du Trombi"
|
||||
|
||||
#: trombi/templates/trombi/comment_moderation.jinja
|
||||
msgid "Accept"
|
||||
msgstr "Accepter"
|
||||
|
||||
#: trombi/templates/trombi/comment_moderation.jinja
|
||||
msgid "Reject"
|
||||
msgstr "Refuser"
|
||||
|
||||
+2
@@ -19,6 +19,8 @@ class Command(BaseCommand):
|
||||
"This may take a few minutes to complete."
|
||||
)
|
||||
for ue in client.fetch_ues():
|
||||
# this is a N+1 query, but it isn't a problem,
|
||||
# as fetching a UE from the UTBM API is taking most of the time anyway
|
||||
db_ue = UE.objects.filter(code=ue.code).first()
|
||||
if db_ue is None:
|
||||
db_ue = UE(code=ue.code, author=root_user)
|
||||
+28
-17
@@ -11,6 +11,16 @@ from pydantic.alias_generators import to_camel
|
||||
from pedagogy.models import UE
|
||||
|
||||
|
||||
class FormationSchema(Schema):
|
||||
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
|
||||
|
||||
code: str
|
||||
label: str = Field(alias="libelle")
|
||||
|
||||
|
||||
FormationListSchema = TypeAdapter(list[FormationSchema])
|
||||
|
||||
|
||||
class UtbmShortUeSchema(Schema):
|
||||
"""Short representation of an UE in the UTBM API.
|
||||
|
||||
@@ -19,19 +29,17 @@ class UtbmShortUeSchema(Schema):
|
||||
The UTBM API returns more data than that.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel)
|
||||
model_config = ConfigDict(validate_by_name=True)
|
||||
|
||||
code: str
|
||||
code_formation: str
|
||||
code_categorie: str | None
|
||||
code_langue: str
|
||||
ouvert_automne: bool
|
||||
ouvert_printemps: bool
|
||||
category: str = Field(alias="codeCategorie")
|
||||
formation: FormationSchema
|
||||
lang: str = Field(alias="codeLangue")
|
||||
open_autumn: bool = Field(alias="ouvertAutomne")
|
||||
open_spring: bool = Field(alias="ouvertPrintemps")
|
||||
|
||||
|
||||
class WorkloadSchema(Schema):
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
code: Literal["TD", "TP", "CM", "THE", "TE"]
|
||||
nbh: int
|
||||
|
||||
@@ -48,22 +56,25 @@ class SemesterUeState(Schema):
|
||||
ShortUeList = TypeAdapter(list[UtbmShortUeSchema])
|
||||
|
||||
|
||||
class SyllabusItemSchema(Schema):
|
||||
model_config = ConfigDict(validate_by_name=True)
|
||||
|
||||
label: str = Field(alias="libelle")
|
||||
value: str | None = Field(alias="valeur")
|
||||
|
||||
|
||||
class UtbmFullUeSchema(Schema):
|
||||
"""Long representation of an UE in the UTBM API."""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel)
|
||||
model_config = ConfigDict(validate_by_name=True, alias_generator=to_camel)
|
||||
|
||||
code: str
|
||||
departement: str = "NA"
|
||||
libelle: str | None
|
||||
objectifs: str | None
|
||||
programme: str | None
|
||||
acquisition_competences: str | None
|
||||
acquisition_notions: str | None
|
||||
langue: str
|
||||
code_langue: str
|
||||
label: str | None = Field(alias="libelle")
|
||||
syllabus: list[SyllabusItemSchema] = Field(alias="listeSaisieSyllabus")
|
||||
lang: str = Field(None, validation_alias=AliasPath("langueEnseignement", "code"))
|
||||
credits_ects: int
|
||||
activites: list[WorkloadSchema]
|
||||
activites: list[WorkloadSchema] = Field(alias="listeActivite")
|
||||
respo_automne: str | None = Field(
|
||||
None, validation_alias=AliasPath("automne", "responsable")
|
||||
)
|
||||
|
||||
+78
-36
@@ -5,8 +5,16 @@ from typing import Iterator
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.utils.functional import cached_property
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from pedagogy.schemas import ShortUeList, UeSchema, UtbmFullUeSchema, UtbmShortUeSchema
|
||||
from pedagogy.schemas import (
|
||||
FormationSchema,
|
||||
UeSchema,
|
||||
UtbmFullUeSchema,
|
||||
UtbmShortUeSchema,
|
||||
)
|
||||
|
||||
FormationListSchema = TypeAdapter(list[FormationSchema])
|
||||
|
||||
|
||||
class UtbmApiClient(requests.Session):
|
||||
@@ -18,28 +26,44 @@ class UtbmApiClient(requests.Session):
|
||||
@cached_property
|
||||
def current_year(self) -> int:
|
||||
"""Fetch from the API the latest existing year"""
|
||||
url = f"{self.BASE_URL}/guides/fr"
|
||||
response = self.get(url)
|
||||
return response.json()[-1]["annee"]
|
||||
url = f"{self.BASE_URL}/guide/"
|
||||
response = self.get(url, {"langue": "fr"})
|
||||
return max(i["annee"] for i in response.json())
|
||||
|
||||
def fetch_short_ues(
|
||||
self, lang: str = "fr", year: int | None = None
|
||||
) -> list[UtbmShortUeSchema]:
|
||||
@cached_property
|
||||
def formations(self) -> list[FormationSchema]:
|
||||
response = self.get(
|
||||
f"{self.BASE_URL}/formation/",
|
||||
{"langue": "fr", "annee_univ": self.current_year, "typeFormation": "ING"},
|
||||
)
|
||||
return FormationListSchema.validate_json(response.text, by_alias=True)
|
||||
|
||||
def fetch_short_ues(self, year: int | None = None) -> list[UtbmShortUeSchema]:
|
||||
"""Get the list of UEs in their short format from the UTBM API"""
|
||||
if year is None:
|
||||
year = self.current_year
|
||||
if lang not in self._cache["short_ues"]:
|
||||
self._cache["short_ues"][lang] = {}
|
||||
if year not in self._cache["short_ues"][lang]:
|
||||
url = f"{self.BASE_URL}/uvs/{lang}/{year}"
|
||||
response = self.get(url)
|
||||
ues = ShortUeList.validate_json(response.content)
|
||||
self._cache["short_ues"][lang][year] = ues
|
||||
return self._cache["short_ues"][lang][year]
|
||||
if year not in self._cache["short_ues"]:
|
||||
ues = []
|
||||
for formation in self.formations:
|
||||
response = self.get(
|
||||
f"{self.BASE_URL}/ue/",
|
||||
{
|
||||
"langue": "fr",
|
||||
"annee_univ": year,
|
||||
"codeFormation": formation.code,
|
||||
},
|
||||
)
|
||||
ues.extend(
|
||||
[
|
||||
UtbmShortUeSchema.model_validate({**ue, "formation": formation})
|
||||
for ue in response.json()
|
||||
if ue["codeCategorie"] is not None
|
||||
]
|
||||
)
|
||||
self._cache["short_ues"][year] = ues
|
||||
return self._cache["short_ues"][year]
|
||||
|
||||
def fetch_ues(
|
||||
self, lang: str = "fr", year: int | None = None
|
||||
) -> Iterator[UeSchema]:
|
||||
def fetch_ues(self, year: int | None = None) -> Iterator[UeSchema]:
|
||||
"""Fetch all UEs from the UTBM API, parsed in a format that we can use.
|
||||
|
||||
Warning:
|
||||
@@ -52,7 +76,7 @@ class UtbmApiClient(requests.Session):
|
||||
"""
|
||||
if year is None:
|
||||
year = self.current_year
|
||||
shorts_ues = self.fetch_short_ues(lang, year)
|
||||
shorts_ues = self.fetch_short_ues(year)
|
||||
# When UEs 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 UE to multiple formations,
|
||||
@@ -65,27 +89,38 @@ class UtbmApiClient(requests.Session):
|
||||
if ue.code not in unique_short_ues:
|
||||
unique_short_ues[ue.code] = ue
|
||||
for ue in unique_short_ues.values():
|
||||
ue_url = f"{self.BASE_URL}/uv/{lang}/{year}/{ue.code}/{ue.code_formation}"
|
||||
response = requests.get(ue_url)
|
||||
response = requests.get(
|
||||
f"{self.BASE_URL}/ue/{ue.code}",
|
||||
{
|
||||
"langue": ue.lang,
|
||||
"annee_univ": year,
|
||||
"codeFormation": ue.formation.code,
|
||||
},
|
||||
)
|
||||
full_ue = UtbmFullUeSchema.model_validate_json(response.content)
|
||||
yield make_clean_ue(ue, full_ue)
|
||||
|
||||
def find_uu(self, lang: str, code: str, year: int | None = None) -> UeSchema | None:
|
||||
"""Find an UE from the UTBM API."""
|
||||
# query the UE list
|
||||
def find_ue(self, code: str, year: int | None = None) -> UeSchema | None:
|
||||
"""Find a UE from the UTBM API."""
|
||||
if not year:
|
||||
year = self.current_year
|
||||
# the UTBM API has no way to fetch a single short ue,
|
||||
# and short ues contain infos that we need and are not
|
||||
# in the full ue schema, so we must fetch everything.
|
||||
short_ues = self.fetch_short_ues(lang, year)
|
||||
short_ues = self.fetch_short_ues(year)
|
||||
short_ue = next((ue for ue in short_ues if ue.code == code), None)
|
||||
if short_ue is None:
|
||||
return None
|
||||
|
||||
# get detailed information about the UE
|
||||
ue_url = f"{self.BASE_URL}/uv/{lang}/{year}/{code}/{short_ue.code_formation}"
|
||||
response = requests.get(ue_url)
|
||||
response = requests.get(
|
||||
f"{self.BASE_URL}/ue/{code}",
|
||||
{
|
||||
"langue": "fr",
|
||||
"annee_univ": year,
|
||||
"codeFormation": short_ue.formation.code,
|
||||
},
|
||||
)
|
||||
full_ue = UtbmFullUeSchema.model_validate_json(response.content)
|
||||
return make_clean_ue(short_ue, full_ue)
|
||||
|
||||
@@ -112,9 +147,9 @@ def make_clean_ue(short_ue: UtbmShortUeSchema, full_ue: UtbmFullUeSchema) -> UeS
|
||||
"ED": "EDIM",
|
||||
"AI": "GI",
|
||||
"AM": "MC",
|
||||
}.get(short_ue.code_formation, "NA")
|
||||
}.get(short_ue.formation.code, "NA")
|
||||
|
||||
match short_ue.ouvert_printemps, short_ue.ouvert_automne:
|
||||
match short_ue.open_spring, short_ue.open_autumn:
|
||||
case True, True:
|
||||
semester = "AUTUMN_AND_SPRING"
|
||||
case True, False:
|
||||
@@ -125,11 +160,11 @@ def make_clean_ue(short_ue: UtbmShortUeSchema, full_ue: UtbmFullUeSchema) -> UeS
|
||||
semester = "CLOSED"
|
||||
|
||||
return UeSchema(
|
||||
title=full_ue.libelle or "",
|
||||
title=full_ue.label or "",
|
||||
code=full_ue.code,
|
||||
credit_type=short_ue.code_categorie or "FREE",
|
||||
credit_type=short_ue.code or "FREE",
|
||||
semester=semester,
|
||||
language=short_ue.code_langue.upper(),
|
||||
language=short_ue.lang.upper(),
|
||||
credits=full_ue.credits_ects,
|
||||
department=department,
|
||||
hours_THE=next((i.nbh for i in full_ue.activites if i.code == "THE"), 0) // 60,
|
||||
@@ -138,8 +173,15 @@ def make_clean_ue(short_ue: UtbmShortUeSchema, full_ue: UtbmFullUeSchema) -> UeS
|
||||
hours_TE=next((i.nbh for i in full_ue.activites if i.code == "TE"), 0) // 60,
|
||||
hours_CM=next((i.nbh for i in full_ue.activites if i.code == "CM"), 0) // 60,
|
||||
manager=full_ue.respo_automne or full_ue.respo_printemps or "",
|
||||
objectives=full_ue.objectifs or "",
|
||||
program=full_ue.programme or "",
|
||||
skills=full_ue.acquisition_competences or "",
|
||||
key_concepts=full_ue.acquisition_notions or "",
|
||||
objectives=next(
|
||||
(i.value for i in full_ue.syllabus if i.label == "Objectifs"), ""
|
||||
),
|
||||
program=next((i.value for i in full_ue.syllabus if i.label == "Programme"), ""),
|
||||
skills=next(
|
||||
(i.value for i in full_ue.syllabus if i.label == "Notions-clefs"), ""
|
||||
),
|
||||
key_concepts=next(
|
||||
(i.value for i in full_ue.syllabus if i.label == "Acquis d'apprentissage"),
|
||||
"",
|
||||
),
|
||||
)
|
||||
|
||||
+1
-1
@@ -493,7 +493,7 @@ SITH_LOG_OPERATION_TYPE = [
|
||||
("REFILLING_DELETION", _("Refilling deletion")),
|
||||
]
|
||||
|
||||
SITH_PEDAGOGY_UTBM_API = "https://extranet1.utbm.fr/gpedago/api/guide"
|
||||
SITH_PEDAGOGY_UTBM_API = "https://api.utbm.fr/offre-formation/public"
|
||||
|
||||
# Defines pagination for cash summary
|
||||
SITH_COUNTER_CASH_SUMMARY_LENGTH = 50
|
||||
|
||||
Reference in New Issue
Block a user