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
|
||||
app/static/links/[0-9]*.*[0-9]
|
||||
|
||||
# Essais locaux
|
||||
xp/
|
||||
|
@ -10,8 +10,10 @@
|
||||
|
||||
"""ScoDoc models: formsemestre
|
||||
"""
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
|
||||
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.formations import Formation
|
||||
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.ues import UniteEns
|
||||
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_vdi import ApoEtapeVDI
|
||||
|
||||
from app.scodoc.sco_utils import translate_assiduites_metric
|
||||
|
||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||
|
||||
|
||||
@ -380,6 +384,73 @@ class FormSemestre(db.Model):
|
||||
_cache[key] = 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]:
|
||||
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
||||
return (
|
||||
|
@ -3,7 +3,6 @@
|
||||
"""
|
||||
import pandas as pd
|
||||
from flask_sqlalchemy.query import Query
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
|
@ -160,6 +160,10 @@ class Module(db.Model):
|
||||
"Identifiant du module à afficher : abbrev ou titre ou 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:
|
||||
"""Clé de tri pour avoir
|
||||
présentation par type (res, sae), parcours, type, numéro
|
||||
|
@ -27,11 +27,12 @@
|
||||
|
||||
"""Accès aux emplois du temps
|
||||
|
||||
XXX usage uniquement experimental pour tests implémentations
|
||||
Lecture et conversion des ics.
|
||||
|
||||
"""
|
||||
from datetime import timezone
|
||||
import re
|
||||
import time
|
||||
import icalendar
|
||||
|
||||
from flask import g, url_for
|
||||
@ -135,6 +136,7 @@ def formsemestre_edt_dict(
|
||||
toujours présents.
|
||||
TODO: spécifier intervalle de dates start et end
|
||||
"""
|
||||
t0 = time.time()
|
||||
group_ids_set = set(group_ids) if group_ids else set()
|
||||
try:
|
||||
events_scodoc, _ = load_and_convert_ics(formsemestre)
|
||||
@ -227,7 +229,9 @@ def formsemestre_edt_dict(
|
||||
"moduleimpl_id": modimpl.id if modimpl else None,
|
||||
}
|
||||
events_cal.append(d)
|
||||
|
||||
log(
|
||||
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
|
||||
)
|
||||
return events_cal
|
||||
|
||||
|
||||
@ -399,56 +403,6 @@ def extract_event_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(
|
||||
formsemestre: FormSemestre,
|
||||
) -> dict[str, ModuleImpl]:
|
||||
|
@ -6,6 +6,10 @@
|
||||
--sco-content-max-width: 1024px;
|
||||
--sco-color-explication: rgb(10, 58, 140);
|
||||
--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,
|
||||
@ -2421,15 +2425,15 @@ div.formation_list_modules {
|
||||
}
|
||||
|
||||
div.formation_list_modules_RESSOURCE {
|
||||
background-color: #f8c844;
|
||||
background-color: var(--sco-color-ressources);
|
||||
}
|
||||
|
||||
div.formation_list_modules_SAE {
|
||||
background-color: #c6ffab;
|
||||
background-color: var(--sco-color-saes);
|
||||
}
|
||||
|
||||
div.formation_list_modules_STANDARD {
|
||||
background-color: #afafc2;
|
||||
background-color: var(--sco-color-mod-std);
|
||||
}
|
||||
|
||||
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,
|
||||
scolar,
|
||||
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