forked from ScoDoc/ScoDoc
WIP: tableau de bord utilisateur
This commit is contained in:
parent
a6448192a6
commit
ae94d8fba4
3
.gitignore
vendored
3
.gitignore
vendored
@ -176,3 +176,6 @@ copy
|
|||||||
|
|
||||||
# Symlinks static ScoDoc
|
# Symlinks static ScoDoc
|
||||||
app/static/links/[0-9]*.*[0-9]
|
app/static/links/[0-9]*.*[0-9]
|
||||||
|
|
||||||
|
# Essais locaux
|
||||||
|
xp/
|
||||||
|
@ -10,8 +10,10 @@
|
|||||||
|
|
||||||
"""ScoDoc models: formsemestre
|
"""ScoDoc models: formsemestre
|
||||||
"""
|
"""
|
||||||
|
from collections import defaultdict
|
||||||
import datetime
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from itertools import chain
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
@ -35,7 +37,11 @@ from app.models.etudiants import Identite
|
|||||||
from app.models.evaluations import Evaluation
|
from app.models.evaluations import Evaluation
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.groups import GroupDescr, Partition
|
from app.models.groups import GroupDescr, Partition
|
||||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
from app.models.moduleimpls import (
|
||||||
|
ModuleImpl,
|
||||||
|
ModuleImplInscription,
|
||||||
|
notes_modules_enseignants,
|
||||||
|
)
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
from app.models.validations import ScolarFormSemestreValidation
|
||||||
@ -45,8 +51,6 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
|
|
||||||
from app.scodoc.sco_utils import translate_assiduites_metric
|
|
||||||
|
|
||||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||||
|
|
||||||
|
|
||||||
@ -380,6 +384,73 @@ class FormSemestre(db.Model):
|
|||||||
_cache[key] = ues
|
_cache[key] = ues
|
||||||
return ues
|
return ues
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_user_formsemestres_annee(
|
||||||
|
cls, user: User
|
||||||
|
) -> tuple[list["FormSemestre"], defaultdict[int, list[ModuleImpl]]]:
|
||||||
|
"""Liste des formsemestres de l'année scolaire
|
||||||
|
dans lesquels user intervient (comme resp., resp. de module ou enseignant),
|
||||||
|
ainsi que la liste des modimpls concernés dans chaque formsemestre
|
||||||
|
Attention: les semestres et modimpls peuvent être de différents départements !
|
||||||
|
"""
|
||||||
|
debut_annee_scolaire = scu.date_debut_annee_scolaire()
|
||||||
|
fin_annee_scolaire = scu.date_fin_annee_scolaire()
|
||||||
|
|
||||||
|
query = FormSemestre.query.filter(
|
||||||
|
FormSemestre.date_fin >= debut_annee_scolaire,
|
||||||
|
FormSemestre.date_debut < fin_annee_scolaire,
|
||||||
|
)
|
||||||
|
# responsable ?
|
||||||
|
formsemestres_resp = query.join(notes_formsemestre_responsables).filter_by(
|
||||||
|
responsable_id=user.id
|
||||||
|
)
|
||||||
|
# Responsable d'un modimpl ?
|
||||||
|
modimpls_resp = (
|
||||||
|
ModuleImpl.query.filter_by(responsable_id=user.id)
|
||||||
|
.join(FormSemestre)
|
||||||
|
.filter(
|
||||||
|
FormSemestre.date_fin >= debut_annee_scolaire,
|
||||||
|
FormSemestre.date_debut < fin_annee_scolaire,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Enseignant dans un modimpl ?
|
||||||
|
modimpls_ens = (
|
||||||
|
ModuleImpl.query.join(notes_modules_enseignants)
|
||||||
|
.filter_by(ens_id=user.id)
|
||||||
|
.join(FormSemestre)
|
||||||
|
.filter(
|
||||||
|
FormSemestre.date_fin >= debut_annee_scolaire,
|
||||||
|
FormSemestre.date_debut < fin_annee_scolaire,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Liste les modimpls, uniques
|
||||||
|
modimpls = modimpls_resp.all()
|
||||||
|
ids = {modimpl.id for modimpl in modimpls}
|
||||||
|
for modimpl in modimpls_ens:
|
||||||
|
if modimpl.id not in ids:
|
||||||
|
modimpls.append(modimpl)
|
||||||
|
ids.add(modimpl.id)
|
||||||
|
# Liste les formsemestres et modimpls associés
|
||||||
|
modimpls_by_formsemestre = defaultdict(lambda: [])
|
||||||
|
formsemestres = formsemestres_resp.all()
|
||||||
|
ids = {formsemestre.id for formsemestre in formsemestres}
|
||||||
|
for modimpl in chain(modimpls_resp, modimpls_ens):
|
||||||
|
if modimpl.formsemestre_id not in ids:
|
||||||
|
formsemestres.append(modimpl.formsemestre)
|
||||||
|
ids.add(modimpl.formsemestre_id)
|
||||||
|
modimpls_by_formsemestre[modimpl.formsemestre_id].append(modimpl)
|
||||||
|
# Tris
|
||||||
|
formsemestres.sort(key=lambda x: (x.departement.acronym,) + x.sort_key())
|
||||||
|
for formsemestre in formsemestres:
|
||||||
|
modimpls = modimpls_by_formsemestre[formsemestre.id]
|
||||||
|
if formsemestre.formation.is_apc():
|
||||||
|
key = lambda x: x.module.sort_key_apc()
|
||||||
|
else:
|
||||||
|
key = lambda x: x.module.sort_key()
|
||||||
|
modimpls.sort(key=key)
|
||||||
|
|
||||||
|
return formsemestres, modimpls_by_formsemestre
|
||||||
|
|
||||||
def get_evaluations(self) -> list[Evaluation]:
|
def get_evaluations(self) -> list[Evaluation]:
|
||||||
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
||||||
return (
|
return (
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
|
@ -160,6 +160,10 @@ class Module(db.Model):
|
|||||||
"Identifiant du module à afficher : abbrev ou titre ou code"
|
"Identifiant du module à afficher : abbrev ou titre ou code"
|
||||||
return self.abbrev or self.titre or self.code
|
return self.abbrev or self.titre or self.code
|
||||||
|
|
||||||
|
def sort_key(self) -> tuple:
|
||||||
|
"""Clé de tri pour formations classiques"""
|
||||||
|
return self.numero or 0, self.code
|
||||||
|
|
||||||
def sort_key_apc(self) -> tuple:
|
def sort_key_apc(self) -> tuple:
|
||||||
"""Clé de tri pour avoir
|
"""Clé de tri pour avoir
|
||||||
présentation par type (res, sae), parcours, type, numéro
|
présentation par type (res, sae), parcours, type, numéro
|
||||||
|
@ -27,11 +27,12 @@
|
|||||||
|
|
||||||
"""Accès aux emplois du temps
|
"""Accès aux emplois du temps
|
||||||
|
|
||||||
XXX usage uniquement experimental pour tests implémentations
|
Lecture et conversion des ics.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import icalendar
|
import icalendar
|
||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
@ -135,6 +136,7 @@ def formsemestre_edt_dict(
|
|||||||
toujours présents.
|
toujours présents.
|
||||||
TODO: spécifier intervalle de dates start et end
|
TODO: spécifier intervalle de dates start et end
|
||||||
"""
|
"""
|
||||||
|
t0 = time.time()
|
||||||
group_ids_set = set(group_ids) if group_ids else set()
|
group_ids_set = set(group_ids) if group_ids else set()
|
||||||
try:
|
try:
|
||||||
events_scodoc, _ = load_and_convert_ics(formsemestre)
|
events_scodoc, _ = load_and_convert_ics(formsemestre)
|
||||||
@ -227,7 +229,9 @@ def formsemestre_edt_dict(
|
|||||||
"moduleimpl_id": modimpl.id if modimpl else None,
|
"moduleimpl_id": modimpl.id if modimpl else None,
|
||||||
}
|
}
|
||||||
events_cal.append(d)
|
events_cal.append(d)
|
||||||
|
log(
|
||||||
|
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
|
||||||
|
)
|
||||||
return events_cal
|
return events_cal
|
||||||
|
|
||||||
|
|
||||||
@ -399,56 +403,6 @@ def extract_event_data(
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
# def extract_event_title(event: icalendar.cal.Event) -> str:
|
|
||||||
# """Extrait le titre à afficher dans nos calendriers.
|
|
||||||
# En effet, le titre présent dans l'ics emploi du temps est souvent complexe et peu parlant.
|
|
||||||
# Par exemple, à l'USPN, Hyperplanning nous donne:
|
|
||||||
# 'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
|
|
||||||
# """
|
|
||||||
# # TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
|
||||||
# if not event.has_key("DESCRIPTION"):
|
|
||||||
# return "-"
|
|
||||||
# description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
|
||||||
# # ici on prend le nom du module
|
|
||||||
# m = re.search(r"Matière : \w+ - ([\w\.\s']+)", description)
|
|
||||||
# if m and len(m.groups()) > 0:
|
|
||||||
# return m.group(1)
|
|
||||||
# # fallback: full description
|
|
||||||
# return description
|
|
||||||
|
|
||||||
|
|
||||||
# def extract_event_module(event: icalendar.cal.Event) -> str:
|
|
||||||
# """Extrait le code module de l'emplois du temps.
|
|
||||||
# Chaine vide si ne le trouve pas.
|
|
||||||
# Par exemple, à l'USPN, Hyperplanning nous donne le code 'VRETR113' dans DESCRIPTION
|
|
||||||
# 'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
|
|
||||||
# """
|
|
||||||
# # TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
|
||||||
# if not event.has_key("DESCRIPTION"):
|
|
||||||
# return "-"
|
|
||||||
# description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
|
||||||
# # extraction du code:
|
|
||||||
# m = re.search(r"Matière : ([A-Z][A-Z0-9]+)", description)
|
|
||||||
# if m and len(m.groups()) > 0:
|
|
||||||
# return m.group(1)
|
|
||||||
# return ""
|
|
||||||
|
|
||||||
|
|
||||||
# def extract_event_group(event: icalendar.cal.Event) -> str:
|
|
||||||
# """Extrait le nom du groupe (TD, ...). "" si pas de match."""
|
|
||||||
# # Utilise ici le SUMMARY
|
|
||||||
# # qui est de la forme
|
|
||||||
# # SUMMARY;LANGUAGE=fr:TP2 GPR1 - VCYR303 - Services reseaux ava (VCYR303) - 1234 - M. VIENNET EMMANUEL - V2ROM - BUT2 RT pa. ROM - Groupe 1
|
|
||||||
# if not event.has_key("SUMMARY"):
|
|
||||||
# return "-"
|
|
||||||
# summary = event.decoded("SUMMARY").decode("utf-8") # assume ics in utf8
|
|
||||||
# # extraction du code:
|
|
||||||
# m = re.search(r".*- ([\w\s]+)$", summary)
|
|
||||||
# if m and len(m.groups()) > 0:
|
|
||||||
# return m.group(1).strip()
|
|
||||||
# return ""
|
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_retreive_modimpls_from_edt_id(
|
def formsemestre_retreive_modimpls_from_edt_id(
|
||||||
formsemestre: FormSemestre,
|
formsemestre: FormSemestre,
|
||||||
) -> dict[str, ModuleImpl]:
|
) -> dict[str, ModuleImpl]:
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
--sco-content-max-width: 1024px;
|
--sco-content-max-width: 1024px;
|
||||||
--sco-color-explication: rgb(10, 58, 140);
|
--sco-color-explication: rgb(10, 58, 140);
|
||||||
--sco-color-background: rgb(242, 242, 238);
|
--sco-color-background: rgb(242, 242, 238);
|
||||||
|
--sco-color-mod-std: #afafc2;
|
||||||
|
--sco-color-ressources: #f8c844;
|
||||||
|
--sco-color-saes: #c6ffab;
|
||||||
|
--sco-color-ues: #0051a9;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@ -2421,15 +2425,15 @@ div.formation_list_modules {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.formation_list_modules_RESSOURCE {
|
div.formation_list_modules_RESSOURCE {
|
||||||
background-color: #f8c844;
|
background-color: var(--sco-color-ressources);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.formation_list_modules_SAE {
|
div.formation_list_modules_SAE {
|
||||||
background-color: #c6ffab;
|
background-color: var(--sco-color-saes);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.formation_list_modules_STANDARD {
|
div.formation_list_modules_STANDARD {
|
||||||
background-color: #afafc2;
|
background-color: var(--sco-color-mod-std);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.formation_list_modules_titre {
|
div.formation_list_modules_titre {
|
||||||
|
96
app/templates/user_board/user_board.j2
Normal file
96
app/templates/user_board/user_board.j2
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{# Tableau de bord utilisateur #}
|
||||||
|
|
||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<style>
|
||||||
|
.ub-formsemestres {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.ub-formsemestre {
|
||||||
|
flex-basis: 256px; /* largeur boite par défaut */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: #fdcf52;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
min-width: 128px;
|
||||||
|
max-width: fit-content;
|
||||||
|
--color-links: rgb(153, 51, 51);
|
||||||
|
}
|
||||||
|
.ub-sem-titre {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 110%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.ub-sem-titre a.stdlink, .ub-sem-titre a.stdlink:visited {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.ub-sem-titre a.stdlink:hover {
|
||||||
|
color: var(--color-links);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.ub-sem-modimpls {
|
||||||
|
background-color: rgb(210, 210, 210);
|
||||||
|
border-bottom-left-radius: 18px;
|
||||||
|
border-bottom-right-radius: 18px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.ub-formsemestre .code {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ub-formsemestre a.formsemestre_status_link:hover {
|
||||||
|
color: var(--color-links);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.ub-formsemestre a.formsemestre_status_link, .ub-formsemestre a.formsemestre_status_link:visited{
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<h2>Votre tableau de bord, {{user.get_nomcomplet()}}</h2>
|
||||||
|
|
||||||
|
<h3>Vos modules</h3>
|
||||||
|
<div class="ub-formsemestres">
|
||||||
|
{% for formsemestre in formsemestres %}
|
||||||
|
<div class="ub-formsemestre">
|
||||||
|
<div class="ub-sem-titre">{{formsemestre.html_link_status()|safe}}</div>
|
||||||
|
<div class="ub-sem-modimpls">
|
||||||
|
{% for modimpl in modimpls_by_formsemestre[formsemestre.id] %}
|
||||||
|
<div>
|
||||||
|
<span class="code"><a class="stdlink" href="{{
|
||||||
|
url_for('notes.moduleimpl_status',
|
||||||
|
scodoc_dept=formsemestre.departement.acronym, moduleimpl_id=modimpl.id)
|
||||||
|
}}">{{modimpl.module.code}}</a>
|
||||||
|
</span>
|
||||||
|
<span><a class="formsemestre_status_link" href="{{
|
||||||
|
url_for('notes.moduleimpl_status',
|
||||||
|
scodoc_dept=formsemestre.departement.acronym, moduleimpl_id=modimpl.id)
|
||||||
|
}}">{{modimpl.module.titre_str()}}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty">pas de modules</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock app_content %}
|
@ -124,4 +124,5 @@ from app.views import (
|
|||||||
scodoc,
|
scodoc,
|
||||||
scolar,
|
scolar,
|
||||||
users,
|
users,
|
||||||
|
user_board,
|
||||||
)
|
)
|
||||||
|
39
app/views/user_board.py
Normal file
39
app/views/user_board.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
Tableau de bord utilisateur
|
||||||
|
|
||||||
|
Emmanuel Viennet, 2023
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from flask import flash, redirect, render_template, url_for
|
||||||
|
from flask import g, request
|
||||||
|
from app.auth.models import User
|
||||||
|
from app.decorators import (
|
||||||
|
scodoc,
|
||||||
|
permission_required,
|
||||||
|
)
|
||||||
|
from app.models import FormSemestre
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
from app.views import scolar_bp as bp
|
||||||
|
from app.views import ScoData
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/user_board/<string:user_name>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def user_board(user_name: str):
|
||||||
|
"""Tableau de bord utilisateur: liens vers ses objets"""
|
||||||
|
user = User.query.filter_by(user_name=user_name).first_or_404()
|
||||||
|
formsemestres, modimpls_by_formsemestre = FormSemestre.get_user_formsemestres_annee(
|
||||||
|
user
|
||||||
|
)
|
||||||
|
# TODO: le calendrier avec ses enseignements
|
||||||
|
return render_template(
|
||||||
|
"user_board/user_board.j2",
|
||||||
|
formsemestres=formsemestres,
|
||||||
|
modimpls_by_formsemestre=modimpls_by_formsemestre,
|
||||||
|
sco=ScoData(),
|
||||||
|
title=f"{user.get_prenomnom()}: tableau de bord",
|
||||||
|
user=user,
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user