Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc
This commit is contained in:
commit
224bb2d281
31
README.md
31
README.md
|
@ -18,15 +18,12 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
|||
|
||||
|
||||
|
||||
### État actuel (26 sept 21)
|
||||
### État actuel (4 dec 21)
|
||||
|
||||
- 9.0 reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
||||
- 9.0 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
||||
- ancien module "Entreprises" (obsolète)
|
||||
|
||||
**Fonctionnalités non intégrées:**
|
||||
|
||||
- génération LaTeX des avis de poursuite d'études
|
||||
|
||||
- ancien module "Entreprises" (obsolete)
|
||||
- 9.1 (branche "PNBUT") est la version de développement.
|
||||
|
||||
|
||||
### Lignes de commandes
|
||||
|
@ -42,7 +39,7 @@ les fichiers locaux (archives, photos, configurations, logs) sous
|
|||
postgresql et la configuration du système Linux.
|
||||
|
||||
### Fichiers locaux
|
||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartienant à l'utilisateur `scodoc`.
|
||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
|
||||
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
|
||||
`/opt/scodoc-data/config`.
|
||||
|
||||
|
@ -89,11 +86,22 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
|||
|
||||
### Tests unitaires
|
||||
|
||||
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||
Avant le premier lancement, créer cette base ainsi:
|
||||
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
|
||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
migrations (changements de schéma) ont eu lieu dans le code.
|
||||
|
||||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
|
||||
flask sco-delete-dept TEST00 && flask sco-create-dept TEST00
|
||||
flask delete-dept TEST00 && flask create-dept TEST00
|
||||
|
||||
Puis dérouler les tests unitaires:
|
||||
|
||||
|
@ -109,7 +117,8 @@ On peut aussi utiliser les tests unitaires pour mettre la base
|
|||
de données de développement dans un état connu, par exemple pour éviter de
|
||||
recréer à la main étudiants et semestres quand on développe.
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée par les tests:
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD
|
||||
utilisée par les tests:
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
|
@ -157,7 +166,7 @@ Sur une machine de DEV, lancer
|
|||
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
|
||||
le fichier `.prof` sera alors écrit dans `/opt/scoidoc-data` (on peut aussi utiliser `/tmp`).
|
||||
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
||||
|
||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||
|
||||
|
|
|
@ -357,7 +357,7 @@ def sco_db_insert_constants():
|
|||
|
||||
current_app.logger.info("Init Sco db")
|
||||
# Modalités:
|
||||
models.NotesFormModalite.insert_modalites()
|
||||
models.FormationModalite.insert_modalites()
|
||||
|
||||
|
||||
def initialize_scodoc_database(erase=False, create_all=False):
|
||||
|
|
|
@ -2,7 +2,25 @@
|
|||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
bp = Blueprint("api", __name__)
|
||||
|
||||
|
||||
def requested_format(default_format="json", allowed_formats=None):
|
||||
"""Extract required format from query string.
|
||||
* default value is json. A list of allowed formats may be provided
|
||||
(['json'] considered if not provided).
|
||||
* if the required format is not in allowed list, returns None.
|
||||
|
||||
NB: if json in not in allowed_formats, format specification is mandatory.
|
||||
"""
|
||||
format_type = request.args.get("format", default_format)
|
||||
if format_type in (allowed_formats or ["json"]):
|
||||
return format_type
|
||||
return None
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
from app.api import sco_api
|
||||
from app.api import logos
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
# Authentication code borrowed from Miguel Grinberg's Mega Tutorial
|
||||
# (see https://github.com/miguelgrinberg/microblog)
|
||||
# and modified for ScoDoc
|
||||
|
||||
# Under The MIT License (MIT)
|
||||
|
||||
|
@ -23,6 +24,7 @@
|
|||
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from flask import g
|
||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||
from app.auth.models import User
|
||||
from app.api.errors import error_response
|
||||
|
@ -35,6 +37,7 @@ token_auth = HTTPTokenAuth()
|
|||
def verify_password(username, password):
|
||||
user = User.query.filter_by(user_name=username).first()
|
||||
if user and user.check_password(password):
|
||||
g.current_user = user
|
||||
return user
|
||||
|
||||
|
||||
|
@ -45,9 +48,30 @@ def basic_auth_error(status):
|
|||
|
||||
@token_auth.verify_token
|
||||
def verify_token(token):
|
||||
return User.check_token(token) if token else None
|
||||
user = User.check_token(token) if token else None
|
||||
g.current_user = user
|
||||
return user
|
||||
|
||||
|
||||
@token_auth.error_handler
|
||||
def token_auth_error(status):
|
||||
return error_response(status)
|
||||
|
||||
|
||||
@token_auth.get_user_roles
|
||||
def get_user_roles(user):
|
||||
return user.roles
|
||||
|
||||
|
||||
# def token_permission_required(permission):
|
||||
# def decorator(f):
|
||||
# @wraps(f)
|
||||
# def decorated_function(*args, **kwargs):
|
||||
# scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
# if not current_user.has_permission(permission, scodoc_dept):
|
||||
# abort(403)
|
||||
# return f(*args, **kwargs)
|
||||
|
||||
# return login_required(decorated_function)
|
||||
|
||||
# return decorator
|
||||
|
|
|
@ -38,19 +38,43 @@
|
|||
# Scolarite/Notes/groups_view
|
||||
# Scolarite/Notes/moduleimpl_status
|
||||
# Scolarite/setGroups
|
||||
from datetime import datetime
|
||||
|
||||
from flask import jsonify, request, url_for, abort
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from flask import jsonify, request, g, send_file
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app import db, log
|
||||
from app.api import bp, requested_format
|
||||
from app.api.auth import token_auth
|
||||
from app.api.errors import bad_request
|
||||
|
||||
from app.api.errors import error_response
|
||||
from app import models
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/ScoDoc/api/list_depts", methods=["GET"])
|
||||
@bp.route("list_depts", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def list_depts():
|
||||
depts = models.Departement.query.filter_by(visible=True).all()
|
||||
data = {"items": [d.to_dict() for d in depts]}
|
||||
data = [d.to_dict() for d in depts]
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/etudiants/courant", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def etudiants():
|
||||
"""Liste de tous les étudiants actuellement inscrits à un semestre
|
||||
en cours.
|
||||
"""
|
||||
# Vérification de l'accès: permission Observateir sur tous les départements
|
||||
# (c'est un exemple à compléter)
|
||||
if not g.current_user.has_permission(Permission.ScoObservateur, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
|
||||
query = db.session.query(Identite).filter(
|
||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
FormSemestreInscription.etudid == Identite.id,
|
||||
FormSemestre.date_debut <= func.now(),
|
||||
FormSemestre.date_fin >= func.now(),
|
||||
)
|
||||
return jsonify([e.to_dict_bul(include_urls=False) for e in query])
|
||||
|
|
|
@ -213,6 +213,9 @@ class User(UserMixin, db.Model):
|
|||
|
||||
@staticmethod
|
||||
def check_token(token):
|
||||
"""Retreive user for given token, chek token's validity
|
||||
and returns the user object.
|
||||
"""
|
||||
user = User.query.filter_by(token=token).first()
|
||||
if user is None or user.token_expiration < datetime.utcnow():
|
||||
return None
|
||||
|
@ -269,7 +272,7 @@ class User(UserMixin, db.Model):
|
|||
"""string repr. of user's roles (with depts)
|
||||
e.g. "Ens_RT, Ens_Info, Secr_CJ"
|
||||
"""
|
||||
return ",".join(f"{r.role.name}_{r.dept or ''}" for r in self.user_roles)
|
||||
return ",".join(f"{r.role.name or ''}_{r.dept or ''}" for r in self.user_roles)
|
||||
|
||||
def is_administrator(self):
|
||||
"True if i'm an active SuperAdmin"
|
||||
|
|
|
@ -0,0 +1,422 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
from flask import url_for, g
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_cache import ResultatsSemestreBUTCache
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_utils import jsnan, fmt_note
|
||||
|
||||
|
||||
class ResultatsSemestreBUT:
|
||||
"""Structure légère pour stocker les résultats du semestre et
|
||||
générer les bulletins.
|
||||
__init__ : charge depuis le cache ou calcule
|
||||
"""
|
||||
|
||||
_cached_attrs = (
|
||||
"sem_cube",
|
||||
"modimpl_inscr_df",
|
||||
"modimpl_coefs_df",
|
||||
"etud_moy_ue",
|
||||
"modimpls_evals_poids",
|
||||
"modimpls_evals_notes",
|
||||
"etud_moy_gen",
|
||||
"etud_moy_gen_ranks",
|
||||
"modimpls_evaluations_complete",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
self.formsemestre = formsemestre
|
||||
self.ues = formsemestre.query_ues().all()
|
||||
self.modimpls = formsemestre.modimpls.all()
|
||||
self.etuds = self.formsemestre.get_inscrits(include_dem=False)
|
||||
self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)}
|
||||
self.saes = [
|
||||
m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
|
||||
]
|
||||
self.ressources = [
|
||||
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
|
||||
]
|
||||
if not self.load_cached():
|
||||
self.compute()
|
||||
self.store()
|
||||
|
||||
def load_cached(self) -> bool:
|
||||
"Load cached dataframes, returns False si pas en cache"
|
||||
data = ResultatsSemestreBUTCache.get(self.formsemestre.id)
|
||||
if not data:
|
||||
return False
|
||||
for attr in self._cached_attrs:
|
||||
setattr(self, attr, data[attr])
|
||||
return True
|
||||
|
||||
def store(self):
|
||||
"Cache our dataframes"
|
||||
ResultatsSemestreBUTCache.set(
|
||||
self.formsemestre.id,
|
||||
{attr: getattr(self, attr) for attr in self._cached_attrs},
|
||||
)
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule toutes les moyennes"
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_evals_notes,
|
||||
modimpls_evaluations,
|
||||
self.modimpls_evaluations_complete,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, ues=self.ues, modimpls=self.modimpls
|
||||
)
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
self.etud_moy_ue = moy_ue.compute_ue_moys(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.modimpls,
|
||||
self.ues,
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs_df,
|
||||
)
|
||||
self.etud_moy_gen = moy_sem.compute_sem_moys(
|
||||
self.etud_moy_ue, self.modimpl_coefs_df
|
||||
)
|
||||
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||
|
||||
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
|
||||
"dict synthèse résultats dans l'UE pour les modules indiqués"
|
||||
d = {}
|
||||
etud_idx = self.etud_index[etud.id]
|
||||
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
|
||||
etud_moy_module = self.sem_cube[etud_idx] # module x UE
|
||||
for mi in modimpls:
|
||||
coef = self.modimpl_coefs_df[mi.id][ue.id]
|
||||
if coef > 0:
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"coef": coef,
|
||||
"moyenne": fmt_note(
|
||||
etud_moy_module[self.modimpl_coefs_df.columns.get_loc(mi.id)][
|
||||
ue_idx
|
||||
]
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_ue_results(self, etud, ue):
|
||||
"dict synthèse résultats UE"
|
||||
d = {
|
||||
"id": ue.id,
|
||||
"numero": ue.numero,
|
||||
"ECTS": {
|
||||
"acquis": 0, # XXX TODO voir jury
|
||||
"total": ue.ects,
|
||||
},
|
||||
"competence": None, # XXX TODO lien avec référentiel
|
||||
"moyenne": {
|
||||
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
|
||||
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
|
||||
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
|
||||
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
|
||||
},
|
||||
"bonus": None, # XXX TODO
|
||||
"malus": None, # XXX TODO voir ce qui est ici
|
||||
"capitalise": None, # "AAAA-MM-JJ" TODO
|
||||
"ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
|
||||
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_mods_results(self, etud, modimpls) -> dict:
|
||||
"""dict synthèse résultats des modules indiqués,
|
||||
avec évaluations de chacun."""
|
||||
d = {}
|
||||
# etud_idx = self.etud_index[etud.id]
|
||||
for mi in modimpls:
|
||||
# mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
|
||||
# # moyennes indicatives (moyennes de moyennes d'UE)
|
||||
# try:
|
||||
# moyennes_etuds = np.nan_to_num(
|
||||
# np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
|
||||
# copy=False,
|
||||
# )
|
||||
# except RuntimeWarning: # all nans in np.nanmean (sur certains etuds sans notes valides)
|
||||
# pass
|
||||
# try:
|
||||
# moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
||||
# except RuntimeWarning: # all nans in np.nanmean
|
||||
# pass
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"titre": mi.module.titre,
|
||||
"code_apogee": mi.module.code_apogee,
|
||||
"url": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=mi.id,
|
||||
),
|
||||
"moyenne": {
|
||||
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
|
||||
# "value": fmt_note(moy_indicative_mod),
|
||||
# "min": fmt_note(moyennes_etuds.min()),
|
||||
# "max": fmt_note(moyennes_etuds.max()),
|
||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": [
|
||||
self.etud_eval_results(etud, e)
|
||||
for eidx, e in enumerate(mi.evaluations)
|
||||
if e.visibulletin
|
||||
and self.modimpls_evaluations_complete[mi.id][eidx]
|
||||
],
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_eval_results(self, etud, e) -> dict:
|
||||
"dict resultats d'un étudiant à une évaluation"
|
||||
eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id] # pd.Series
|
||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||
d = {
|
||||
"id": e.id,
|
||||
"description": e.description,
|
||||
"date": e.jour.isoformat() if e.jour else None,
|
||||
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
|
||||
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
|
||||
"coef": e.coefficient,
|
||||
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
|
||||
"note": {
|
||||
"value": fmt_note(
|
||||
self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min()),
|
||||
"max": fmt_note(notes_ok.max()),
|
||||
"moy": fmt_note(notes_ok.mean()),
|
||||
},
|
||||
"url": url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
def bulletin_etud(self, etud, formsemestre) -> dict:
|
||||
"""Le bulletin de l'étudiant dans ce semestre"""
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
d = {
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"etudiant": etud.to_dict_bul(),
|
||||
"formation": {
|
||||
"id": formsemestre.formation.id,
|
||||
"acronyme": formsemestre.formation.acronyme,
|
||||
"titre_officiel": formsemestre.formation.titre_officiel,
|
||||
"titre": formsemestre.formation.titre,
|
||||
},
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etat_inscription": etat_inscription,
|
||||
"options": bulletin_option_affichage(formsemestre),
|
||||
}
|
||||
semestre_infos = {
|
||||
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
|
||||
"date_debut": formsemestre.date_debut.isoformat(),
|
||||
"date_fin": formsemestre.date_fin.isoformat(),
|
||||
"annee_universitaire": self.formsemestre.annee_scolaire_str(),
|
||||
"inscription": "TODO-MM-JJ", # XXX TODO
|
||||
"numero": formsemestre.semestre_id,
|
||||
"groupes": [], # XXX TODO
|
||||
"absences": { # XXX TODO
|
||||
"injustifie": 1,
|
||||
"total": 33,
|
||||
},
|
||||
}
|
||||
semestre_infos.update(
|
||||
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
||||
)
|
||||
if etat_inscription == scu.INSCRIT:
|
||||
semestre_infos.update(
|
||||
{
|
||||
"notes": { # moyenne des moyennes générales du semestre
|
||||
"value": fmt_note(self.etud_moy_gen[etud.id]),
|
||||
"min": fmt_note(self.etud_moy_gen.min()),
|
||||
"moy": fmt_note(self.etud_moy_gen.mean()),
|
||||
"max": fmt_note(self.etud_moy_gen.max()),
|
||||
},
|
||||
"rang": { # classement wrt moyenne général, indicatif
|
||||
"value": self.etud_moy_gen_ranks[etud.id],
|
||||
"total": len(self.etuds),
|
||||
},
|
||||
},
|
||||
)
|
||||
d.update(
|
||||
{
|
||||
"ressources": self.etud_mods_results(etud, self.ressources),
|
||||
"saes": self.etud_mods_results(etud, self.saes),
|
||||
"ues": {
|
||||
ue.acronyme: self.etud_ue_results(etud, ue) for ue in self.ues
|
||||
},
|
||||
"semestre": semestre_infos,
|
||||
},
|
||||
)
|
||||
else:
|
||||
semestre_infos.update(
|
||||
{
|
||||
"notes": {
|
||||
"value": "DEM",
|
||||
"min": "",
|
||||
"moy": "",
|
||||
"max": "",
|
||||
},
|
||||
"rang": {"value": "DEM", "total": len(self.etuds)},
|
||||
}
|
||||
)
|
||||
d.update(
|
||||
{
|
||||
"semestre": semestre_infos,
|
||||
"ressources": {},
|
||||
"saes": {},
|
||||
"ues": {},
|
||||
}
|
||||
)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def bulletin_option_affichage(formsemestre):
|
||||
"dict avec les options d'affichages (préférences) pour ce semestre"
|
||||
prefs = sco_preferences.SemPreferences(formsemestre.id)
|
||||
fields = (
|
||||
"bul_show_abs",
|
||||
"bul_show_abs_modules",
|
||||
"bul_show_ects",
|
||||
"bul_show_codemodules",
|
||||
"bul_show_matieres",
|
||||
"bul_show_rangs",
|
||||
"bul_show_ue_rangs",
|
||||
"bul_show_mod_rangs",
|
||||
"bul_show_moypromo",
|
||||
"bul_show_minmax",
|
||||
"bul_show_minmax_mod",
|
||||
"bul_show_minmax_eval",
|
||||
"bul_show_coef",
|
||||
"bul_show_ue_cap_details",
|
||||
"bul_show_ue_cap_current",
|
||||
"bul_show_temporary",
|
||||
"bul_temporary_txt",
|
||||
"bul_show_uevalid",
|
||||
"bul_show_date_inscr",
|
||||
)
|
||||
# on enlève le "bul_" de la clé:
|
||||
return {field[4:]: prefs[field] for field in fields}
|
||||
|
||||
|
||||
# Pour raccorder le code des anciens bulletins qui attendent une NoteTable
|
||||
class APCNotesTableCompat:
|
||||
"""Implementation partielle de NotesTable pour les formations APC
|
||||
Accès aux notes et rangs.
|
||||
"""
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
self.results = ResultatsSemestreBUT(formsemestre)
|
||||
nb_etuds = len(self.results.etuds)
|
||||
self.rangs = self.results.etud_moy_gen_ranks
|
||||
self.moy_min = self.results.etud_moy_gen.min()
|
||||
self.moy_max = self.results.etud_moy_gen.max()
|
||||
self.moy_moy = self.results.etud_moy_gen.mean()
|
||||
self.bonus = defaultdict(lambda: 0.0) # XXX
|
||||
self.ue_rangs = {
|
||||
u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.results.ues
|
||||
}
|
||||
self.mod_rangs = {
|
||||
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.results.modimpls
|
||||
}
|
||||
|
||||
def get_ues(self):
|
||||
ues = []
|
||||
for ue in self.results.ues:
|
||||
d = ue.to_dict()
|
||||
d.update(
|
||||
{
|
||||
"max": self.results.etud_moy_ue[ue.id].max(),
|
||||
"min": self.results.etud_moy_ue[ue.id].min(),
|
||||
"moy": self.results.etud_moy_ue[ue.id].mean(),
|
||||
"nb_moy": len(self.results.etud_moy_ue),
|
||||
}
|
||||
)
|
||||
ues.append(d)
|
||||
return ues
|
||||
|
||||
def get_modimpls(self):
|
||||
return [m.to_dict() for m in self.results.modimpls]
|
||||
|
||||
def get_etud_moy_gen(self, etudid):
|
||||
return self.results.etud_moy_gen[etudid]
|
||||
|
||||
def get_moduleimpls_attente(self):
|
||||
return [] # XXX TODO
|
||||
|
||||
def get_etud_rang(self, etudid):
|
||||
return self.rangs[etudid]
|
||||
|
||||
def get_etud_rang_group(self, etudid, group_id):
|
||||
return (None, 0) # XXX unimplemented TODO
|
||||
|
||||
def get_etud_ue_status(self, etudid, ue_id):
|
||||
return {
|
||||
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
|
||||
"is_capitalized": False, # XXX TODO
|
||||
}
|
||||
|
||||
def get_etud_mod_moy(self, moduleimpl_id, etudid):
|
||||
mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
|
||||
etud_idx = self.results.etud_index[etudid]
|
||||
# moyenne sur les UE:
|
||||
self.results.sem_cube[etud_idx, mod_idx].mean()
|
||||
|
||||
def get_mod_stats(self, moduleimpl_id):
|
||||
return {
|
||||
"moy": "-",
|
||||
"max": "-",
|
||||
"min": "-",
|
||||
"nb_notes": "-",
|
||||
"nb_missing": "-",
|
||||
"nb_valid_evals": "-",
|
||||
}
|
||||
|
||||
def get_evals_in_mod(self, moduleimpl_id):
|
||||
mi = ModuleImpl.query.get(moduleimpl_id)
|
||||
evals_results = []
|
||||
for e in mi.evaluations:
|
||||
d = e.to_dict()
|
||||
d["heure_debut"] = e.heure_debut # datetime.time
|
||||
d["heure_fin"] = e.heure_fin
|
||||
d["jour"] = e.jour # datetime
|
||||
d["notes"] = {
|
||||
etud.id: {
|
||||
"etudid": etud.id,
|
||||
"value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][
|
||||
etud.id
|
||||
],
|
||||
}
|
||||
for etud in self.results.etuds
|
||||
}
|
||||
evals_results.append(d)
|
||||
return evals_results
|
|
@ -0,0 +1,326 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Génération du bulletin en format XML / compatibilité ScoDoc 7
|
||||
|
||||
=> exporte quelques résultats BUT dans le format des anciens bulletins XML ScoDoc 7
|
||||
afin d'avoir un affichage acceptable sur les ENT anciens.
|
||||
|
||||
Les plate-formes modernes utilisent uniquement la version JSON (but/bulletin_but.py)
|
||||
"""
|
||||
|
||||
|
||||
import datetime
|
||||
from xml.etree import ElementTree
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
from app import log
|
||||
from app.but import bulletin_but
|
||||
from app.models import FormSemestre, Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_xml
|
||||
|
||||
|
||||
def bulletin_but_xml_compat(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
doc=None, # XML document
|
||||
force_publishing=False,
|
||||
xml_nodate=False,
|
||||
xml_with_decisions=False, # inclue les decisions même si non publiées
|
||||
version="long",
|
||||
) -> str:
|
||||
"""Bulletin XML au format ScoDoc 7, avec informations "BUT" """
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
log(
|
||||
"bulletin_but_xml_compat( formsemestre_id=%s, etudid=%s )"
|
||||
% (formsemestre_id, etudid)
|
||||
)
|
||||
sem = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
results = bulletin_but.ResultatsSemestreBUT(sem)
|
||||
nb_inscrits = len(results.etuds)
|
||||
if sem.bul_hide_xml or force_publishing:
|
||||
published = "1"
|
||||
else:
|
||||
published = "0"
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
docdate = datetime.datetime.now().isoformat()
|
||||
el = {
|
||||
"etudid": str(etudid),
|
||||
"formsemestre_id": str(formsemestre_id),
|
||||
"date": docdate,
|
||||
"publie": published,
|
||||
}
|
||||
if sem.etapes:
|
||||
el["etape_apo"] = sem.etapes[0].etape_apo or ""
|
||||
n = 2
|
||||
for et in sem.etapes[1:]:
|
||||
el["etape_apo" + str(n)] = et.etape_apo or ""
|
||||
n += 1
|
||||
x = Element("bulletinetud", **el)
|
||||
if doc:
|
||||
is_appending = True
|
||||
doc.append(x)
|
||||
else:
|
||||
is_appending = False
|
||||
doc = x
|
||||
# Infos sur l'etudiant
|
||||
doc.append(
|
||||
Element(
|
||||
"etudiant",
|
||||
etudid=str(etudid),
|
||||
code_nip=etud.code_nip or "",
|
||||
code_ine=etud.code_ine or "",
|
||||
nom=scu.quote_xml_attr(etud.nom),
|
||||
prenom=scu.quote_xml_attr(etud.prenom),
|
||||
civilite=scu.quote_xml_attr(etud.civilite_str()),
|
||||
sexe=scu.quote_xml_attr(etud.civilite_str()), # compat
|
||||
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
|
||||
email=scu.quote_xml_attr(etud.get_first_email() or ""),
|
||||
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
|
||||
)
|
||||
)
|
||||
# Disponible pour publication ?
|
||||
if not published:
|
||||
return doc # stop !
|
||||
# Moyenne générale:
|
||||
doc.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(results.etud_moy_gen[etud.id]),
|
||||
min=scu.fmt_note(results.etud_moy_gen.min()),
|
||||
max=scu.fmt_note(results.etud_moy_gen.max()),
|
||||
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
|
||||
)
|
||||
)
|
||||
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
|
||||
bonus = 0 # XXX TODO valeur du bonus sport
|
||||
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
|
||||
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
|
||||
doc.append(Element("note_max", value="20")) # notes toujours sur 20
|
||||
doc.append(Element("bonus_sport_culture", value=str(bonus)))
|
||||
# Liste les UE / modules /evals
|
||||
for ue in results.ues:
|
||||
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
|
||||
nb_inscrits_ue = (
|
||||
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
|
||||
)
|
||||
x_ue = Element(
|
||||
"ue",
|
||||
id=str(ue.id),
|
||||
numero=scu.quote_xml_attr(ue.numero),
|
||||
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
|
||||
titre=scu.quote_xml_attr(ue.titre or ""),
|
||||
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
|
||||
)
|
||||
doc.append(x_ue)
|
||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||
v = results.etud_moy_ue[ue.id][etud.id]
|
||||
else:
|
||||
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
|
||||
x_ue.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(v),
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
)
|
||||
)
|
||||
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
|
||||
x_ue.append(Element("rang", value=str(rang_ue)))
|
||||
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
|
||||
# Liste les modules rattachés à cette UE
|
||||
for modimpl in results.modimpls:
|
||||
# Liste ici uniquement les modules rattachés à cette UE
|
||||
if modimpl.module.ue.id == ue.id:
|
||||
mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
|
||||
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
|
||||
x_mod = Element(
|
||||
"module",
|
||||
id=str(modimpl.id),
|
||||
code=str(modimpl.module.code or ""),
|
||||
coefficient=str(coef),
|
||||
numero=str(modimpl.module.numero or 0),
|
||||
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
|
||||
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
|
||||
code_apogee=scu.quote_xml_attr(modimpl.module.code_apogee or ""),
|
||||
)
|
||||
x_ue.append(x_mod)
|
||||
x_mod.append(
|
||||
Element(
|
||||
"note",
|
||||
value=mod_moy,
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()),
|
||||
)
|
||||
)
|
||||
# XXX TODO rangs et effectifs
|
||||
# --- notes de chaque eval:
|
||||
if version != "short":
|
||||
for e in modimpl.evaluations:
|
||||
if e.visibulletin or version == "long":
|
||||
x_eval = Element(
|
||||
"evaluation",
|
||||
jour=e.jour.isoformat() if e.jour else "",
|
||||
heure_debut=e.heure_debut.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
heure_fin=e.heure_fin.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
evaluation_type=str(e.evaluation_type),
|
||||
description=scu.quote_xml_attr(e.description),
|
||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
note_max_origin=str(e.note_max),
|
||||
)
|
||||
x_mod.append(x_eval)
|
||||
x_eval.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(
|
||||
results.modimpls_evals_notes[e.moduleimpl_id][
|
||||
e.id
|
||||
][etud.id]
|
||||
),
|
||||
)
|
||||
)
|
||||
# XXX TODO: Evaluations incomplètes ou futures: XXX
|
||||
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
|
||||
|
||||
# --- Absences
|
||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||
nbabs, nbabsjust = sem.get_abs_count(etud.id)
|
||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||
|
||||
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
||||
# TODO : refactoring
|
||||
|
||||
# --- Decision Jury
|
||||
if (
|
||||
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
|
||||
or xml_with_decisions
|
||||
):
|
||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
format="xml",
|
||||
show_uevalid=sco_preferences.get_preference(
|
||||
"bul_show_uevalid", formsemestre_id
|
||||
),
|
||||
)
|
||||
x_situation = Element("situation")
|
||||
x_situation.text = scu.quote_xml_attr(infos["situation"])
|
||||
doc.append(x_situation)
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
etat = decision["etat"]
|
||||
if decision["decision_sem"]:
|
||||
code = decision["decision_sem"]["code"] or ""
|
||||
else:
|
||||
code = ""
|
||||
if (
|
||||
decision["decision_sem"]
|
||||
and "compense_formsemestre_id" in decision["decision_sem"]
|
||||
):
|
||||
doc.append(
|
||||
Element(
|
||||
"decision",
|
||||
code=code,
|
||||
etat=str(etat),
|
||||
compense_formsemestre_id=str(
|
||||
decision["decision_sem"]["compense_formsemestre_id"] or ""
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
doc.append(Element("decision", code=code, etat=str(etat)))
|
||||
|
||||
if decision[
|
||||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
ue_id=str(ue["ue_id"]),
|
||||
numero=scu.quote_xml_attr(ue["numero"]),
|
||||
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
||||
titre=scu.quote_xml_attr(ue["titre"]),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
)
|
||||
)
|
||||
|
||||
for aut in decision["autorisations"]:
|
||||
doc.append(
|
||||
Element(
|
||||
"autorisation_inscription", semestre_id=str(aut["semestre_id"])
|
||||
)
|
||||
)
|
||||
else:
|
||||
doc.append(Element("decision", code="", etat="DEM"))
|
||||
# --- Appreciations
|
||||
cnx = ndb.GetDBConnexion()
|
||||
apprecs = sco_etud.appreciations_list(
|
||||
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
)
|
||||
for appr in apprecs:
|
||||
x_appr = Element(
|
||||
"appreciation",
|
||||
date=ndb.DateDMYtoISO(appr["date"]),
|
||||
)
|
||||
x_appr.text = scu.quote_xml_attr(appr["comment"])
|
||||
doc.append(x_appr)
|
||||
|
||||
if is_appending:
|
||||
return None
|
||||
else:
|
||||
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
||||
|
||||
|
||||
"""
|
||||
formsemestre_id=718
|
||||
etudid=12496
|
||||
from app.but.bulletin_but import *
|
||||
mapp.set_sco_dept("RT")
|
||||
sem = FormSemestre.query.get(formsemestre_id)
|
||||
r = ResultatsSemestreBUT(sem)
|
||||
"""
|
|
@ -0,0 +1,35 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""ScoDoc 9 : Formulaires / référentiel de compétence
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
from wtforms import SelectField, SubmitField
|
||||
|
||||
|
||||
class FormationRefCompForm(FlaskForm):
|
||||
referentiel_competence = SelectField("Référentiels déjà chargés")
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
|
||||
class RefCompLoadForm(FlaskForm):
|
||||
upload = FileField(
|
||||
label="Sélectionner un fichier XML Orébut",
|
||||
validators=[
|
||||
FileRequired(),
|
||||
FileAllowed(
|
||||
[
|
||||
"xml",
|
||||
],
|
||||
"Fichier XML Orébut seulement",
|
||||
),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler")
|
|
@ -0,0 +1,126 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
from xml.etree import ElementTree
|
||||
from typing import TextIO
|
||||
|
||||
from app import db
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcCompetence,
|
||||
ApcSituationPro,
|
||||
ApcAppCritique,
|
||||
ApcComposanteEssentielle,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcAnneeParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoFormatError
|
||||
|
||||
|
||||
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
||||
"""Importation XML Orébut
|
||||
peut lever TypeError ou ScoFormatError
|
||||
Résultat: instance de ApcReferentielCompetences
|
||||
"""
|
||||
try:
|
||||
root = ElementTree.XML(xml_data)
|
||||
except ElementTree.ParseError:
|
||||
raise ScoFormatError("fichier XML Orébut invalide")
|
||||
if root.tag != "referentiel_competence":
|
||||
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
|
||||
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
|
||||
args["dept_id"] = dept_id
|
||||
args["scodoc_orig_filename"] = orig_filename
|
||||
ref = ApcReferentielCompetences(**args)
|
||||
db.session.add(ref)
|
||||
competences = root.find("competences")
|
||||
if not competences:
|
||||
raise ScoFormatError("élément 'competences' manquant")
|
||||
for competence in competences.findall("competence"):
|
||||
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
|
||||
ref.competences.append(c)
|
||||
# --- SITUATIONS
|
||||
situations = competence.find("situations")
|
||||
for situation in situations:
|
||||
libelle = "".join(situation.itertext()).strip()
|
||||
s = ApcSituationPro(competence_id=c.id, libelle=libelle)
|
||||
c.situations.append(s)
|
||||
# --- COMPOSANTES ESSENTIELLES
|
||||
composantes = competence.find("composantes_essentielles")
|
||||
for composante in composantes:
|
||||
libelle = "".join(composante.itertext()).strip()
|
||||
ce = ApcComposanteEssentielle(libelle=libelle)
|
||||
c.composantes_essentielles.append(ce)
|
||||
# --- NIVEAUX (années)
|
||||
niveaux = competence.find("niveaux")
|
||||
for niveau in niveaux:
|
||||
niv = ApcNiveau(**ApcNiveau.attr_from_xml(niveau.attrib))
|
||||
c.niveaux.append(niv)
|
||||
acs = niveau.find("acs")
|
||||
for ac in acs:
|
||||
libelle = "".join(ac.itertext()).strip()
|
||||
code = ac.attrib["code"]
|
||||
niv.app_critiques.append(ApcAppCritique(code=code, libelle=libelle))
|
||||
# --- PARCOURS
|
||||
parcours = root.find("parcours")
|
||||
if not parcours:
|
||||
raise ScoFormatError("élément 'parcours' manquant")
|
||||
for parcour in parcours.findall("parcour"):
|
||||
parc = ApcParcours(**ApcParcours.attr_from_xml(parcour.attrib))
|
||||
ref.parcours.append(parc)
|
||||
for annee in parcour.findall("annee"):
|
||||
a = ApcAnneeParcours(**ApcAnneeParcours.attr_from_xml(annee.attrib))
|
||||
parc.annees.append(a)
|
||||
for competence in annee.findall("competence"):
|
||||
nom = competence.attrib["nom"]
|
||||
niveau = int(competence.attrib["niveau"])
|
||||
# Retrouve la competence
|
||||
comp = ref.competences.filter_by(titre=nom).all()
|
||||
if len(comp) == 0:
|
||||
raise ScoFormatError(f"competence {nom} référencée mais on définie")
|
||||
elif len(comp) > 1:
|
||||
raise ScoFormatError(f"competence {nom} ambigüe")
|
||||
ass = ApcParcoursNiveauCompetence(
|
||||
niveau=niveau, annee_parcours=a, competence=comp[0]
|
||||
)
|
||||
db.session.add(ass)
|
||||
|
||||
db.session.commit()
|
||||
return ref
|
||||
|
||||
|
||||
"""
|
||||
xmlfile = open("but-RT-refcomp-30112021.xml")
|
||||
tree = ElementTree.parse(xmlfile)
|
||||
# get root element
|
||||
root = tree.getroot()
|
||||
assert root.tag == "referentiel_competence"
|
||||
|
||||
ref = ApcReferentielCompetences(**ApcReferentielCompetences.attr_from_xml(root.attrib))
|
||||
|
||||
competences = root.find("competences")
|
||||
if not competences:
|
||||
raise ScoFormatError("élément 'competences' manquant")
|
||||
|
||||
competence = competences.findall("competence")[0] # XXX
|
||||
|
||||
from app.but.import_refcomp import *
|
||||
dept_id = models.Departement.query.first().id
|
||||
data = open("tests/data/but-RT-refcomp-exemple.xml").read()
|
||||
ref = orebut_import_refcomp(data, dept_id)
|
||||
#------
|
||||
from app.but.import_refcomp import *
|
||||
ref = ApcReferentielCompetences.query.first()
|
||||
p = ApcParcours(code="PARC", libelle="Parcours Test")
|
||||
ref.parcours.append(p)
|
||||
annee = ApcAnneeParcours(numero=1)
|
||||
p.annees.append(annee)
|
||||
annee.competences
|
||||
c = ref.competences.filter_by(titre="Administrer").first()
|
||||
annee.competences.append(c)
|
||||
"""
|
|
@ -0,0 +1,49 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""caches pour tables APC
|
||||
"""
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
|
||||
class ModuleCoefsCache(sco_cache.ScoDocCache):
|
||||
"""Cache for module coefs
|
||||
Clé: formation_id.semestre_idx
|
||||
Valeur: DataFrame (df_load_module_coefs)
|
||||
"""
|
||||
|
||||
prefix = "MCO"
|
||||
|
||||
|
||||
class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
||||
"""Cache for poids evals
|
||||
Clé: moduleimpl_id
|
||||
Valeur: DataFrame (df_load_evaluations_poids)
|
||||
"""
|
||||
|
||||
prefix = "EPC"
|
|
@ -0,0 +1,70 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Matrices d'inscription aux modules d'un semestre
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
|
||||
#
|
||||
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
|
||||
# sur test debug 116 etuds, 18 modules, on est autour de 250ms.
|
||||
# On a testé trois approches, ci-dessous (et retenu la 1ere)
|
||||
#
|
||||
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
||||
"""Charge la matrice des inscriptions aux modules du semestre
|
||||
rows: etudid
|
||||
columns: moduleimpl_id (en chaîne)
|
||||
value: bool (0/1 inscrit ou pas)
|
||||
"""
|
||||
# méthode la moins lente: une requete par module, merge les dataframes
|
||||
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||
etudids = [i.etudid for i in formsemestre.get_inscrits(include_dem=False)]
|
||||
df = pd.DataFrame(index=etudids, dtype=int)
|
||||
for moduleimpl_id in moduleimpl_ids:
|
||||
ins_df = pd.read_sql_query(
|
||||
"""SELECT etudid, 1 AS "%(moduleimpl_id)s"
|
||||
FROM notes_moduleimpl_inscription
|
||||
WHERE moduleimpl_id=%(moduleimpl_id)s""",
|
||||
db.engine,
|
||||
params={"moduleimpl_id": moduleimpl_id},
|
||||
index_col="etudid",
|
||||
dtype=int,
|
||||
)
|
||||
df = df.merge(ins_df, how="left", left_index=True, right_index=True)
|
||||
# les colonnes de df sont en float (Nan) quand il n'y a
|
||||
# aucun inscrit au module.
|
||||
df.fillna(0, inplace=True) # les non-inscrits
|
||||
return df.astype(bool) # x100 25.5s 15s 17s
|
||||
|
||||
|
||||
# chrono avec timeit:
|
||||
# timeit.timeit('x = df_load_module_inscr_v0(696)', number=100, globals=globals())
|
||||
|
||||
|
||||
def df_load_modimpl_inscr_v0(formsemestre):
|
||||
# methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente
|
||||
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||
etudids = [i.etudid for i in formsemestre.inscriptions]
|
||||
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
||||
for modimpl in formsemestre.modimpls:
|
||||
ins_mod = df[modimpl.id]
|
||||
for inscr in modimpl.inscriptions:
|
||||
ins_mod[inscr.etudid] = True
|
||||
return df # x100 30.7s 46s 32s
|
||||
|
||||
|
||||
def df_load_modimpl_inscr_v2(formsemestre):
|
||||
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||
etudids = [i.etudid for i in formsemestre.inscriptions]
|
||||
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
||||
cursor = db.engine.execute(
|
||||
"select moduleimpl_id, etudid from notes_moduleimpl_inscription i, notes_moduleimpl m where i.moduleimpl_id = m.id and m.formsemestre_id = %(formsemestre_id)s",
|
||||
{"formsemestre_id": formsemestre.id},
|
||||
)
|
||||
for moduleimpl_id, etudid in cursor:
|
||||
df[moduleimpl_id][etudid] = True
|
||||
return df # x100 44s, 31s, 29s, 28s
|
|
@ -0,0 +1,238 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ)
|
||||
|
||||
Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une
|
||||
évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la
|
||||
moyenne générale d'une UE.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.core.frame import DataFrame
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def df_load_evaluations_poids(
|
||||
moduleimpl_id: int, default_poids=1.0
|
||||
) -> tuple[pd.DataFrame, list]:
|
||||
"""Charge poids des évaluations d'un module et retourne un dataframe
|
||||
rows = evaluations, columns = UE, value = poids (float).
|
||||
Les valeurs manquantes (évaluations sans coef vers des UE) sont
|
||||
remplies par default_poids.
|
||||
Résultat: (evals_poids, liste de UE du semestre)
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
|
||||
ue_ids = [ue.id for ue in ues]
|
||||
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
||||
for eval_poids in EvaluationUEPoids.query.join(
|
||||
EvaluationUEPoids.evaluation
|
||||
).filter_by(moduleimpl_id=moduleimpl_id):
|
||||
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
|
||||
if default_poids is not None:
|
||||
df.fillna(value=default_poids, inplace=True)
|
||||
return df, ues
|
||||
|
||||
|
||||
def check_moduleimpl_conformity(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
|
||||
) -> bool:
|
||||
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
|
||||
au PN.
|
||||
Un module est dit *conforme* si et seulement si la somme des poids de ses
|
||||
évaluations vers une UE de coefficient non nul est non nulle.
|
||||
"""
|
||||
nb_evals, nb_ues = evals_poids.shape
|
||||
if nb_evals == 0:
|
||||
return True # modules vides conformes
|
||||
if nb_ues == 0:
|
||||
return False # situation absurde (pas d'UE)
|
||||
if len(modules_coefficients) != nb_ues:
|
||||
raise ValueError("check_moduleimpl_conformity: nb ue incoherent")
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
|
||||
check = all(
|
||||
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0)
|
||||
== module_evals_poids
|
||||
)
|
||||
return check
|
||||
|
||||
|
||||
def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
|
||||
"""Construit un dataframe avec toutes les notes de toutes les évaluations du module.
|
||||
colonnes: le nom de la colonne est l'evaluation_id (int)
|
||||
index (lignes): etudid (int)
|
||||
|
||||
Résultat: (evals_notes, liste de évaluations du moduleimpl,
|
||||
liste de booleens indiquant si l'évaluation est "complete")
|
||||
|
||||
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
|
||||
|
||||
Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs:
|
||||
note : float (valeur enregistrée brute, non normalisée sur 20)
|
||||
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
|
||||
absent: NOTES_ABSENCE (NULL en bd)
|
||||
excusé: NOTES_NEUTRALISE (voir sco_utils)
|
||||
attente: NOTES_ATTENTE
|
||||
|
||||
L'évaluation "complete" (prise en compte dans les calculs) si:
|
||||
- soit tous les étudiants inscrits au module ont des notes
|
||||
- soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete)
|
||||
|
||||
N'utilise pas de cache ScoDoc.
|
||||
"""
|
||||
# L'index du dataframe est la liste des étudiants inscrits au semestre,
|
||||
# sans les démissionnaires
|
||||
etudids = [
|
||||
e.etudid
|
||||
for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits(
|
||||
include_dem=False
|
||||
)
|
||||
]
|
||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||
# --- Calcul nombre d'inscrits pour détermnier si évaluation "complete":
|
||||
if evaluations:
|
||||
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
|
||||
inscrits_module = {
|
||||
ins.etud.id for ins in evaluations[0].moduleimpl.inscriptions
|
||||
}.intersection(etudids)
|
||||
nb_inscrits_module = len(inscrits_module)
|
||||
else:
|
||||
nb_inscrits_module = 0
|
||||
# empty df with all students:
|
||||
evals_notes = pd.DataFrame(index=etudids, dtype=float)
|
||||
evaluations_completes = []
|
||||
for evaluation in evaluations:
|
||||
eval_df = pd.read_sql_query(
|
||||
"""SELECT n.etudid, n.value AS "%(evaluation_id)s"
|
||||
FROM notes_notes n, notes_moduleimpl_inscription i
|
||||
WHERE evaluation_id=%(evaluation_id)s
|
||||
AND n.etudid = i.etudid
|
||||
AND i.moduleimpl_id = %(moduleimpl_id)s
|
||||
ORDER BY n.etudid
|
||||
""",
|
||||
db.engine,
|
||||
params={
|
||||
"evaluation_id": evaluation.id,
|
||||
"moduleimpl_id": evaluation.moduleimpl.id,
|
||||
},
|
||||
index_col="etudid",
|
||||
)
|
||||
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
|
||||
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||
is_complete = (
|
||||
len(set(eval_df.index).intersection(etudids)) == nb_inscrits_module
|
||||
) or evaluation.publish_incomplete
|
||||
evaluations_completes.append(is_complete)
|
||||
# NULL en base => ABS (= -999)
|
||||
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
||||
# Ce merge met à NULL les élements non présents
|
||||
# (notes non saisies ou etuds non inscrits au module):
|
||||
evals_notes = evals_notes.merge(
|
||||
eval_df, how="left", left_index=True, right_index=True
|
||||
)
|
||||
# Force columns names to integers (evaluation ids)
|
||||
evals_notes.columns = pd.Int64Index(
|
||||
[int(x) for x in evals_notes.columns], dtype="int64"
|
||||
)
|
||||
return evals_notes, evaluations, evaluations_completes
|
||||
|
||||
|
||||
def compute_module_moy(
|
||||
evals_notes_df: pd.DataFrame,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evaluations: list,
|
||||
evaluations_completes: list,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule les moyennes des étudiants dans ce module
|
||||
|
||||
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
|
||||
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
|
||||
NOTES_ABSENCE.
|
||||
Les NaN désignent les notes manquantes (non saisies).
|
||||
|
||||
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
|
||||
- evaluations: séquence d'évaluations (utilisées pour le coef et
|
||||
le barème)
|
||||
|
||||
- evaluations_completes: séquence de booléens indiquant les
|
||||
évals à prendre en compte.
|
||||
|
||||
Résultat: DataFrame, colonnes UE, lignes etud
|
||||
= la note de l'étudiant dans chaque UE pour ce module.
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant à des notes)
|
||||
ne donnent pas de coef vers cette UE.
|
||||
"""
|
||||
nb_etuds, nb_evals = evals_notes_df.shape
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
|
||||
if nb_etuds == 0:
|
||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
|
||||
evals_coefs = (
|
||||
np.array(
|
||||
[e.coefficient for e in evaluations],
|
||||
dtype=float,
|
||||
)
|
||||
* evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
evals_poids = evals_poids_df.values * evals_coefs
|
||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||
assert evals_poids.shape == (nb_evals, nb_ues)
|
||||
# Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20:
|
||||
evals_notes = np.where(
|
||||
evals_notes_df.values > scu.NOTES_ABSENCE, evals_notes_df.values, 0.0
|
||||
) / [e.note_max / 20.0 for e in evaluations]
|
||||
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
|
||||
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
|
||||
# Note: les NaN sont remplacés par des 0 dans evals_notes
|
||||
# et dans dans evals_poids_etuds
|
||||
# (rappel: la comparaison est toujours false face à un NaN)
|
||||
# shape: (nb_etuds, nb_evals, nb_ues)
|
||||
poids_stacked = np.stack([evals_poids] * nb_etuds)
|
||||
evals_poids_etuds = np.where(
|
||||
np.stack([evals_notes_df.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked,
|
||||
0,
|
||||
)
|
||||
# Calcule la moyenne pondérée sur les notes disponibles:
|
||||
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_poids_etuds * evals_notes_stacked, axis=1
|
||||
) / np.sum(evals_poids_etuds, axis=1)
|
||||
etuds_moy_module_df = pd.DataFrame(
|
||||
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
|
||||
)
|
||||
return etuds_moy_module_df
|
|
@ -0,0 +1,79 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions de calcul des moyennes de semestre (indicatives dans le BUT)
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def compute_sem_moys(etud_moy_ue_df, modimpl_coefs_df):
|
||||
"""Calcule la moyenne générale indicative
|
||||
= moyenne des moyennes d'UE, pondérée par la somme de leurs coefs
|
||||
|
||||
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
|
||||
modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE
|
||||
|
||||
Result: panda Series, index etudid, valeur float (moyenne générale)
|
||||
"""
|
||||
moy_gen = (etud_moy_ue_df * modimpl_coefs_df.values.sum(axis=1)).sum(
|
||||
axis=1
|
||||
) / modimpl_coefs_df.values.sum()
|
||||
return moy_gen
|
||||
|
||||
|
||||
def comp_ranks_series(notes: pd.Series):
|
||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique)
|
||||
en tenant compte des ex-aequos
|
||||
Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang
|
||||
"""
|
||||
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
|
||||
rangs = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
|
||||
N = len(notes)
|
||||
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
|
||||
notes_i = notes.iat
|
||||
for i, etudid in enumerate(notes.index):
|
||||
# test ex-aequo
|
||||
if i < (N - 1):
|
||||
next = notes_i[i + 1]
|
||||
else:
|
||||
next = None
|
||||
val = notes_i[i]
|
||||
if nb_ex:
|
||||
srang = "%d ex" % (i + 1 - nb_ex)
|
||||
if val == next:
|
||||
nb_ex += 1
|
||||
else:
|
||||
nb_ex = 0
|
||||
else:
|
||||
if val == next:
|
||||
srang = "%d ex" % (i + 1 - nb_ex)
|
||||
nb_ex = 1
|
||||
else:
|
||||
srang = "%d" % (i + 1)
|
||||
rangs[etudid] = srang
|
||||
return rangs
|
|
@ -0,0 +1,230 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions de calcul des moyennes d'UE
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
|
||||
from app.comp import moy_mod
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
||||
|
||||
def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
|
||||
"""Charge les coefs des modules de la formation pour le semestre indiqué.
|
||||
|
||||
Ces coefs lient les modules à chaque UE.
|
||||
|
||||
Résultat: (module_coefs_df, ues, modules)
|
||||
DataFrame rows = UEs, columns = modules, value = coef.
|
||||
|
||||
Considère toutes les UE (sauf sport) et modules du semestre.
|
||||
Les coefs non définis (pas en base) sont mis à zéro.
|
||||
|
||||
Si semestre_idx None, prend toutes les UE de la formation.
|
||||
"""
|
||||
ues = (
|
||||
UniteEns.query.filter_by(formation_id=formation_id)
|
||||
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
modules = Module.query.filter_by(formation_id=formation_id).order_by(
|
||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||
)
|
||||
if semestre_idx is not None:
|
||||
ues = ues.filter_by(semestre_idx=semestre_idx)
|
||||
modules = modules.filter_by(semestre_id=semestre_idx)
|
||||
ues = ues.all()
|
||||
modules = modules.all()
|
||||
ue_ids = [ue.id for ue in ues]
|
||||
module_ids = [module.id for module in modules]
|
||||
module_coefs_df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
|
||||
query = (
|
||||
db.session.query(ModuleUECoef)
|
||||
.filter(UniteEns.formation_id == formation_id)
|
||||
.filter(ModuleUECoef.ue_id == UniteEns.id)
|
||||
)
|
||||
if semestre_idx is not None:
|
||||
query = query.filter(UniteEns.semestre_idx == semestre_idx)
|
||||
|
||||
for mod_coef in query:
|
||||
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
|
||||
|
||||
module_coefs_df.fillna(value=0, inplace=True)
|
||||
|
||||
return module_coefs_df, ues, modules
|
||||
|
||||
|
||||
def df_load_modimpl_coefs(
|
||||
formsemestre: models.FormSemestre, ues=None, modimpls=None
|
||||
) -> pd.DataFrame:
|
||||
"""Charge les coefs des modules du formsemestre indiqué.
|
||||
|
||||
Comme df_load_module_coefs mais prend seulement les UE
|
||||
et modules du formsemestre.
|
||||
Si ues et modimpls sont None, prend tous ceux du formsemestre.
|
||||
Résultat: (module_coefs_df, ues, modules)
|
||||
DataFrame rows = UEs, columns = modimpl, value = coef.
|
||||
"""
|
||||
if ues is None:
|
||||
ues = formsemestre.query_ues().all()
|
||||
ue_ids = [x.id for x in ues]
|
||||
if modimpls is None:
|
||||
modimpls = formsemestre.modimpls.all()
|
||||
modimpl_ids = [x.id for x in modimpls]
|
||||
mod2impl = {m.module.id: m.id for m in modimpls}
|
||||
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
|
||||
mod_coefs = (
|
||||
db.session.query(ModuleUECoef)
|
||||
.filter(ModuleUECoef.module_id == ModuleImpl.module_id)
|
||||
.filter(ModuleImpl.formsemestre_id == formsemestre.id)
|
||||
)
|
||||
|
||||
for mod_coef in mod_coefs:
|
||||
modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
|
||||
modimpl_coefs_df.fillna(value=0, inplace=True)
|
||||
return modimpl_coefs_df, ues, modimpls
|
||||
|
||||
|
||||
def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
||||
"""Réuni les notes moyennes des modules du semestre en un "cube"
|
||||
|
||||
modimpls_notes : liste des moyennes de module
|
||||
(DataFrames rendus par compute_module_moy, (etud x UE))
|
||||
Resultat: ndarray (etud x module x UE)
|
||||
"""
|
||||
modimpls_notes_arr = [df.values for df in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x UE)
|
||||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
def notes_sem_load_cube(formsemestre):
|
||||
"""Calcule le cube des notes du semestre
|
||||
(charge toutes les notes, calcule les moyenne des modules
|
||||
et assemble le cube)
|
||||
Resultat:
|
||||
sem_cube : ndarray (etuds x modimpls x UEs)
|
||||
modimpls_evals_poids dict { modimpl.id : evals_poids }
|
||||
modimpls_evals_notes dict { modimpl.id : evals_notes }
|
||||
modimpls_evaluations dict { modimpl.id : liste des évaluations }
|
||||
modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
|
||||
"""
|
||||
modimpls_evals_poids = {}
|
||||
modimpls_evals_notes = {}
|
||||
modimpls_evaluations = {}
|
||||
modimpls_evaluations_complete = {}
|
||||
modimpls_notes = []
|
||||
for modimpl in formsemestre.modimpls:
|
||||
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
|
||||
modimpl.id
|
||||
)
|
||||
evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id)
|
||||
etuds_moy_module = moy_mod.compute_module_moy(
|
||||
evals_notes, evals_poids, evaluations, evaluations_completes
|
||||
)
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_evals_notes[modimpl.id] = evals_notes
|
||||
modimpls_evaluations[modimpl.id] = evaluations
|
||||
modimpls_evaluations_complete[modimpl.id] = evaluations_completes
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
return (
|
||||
notes_sem_assemble_cube(modimpls_notes),
|
||||
modimpls_evals_poids,
|
||||
modimpls_evals_notes,
|
||||
modimpls_evaluations,
|
||||
modimpls_evaluations_complete,
|
||||
)
|
||||
|
||||
|
||||
def compute_ue_moys(
|
||||
sem_cube: np.array,
|
||||
etuds: list,
|
||||
modimpls: list,
|
||||
ues: list,
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs_df: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcul de la moyenne d'UE
|
||||
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
|
||||
NI non inscrit à (au moins un) module de cette UE
|
||||
NA pas de notes disponibles
|
||||
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
|
||||
|
||||
sem_cube: notes moyennes aux modules
|
||||
ndarray (etuds x modimpls x UEs)
|
||||
(floats avec des NaN)
|
||||
etuds : lites des étudiants (dim. 0 du cube)
|
||||
modimpls : liste des modules à considérer (dim. 1 du cube)
|
||||
ues : liste des UE (dim. 2 du cube)
|
||||
module_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
module_coefs_df: matrice coefficients (UE x modimpl)
|
||||
|
||||
Resultat: DataFrame columns UE, rows etudid
|
||||
"""
|
||||
nb_etuds, nb_modules, nb_ues = sem_cube.shape
|
||||
assert len(etuds) == nb_etuds
|
||||
assert len(modimpls) == nb_modules
|
||||
assert len(ues) == nb_ues
|
||||
assert modimpl_inscr_df.shape[0] == nb_etuds
|
||||
assert modimpl_inscr_df.shape[1] == nb_modules
|
||||
assert modimpl_coefs_df.shape[0] == nb_ues
|
||||
assert modimpl_coefs_df.shape[1] == nb_modules
|
||||
modimpl_inscr = modimpl_inscr_df.values
|
||||
modimpl_coefs = modimpl_coefs_df.values
|
||||
if nb_etuds == 0:
|
||||
return pd.DataFrame(
|
||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
||||
# Duplique les inscriptions sur les UEs:
|
||||
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
|
||||
# Enlève les NaN du numérateur:
|
||||
# si on veut prendre en compte les modules avec notes neutralisées ?
|
||||
sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0)
|
||||
|
||||
# Ne prend pas en compte les notes des étudiants non inscrits au module:
|
||||
# Annule les notes:
|
||||
sem_cube_inscrits = np.where(modimpl_inscr_stacked, sem_cube_no_nan, 0.0)
|
||||
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
|
||||
modimpl_coefs_etuds = np.where(
|
||||
modimpl_inscr_stacked, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
|
||||
)
|
||||
# Annule les coefs des modules NaN
|
||||
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
|
||||
#
|
||||
# Version vectorisée
|
||||
#
|
||||
etud_moy_ue = np.sum(
|
||||
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
|
||||
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||||
return pd.DataFrame(
|
||||
etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
|
@ -50,8 +50,15 @@ def scodoc(func):
|
|||
|
||||
@wraps(func)
|
||||
def scodoc_function(*args, **kwargs):
|
||||
# print("@scodoc")
|
||||
# interdit les POST si pas loggué
|
||||
if request.method == "POST" and not current_user.is_authenticated:
|
||||
if (
|
||||
request.method == "POST"
|
||||
and not current_user.is_authenticated
|
||||
and not request.form.get(
|
||||
"__ac_password"
|
||||
) # exception pour compat API ScoDoc7
|
||||
):
|
||||
current_app.logger.info(
|
||||
"POST by non authenticated user (request.form=%s)",
|
||||
str(request.form)[:2048],
|
||||
|
@ -71,6 +78,7 @@ def scodoc(func):
|
|||
# current_app.logger.info("setting dept to None")
|
||||
g.scodoc_dept = None
|
||||
g.scodoc_dept_id = -1 # invalide
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return scodoc_function
|
||||
|
@ -100,8 +108,8 @@ def permission_required_compat_scodoc7(permission):
|
|||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# current_app.logger.warning("PERMISSION; kwargs=%s" % str(kwargs))
|
||||
# cherche les paramètre d'auth:
|
||||
# print("@permission_required_compat_scodoc7")
|
||||
auth_ok = False
|
||||
if request.method == "GET":
|
||||
user_name = request.args.get("__ac_name")
|
||||
|
@ -116,7 +124,6 @@ def permission_required_compat_scodoc7(permission):
|
|||
if u and u.check_password(user_password):
|
||||
auth_ok = True
|
||||
flask_login.login_user(u)
|
||||
|
||||
# reprend le chemin classique:
|
||||
scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
|
||||
|
@ -153,6 +160,7 @@ def scodoc7func(func):
|
|||
2. or be called directly from Python.
|
||||
|
||||
"""
|
||||
# print("@scodoc7func")
|
||||
# Détermine si on est appelé via une route ("toplevel")
|
||||
# ou par un appel de fonction python normal.
|
||||
top_level = not hasattr(g, "scodoc7_decorated")
|
||||
|
|
|
@ -30,37 +30,44 @@ from app.models.etudiants import (
|
|||
EtudAnnotation,
|
||||
)
|
||||
from app.models.events import Scolog, ScolarNews
|
||||
from app.models.formations import (
|
||||
NotesFormation,
|
||||
NotesUE,
|
||||
NotesMatiere,
|
||||
NotesModule,
|
||||
NotesTag,
|
||||
notes_modules_tags,
|
||||
)
|
||||
from app.models.formations import Formation, Matiere
|
||||
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.formsemestre import (
|
||||
FormSemestre,
|
||||
NotesFormsemestreEtape,
|
||||
NotesFormModalite,
|
||||
NotesFormsemestreUECoef,
|
||||
NotesFormsemestreUEComputationExpr,
|
||||
NotesFormsemestreCustomMenu,
|
||||
NotesFormsemestreInscription,
|
||||
FormSemestreEtape,
|
||||
FormationModalite,
|
||||
FormSemestreUECoef,
|
||||
FormSemestreUEComputationExpr,
|
||||
FormSemestreCustomMenu,
|
||||
FormSemestreInscription,
|
||||
notes_formsemestre_responsables,
|
||||
NotesModuleImpl,
|
||||
notes_modules_enseignants,
|
||||
NotesModuleImplInscription,
|
||||
NotesEvaluation,
|
||||
NotesSemSet,
|
||||
notes_semset_formsemestre,
|
||||
)
|
||||
from app.models.moduleimpls import (
|
||||
ModuleImpl,
|
||||
notes_modules_enseignants,
|
||||
ModuleImplInscription,
|
||||
)
|
||||
from app.models.evaluations import (
|
||||
Evaluation,
|
||||
EvaluationUEPoids,
|
||||
)
|
||||
from app.models.groups import Partition, GroupDescr, group_membership
|
||||
from app.models.notes import (
|
||||
ScolarEvent,
|
||||
ScolarFormsemestreValidation,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarAutorisationInscription,
|
||||
NotesAppreciations,
|
||||
BulAppreciations,
|
||||
NotesNotes,
|
||||
NotesNotesLog,
|
||||
)
|
||||
from app.models.preferences import ScoPreference, ScoDocSiteConfig
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcCompetence,
|
||||
ApcSituationPro,
|
||||
ApcAppCritique,
|
||||
)
|
||||
|
|
|
@ -49,7 +49,7 @@ class AbsenceNotification(db.Model):
|
|||
nbabsjust = db.Column(db.Integer)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
"""ScoDoc 9 models : Formation BUT 2021
|
||||
"""
|
||||
from enum import unique
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class APCFormation(db.Model):
|
||||
"""Formation par compétence"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
specialite = db.Column(db.Text(), nullable=False) # "RT"
|
||||
specialite_long = db.Column(
|
||||
db.Text(), nullable=False
|
||||
) # "Réseaux et télécommunications"
|
|
@ -0,0 +1,287 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
"""
|
||||
from datetime import datetime
|
||||
from enum import unique
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class XMLModel:
|
||||
_xml_attribs = {} # to be overloaded
|
||||
id = "_"
|
||||
|
||||
@classmethod
|
||||
def attr_from_xml(cls, args: dict) -> dict:
|
||||
"""dict with attributes imported from Orébut XML
|
||||
and renamed for our models.
|
||||
The mapping is specified by the _xml_attribs
|
||||
attribute in each model class.
|
||||
"""
|
||||
return {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">'
|
||||
|
||||
|
||||
class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
specialite = db.Column(db.Text())
|
||||
specialite_long = db.Column(db.Text())
|
||||
type_titre = db.Column(db.Text())
|
||||
_xml_attribs = { # Orébut xml attrib : attribute
|
||||
"type": "type_titre",
|
||||
}
|
||||
# ScoDoc specific fields:
|
||||
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
scodoc_orig_filename = db.Column(db.Text())
|
||||
# Relations:
|
||||
competences = db.relationship(
|
||||
"ApcCompetence",
|
||||
backref="referentiel",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
parcours = db.relationship(
|
||||
"ApcParcours",
|
||||
backref="referentiel",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
formations = db.relationship("Formation", backref="referentiel_competence")
|
||||
|
||||
def to_dict(self):
|
||||
"""Représentation complète du ref. de comp.
|
||||
comme un dict.
|
||||
"""
|
||||
return {
|
||||
"dept_id": self.dept_id,
|
||||
"specialite": self.specialite,
|
||||
"specialite_long": self.specialite_long,
|
||||
"type_titre": self.type_titre,
|
||||
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
|
||||
if self.scodoc_date_loaded
|
||||
else "",
|
||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||
"competences": {x.titre: x.to_dict() for x in self.competences},
|
||||
"parcours": {x.code: x.to_dict() for x in self.parcours},
|
||||
}
|
||||
|
||||
|
||||
class ApcCompetence(db.Model, XMLModel):
|
||||
__table_args__ = (
|
||||
# les compétences dans Orébut sont identifiées par leur "titre"
|
||||
# unique au sein d'un référentiel:
|
||||
db.UniqueConstraint(
|
||||
"referentiel_id", "titre", name="apc_competence_referentiel_id_titre_key"
|
||||
),
|
||||
)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
)
|
||||
titre = db.Column(db.Text(), nullable=False, index=True)
|
||||
titre_long = db.Column(db.Text())
|
||||
couleur = db.Column(db.Text())
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
_xml_attribs = { # xml_attrib : attribute
|
||||
"name": "titre",
|
||||
"libelle_long": "titre_long",
|
||||
}
|
||||
situations = db.relationship(
|
||||
"ApcSituationPro",
|
||||
backref="competence",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
composantes_essentielles = db.relationship(
|
||||
"ApcComposanteEssentielle",
|
||||
backref="competence",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
niveaux = db.relationship(
|
||||
"ApcNiveau",
|
||||
backref="competence",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"titre": self.titre,
|
||||
"titre_long": self.titre_long,
|
||||
"couleur": self.couleur,
|
||||
"numero": self.numero,
|
||||
"situations": [x.to_dict() for x in self.situations],
|
||||
"composantes_essentielles": [
|
||||
x.to_dict() for x in self.composantes_essentielles
|
||||
],
|
||||
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
||||
}
|
||||
|
||||
|
||||
class ApcSituationPro(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
# aucun attribut (le text devient le libellé)
|
||||
def to_dict(self):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
|
||||
class ApcComposanteEssentielle(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
|
||||
def to_dict(self):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
|
||||
class ApcNiveau(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
annee = db.Column(db.Text(), nullable=False) # "BUT2"
|
||||
# L'ordre est l'année d'apparition de ce niveau
|
||||
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
|
||||
app_critiques = db.relationship(
|
||||
"ApcAppCritique",
|
||||
backref="niveau",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"libelle": self.libelle,
|
||||
"annee": self.annee,
|
||||
"ordre": self.ordre,
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
|
||||
}
|
||||
|
||||
|
||||
class ApcAppCritique(db.Model, XMLModel):
|
||||
"Apprentissage Critique BUT"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
niveau_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"), nullable=False)
|
||||
code = db.Column(db.Text(), nullable=False, index=True)
|
||||
libelle = db.Column(db.Text())
|
||||
|
||||
modules = db.relationship(
|
||||
"Module",
|
||||
secondary="apc_modules_acs",
|
||||
lazy="dynamic",
|
||||
backref=db.backref("app_critiques", lazy="dynamic"),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
def get_label(self):
|
||||
return self.code + " - " + self.titre
|
||||
|
||||
def __repr__(self):
|
||||
return "<AppCritique {}>".format(self.code)
|
||||
|
||||
def get_saes(self):
|
||||
"""Liste des SAE associées"""
|
||||
return [m for m in self.modules if m.module_type == ModuleType.SAE]
|
||||
|
||||
|
||||
ApcAppCritiqueModules = db.Table(
|
||||
"apc_modules_acs",
|
||||
db.Column("module_id", db.ForeignKey("notes_modules.id")),
|
||||
db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")),
|
||||
)
|
||||
|
||||
|
||||
class ApcParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
annees = db.relationship(
|
||||
"ApcAnneeParcours",
|
||||
backref="parcours",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"code": self.code,
|
||||
"numero": self.numero,
|
||||
"libelle": self.libelle,
|
||||
"annees": {x.ordre: x.to_dict() for x in self.annees},
|
||||
}
|
||||
|
||||
|
||||
class ApcAnneeParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
parcours_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
|
||||
)
|
||||
ordre = db.Column(db.Integer)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ordre": self.ordre,
|
||||
"competences": {
|
||||
x.competence.titre: {"niveau": x.niveau}
|
||||
for x in self.niveaux_competences
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ApcParcoursNiveauCompetence(db.Model):
|
||||
"""Association entre année de parcours et compétence.
|
||||
Le "niveau" de la compétence est donné ici
|
||||
(convention Orébut)
|
||||
"""
|
||||
|
||||
competence_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
annee_parcours_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_annee_parcours.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
niveau = db.Column(db.Integer, nullable=False) # 1, 2, 3
|
||||
competence = db.relationship(
|
||||
ApcCompetence,
|
||||
backref=db.backref(
|
||||
"annee_parcours",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
||||
annee_parcours = db.relationship(
|
||||
ApcAnneeParcours,
|
||||
backref=db.backref(
|
||||
"niveaux_competences",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
|
@ -21,9 +21,7 @@ class Departement(db.Model):
|
|||
|
||||
entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement")
|
||||
etudiants = db.relationship("Identite", lazy="dynamic", backref="departement")
|
||||
formations = db.relationship(
|
||||
"NotesFormation", lazy="dynamic", backref="departement"
|
||||
)
|
||||
formations = db.relationship("Formation", lazy="dynamic", backref="departement")
|
||||
formsemestres = db.relationship(
|
||||
"FormSemestre", lazy="dynamic", backref="departement"
|
||||
)
|
||||
|
@ -44,3 +42,8 @@ class Departement(db.Model):
|
|||
"date_creation": self.date_creation,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_acronym(cls, acronym):
|
||||
dept = cls.query.filter_by(acronym=acronym).first_or_404()
|
||||
return dept
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
et données rattachées (adresses, annotations, ...)
|
||||
"""
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
|
@ -41,11 +44,81 @@ class Identite(db.Model):
|
|||
code_nip = db.Column(db.Text())
|
||||
code_ine = db.Column(db.Text())
|
||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archive
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||
#
|
||||
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
|
||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Etud {self.id} {self.nom} {self.prenom}>"
|
||||
|
||||
def civilite_str(self):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
personnes ne souhaitant pas d'affichage).
|
||||
"""
|
||||
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
|
||||
|
||||
def nom_disp(self):
|
||||
"nom à afficher"
|
||||
if self.nom_usuel:
|
||||
return (
|
||||
(self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel
|
||||
)
|
||||
else:
|
||||
return self.nom
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"le mail associé à la première adrese de l'étudiant, ou None"
|
||||
return self.adresses[0].email or None if self.adresses.count() > 0 else None
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
"""Infos exportées dans les bulletins"""
|
||||
from app.scodoc import sco_photos
|
||||
|
||||
d = {
|
||||
"civilite": self.civilite,
|
||||
"code_ine": self.code_ine,
|
||||
"code_nip": self.code_nip,
|
||||
"date_naissance": self.date_naissance.isoformat()
|
||||
if self.date_naissance
|
||||
else None,
|
||||
"email": self.get_first_email(),
|
||||
"emailperso": self.get_first_email("emailperso"),
|
||||
"etudid": self.id,
|
||||
"nom": self.nom_disp(),
|
||||
"prenom": self.prenom,
|
||||
}
|
||||
if include_urls:
|
||||
d["fiche_url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||
)
|
||||
d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),)
|
||||
return d
|
||||
|
||||
def inscription_courante(self):
|
||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||
"""
|
||||
r = [
|
||||
ins
|
||||
for ins in self.formsemestre_inscriptions
|
||||
if ins.formsemestre.est_courant()
|
||||
]
|
||||
return r[0] if r else None
|
||||
|
||||
def etat_inscription(self, formsemestre_id):
|
||||
"""etat de l'inscription de cet étudiant au semestre:
|
||||
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
||||
"""
|
||||
# voir si ce n'est pas trop lent:
|
||||
ins = models.FormSemestreInscription.query.filter_by(
|
||||
etudid=self.id, formsemestre_id=formsemestre_id
|
||||
).first()
|
||||
if ins:
|
||||
return ins.etat
|
||||
return False
|
||||
|
||||
|
||||
class Adresse(db.Model):
|
||||
"""Adresse d'un étudiant
|
||||
|
@ -149,10 +222,13 @@ class ItemSuiviTag(db.Model):
|
|||
# Association tag <-> module
|
||||
itemsuivi_tags_assoc = db.Table(
|
||||
"itemsuivi_tags_assoc",
|
||||
db.Column("tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id")),
|
||||
db.Column("itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id")),
|
||||
db.Column(
|
||||
"tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id", ondelete="CASCADE")
|
||||
),
|
||||
db.Column(
|
||||
"itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id", ondelete="CASCADE")
|
||||
),
|
||||
)
|
||||
# ON DELETE CASCADE ?
|
||||
|
||||
|
||||
class EtudAnnotation(db.Model):
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""ScoDoc models: evaluations
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models import UniteEns
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_evaluation_db
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
|
||||
__tablename__ = "notes_evaluation"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
evaluation_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
||||
)
|
||||
jour = db.Column(db.Date)
|
||||
heure_debut = db.Column(db.Time)
|
||||
heure_fin = db.Column(db.Time)
|
||||
description = db.Column(db.Text)
|
||||
note_max = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float)
|
||||
visibulletin = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
publish_incomplete = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
|
||||
evaluation_type = db.Column(
|
||||
db.Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
# ordre de presentation (par défaut, le plus petit numero
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">"""
|
||||
|
||||
def to_dict(self):
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
e["evaluation_id"] = self.id
|
||||
e["jour"] = ndb.DateISOtoDMY(e["jour"])
|
||||
e["numero"] = ndb.int_null_is_zero(e["numero"])
|
||||
return sco_evaluation_db.evaluation_enrich_dict(e)
|
||||
|
||||
def from_dict(self, data):
|
||||
"""Set evaluation attributes from given dict values."""
|
||||
sco_evaluation_db._check_evaluation_args(data)
|
||||
for k in self.__dict__.keys():
|
||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||
setattr(self, k, data[k])
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
Attention: la copie n'a pas d'id avant le prochain commit
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("id") # get rid of id
|
||||
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
|
||||
for k in not_copying:
|
||||
d.pop(k)
|
||||
copy = self.__class__(**d)
|
||||
db.session.add(copy)
|
||||
return copy
|
||||
|
||||
def set_ue_poids(self, ue, poids: float) -> None:
|
||||
"""Set poids évaluation vers cette UE"""
|
||||
self.update_ue_poids_dict({ue.id: poids})
|
||||
|
||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""set poids vers les UE (remplace existants)
|
||||
ue_poids_dict = { ue_id : poids }
|
||||
"""
|
||||
L = []
|
||||
for ue_id, poids in ue_poids_dict.items():
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
||||
self.ue_poids = L
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""update poids vers UE (ajoute aux existants)"""
|
||||
current = self.get_ue_poids_dict()
|
||||
current.update(ue_poids_dict)
|
||||
self.set_ue_poids_dict(current)
|
||||
|
||||
def get_ue_poids_dict(self) -> dict:
|
||||
"""returns { ue_id : poids }"""
|
||||
return {p.ue.id: p.poids for p in self.ue_poids}
|
||||
|
||||
def get_ue_poids_str(self) -> str:
|
||||
"""string describing poids, for excel cells and pdfs
|
||||
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||
ils ne sont pas affichés.
|
||||
"""
|
||||
return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids])
|
||||
|
||||
|
||||
class EvaluationUEPoids(db.Model):
|
||||
"""Poids des évaluations (BUT)
|
||||
association many to many
|
||||
"""
|
||||
|
||||
evaluation_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_evaluation.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
poids = db.Column(
|
||||
db.Float,
|
||||
nullable=False,
|
||||
)
|
||||
evaluation = db.relationship(
|
||||
Evaluation,
|
||||
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
||||
)
|
||||
ue = db.relationship(
|
||||
UniteEns,
|
||||
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
|
|
@ -1,13 +1,16 @@
|
|||
"""ScoDoc8 models : Formations (hors BUT)
|
||||
"""ScoDoc 9 models : Formations
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.comp import df_cache
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class NotesFormation(db.Model):
|
||||
class Formation(db.Model):
|
||||
"""Programme pédagogique d'une formation"""
|
||||
|
||||
__tablename__ = "notes_formations"
|
||||
|
@ -30,51 +33,97 @@ class NotesFormation(db.Model):
|
|||
type_parcours = db.Column(db.Integer, default=0, server_default="0")
|
||||
code_specialite = db.Column(db.String(SHORT_STR_LEN))
|
||||
|
||||
ues = db.relationship("NotesUE", backref="formation", lazy="dynamic")
|
||||
# Optionnel, pour les formations type BUT
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id")
|
||||
)
|
||||
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
|
||||
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
||||
ues = db.relationship("NotesUE", lazy="dynamic", backref="formation")
|
||||
ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>"
|
||||
|
||||
def to_dict(self):
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["formation_id"] = self.id
|
||||
return e
|
||||
|
||||
class NotesUE(db.Model):
|
||||
"""Unité d'Enseignement"""
|
||||
def get_parcours(self):
|
||||
"""get l'instance de TypeParcours de cette formation"""
|
||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
||||
|
||||
__tablename__ = "notes_ue"
|
||||
def is_apc(self):
|
||||
"True si formation APC avec SAE (BUT)"
|
||||
return self.get_parcours().APC_SAE
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
ue_id = db.synonym("id")
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
acronyme = db.Column(db.Text(), nullable=False)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
titre = db.Column(db.Text())
|
||||
# Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)",
|
||||
# 4 "élective"
|
||||
type = db.Column(db.Integer, default=0, server_default="0")
|
||||
# Les UE sont "compatibles" (pour la capitalisation) ssi elles ont ^m code
|
||||
# note: la fonction SQL notes_newid_ucod doit être créée à part
|
||||
ue_code = db.Column(
|
||||
db.String(SHORT_STR_LEN),
|
||||
server_default=db.text("notes_newid_ucod()"),
|
||||
nullable=False,
|
||||
)
|
||||
ects = db.Column(db.Float) # nombre de credits ECTS
|
||||
is_external = db.Column(db.Boolean(), default=False, server_default="false")
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||
coefficient = db.Column(db.Float)
|
||||
def get_module_coefs(self, semestre_idx: int = None):
|
||||
"""Les coefs des modules vers les UE (accès via cache)"""
|
||||
from app.comp import moy_ue
|
||||
|
||||
# relations
|
||||
matieres = db.relationship("NotesMatiere", lazy="dynamic", backref="ue")
|
||||
modules = db.relationship("NotesModule", lazy="dynamic", backref="ue")
|
||||
if semestre_idx is None:
|
||||
key = f"{self.id}"
|
||||
else:
|
||||
key = f"{self.id}.{semestre_idx}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
|
||||
modules_coefficients = df_cache.ModuleCoefsCache.get(key)
|
||||
if modules_coefficients is None:
|
||||
modules_coefficients, _, _ = moy_ue.df_load_module_coefs(
|
||||
self.id, semestre_idx
|
||||
)
|
||||
df_cache.ModuleCoefsCache.set(key, modules_coefficients)
|
||||
return modules_coefficients
|
||||
|
||||
def has_locked_sems(self):
|
||||
"True if there is a locked formsemestre in this formation"
|
||||
return len(self.formsemestres.filter_by(etat=False).all()) > 0
|
||||
|
||||
def invalidate_module_coefs(self, semestre_idx: int = None):
|
||||
"""Invalide les coefficients de modules cachés.
|
||||
Si semestre_idx est None, invalide tous les semestres,
|
||||
sinon invalide le semestre indiqué et le cache de la formation.
|
||||
"""
|
||||
if semestre_idx is None:
|
||||
keys = {f"{self.id}.{m.semestre_id}" for m in self.modules}
|
||||
else:
|
||||
keys = f"{self.id}.{semestre_idx}"
|
||||
df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"})
|
||||
sco_cache.invalidate_formsemestre()
|
||||
|
||||
def invalidate_cached_sems(self):
|
||||
for sem in self.formsemestres:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
|
||||
|
||||
def force_semestre_modules_aux_ues(self) -> None:
|
||||
"""
|
||||
Affecte à chaque module de cette formation le semestre de son UE de rattachement,
|
||||
si elle en a une.
|
||||
Devrait être appelé lorsqu'on change le type de formation vers le BUT, et aussi
|
||||
lorsqu'on change le semestre d'une UE BUT.
|
||||
Utile pour la migration des anciennes formations vers le BUT.
|
||||
Invalide les caches coefs/poids.
|
||||
"""
|
||||
if not self.is_apc():
|
||||
return
|
||||
change = False
|
||||
for mod in self.modules:
|
||||
if (
|
||||
mod.ue.semestre_idx is not None
|
||||
and mod.ue.semestre_idx > 0
|
||||
and mod.semestre_id != mod.ue.semestre_idx
|
||||
):
|
||||
mod.semestre_id = mod.ue.semestre_idx
|
||||
db.session.add(mod)
|
||||
change = True
|
||||
db.session.commit()
|
||||
if change:
|
||||
self.invalidate_module_coefs()
|
||||
|
||||
|
||||
class NotesMatiere(db.Model):
|
||||
class Matiere(db.Model):
|
||||
"""Matières: regroupe les modules d'une UE
|
||||
La matière a peu d'utilité en dehors de la présentation des modules
|
||||
d'une UE.
|
||||
|
@ -89,60 +138,4 @@ class NotesMatiere(db.Model):
|
|||
titre = db.Column(db.Text())
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
|
||||
modules = db.relationship("NotesModule", lazy="dynamic", backref="matiere")
|
||||
|
||||
|
||||
class NotesModule(db.Model):
|
||||
"""Module"""
|
||||
|
||||
__tablename__ = "notes_modules"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
module_id = db.synonym("id")
|
||||
titre = db.Column(db.Text())
|
||||
abbrev = db.Column(db.Text()) # nom court
|
||||
# certains départements ont des codes infiniment longs: donc Text !
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
heures_cours = db.Column(db.Float)
|
||||
heures_td = db.Column(db.Float)
|
||||
heures_tp = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float) # coef PPN
|
||||
ects = db.Column(db.Float) # Crédits ECTS
|
||||
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
|
||||
# pas un id mais le numéro du semestre: 1, 2, ...
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
module_type = db.Column(db.Integer) # NULL ou 0:defaut, 1: malus (NOTES_MALUS)
|
||||
# Relations:
|
||||
modimpls = db.relationship("NotesModuleImpl", backref="module", lazy="dynamic")
|
||||
|
||||
|
||||
class NotesTag(db.Model):
|
||||
"""Tag sur un module"""
|
||||
|
||||
__tablename__ = "notes_tags"
|
||||
__table_args__ = (db.UniqueConstraint("title", "dept_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tag_id = db.synonym("id")
|
||||
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
title = db.Column(db.Text(), nullable=False)
|
||||
|
||||
|
||||
# Association tag <-> module
|
||||
notes_modules_tags = db.Table(
|
||||
"notes_modules_tags",
|
||||
db.Column(
|
||||
"tag_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
|
||||
),
|
||||
db.Column(
|
||||
"module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
|
||||
),
|
||||
)
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""ScoDoc models
|
||||
"""ScoDoc models: formsemestre
|
||||
"""
|
||||
from typing import Any
|
||||
import datetime
|
||||
|
||||
import flask_sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models import UniteEns
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.modules import Module
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
|
||||
|
||||
class FormSemestre(db.Model):
|
||||
"""Mise en oeuvre d'un semestre de formation
|
||||
was notes_formsemestre
|
||||
"""
|
||||
"""Mise en oeuvre d'un semestre de formation"""
|
||||
|
||||
__tablename__ = "notes_formsemestre"
|
||||
|
||||
|
@ -32,7 +42,7 @@ class FormSemestre(db.Model):
|
|||
) # False si verrouillé
|
||||
modalite = db.Column(
|
||||
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
||||
)
|
||||
) # "FI", "FAP", "FC", ...
|
||||
# gestion compensation sem DUT:
|
||||
gestion_compensation = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
|
@ -72,20 +82,172 @@ class FormSemestre(db.Model):
|
|||
|
||||
# Relations:
|
||||
etapes = db.relationship(
|
||||
"NotesFormsemestreEtape", cascade="all,delete", backref="formsemestre"
|
||||
"FormSemestreEtape", cascade="all,delete", backref="formsemestre"
|
||||
)
|
||||
formsemestres = db.relationship(
|
||||
"NotesModuleImpl", backref="formsemestre", lazy="dynamic"
|
||||
modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic")
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="notes_formsemestre_inscription",
|
||||
viewonly=True,
|
||||
lazy="dynamic",
|
||||
)
|
||||
responsables = db.relationship(
|
||||
"User",
|
||||
secondary="notes_formsemestre_responsables",
|
||||
lazy=True,
|
||||
backref=db.backref("formsemestres", lazy=True),
|
||||
)
|
||||
|
||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archive
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FormSemestre, self).__init__(**kwargs)
|
||||
if self.modalite is None:
|
||||
self.modalite = NotesFormModalite.DEFAULT_MODALITE
|
||||
self.modalite = FormationModalite.DEFAULT_MODALITE
|
||||
|
||||
def to_dict(self):
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
d["formsemestre_id"] = self.id
|
||||
d["date_debut"] = (
|
||||
self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
|
||||
)
|
||||
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") if self.date_fin else ""
|
||||
d["responsables"] = [u.id for u in self.responsables]
|
||||
return d
|
||||
|
||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||
"""UE des modules de ce semestre, triées par numéro.
|
||||
- Formations classiques: les UEs auxquelles appartiennent
|
||||
les modules mis en place dans ce semestre.
|
||||
- Formations APC / BUT: les UEs de la formation qui ont
|
||||
le même numéro de semestre que ce formsemestre.
|
||||
"""
|
||||
if self.formation.get_parcours().APC_SAE:
|
||||
sem_ues = UniteEns.query.filter_by(
|
||||
formation=self.formation, semestre_idx=self.semestre_id
|
||||
)
|
||||
else:
|
||||
sem_ues = db.session.query(UniteEns).filter(
|
||||
ModuleImpl.formsemestre_id == self.id,
|
||||
Module.id == ModuleImpl.module_id,
|
||||
UniteEns.id == Module.ue_id,
|
||||
)
|
||||
if not with_sport:
|
||||
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
||||
return sem_ues.order_by(UniteEns.numero)
|
||||
|
||||
def est_courant(self) -> bool:
|
||||
"""Vrai si la date actuelle (now) est dans le semestre
|
||||
(les dates de début et fin sont incluses)
|
||||
"""
|
||||
today = datetime.date.today()
|
||||
return (self.date_debut <= today) and (today <= self.date_fin)
|
||||
|
||||
def est_decale(self):
|
||||
"""Vrai si semestre "décalé"
|
||||
c'est à dire semestres impairs commençant entre janvier et juin
|
||||
et les pairs entre juillet et decembre
|
||||
"""
|
||||
if self.semestre_id <= 0:
|
||||
return False # formations sans semestres
|
||||
return (self.semestre_id % 2 and self.date_debut.month <= 6) or (
|
||||
not self.semestre_id % 2 and self.date_debut.month > 6
|
||||
)
|
||||
|
||||
def etapes_apo_str(self) -> str:
|
||||
"""Chaine décrivant les étapes de ce semestre
|
||||
ex: "V1RT, V1RT3, V1RT4"
|
||||
"""
|
||||
if not self.etapes:
|
||||
return ""
|
||||
return ", ".join([str(x.etape_apo) for x in self.etapes])
|
||||
|
||||
def responsables_str(self, abbrev_prenom=True) -> str:
|
||||
"""chaîne "J. Dupond, X. Martin"
|
||||
ou "Jacques Dupond, Xavier Martin"
|
||||
"""
|
||||
if not self.responsables:
|
||||
return ""
|
||||
if abbrev_prenom:
|
||||
return ", ".join([u.get_prenomnom() for u in self.responsables])
|
||||
else:
|
||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||
|
||||
def annee_scolaire_str(self):
|
||||
"2021 - 2022"
|
||||
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
|
||||
|
||||
def session_id(self) -> str:
|
||||
"""identifiant externe de semestre de formation
|
||||
Exemple: RT-DUT-FI-S1-ANNEE
|
||||
|
||||
DEPT-TYPE-MODALITE+-S?|SPECIALITE
|
||||
|
||||
TYPE=DUT|LP*|M*
|
||||
MODALITE=FC|FI|FA (si plusieurs, en inverse alpha)
|
||||
|
||||
SPECIALITE=[A-Z]+ EON,ASSUR, ... (si pas Sn ou SnD)
|
||||
|
||||
ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013)
|
||||
"""
|
||||
imputation_dept = sco_preferences.get_preference("ImputationDept", self.id)
|
||||
if not imputation_dept:
|
||||
imputation_dept = sco_preferences.get_preference("DeptName")
|
||||
imputation_dept = imputation_dept.upper()
|
||||
parcours_name = self.formation.get_parcours().NAME
|
||||
modalite = self.modalite
|
||||
# exception pour code Apprentissage:
|
||||
modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA")
|
||||
if self.semestre_id > 0:
|
||||
decale = "D" if self.est_decale() else ""
|
||||
semestre_id = f"S{self.semestre_id}{decale}"
|
||||
else:
|
||||
semestre_id = self.formation.code_specialite or ""
|
||||
annee_sco = str(
|
||||
scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||
)
|
||||
return scu.sanitize_string(
|
||||
"-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco))
|
||||
)
|
||||
|
||||
def titre_mois(self) -> str:
|
||||
"""Le titre et les dates du semestre, pour affichage dans des listes
|
||||
Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)"
|
||||
"""
|
||||
return f"""{self.titre_num()} {self.modalite or ''} ({
|
||||
scu.MONTH_NAMES_ABBREV[self.date_debut.month-1]} {
|
||||
self.date_debut.year} - {
|
||||
scu.MONTH_NAMES_ABBREV[self.date_fin.month -1]} {
|
||||
self.date_fin.year})"""
|
||||
|
||||
def titre_num(self) -> str:
|
||||
"""Le titre est le semestre, ex ""DUT Informatique semestre 2"" """
|
||||
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
|
||||
return self.titre
|
||||
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
|
||||
|
||||
def get_abs_count(self, etudid):
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
from app.scodoc import sco_abs
|
||||
|
||||
return sco_abs.get_abs_count_in_interval(
|
||||
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
||||
)
|
||||
|
||||
def get_inscrits(self, include_dem=False) -> list:
|
||||
"""Liste des étudiants inscrits à ce semestre
|
||||
Si all, tous les étudiants, avec les démissionnaires.
|
||||
"""
|
||||
if include_dem:
|
||||
return [ins.etud for ins in self.inscriptions]
|
||||
else:
|
||||
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
|
||||
|
||||
|
||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||
|
@ -100,7 +262,7 @@ notes_formsemestre_responsables = db.Table(
|
|||
)
|
||||
|
||||
|
||||
class NotesFormsemestreEtape(db.Model):
|
||||
class FormSemestreEtape(db.Model):
|
||||
"""Étape Apogée associées au semestre"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_etapes"
|
||||
|
@ -109,10 +271,16 @@ class NotesFormsemestreEtape(db.Model):
|
|||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
etape_apo = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Etape {self.id} apo={self.etape_apo}>"
|
||||
|
||||
def as_apovdi(self):
|
||||
return ApoEtapeVDI(self.etape_apo)
|
||||
|
||||
|
||||
class NotesFormModalite(db.Model):
|
||||
class FormationModalite(db.Model):
|
||||
"""Modalités de formation, utilisées pour la présentation
|
||||
(grouper les semestres, générer des codes, etc.)
|
||||
"""
|
||||
|
@ -139,7 +307,7 @@ class NotesFormModalite(db.Model):
|
|||
numero = 0
|
||||
try:
|
||||
for (code, titre) in (
|
||||
(NotesFormModalite.DEFAULT_MODALITE, "Formation Initiale"),
|
||||
(FormationModalite.DEFAULT_MODALITE, "Formation Initiale"),
|
||||
("FAP", "Apprentissage"),
|
||||
("FC", "Formation Continue"),
|
||||
("DEC", "Formation Décalées"),
|
||||
|
@ -150,9 +318,9 @@ class NotesFormModalite(db.Model):
|
|||
("EXT", "Extérieur"),
|
||||
("OTHER", "Autres formations"),
|
||||
):
|
||||
modalite = NotesFormModalite.query.filter_by(modalite=code).first()
|
||||
modalite = FormationModalite.query.filter_by(modalite=code).first()
|
||||
if modalite is None:
|
||||
modalite = NotesFormModalite(
|
||||
modalite = FormationModalite(
|
||||
modalite=code, titre=titre, numero=numero
|
||||
)
|
||||
db.session.add(modalite)
|
||||
|
@ -163,7 +331,7 @@ class NotesFormModalite(db.Model):
|
|||
raise
|
||||
|
||||
|
||||
class NotesFormsemestreUECoef(db.Model):
|
||||
class FormSemestreUECoef(db.Model):
|
||||
"""Coef des UE capitalisees arrivant dans ce semestre"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_uecoef"
|
||||
|
@ -182,7 +350,7 @@ class NotesFormsemestreUECoef(db.Model):
|
|||
coefficient = db.Column(db.Float, nullable=False)
|
||||
|
||||
|
||||
class NotesFormsemestreUEComputationExpr(db.Model):
|
||||
class FormSemestreUEComputationExpr(db.Model):
|
||||
"""Formules utilisateurs pour calcul moyenne UE"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_ue_computation_expr"
|
||||
|
@ -202,7 +370,7 @@ class NotesFormsemestreUEComputationExpr(db.Model):
|
|||
computation_expr = db.Column(db.Text())
|
||||
|
||||
|
||||
class NotesFormsemestreCustomMenu(db.Model):
|
||||
class FormSemestreCustomMenu(db.Model):
|
||||
"""Menu custom associe au semestre"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_custommenu"
|
||||
|
@ -218,7 +386,7 @@ class NotesFormsemestreCustomMenu(db.Model):
|
|||
idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu
|
||||
|
||||
|
||||
class NotesFormsemestreInscription(db.Model):
|
||||
class FormSemestreInscription(db.Model):
|
||||
"""Inscription à un semestre de formation"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_inscription"
|
||||
|
@ -227,100 +395,30 @@ class NotesFormsemestreInscription(db.Model):
|
|||
id = db.Column(db.Integer, primary_key=True)
|
||||
formsemestre_inscription_id = db.synonym("id")
|
||||
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"))
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
etud = db.relationship(
|
||||
Identite,
|
||||
backref=db.backref("formsemestre_inscriptions", cascade="all, delete-orphan"),
|
||||
)
|
||||
formsemestre = db.relationship(
|
||||
FormSemestre,
|
||||
backref=db.backref(
|
||||
"inscriptions",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="FormSemestreInscription.etudid",
|
||||
),
|
||||
)
|
||||
# I inscrit, D demission en cours de semestre, DEF si "defaillant"
|
||||
etat = db.Column(db.String(CODE_STR_LEN))
|
||||
etat = db.Column(db.String(CODE_STR_LEN), index=True)
|
||||
# etape apogee d'inscription (experimental 2020)
|
||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
|
||||
|
||||
class NotesModuleImpl(db.Model):
|
||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl"
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_id = db.synonym("id")
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id"),
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
computation_expr = db.Column(db.Text())
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
notes_modules_enseignants = db.Table(
|
||||
"notes_modules_enseignants",
|
||||
db.Column(
|
||||
"moduleimpl_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
),
|
||||
db.Column("ens_id", db.Integer, db.ForeignKey("user.id")),
|
||||
# ? db.UniqueConstraint("moduleimpl_id", "ens_id"),
|
||||
)
|
||||
# XXX il manque probablement une relation pour gérer cela
|
||||
|
||||
|
||||
class NotesModuleImplInscription(db.Model):
|
||||
"""Inscription à un module (etudiants,moduleimpl)"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl_inscription"
|
||||
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_inscription_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
index=True,
|
||||
)
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
||||
|
||||
|
||||
class NotesEvaluation(db.Model):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
|
||||
__tablename__ = "notes_evaluation"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
evaluation_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
||||
)
|
||||
jour = db.Column(db.Date)
|
||||
heure_debut = db.Column(db.Time)
|
||||
heure_fin = db.Column(db.Time)
|
||||
description = db.Column(db.Text)
|
||||
note_max = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float)
|
||||
visibulletin = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
publish_incomplete = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
|
||||
evaluation_type = db.Column(
|
||||
db.Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
# ordre de presentation (par défaut, le plus petit numero
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer)
|
||||
|
||||
|
||||
class NotesSemSet(db.Model):
|
||||
"""semsets: ensemble de formsemestres pour exports Apogée"""
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
"""ScoDoc models: moduleimpls
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app.comp import df_cache
|
||||
from app.models import UniteEns, Identite
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ModuleImpl(db.Model):
|
||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl"
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_id = db.synonym("id")
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id"),
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
computation_expr = db.Column(db.Text())
|
||||
|
||||
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
|
||||
enseignants = db.relationship(
|
||||
"User",
|
||||
secondary="notes_modules_enseignants",
|
||||
lazy="dynamic",
|
||||
backref="moduleimpl",
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ModuleImpl, self).__init__(**kwargs)
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||
if evaluations_poids is None:
|
||||
from app.comp import moy_mod
|
||||
|
||||
evaluations_poids, _ = moy_mod.df_load_evaluations_poids(self.id)
|
||||
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
|
||||
return evaluations_poids
|
||||
|
||||
def invalidate_evaluations_poids(self):
|
||||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(self) -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
"""
|
||||
if not self.module.formation.get_parcours().APC_SAE or (
|
||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||
and self.module.module_type != scu.ModuleType.SAE
|
||||
):
|
||||
return True
|
||||
from app.comp import moy_mod
|
||||
|
||||
return moy_mod.check_moduleimpl_conformity(
|
||||
self,
|
||||
self.get_evaluations_poids(),
|
||||
self.module.formation.get_module_coefs(self.module.semestre_id),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["moduleimpl_id"] = self.id
|
||||
e["ens"] = [
|
||||
{"moduleimpl_id": self.id, "ens_id": e.id} for e in self.enseignants
|
||||
]
|
||||
e["module"] = self.module.to_dict()
|
||||
return e
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
notes_modules_enseignants = db.Table(
|
||||
"notes_modules_enseignants",
|
||||
db.Column(
|
||||
"moduleimpl_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
),
|
||||
db.Column("ens_id", db.Integer, db.ForeignKey("user.id")),
|
||||
# ? db.UniqueConstraint("moduleimpl_id", "ens_id"),
|
||||
)
|
||||
# XXX il manque probablement une relation pour gérer cela
|
||||
|
||||
|
||||
class ModuleImplInscription(db.Model):
|
||||
"""Inscription à un module (etudiants,moduleimpl)"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl_inscription"
|
||||
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_inscription_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
index=True,
|
||||
)
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
||||
etud = db.relationship(
|
||||
Identite,
|
||||
backref=db.backref("moduleimpl_inscriptions", cascade="all, delete-orphan"),
|
||||
)
|
||||
modimpl = db.relationship(
|
||||
ModuleImpl,
|
||||
backref=db.backref("inscriptions", cascade="all, delete-orphan"),
|
||||
)
|
|
@ -0,0 +1,204 @@
|
|||
"""ScoDoc 9 models : Modules
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class Module(db.Model):
|
||||
"""Module"""
|
||||
|
||||
__tablename__ = "notes_modules"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
module_id = db.synonym("id")
|
||||
titre = db.Column(db.Text())
|
||||
abbrev = db.Column(db.Text()) # nom court
|
||||
# certains départements ont des codes infiniment longs: donc Text !
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
heures_cours = db.Column(db.Float)
|
||||
heures_td = db.Column(db.Float)
|
||||
heures_tp = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float) # coef PPN (sauf en APC)
|
||||
ects = db.Column(db.Float) # Crédits ECTS
|
||||
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
|
||||
# pas un id mais le numéro du semestre: 1, 2, ...
|
||||
# note: en APC, le semestre qui fait autorité est celui de l'UE
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
|
||||
module_type = db.Column(db.Integer)
|
||||
# Relations:
|
||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
||||
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
||||
tags = db.relationship(
|
||||
"NotesTag",
|
||||
secondary="notes_modules_tags",
|
||||
lazy=True,
|
||||
backref=db.backref("modules", lazy=True),
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.ue_coefs = []
|
||||
super(Module, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<Module{ModuleType(self.module_type).name} id={self.id} code={self.code}>"
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["module_id"] = self.id
|
||||
e["heures_cours"] = 0.0 if self.heures_cours is None else self.heures_cours
|
||||
e["heures_td"] = 0.0 if self.heures_td is None else self.heures_td
|
||||
e["heures_tp"] = 0.0 if self.heures_tp is None else self.heures_tp
|
||||
e["numero"] = 0 if self.numero is None else self.numero
|
||||
e["coefficient"] = 0.0 if self.coefficient is None else self.coefficient
|
||||
e["module_type"] = 0 if self.module_type is None else self.module_type
|
||||
return e
|
||||
|
||||
def is_apc(self):
|
||||
"True si module SAÉ ou Ressource"
|
||||
return self.module_type and scu.ModuleType(self.module_type) in {
|
||||
scu.ModuleType.RESSOURCE,
|
||||
scu.ModuleType.SAE,
|
||||
}
|
||||
|
||||
def type_name(self):
|
||||
return scu.MODULE_TYPE_NAMES[self.module_type]
|
||||
|
||||
def set_ue_coef(self, ue, coef: float) -> None:
|
||||
"""Set coef module vers cette UE"""
|
||||
self.update_ue_coef_dict({ue.id: coef})
|
||||
|
||||
def set_ue_coef_dict(self, ue_coef_dict: dict) -> None:
|
||||
"""set coefs vers les UE (remplace existants)
|
||||
ue_coef_dict = { ue_id : coef }
|
||||
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
|
||||
"""
|
||||
changed = False
|
||||
for ue_id, coef in ue_coef_dict.items():
|
||||
# Existant ?
|
||||
coefs = [c for c in self.ue_coefs if c.ue_id == ue_id]
|
||||
if coefs:
|
||||
ue_coef = coefs[0]
|
||||
if coef == 0.0: # supprime ce coef
|
||||
db.session.delete(ue_coef)
|
||||
changed = True
|
||||
elif coef != ue_coef.coef:
|
||||
ue_coef.coef = coef
|
||||
db.session.add(ue_coef)
|
||||
changed = True
|
||||
else:
|
||||
# crée nouveau coef:
|
||||
if coef != 0.0:
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
|
||||
self.ue_coefs.append(ue_coef)
|
||||
changed = True
|
||||
if changed:
|
||||
self.formation.invalidate_module_coefs()
|
||||
|
||||
def update_ue_coef_dict(self, ue_coef_dict: dict):
|
||||
"""update coefs vers UE (ajoute aux existants)"""
|
||||
current = self.get_ue_coef_dict()
|
||||
current.update(ue_coef_dict)
|
||||
self.set_ue_coef_dict(current)
|
||||
|
||||
def get_ue_coef_dict(self):
|
||||
"""returns { ue_id : coef }"""
|
||||
return {p.ue.id: p.coef for p in self.ue_coefs}
|
||||
|
||||
def delete_ue_coef(self, ue):
|
||||
"""delete coef"""
|
||||
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
||||
if ue_coef:
|
||||
db.session.delete(ue_coef)
|
||||
self.formation.invalidate_module_coefs()
|
||||
|
||||
def get_ue_coefs_sorted(self):
|
||||
"les coefs d'UE, trié par numéro d'UE"
|
||||
# je n'ai pas su mettre un order_by sur le backref sans avoir
|
||||
# à redéfinir les relationships...
|
||||
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||
|
||||
def ue_coefs_descr(self):
|
||||
"""List of tuples [ (ue_acronyme, coef) ]"""
|
||||
return [(c.ue.acronyme, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||
|
||||
|
||||
class ModuleUECoef(db.Model):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
En mode APC, ces coefs remplacent le coefficient "PPN" du module.
|
||||
"""
|
||||
|
||||
__tablename__ = "module_ue_coef"
|
||||
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
coef = db.Column(
|
||||
db.Float,
|
||||
nullable=False,
|
||||
)
|
||||
module = db.relationship(
|
||||
Module,
|
||||
backref=db.backref(
|
||||
"ue_coefs",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
||||
ue = db.relationship(
|
||||
"UniteEns",
|
||||
backref=db.backref(
|
||||
"module_ue_coefs",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class NotesTag(db.Model):
|
||||
"""Tag sur un module"""
|
||||
|
||||
__tablename__ = "notes_tags"
|
||||
__table_args__ = (db.UniqueConstraint("title", "dept_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tag_id = db.synonym("id")
|
||||
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
title = db.Column(db.Text(), nullable=False)
|
||||
|
||||
|
||||
# Association tag <-> module
|
||||
notes_modules_tags = db.Table(
|
||||
"notes_modules_tags",
|
||||
db.Column(
|
||||
"tag_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
|
||||
),
|
||||
db.Column(
|
||||
"module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
|
||||
),
|
||||
)
|
||||
|
||||
from app.models.ues import UniteEns
|
|
@ -40,7 +40,7 @@ class ScolarEvent(db.Model):
|
|||
)
|
||||
|
||||
|
||||
class ScolarFormsemestreValidation(db.Model):
|
||||
class ScolarFormSemestreValidation(db.Model):
|
||||
"""Décisions de jury"""
|
||||
|
||||
__tablename__ = "scolar_formsemestre_validation"
|
||||
|
@ -52,16 +52,19 @@ class ScolarFormsemestreValidation(db.Model):
|
|||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id"),
|
||||
index=True,
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id"),
|
||||
index=True,
|
||||
)
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False)
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||
# NULL pour les UE, True|False pour les semestres:
|
||||
assidu = db.Column(db.Boolean)
|
||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
@ -100,9 +103,10 @@ class ScolarAutorisationInscription(db.Model):
|
|||
)
|
||||
|
||||
|
||||
class NotesAppreciations(db.Model):
|
||||
class BulAppreciations(db.Model):
|
||||
"""Appréciations sur bulletins"""
|
||||
|
||||
__tablename__ = "notes_appreciations"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
etudid = db.Column(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"""
|
||||
from app import db, log
|
||||
from app.scodoc import bonus_sport
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class ScoPreference(db.Model):
|
||||
|
@ -94,7 +95,7 @@ class ScoDocSiteConfig(db.Model):
|
|||
"""returns bonus func with specified name.
|
||||
If name not specified, return the configured function.
|
||||
None if no bonus function configured.
|
||||
Raises NameError if func_name not found in module bonus_sport.
|
||||
Raises ScoValueError if func_name not found in module bonus_sport.
|
||||
"""
|
||||
if func_name is None:
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
|
@ -103,7 +104,13 @@ class ScoDocSiteConfig(db.Model):
|
|||
func_name = c.value
|
||||
if func_name == "": # pas de bonus défini
|
||||
return None
|
||||
return getattr(bonus_sport, func_name)
|
||||
try:
|
||||
return getattr(bonus_sport, func_name)
|
||||
except AttributeError:
|
||||
raise ScoValueError(
|
||||
f"""Fonction de calcul maison inexistante: {func_name}.
|
||||
(contacter votre administrateur local)."""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_names(cls):
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class UniteEns(db.Model):
|
||||
"""Unité d'Enseignement (UE)"""
|
||||
|
||||
__tablename__ = "notes_ue"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
ue_id = db.synonym("id")
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
acronyme = db.Column(db.Text(), nullable=False)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
titre = db.Column(db.Text())
|
||||
# Le semestre_idx n'est pas un id mais le numéro du semestre: 1, 2, ...
|
||||
# En ScoDoc7 et pour les formations classiques, il est NULL
|
||||
# (le numéro du semestre étant alors déterminé par celui des modules de l'UE)
|
||||
# Pour les formations APC, il est obligatoire (de 1 à 6 pour le BUT):
|
||||
semestre_idx = db.Column(db.Integer, nullable=True, index=True)
|
||||
# Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)",
|
||||
# 4 "élective"
|
||||
type = db.Column(db.Integer, default=0, server_default="0")
|
||||
# Les UE sont "compatibles" (pour la capitalisation) ssi elles ont ^m code
|
||||
# note: la fonction SQL notes_newid_ucod doit être créée à part
|
||||
ue_code = db.Column(
|
||||
db.String(SHORT_STR_LEN),
|
||||
server_default=db.text("notes_newid_ucod()"),
|
||||
nullable=False,
|
||||
)
|
||||
ects = db.Column(db.Float) # nombre de credits ECTS
|
||||
is_external = db.Column(db.Boolean(), default=False, server_default="false")
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||
coefficient = db.Column(db.Float)
|
||||
|
||||
# relations
|
||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
|
||||
|
||||
def to_dict(self):
|
||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
e["ue_id"] = self.id
|
||||
e["numero"] = e["numero"] if e["numero"] else 0
|
||||
e["ects"] = e["ects"] if e["ects"] else 0.0
|
||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||
return e
|
||||
|
||||
def is_locked(self):
|
||||
"""True if UE should not be modified
|
||||
(contains modules used in a locked formsemestre)
|
||||
"""
|
||||
# XXX todo : à ré-écrire avec SQLAlchemy
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
return sco_edit_ue.ue_is_locked(self.id)
|
||||
|
||||
def guess_semestre_idx(self) -> None:
|
||||
"""Lorsqu'on prend une ancienne formation non APC,
|
||||
les UE n'ont pas d'indication de semestre.
|
||||
Cette méthode fixe le semestre en prenant celui du premier module,
|
||||
ou à défaut le met à 1.
|
||||
"""
|
||||
if self.semestre_idx is None:
|
||||
module = self.modules.first()
|
||||
if module is None:
|
||||
self.semestre_idx = 1
|
||||
else:
|
||||
self.semestre_idx = module.semestre_id
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def get_ressources(self):
|
||||
"Liste des modules ressources rattachés à cette UE"
|
||||
return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all()
|
||||
|
||||
def get_saes(self):
|
||||
"Liste des modules SAE rattachés à cette UE"
|
||||
return self.modules.filter_by(module_type=scu.ModuleType.SAE).all()
|
||||
|
||||
def get_modules_not_apc(self):
|
||||
"Listes des modules non SAE et non ressource (standards, mais aussi bonus...)"
|
||||
return self.modules.filter(
|
||||
(Module.module_type != scu.ModuleType.SAE),
|
||||
(Module.module_type != scu.ModuleType.RESSOURCE),
|
||||
).all()
|
|
@ -44,6 +44,7 @@ import unicodedata
|
|||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
PE_DEBUG = 0
|
||||
|
||||
|
@ -201,11 +202,11 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
|
|||
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
|
||||
|
||||
# Logos: (add to logos/ directory in zip)
|
||||
logos_names = ["logo_header.jpg", "logo_footer.jpg"]
|
||||
for f in logos_names:
|
||||
logo = os.path.join(scu.SCODOC_LOGOS_DIR, f)
|
||||
if os.path.isfile(logo):
|
||||
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f)
|
||||
logos_names = ["header", "footer"]
|
||||
for name in logos_names:
|
||||
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
||||
if logo is not None:
|
||||
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
|
|
@ -51,7 +51,7 @@ def TrivialFormulator(
|
|||
allow_null : if true, field can be left empty (default true)
|
||||
type : 'string', 'int', 'float' (default to string), 'list' (only for hidden)
|
||||
readonly : default False. if True, no form element, display current value.
|
||||
convert_numbers: covert int and float values (from string)
|
||||
convert_numbers: convert int and float values (from string)
|
||||
allowed_values : list of possible values (default: any value)
|
||||
validator : function validating the field (called with (value,field)).
|
||||
min_value : minimum value (for floats and ints)
|
||||
|
@ -334,7 +334,7 @@ class TF(object):
|
|||
buttons_markup = ""
|
||||
if self.submitbutton:
|
||||
buttons_markup += (
|
||||
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s/>'
|
||||
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s>'
|
||||
% (
|
||||
self.formid,
|
||||
self.formid,
|
||||
|
@ -344,7 +344,7 @@ class TF(object):
|
|||
)
|
||||
if self.cancelbutton:
|
||||
buttons_markup += (
|
||||
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s"/>'
|
||||
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s">'
|
||||
% (self.formid, self.formid, self.cancelbutton)
|
||||
)
|
||||
|
||||
|
@ -364,7 +364,7 @@ class TF(object):
|
|||
'<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s>'
|
||||
% (self.form_url, self.method, self.formid, enctype, name, klass)
|
||||
)
|
||||
R.append('<input type="hidden" name="%s_submitted" value="1"/>' % self.formid)
|
||||
R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
|
||||
if self.top_buttons:
|
||||
R.append(buttons_markup + "<p></p>")
|
||||
R.append('<table class="tf">')
|
||||
|
@ -406,7 +406,7 @@ class TF(object):
|
|||
else:
|
||||
checked = ""
|
||||
lab.append(
|
||||
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s/>'
|
||||
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s>'
|
||||
% ("tf-checked", field, checked)
|
||||
)
|
||||
if title_bubble:
|
||||
|
@ -439,13 +439,13 @@ class TF(object):
|
|||
add_no_enter_js = True
|
||||
# lem.append('onchange="document.%s.%s.focus()"'%(name,nextitemname))
|
||||
# lem.append('onblur="document.%s.%s.focus()"'%(name,nextitemname))
|
||||
lem.append(('value="%(' + field + ')s" />') % values)
|
||||
lem.append(('value="%(' + field + ')s" >') % values)
|
||||
elif input_type == "password":
|
||||
lem.append(
|
||||
'<input type="password" name="%s" id="%s" size="%d" %s'
|
||||
% (field, wid, size, attribs)
|
||||
)
|
||||
lem.append(('value="%(' + field + ')s" />') % values)
|
||||
lem.append(('value="%(' + field + ')s" >') % values)
|
||||
elif input_type == "radio":
|
||||
labels = descr.get("labels", descr["allowed_values"])
|
||||
for i in range(len(labels)):
|
||||
|
@ -549,24 +549,24 @@ class TF(object):
|
|||
if descr.get("type", "") == "list":
|
||||
for v in values[field]:
|
||||
lem.append(
|
||||
'<input type="hidden" name="%s:list" value="%s" %s />'
|
||||
'<input type="hidden" name="%s:list" value="%s" %s >'
|
||||
% (field, v, attribs)
|
||||
)
|
||||
else:
|
||||
lem.append(
|
||||
'<input type="hidden" name="%s" id="%s" value="%s" %s />'
|
||||
'<input type="hidden" name="%s" id="%s" value="%s" %s >'
|
||||
% (field, wid, values[field], attribs)
|
||||
)
|
||||
elif input_type == "separator":
|
||||
pass
|
||||
elif input_type == "file":
|
||||
lem.append(
|
||||
'<input type="file" name="%s" size="%s" value="%s" %s/>'
|
||||
'<input type="file" name="%s" size="%s" value="%s" %s>'
|
||||
% (field, size, values[field], attribs)
|
||||
)
|
||||
elif input_type == "date": # JavaScript widget for date input
|
||||
lem.append(
|
||||
'<input type="text" name="%s" size="10" value="%s" class="datepicker"/>'
|
||||
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
|
||||
% (field, values[field])
|
||||
)
|
||||
elif input_type == "text_suggest":
|
||||
|
@ -716,7 +716,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
|||
bool_val = 0
|
||||
R.append(labels[bool_val])
|
||||
if bool_val:
|
||||
R.append('<input type="hidden" name="%s" value="1"/>' % field)
|
||||
R.append('<input type="hidden" name="%s" value="1">' % field)
|
||||
else:
|
||||
labels = descr.get("labels", descr["allowed_values"])
|
||||
for i in range(len(labels)):
|
||||
|
|
|
@ -498,7 +498,7 @@ class GenTable(object):
|
|||
headline = []
|
||||
return "\n".join(
|
||||
[
|
||||
self.text_fields_separator.join([x for x in line])
|
||||
self.text_fields_separator.join([str(x) for x in line])
|
||||
for line in headline + self.get_data_list()
|
||||
]
|
||||
)
|
||||
|
|
|
@ -87,15 +87,18 @@ Problème de connexion (identifiant, mot de passe): <em>contacter votre responsa
|
|||
)
|
||||
|
||||
|
||||
_HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
_HTML_BEGIN = """<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<title>%(page_title)s</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<meta name="LANG" content="fr" />
|
||||
<meta name="DESCRIPTION" content="ScoDoc" />
|
||||
<title>%(page_title)s</title>
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
||||
|
||||
|
@ -125,7 +128,7 @@ _HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?>
|
|||
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
|
||||
H = [
|
||||
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
|
||||
"""</head><body class="gtrcontent" id="gtrcontent">""",
|
||||
"""</head><body id="gtrcontent">""",
|
||||
scu.CUSTOM_HTML_HEADER_CNX,
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
@ -140,8 +143,6 @@ def sco_header(
|
|||
javascripts=[], # additionals JS filenames to load
|
||||
scripts=[], # script to put in page header
|
||||
bodyOnLoad="", # JS
|
||||
init_jquery=True, # load and init jQuery
|
||||
init_jquery_ui=True, # include all stuff for jquery-ui and initialize scripts
|
||||
init_qtip=False, # include qTip
|
||||
init_google_maps=False, # Google maps
|
||||
init_datatables=True,
|
||||
|
@ -176,9 +177,6 @@ def sco_header(
|
|||
else:
|
||||
params["margin_left"] = "140px"
|
||||
|
||||
if init_jquery_ui or init_qtip or init_datatables:
|
||||
init_jquery = True
|
||||
|
||||
H = [
|
||||
"""<!DOCTYPE html><html lang="fr">
|
||||
<head>
|
||||
|
@ -191,11 +189,10 @@ def sco_header(
|
|||
% params
|
||||
]
|
||||
# jQuery UI
|
||||
if init_jquery_ui:
|
||||
# can modify loaded theme here
|
||||
H.append(
|
||||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
||||
)
|
||||
# can modify loaded theme here
|
||||
H.append(
|
||||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
||||
)
|
||||
if init_google_maps:
|
||||
# It may be necessary to add an API key:
|
||||
H.append('<script src="https://maps.google.com/maps/api/js"></script>')
|
||||
|
@ -224,12 +221,11 @@ def sco_header(
|
|||
)
|
||||
|
||||
# jQuery
|
||||
if init_jquery:
|
||||
H.append(
|
||||
"""<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
||||
"""
|
||||
)
|
||||
H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
|
||||
H.append(
|
||||
"""<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
||||
"""
|
||||
)
|
||||
H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
|
||||
# qTip
|
||||
if init_qtip:
|
||||
H.append(
|
||||
|
@ -239,12 +235,11 @@ def sco_header(
|
|||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
|
||||
)
|
||||
|
||||
if init_jquery_ui:
|
||||
H.append(
|
||||
'<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
|
||||
)
|
||||
# H.append('<script src="/ScoDoc/static/libjs/jquery-ui/js/jquery-ui-i18n.js"></script>')
|
||||
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
|
||||
H.append(
|
||||
'<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
|
||||
)
|
||||
|
||||
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
|
||||
if init_google_maps:
|
||||
H.append(
|
||||
'<script src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>'
|
||||
|
@ -260,7 +255,7 @@ def sco_header(
|
|||
|
||||
H.append(
|
||||
"""<style>
|
||||
.gtrcontent {
|
||||
#gtrcontent {
|
||||
margin-left: %(margin_left)s;
|
||||
height: 100%%;
|
||||
margin-bottom: 10px;
|
||||
|
@ -284,7 +279,7 @@ def sco_header(
|
|||
#
|
||||
if not no_side_bar:
|
||||
H.append(html_sidebar.sidebar())
|
||||
H.append("""<div class="gtrcontent" id="gtrcontent">""")
|
||||
H.append("""<div id="gtrcontent">""")
|
||||
#
|
||||
# Barre menu semestre:
|
||||
H.append(formsemestre_page_title())
|
||||
|
|
|
@ -40,7 +40,7 @@ from app.scodoc.sco_permissions import Permission
|
|||
def sidebar_common():
|
||||
"partie commune à toutes les sidebar"
|
||||
H = [
|
||||
f"""<a class="scodoc_title" href="{url_for("scodoc.index", scodoc_dept=g.scodoc_dept)}">ScoDoc 9</a>
|
||||
f"""<a class="scodoc_title" href="{url_for("scodoc.index", scodoc_dept=g.scodoc_dept)}">ScoDoc 9.1</a>
|
||||
<div id="authuser"><a id="authuserlink" href="{
|
||||
url_for("users.user_info_page",
|
||||
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
||||
|
|
|
@ -32,8 +32,11 @@ from operator import itemgetter
|
|||
|
||||
from flask import g, url_for
|
||||
|
||||
from app.but import bulletin_but
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import ScoDocSiteConfig
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_formulas import NoteVector
|
||||
|
@ -140,7 +143,7 @@ def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
|
|||
return s
|
||||
|
||||
|
||||
class NotesTable(object):
|
||||
class NotesTable:
|
||||
"""Une NotesTable représente un tableau de notes pour un semestre de formation.
|
||||
Les colonnes sont des modules.
|
||||
Les lignes des étudiants.
|
||||
|
@ -197,9 +200,7 @@ class NotesTable(object):
|
|||
self.inscrlist.sort(key=itemgetter("nomp"))
|
||||
|
||||
# { etudid : rang dans l'ordre alphabetique }
|
||||
rangalpha = {}
|
||||
for i in range(len(self.inscrlist)):
|
||||
rangalpha[self.inscrlist[i]["etudid"]] = i
|
||||
self.rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
|
||||
|
||||
self.bonus = scu.DictDefault(defaultvalue=0)
|
||||
# Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
|
||||
|
@ -244,6 +245,14 @@ class NotesTable(object):
|
|||
self.formation["type_parcours"]
|
||||
)
|
||||
|
||||
# En APC, il faut avoir toutes les UE du semestre
|
||||
# (elles n'ont pas nécessairement un module rattaché):
|
||||
if self.parcours.APC_SAE:
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
for ue in formsemestre.query_ues():
|
||||
if ue.id not in self.uedict:
|
||||
self.uedict[ue.id] = ue.to_dict()
|
||||
|
||||
# Decisions jury et UE capitalisées
|
||||
self.comp_decisions_jury()
|
||||
self.comp_ue_capitalisees()
|
||||
|
@ -253,7 +262,7 @@ class NotesTable(object):
|
|||
self._ues.sort(key=lambda u: u["numero"])
|
||||
|
||||
T = []
|
||||
# XXX self.comp_ue_coefs(cnx)
|
||||
|
||||
self.moy_gen = {} # etudid : moy gen (avec UE capitalisées)
|
||||
self.moy_ue = {} # ue_id : { etudid : moy ue } (valeur numerique)
|
||||
self.etud_moy_infos = {} # etudid : resultats de comp_etud_moy_gen()
|
||||
|
@ -293,22 +302,12 @@ class NotesTable(object):
|
|||
t.append(val)
|
||||
#
|
||||
t.append(etudid)
|
||||
T.append(tuple(t))
|
||||
T.append(t)
|
||||
|
||||
self.T = T
|
||||
# tri par moyennes décroissantes,
|
||||
# en laissant les demissionnaires a la fin, par ordre alphabetique
|
||||
def row_key(x):
|
||||
"""clé de tri par moyennes décroissantes,
|
||||
en laissant les demissionnaires a la fin, par ordre alphabetique.
|
||||
(moy_gen, rang_alpha)
|
||||
"""
|
||||
try:
|
||||
moy = -float(x[0])
|
||||
except (ValueError, TypeError):
|
||||
moy = 1000.0
|
||||
return (moy, rangalpha[x[-1]])
|
||||
|
||||
T.sort(key=row_key)
|
||||
self.T = T
|
||||
self.T.sort(key=self._row_key)
|
||||
|
||||
if len(valid_moy):
|
||||
self.moy_min = min(valid_moy)
|
||||
|
@ -338,7 +337,7 @@ class NotesTable(object):
|
|||
ue_eff = len(
|
||||
[x for x in val_ids if isinstance(x[0], float)]
|
||||
) # nombre d'étudiants avec une note dans l'UE
|
||||
val_ids.sort(key=row_key)
|
||||
val_ids.sort(key=self._row_key)
|
||||
ue_rangs[ue_id] = (
|
||||
comp_ranks(val_ids),
|
||||
ue_eff,
|
||||
|
@ -349,13 +348,24 @@ class NotesTable(object):
|
|||
for modimpl in self._modimpls:
|
||||
vals = self._modmoys[modimpl["moduleimpl_id"]]
|
||||
val_ids = [(vals[etudid], etudid) for etudid in vals.keys()]
|
||||
val_ids.sort(key=row_key)
|
||||
val_ids.sort(key=self._row_key)
|
||||
self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals))
|
||||
#
|
||||
self.compute_moy_moy()
|
||||
#
|
||||
log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.")
|
||||
|
||||
def _row_key(self, x):
|
||||
"""clé de tri par moyennes décroissantes,
|
||||
en laissant les demissionnaires a la fin, par ordre alphabetique.
|
||||
(moy_gen, rang_alpha)
|
||||
"""
|
||||
try:
|
||||
moy = -float(x[0])
|
||||
except (ValueError, TypeError):
|
||||
moy = 1000.0
|
||||
return (moy, self.rang_alpha[x[-1]])
|
||||
|
||||
def get_etudids(self, sorted=False):
|
||||
if sorted:
|
||||
# Tri par moy. generale décroissante
|
||||
|
@ -607,7 +617,7 @@ class NotesTable(object):
|
|||
# si 'NI', etudiant non inscrit a ce module
|
||||
if val != "NI":
|
||||
est_inscrit = True
|
||||
if modimpl["module"]["module_type"] == scu.MODULE_STANDARD:
|
||||
if modimpl["module"]["module_type"] == ModuleType.STANDARD:
|
||||
coef = modimpl["module"]["coefficient"]
|
||||
if modimpl["ue"]["type"] != UE_SPORT:
|
||||
notes.append(val, name=modimpl["module"]["code"])
|
||||
|
@ -630,7 +640,8 @@ class NotesTable(object):
|
|||
matiere_sum_notes += val * coef
|
||||
matiere_sum_coefs += coef
|
||||
matiere_id_last = matiere_id
|
||||
except:
|
||||
except TypeError: # val == "NI" "NA"
|
||||
assert val == "NI" or val == "NA" or val == "ERR"
|
||||
nb_missing = nb_missing + 1
|
||||
coefs.append(0)
|
||||
coefs_mask.append(0)
|
||||
|
@ -643,11 +654,17 @@ class NotesTable(object):
|
|||
except:
|
||||
# log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
|
||||
pass
|
||||
elif modimpl["module"]["module_type"] == scu.MODULE_MALUS:
|
||||
elif modimpl["module"]["module_type"] == ModuleType.MALUS:
|
||||
try:
|
||||
ue_malus += val
|
||||
except:
|
||||
pass # si non inscrit ou manquant, ignore
|
||||
elif modimpl["module"]["module_type"] in (
|
||||
ModuleType.RESSOURCE,
|
||||
ModuleType.SAE,
|
||||
):
|
||||
# XXX temporaire pour ne pas bloquer durant le dev
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
"invalid module type (%s)" % modimpl["module"]["module_type"]
|
||||
|
@ -671,7 +688,7 @@ class NotesTable(object):
|
|||
|
||||
# Recalcule la moyenne en utilisant une formule utilisateur
|
||||
expr_diag = {}
|
||||
formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id, cnx)
|
||||
formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id)
|
||||
if formula:
|
||||
moy = sco_compute_moy.compute_user_formula(
|
||||
self.sem,
|
||||
|
@ -728,9 +745,7 @@ class NotesTable(object):
|
|||
|
||||
Prend toujours en compte les UE capitalisées.
|
||||
"""
|
||||
# log('comp_etud_moy_gen(etudid=%s)' % etudid)
|
||||
|
||||
# Si l'étudiant a Demissionné ou est DEFaillant, on n'enregistre pas ses moyennes
|
||||
# Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes
|
||||
block_computation = (
|
||||
self.inscrdict[etudid]["etat"] == "D"
|
||||
or self.inscrdict[etudid]["etat"] == DEF
|
||||
|
@ -965,8 +980,8 @@ class NotesTable(object):
|
|||
def get_table_moyennes_triees(self):
|
||||
return self.T
|
||||
|
||||
def get_etud_rang(self, etudid):
|
||||
return self.rangs[etudid]
|
||||
def get_etud_rang(self, etudid) -> str:
|
||||
return self.rangs.get(etudid, "999")
|
||||
|
||||
def get_etud_rang_group(self, etudid, group_id):
|
||||
"""Returns rank of etud in this group and number of etuds in group.
|
||||
|
@ -1320,3 +1335,27 @@ class NotesTable(object):
|
|||
for e in self.get_evaluations_etats()
|
||||
if e["moduleimpl_id"] == moduleimpl_id
|
||||
]
|
||||
|
||||
def apc_recompute_moyennes(self):
|
||||
"""recalcule les moyennes en APC (BUT)
|
||||
et modifie en place le tableau T.
|
||||
XXX Raccord provisoire avant refonte de cette classe.
|
||||
"""
|
||||
assert self.parcours.APC_SAE
|
||||
formsemestre = FormSemestre.query.get(self.formsemestre_id)
|
||||
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||
|
||||
# Rappel des épisodes précédents: T est une liste de liste
|
||||
# Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
||||
ues = self.get_ues() # incluant le(s) UE de sport
|
||||
for t in self.T:
|
||||
etudid = t[-1]
|
||||
if etudid in results.etud_moy_gen: # evite les démissionnaires
|
||||
t[0] = results.etud_moy_gen[etudid]
|
||||
for i, ue in enumerate(ues, start=1):
|
||||
if ue["type"] != UE_SPORT:
|
||||
t[i] = results.etud_moy_ue[ue["id"]][etudid]
|
||||
# re-trie selon la nouvelle moyenne générale:
|
||||
self.T.sort(key=self._row_key)
|
||||
# Remplace aussi le rang:
|
||||
self.rangs = results.etud_moy_gen_ranks
|
||||
|
|
|
@ -53,7 +53,7 @@ def close_db_connection():
|
|||
del g.db_conn
|
||||
|
||||
|
||||
def GetDBConnexion(autocommit=True): # on n'utilise plus autocommit
|
||||
def GetDBConnexion():
|
||||
return g.db_conn
|
||||
|
||||
|
||||
|
@ -602,15 +602,15 @@ BOOL_STR = {
|
|||
"false": False,
|
||||
"0": False,
|
||||
"1": True,
|
||||
"true": "true",
|
||||
"true": True,
|
||||
}
|
||||
|
||||
|
||||
def bool_or_str(x):
|
||||
def bool_or_str(x) -> bool:
|
||||
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
|
||||
if isinstance(x, str):
|
||||
return BOOL_STR[x.lower()]
|
||||
return x
|
||||
return bool(x)
|
||||
|
||||
|
||||
# post filtering
|
||||
|
|
|
@ -546,21 +546,21 @@ def list_abs_non_just(etudid, datedebut):
|
|||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT ETUDID, JOUR, MATIN FROM ABSENCES A
|
||||
"""SELECT ETUDID, JOUR, MATIN FROM ABSENCES A
|
||||
WHERE A.ETUDID = %(etudid)s
|
||||
AND A.estabs
|
||||
AND A.estabs
|
||||
AND A.jour >= %(datedebut)s
|
||||
EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B
|
||||
WHERE B.estjust
|
||||
EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B
|
||||
WHERE B.estjust
|
||||
AND B.ETUDID = %(etudid)s
|
||||
ORDER BY JOUR
|
||||
""",
|
||||
vars(),
|
||||
)
|
||||
A = cursor.dictfetchall()
|
||||
for a in A:
|
||||
abs_list = cursor.dictfetchall()
|
||||
for a in abs_list:
|
||||
a["description"] = _get_abs_description(a, cursor=cursor)
|
||||
return A
|
||||
return abs_list
|
||||
|
||||
|
||||
def list_abs_just(etudid, datedebut):
|
||||
|
@ -570,7 +570,7 @@ def list_abs_just(etudid, datedebut):
|
|||
cursor.execute(
|
||||
"""SELECT DISTINCT A.ETUDID, A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B
|
||||
WHERE A.ETUDID = %(etudid)s
|
||||
AND A.ETUDID = B.ETUDID
|
||||
AND A.ETUDID = B.ETUDID
|
||||
AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR >= %(datedebut)s
|
||||
AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)
|
||||
ORDER BY A.JOUR
|
||||
|
@ -638,8 +638,12 @@ def add_absence(
|
|||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT into absences (etudid,jour,estabs,estjust,matin,description, moduleimpl_id)
|
||||
VALUES (%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s, %(description)s, %(moduleimpl_id)s )
|
||||
INSERT into absences
|
||||
(etudid, jour, estabs, estjust, matin, description, moduleimpl_id)
|
||||
VALUES
|
||||
(%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s,
|
||||
%(description)s, %(moduleimpl_id)s
|
||||
)
|
||||
""",
|
||||
vars(),
|
||||
)
|
||||
|
@ -1028,20 +1032,26 @@ def get_abs_count(etudid, sem):
|
|||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
date_fin = sem["date_fin_iso"]
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin
|
||||
return get_abs_count_in_interval(etudid, sem["date_debut_iso"], sem["date_fin_iso"])
|
||||
|
||||
|
||||
def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r:
|
||||
nb_abs = count_abs( # was CountAbs XXX
|
||||
nb_abs = count_abs(
|
||||
etudid=etudid,
|
||||
debut=date_debut,
|
||||
fin=date_fin,
|
||||
debut=date_debut_iso,
|
||||
fin=date_fin_iso,
|
||||
)
|
||||
nb_abs_just = count_abs_just( # XXX was CountAbsJust
|
||||
nb_abs_just = count_abs_just(
|
||||
etudid=etudid,
|
||||
debut=date_debut,
|
||||
fin=date_fin,
|
||||
debut=date_debut_iso,
|
||||
fin=date_fin_iso,
|
||||
)
|
||||
r = (nb_abs, nb_abs_just)
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"""
|
||||
import datetime
|
||||
|
||||
from flask import url_for, g, request
|
||||
from flask import url_for, g, request, abort
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import notesdb as ndb
|
||||
|
@ -773,7 +773,8 @@ def CalAbs(etudid, sco_year=None):
|
|||
|
||||
|
||||
def ListeAbsEtud(
|
||||
etudid,
|
||||
etudid=None,
|
||||
code_nip=None,
|
||||
with_evals=True,
|
||||
format="html",
|
||||
absjust_only=0,
|
||||
|
@ -790,11 +791,16 @@ def ListeAbsEtud(
|
|||
absjust_only: si vrai, renvoie table absences justifiées
|
||||
sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005"
|
||||
"""
|
||||
absjust_only = int(absjust_only) # si vrai, table absjust seule (export xls ou pdf)
|
||||
# si absjust_only, table absjust seule (export xls ou pdf)
|
||||
absjust_only = ndb.bool_or_str(absjust_only)
|
||||
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
|
||||
etudid = etudid or False
|
||||
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
||||
if not etuds:
|
||||
log(f"ListeAbsEtud: no etuds with etudid={etudid} or nip={code_nip}")
|
||||
abort(404)
|
||||
etud = etuds[0]
|
||||
etudid = etud["etudid"]
|
||||
# Liste des absences et titres colonnes tables:
|
||||
titles, columns_ids, absnonjust, absjust = _tables_abs_etud(
|
||||
etudid, datedebut, with_evals=with_evals, format=format
|
||||
|
|
|
@ -256,8 +256,8 @@ def apo_table_compare_etud_results(A, B):
|
|||
"prenom": "Prénom",
|
||||
"elt_code": "Element",
|
||||
"type_res": "Type",
|
||||
"val_A": "A: %s" % A.orig_filename or "",
|
||||
"val_B": "B: %s" % B.orig_filename or "",
|
||||
"val_A": "A: %s" % (A.orig_filename or ""),
|
||||
"val_B": "B: %s" % (B.orig_filename or ""),
|
||||
},
|
||||
columns_ids=("nip", "nom", "prenom", "elt_code", "type_res", "val_A", "val_B"),
|
||||
html_class="table_leftalign",
|
||||
|
|
|
@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
|
|||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError, FormatError
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc.sco_codes_parcours import code_semestre_validant
|
||||
|
@ -681,7 +681,7 @@ class ApoData(object):
|
|||
self.periode = periode #
|
||||
try:
|
||||
self.read_csv(data)
|
||||
except FormatError as e:
|
||||
except ScoFormatError as e:
|
||||
# essaie de retrouver le nom du fichier pour enrichir le message d'erreur
|
||||
filename = ""
|
||||
if self.orig_filename is None:
|
||||
|
@ -689,7 +689,7 @@ class ApoData(object):
|
|||
filename = self.titles.get("apoC_Fichier_Exp", filename)
|
||||
else:
|
||||
filename = self.orig_filename
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename
|
||||
+ e.args[0]
|
||||
+ "</p>"
|
||||
|
@ -759,13 +759,13 @@ class ApoData(object):
|
|||
|
||||
def read_csv(self, data: str):
|
||||
if not data:
|
||||
raise FormatError("Fichier Apogée vide !")
|
||||
raise ScoFormatError("Fichier Apogée vide !")
|
||||
|
||||
f = StringIOFileLineWrapper(data) # pour traiter comme un fichier
|
||||
# check that we are at the begining of Apogee CSV
|
||||
line = f.readline().strip()
|
||||
if line != "XX-APO_TITRES-XX":
|
||||
raise FormatError("format incorrect: pas de XX-APO_TITRES-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
|
||||
|
||||
# 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
|
||||
idx = data.index("XX-APO_VALEURS-XX")
|
||||
|
@ -779,13 +779,13 @@ class ApoData(object):
|
|||
# 3-- La section XX-APO_TYP_RES-XX est ignorée:
|
||||
line = f.readline().strip()
|
||||
if line != "XX-APO_TYP_RES-XX":
|
||||
raise FormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
|
||||
_apo_skip_section(f)
|
||||
|
||||
# 4-- Définition de colonnes: (on y trouve aussi l'étape)
|
||||
line = f.readline().strip()
|
||||
if line != "XX-APO_COLONNES-XX":
|
||||
raise FormatError("format incorrect: pas de XX-APO_COLONNES-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_COLONNES-XX")
|
||||
self.cols = _apo_read_cols(f)
|
||||
self.apo_elts = self._group_elt_cols(self.cols)
|
||||
|
||||
|
@ -794,7 +794,7 @@ class ApoData(object):
|
|||
while True: # skip
|
||||
line = f.readline()
|
||||
if not line:
|
||||
raise FormatError("format incorrect: pas de XX-APO_VALEURS-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX")
|
||||
if line.strip() == "XX-APO_VALEURS-XX":
|
||||
break
|
||||
self.column_titles = f.readline()
|
||||
|
@ -885,7 +885,7 @@ class ApoData(object):
|
|||
"""
|
||||
m = re.match("[12][0-9]{3}", self.titles["apoC_annee"])
|
||||
if not m:
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
'Annee scolaire (apoC_annee) invalide: "%s"' % self.titles["apoC_annee"]
|
||||
)
|
||||
return int(m.group(0))
|
||||
|
@ -943,7 +943,7 @@ class ApoData(object):
|
|||
log("Fichier Apogee invalide:")
|
||||
log("Colonnes declarees: %s" % declared)
|
||||
log("Colonnes presentes: %s" % present)
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"""Fichier Apogee invalide<br/>Colonnes declarees: <tt>%s</tt>
|
||||
<br/>Colonnes presentes: <tt>%s</tt>"""
|
||||
% (declared, present)
|
||||
|
@ -1032,7 +1032,7 @@ def _apo_read_cols(f):
|
|||
line = f.readline().strip(" " + APO_NEWLINE)
|
||||
fs = line.split(APO_SEP)
|
||||
if fs[0] != "apoL_a01_code":
|
||||
raise FormatError("invalid line: %s (expecting apoL_a01_code)" % line)
|
||||
raise ScoFormatError("invalid line: %s (expecting apoL_a01_code)" % line)
|
||||
col_keys = fs
|
||||
|
||||
while True: # skip premiere partie (apoL_a02_nom, ...)
|
||||
|
@ -1052,14 +1052,14 @@ def _apo_read_cols(f):
|
|||
# sanity check
|
||||
col_id = fs[0] # apoL_c0001, ...
|
||||
if col_id in cols:
|
||||
raise FormatError("duplicate column definition: %s" % col_id)
|
||||
raise ScoFormatError("duplicate column definition: %s" % col_id)
|
||||
m = re.match(r"^apoL_c([0-9]{4})$", col_id)
|
||||
if not m:
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"invalid column id: %s (expecting apoL_c%04d)" % (line, col_id)
|
||||
)
|
||||
if int(m.group(1)) != i:
|
||||
raise FormatError("invalid column id: %s for index %s" % (col_id, i))
|
||||
raise ScoFormatError("invalid column id: %s for index %s" % (col_id, i))
|
||||
|
||||
cols[col_id] = DictCol(list(zip(col_keys, fs)))
|
||||
cols[col_id].lineno = f.lineno # for debuging purpose
|
||||
|
@ -1083,14 +1083,14 @@ def _apo_read_TITRES(f):
|
|||
else:
|
||||
log("Error read CSV: \nline=%s\nfields=%s" % (line, fields))
|
||||
log(dir(f))
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"Fichier Apogee incorrect (section titres, %d champs au lieu de 2)"
|
||||
% len(fields)
|
||||
)
|
||||
d[k] = v
|
||||
#
|
||||
if not d.get("apoC_Fichier_Exp", None):
|
||||
raise FormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
|
||||
raise ScoFormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
|
||||
# keep only basename: may be a windows or unix pathname
|
||||
s = d["apoC_Fichier_Exp"].split("/")[-1]
|
||||
s = s.split("\\")[-1] # for DOS paths, eg C:\TEMP\VL4RT_V3ASR.TXT
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
qui est une description (humaine, format libre) de l'archive.
|
||||
|
||||
"""
|
||||
import chardet
|
||||
import datetime
|
||||
import glob
|
||||
import mimetypes
|
||||
|
@ -203,8 +204,16 @@ class BaseArchiver(object):
|
|||
def get_archive_description(self, archive_id):
|
||||
"""Return description of archive"""
|
||||
self.initialize()
|
||||
with open(os.path.join(archive_id, "_description.txt")) as f:
|
||||
descr = f.read()
|
||||
filename = os.path.join(archive_id, "_description.txt")
|
||||
try:
|
||||
with open(filename) as f:
|
||||
descr = f.read()
|
||||
except UnicodeDecodeError:
|
||||
# some (old) files may have saved under exotic encodings
|
||||
with open(filename, "rb") as f:
|
||||
data = f.read()
|
||||
descr = data.decode(chardet.detect(data)["encoding"])
|
||||
|
||||
return descr
|
||||
|
||||
def create_obj_archive(self, oid: int, description: str):
|
||||
|
|
|
@ -45,6 +45,7 @@ from flask_login import current_user
|
|||
from flask_mail import Message
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
@ -59,7 +60,7 @@ from app.scodoc import sco_bulletins_xml
|
|||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
|
@ -428,7 +429,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
|
|||
mod_moy = nt.get_etud_mod_moy(
|
||||
modimpl["moduleimpl_id"], etudid
|
||||
) # peut etre 'NI'
|
||||
is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS
|
||||
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
||||
if bul_show_abs_modules:
|
||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
||||
mod_abs = [nbabs, nbabsjust]
|
||||
|
@ -558,7 +559,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
|
|||
mod["evaluations_incompletes"] = []
|
||||
if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id):
|
||||
complete_eval_ids = set([e["evaluation_id"] for e in evals])
|
||||
all_evals = sco_evaluations.do_evaluation_list(
|
||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
||||
)
|
||||
all_evals.reverse() # plus ancienne d'abord
|
||||
|
|
|
@ -31,12 +31,16 @@
|
|||
import datetime
|
||||
import json
|
||||
|
||||
from app.but import bulletin_but
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_photos
|
||||
|
@ -82,9 +86,17 @@ def formsemestre_bulletinetud_published_dict(
|
|||
"""
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
etud = Identite.query.get(etudid)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
if formsemestre.formation.is_apc():
|
||||
nt = bulletin_but.APCNotesTableCompat(formsemestre)
|
||||
else:
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
|
||||
d = {}
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = 1
|
||||
else:
|
||||
|
@ -129,6 +141,11 @@ def formsemestre_bulletinetud_published_dict(
|
|||
if not published:
|
||||
return d # stop !
|
||||
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
if etat_inscription != scu.INSCRIT:
|
||||
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
|
||||
return d
|
||||
|
||||
# Groupes:
|
||||
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)
|
||||
partitions_etud_groups = {} # { partition_id : { etudid : group } }
|
||||
|
@ -136,7 +153,6 @@ def formsemestre_bulletinetud_published_dict(
|
|||
pid = partition["partition_id"]
|
||||
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
||||
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
|
||||
ues = nt.get_ues()
|
||||
modimpls = nt.get_modimpls()
|
||||
nbetuds = len(nt.rangs)
|
||||
|
@ -277,7 +293,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||
if sco_preferences.get_preference(
|
||||
"bul_show_all_evals", formsemestre_id
|
||||
):
|
||||
all_evals = sco_evaluations.do_evaluation_list(
|
||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
||||
)
|
||||
all_evals.reverse() # plus ancienne d'abord
|
||||
|
@ -325,28 +341,61 @@ def formsemestre_bulletinetud_published_dict(
|
|||
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
||||
|
||||
# --- Decision Jury
|
||||
d.update(
|
||||
dict_decision_jury(etudid, formsemestre_id, with_decisions=xml_with_decisions)
|
||||
)
|
||||
# --- Appreciations
|
||||
cnx = ndb.GetDBConnexion()
|
||||
apprecs = sco_etud.appreciations_list(
|
||||
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
)
|
||||
d["appreciation"] = []
|
||||
for app in apprecs:
|
||||
d["appreciation"].append(
|
||||
dict(
|
||||
comment=scu.quote_xml_attr(app["comment"]),
|
||||
date=ndb.DateDMYtoISO(app["date"]),
|
||||
)
|
||||
)
|
||||
|
||||
#
|
||||
return d
|
||||
|
||||
|
||||
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False):
|
||||
"dict avec decision pour bulletins json"
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
d = {}
|
||||
if (
|
||||
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
|
||||
or xml_with_decisions
|
||||
or with_decisions
|
||||
):
|
||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
format="xml",
|
||||
show_uevalid=sco_preferences.get_preference(
|
||||
"bul_show_uevalid", formsemestre_id
|
||||
),
|
||||
)
|
||||
d["situation"] = scu.quote_xml_attr(infos["situation"])
|
||||
d["situation"] = infos["situation"]
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
etat = decision["etat"]
|
||||
if decision["decision_sem"]:
|
||||
code = decision["decision_sem"]["code"]
|
||||
date = ndb.DateDMYtoISO(
|
||||
dpv["decisions"][0]["decision_sem"]["event_date"]
|
||||
)
|
||||
else:
|
||||
code = ""
|
||||
date = ""
|
||||
|
||||
d["decision"] = dict(code=code, etat=etat)
|
||||
d["decision"] = dict(
|
||||
code=code,
|
||||
etat=etat,
|
||||
date=date,
|
||||
)
|
||||
if (
|
||||
decision["decision_sem"]
|
||||
and "compense_formsemestre_id" in decision["decision_sem"]
|
||||
|
@ -364,11 +413,11 @@ def formsemestre_bulletinetud_published_dict(
|
|||
d["decision_ue"].append(
|
||||
dict(
|
||||
ue_id=ue["ue_id"],
|
||||
numero=scu.quote_xml_attr(ue["numero"]),
|
||||
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
||||
titre=scu.quote_xml_attr(ue["titre"]),
|
||||
numero=ue["numero"],
|
||||
acronyme=ue["acronyme"],
|
||||
titre=ue["titre"],
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
ects=scu.quote_xml_attr(ue["ects"] or ""),
|
||||
ects=ue["ects"] or "",
|
||||
)
|
||||
)
|
||||
d["autorisation_inscription"] = []
|
||||
|
@ -378,20 +427,4 @@ def formsemestre_bulletinetud_published_dict(
|
|||
)
|
||||
else:
|
||||
d["decision"] = dict(code="", etat="DEM")
|
||||
|
||||
# --- Appreciations
|
||||
cnx = ndb.GetDBConnexion()
|
||||
apprecs = sco_etud.appreciations_list(
|
||||
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
)
|
||||
d["appreciation"] = []
|
||||
for app in apprecs:
|
||||
d["appreciation"].append(
|
||||
dict(
|
||||
comment=scu.quote_xml_attr(app["comment"]),
|
||||
date=ndb.DateDMYtoISO(app["date"]),
|
||||
)
|
||||
)
|
||||
|
||||
#
|
||||
return d
|
||||
|
|
|
@ -51,23 +51,24 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
|
|||
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
from pydoc import html
|
||||
|
||||
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
|
||||
|
||||
from flask import g, url_for, request
|
||||
from flask import g, request
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app import log, ScoValueError
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
import sco_version
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
def pdfassemblebulletins(
|
||||
|
@ -110,6 +111,17 @@ def pdfassemblebulletins(
|
|||
return data
|
||||
|
||||
|
||||
def replacement_function(match):
|
||||
balise = match.group(1)
|
||||
name = match.group(3)
|
||||
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
||||
if logo is not None:
|
||||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||
raise ScoValueError(
|
||||
'balise "%s": logo "%s" introuvable' % (html.escape(balise), html.escape(name))
|
||||
)
|
||||
|
||||
|
||||
def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
||||
"""Process a field given in preferences, returns
|
||||
- if format = 'pdf': a list of Platypus objects
|
||||
|
@ -141,24 +153,18 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
|||
return text
|
||||
# --- PDF format:
|
||||
# handle logos:
|
||||
image_dir = scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/"
|
||||
if not os.path.exists(image_dir):
|
||||
image_dir = scu.SCODOC_LOGOS_DIR + "/" # use global logos
|
||||
if not os.path.exists(image_dir):
|
||||
log(f"Warning: missing global logo directory ({image_dir})")
|
||||
image_dir = None
|
||||
|
||||
text = re.sub(
|
||||
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
|
||||
) # remove forbidden src attribute
|
||||
if image_dir is not None:
|
||||
text = re.sub(
|
||||
r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>',
|
||||
r'<img\1src="%s/logo_\2.jpg"\3/>' % image_dir,
|
||||
text,
|
||||
)
|
||||
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
|
||||
# tentatives d'acceder à d'autres fichiers !
|
||||
text = re.sub(
|
||||
r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)',
|
||||
replacement_function,
|
||||
text,
|
||||
)
|
||||
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
|
||||
# tentatives d'acceder à d'autres fichiers !
|
||||
# la protection contre des noms malveillants est aussi assurée par l'utilisation de
|
||||
# secure_filename dans la classe Logo
|
||||
|
||||
# log('field: %s' % (text))
|
||||
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars)
|
||||
|
|
|
@ -47,11 +47,13 @@ from xml.etree.ElementTree import Element
|
|||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_photos
|
||||
|
@ -78,6 +80,18 @@ def make_xml_formsemestre_bulletinetud(
|
|||
log("xml_bulletin( formsemestre_id=%s, etudid=%s )" % (formsemestre_id, etudid))
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.formation.is_apc():
|
||||
return bulletin_but_xml_compat(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
doc=doc,
|
||||
force_publishing=force_publishing,
|
||||
xml_nodate=xml_nodate,
|
||||
xml_with_decisions=xml_with_decisions, # inclue les decisions même si non publiées
|
||||
version=version,
|
||||
)
|
||||
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = "1"
|
||||
else:
|
||||
|
@ -289,7 +303,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||
if sco_preferences.get_preference(
|
||||
"bul_show_all_evals", formsemestre_id
|
||||
):
|
||||
all_evals = sco_evaluations.do_evaluation_list(
|
||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
||||
)
|
||||
all_evals.reverse() # plus ancienne d'abord
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
Ré-écrite pour ScoDoc8, utilise flask_caching et REDIS
|
||||
|
||||
ScoDoc est maintenant multiprocessus / mono-thread, avec un cache en mémoire partagé.
|
||||
ScoDoc est maintenant multiprocessus / mono-thread, avec un cache partagé.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -46,9 +46,9 @@
|
|||
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
|
||||
#
|
||||
# Bulletins PDF:
|
||||
# sco_cache.PDFBulCache.get(formsemestre_id, version)
|
||||
# sco_cache.PDFBulCache.set(formsemestre_id, version, filename, pdfdoc)
|
||||
# sco_cache.PDFBulCache.delete(formsemestre_id) suppr. toutes les versions
|
||||
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
|
||||
# sco_cache.SemBulletinsPDFCache.set(formsemestre_id, version, filename, pdfdoc)
|
||||
# sco_cache.SemBulletinsPDFCache.delete(formsemestre_id) suppr. toutes les versions
|
||||
|
||||
# Evaluations:
|
||||
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
|
||||
|
@ -155,6 +155,16 @@ class EvaluationCache(ScoDocCache):
|
|||
cls.delete_many(evaluation_ids)
|
||||
|
||||
|
||||
class ResultatsSemestreBUTCache(ScoDocCache):
|
||||
"""Cache pour les résultats ResultatsSemestreBUT.
|
||||
Clé: formsemestre_id
|
||||
Valeur: { un paquet de dataframes }
|
||||
"""
|
||||
|
||||
prefix = "RBUT"
|
||||
timeout = 1 * 60 # ttl 1 minutes (en phase de mise au point)
|
||||
|
||||
|
||||
class AbsSemEtudCache(ScoDocCache):
|
||||
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
|
||||
Ce cache étant indépendant des semestres, le compte peut être faux lorsqu'on
|
||||
|
@ -289,6 +299,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||
|
||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||
ResultatsSemestreBUTCache.delete_many(formsemestre_ids)
|
||||
|
||||
|
||||
class DefferedSemCacheManager:
|
||||
|
|
|
@ -28,6 +28,41 @@
|
|||
"""Semestres: Codes gestion parcours (constantes)
|
||||
"""
|
||||
import collections
|
||||
import enum
|
||||
|
||||
from app import log
|
||||
|
||||
|
||||
@enum.unique
|
||||
class CodesParcours(enum.IntEnum):
|
||||
"""Codes numériques de sparcours, enregistrés en base
|
||||
dans notes_formations.type_parcours
|
||||
Ne pas modifier.
|
||||
"""
|
||||
|
||||
Legacy = 0
|
||||
DUT = 100
|
||||
DUT4 = 110
|
||||
DUTMono = 120
|
||||
DUT2 = 130
|
||||
LP = 200
|
||||
LP2sem = 210
|
||||
LP2semEvry = 220
|
||||
LP2014 = 230
|
||||
LP2sem2014 = 240
|
||||
M2 = 250
|
||||
M2noncomp = 251
|
||||
Mono = 300
|
||||
MasterLMD = 402
|
||||
MasterIG = 403
|
||||
LicenceUCAC3 = 501
|
||||
MasterUCAC2 = 502
|
||||
MonoUCAC = 503
|
||||
GEN_6_SEM = 600
|
||||
BUT = 700
|
||||
ISCID6 = 1001
|
||||
ISCID4 = 1002
|
||||
|
||||
|
||||
NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok
|
||||
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
|
||||
|
@ -43,6 +78,7 @@ UE_STAGE_LP = 2 # ue "projet tuteuré et stage" dans les Lic. Pro.
|
|||
UE_STAGE_10 = 3 # ue "stage" avec moyenne requise > 10
|
||||
UE_ELECTIVE = 4 # UE "élective" dans certains parcours (UCAC?, ISCID)
|
||||
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
||||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||
|
||||
|
||||
def UE_is_fondamentale(ue_type):
|
||||
|
@ -61,7 +97,8 @@ UE_TYPE_NAME = {
|
|||
UE_STAGE_LP: "Projet tuteuré et stage (Lic. Pro.)",
|
||||
UE_STAGE_10: "Stage (moyenne min. 10/20)",
|
||||
UE_ELECTIVE: "Elective (ISCID)",
|
||||
UE_PROFESSIONNELLE: "Professionnelle (ISCID)"
|
||||
UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
|
||||
UE_OPTIONNELLE: "Optionnelle",
|
||||
# UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
|
||||
# UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
|
||||
}
|
||||
|
@ -214,6 +251,8 @@ class TypeParcours(object):
|
|||
ALLOWED_UE_TYPES = list(
|
||||
UE_TYPE_NAME.keys()
|
||||
) # par defaut, autorise tous les types d'UE
|
||||
APC_SAE = False # Approche par compétences avec ressources et SAÉs
|
||||
USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp.
|
||||
|
||||
def check(self, formation=None):
|
||||
return True, "" # status, diagnostic_message
|
||||
|
@ -253,13 +292,27 @@ class TypeParcours(object):
|
|||
return False, """<b>%d UE sous la barre</b>""" % n
|
||||
|
||||
|
||||
TYPES_PARCOURS = (
|
||||
collections.OrderedDict()
|
||||
) # liste des parcours définis (instances de sous-classes de TypeParcours)
|
||||
# Parcours définis (instances de sous-classes de TypeParcours):
|
||||
TYPES_PARCOURS = collections.OrderedDict() # type : Parcours
|
||||
|
||||
|
||||
def register_parcours(Parcours):
|
||||
TYPES_PARCOURS[Parcours.TYPE_PARCOURS] = Parcours
|
||||
TYPES_PARCOURS[int(Parcours.TYPE_PARCOURS)] = Parcours
|
||||
|
||||
|
||||
class ParcoursBUT(TypeParcours):
|
||||
"""BUT Bachelor Universitaire de Technologie"""
|
||||
|
||||
TYPE_PARCOURS = 700
|
||||
NAME = "BUT"
|
||||
NB_SEM = 6
|
||||
COMPENSATION_UE = False
|
||||
APC_SAE = True
|
||||
USE_REFERENTIEL_COMPETENCES = True
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||
|
||||
|
||||
register_parcours(ParcoursBUT())
|
||||
|
||||
|
||||
class ParcoursDUT(TypeParcours):
|
||||
|
@ -302,7 +355,7 @@ register_parcours(ParcoursDUTMono())
|
|||
class ParcoursDUT2(ParcoursDUT):
|
||||
"""DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
|
||||
|
||||
TYPE_PARCOURS = 130
|
||||
TYPE_PARCOURS = CodesParcours.DUT2
|
||||
NAME = "DUT2"
|
||||
NB_SEM = 2
|
||||
|
||||
|
@ -315,7 +368,7 @@ class ParcoursLP(TypeParcours):
|
|||
(pour anciennes LP. Après 2014, préférer ParcoursLP2014)
|
||||
"""
|
||||
|
||||
TYPE_PARCOURS = 200
|
||||
TYPE_PARCOURS = CodesParcours.LP
|
||||
NAME = "LP"
|
||||
NB_SEM = 1
|
||||
COMPENSATION_UE = False
|
||||
|
@ -332,7 +385,7 @@ register_parcours(ParcoursLP())
|
|||
class ParcoursLP2sem(ParcoursLP):
|
||||
"""Licence Pro (en deux "semestres")"""
|
||||
|
||||
TYPE_PARCOURS = 210
|
||||
TYPE_PARCOURS = CodesParcours.LP2sem
|
||||
NAME = "LP2sem"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
|
@ -345,7 +398,7 @@ register_parcours(ParcoursLP2sem())
|
|||
class ParcoursLP2semEvry(ParcoursLP):
|
||||
"""Licence Pro (en deux "semestres", U. Evry)"""
|
||||
|
||||
TYPE_PARCOURS = 220
|
||||
TYPE_PARCOURS = CodesParcours.LP2semEvry
|
||||
NAME = "LP2semEvry"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
|
@ -371,7 +424,7 @@ class ParcoursLP2014(TypeParcours):
|
|||
# l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
|
||||
# vérifiés par ScoDoc)
|
||||
|
||||
TYPE_PARCOURS = 230
|
||||
TYPE_PARCOURS = CodesParcours.LP2014
|
||||
NAME = "LP2014"
|
||||
NB_SEM = 1
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
|
||||
|
@ -415,7 +468,7 @@ register_parcours(ParcoursLP2014())
|
|||
class ParcoursLP2sem2014(ParcoursLP):
|
||||
"""Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
|
||||
|
||||
TYPE_PARCOURS = 240
|
||||
TYPE_PARCOURS = CodesParcours.LP2sem2014
|
||||
NAME = "LP2014_2sem"
|
||||
NB_SEM = 2
|
||||
|
||||
|
@ -427,7 +480,7 @@ register_parcours(ParcoursLP2sem2014())
|
|||
class ParcoursM2(TypeParcours):
|
||||
"""Master 2 (en deux "semestres")"""
|
||||
|
||||
TYPE_PARCOURS = 250
|
||||
TYPE_PARCOURS = CodesParcours.M2
|
||||
NAME = "M2sem"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
|
@ -440,7 +493,7 @@ register_parcours(ParcoursM2())
|
|||
class ParcoursM2noncomp(ParcoursM2):
|
||||
"""Master 2 (en deux "semestres") sans compensation"""
|
||||
|
||||
TYPE_PARCOURS = 251
|
||||
TYPE_PARCOURS = CodesParcours.M2noncomp
|
||||
NAME = "M2noncomp"
|
||||
COMPENSATION_UE = False
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
@ -452,7 +505,7 @@ register_parcours(ParcoursM2noncomp())
|
|||
class ParcoursMono(TypeParcours):
|
||||
"""Formation générique en une session"""
|
||||
|
||||
TYPE_PARCOURS = 300
|
||||
TYPE_PARCOURS = CodesParcours.Mono
|
||||
NAME = "Mono"
|
||||
NB_SEM = 1
|
||||
COMPENSATION_UE = False
|
||||
|
@ -465,7 +518,7 @@ register_parcours(ParcoursMono())
|
|||
class ParcoursLegacy(TypeParcours):
|
||||
"""DUT (ancien ScoDoc, ne plus utiliser)"""
|
||||
|
||||
TYPE_PARCOURS = 0
|
||||
TYPE_PARCOURS = CodesParcours.Legacy
|
||||
NAME = "DUT"
|
||||
NB_SEM = 4
|
||||
COMPENSATION_UE = None # backward compat: defini dans formsemestre
|
||||
|
@ -499,7 +552,7 @@ class ParcoursBachelorISCID6(ParcoursISCID):
|
|||
"""ISCID: Bachelor en 3 ans (6 sem.)"""
|
||||
|
||||
NAME = "ParcoursBachelorISCID6"
|
||||
TYPE_PARCOURS = 1001
|
||||
TYPE_PARCOURS = CodesParcours.ISCID6
|
||||
NAME = ""
|
||||
NB_SEM = 6
|
||||
ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme
|
||||
|
@ -510,7 +563,7 @@ register_parcours(ParcoursBachelorISCID6())
|
|||
|
||||
class ParcoursMasterISCID4(ParcoursISCID):
|
||||
"ISCID: Master en 2 ans (4 sem.)"
|
||||
TYPE_PARCOURS = 1002
|
||||
TYPE_PARCOURS = CodesParcours.ISCID4
|
||||
NAME = "ParcoursMasterISCID4"
|
||||
NB_SEM = 4
|
||||
ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme
|
||||
|
@ -519,6 +572,34 @@ class ParcoursMasterISCID4(ParcoursISCID):
|
|||
register_parcours(ParcoursMasterISCID4())
|
||||
|
||||
|
||||
class ParcoursILEPS(TypeParcours):
|
||||
"""Superclasse pour les parcours de l'ILEPS"""
|
||||
|
||||
# SESSION_NAME = "année"
|
||||
# SESSION_NAME_A = "de l'"
|
||||
# SESSION_ABBRV = 'A' # A1, A2, ...
|
||||
COMPENSATION_UE = False
|
||||
UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE]
|
||||
# Barre moy gen. pour validation semestre:
|
||||
BARRE_MOY = 10.0
|
||||
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")
|
||||
# et pas de barre (-1.) pour UE élective.
|
||||
BARRE_UE = {UE_STANDARD: 8.0, UE_OPTIONNELLE: 0.0}
|
||||
BARRE_UE_DEFAULT = 0.0 # pas de barre sur les autres UE
|
||||
|
||||
|
||||
class ParcoursLicenceILEPS6(ParcoursILEPS):
|
||||
"""ILEPS: Licence 6 semestres"""
|
||||
|
||||
TYPE_PARCOURS = 1010
|
||||
NAME = "LicenceILEPS6"
|
||||
NB_SEM = 6
|
||||
|
||||
|
||||
register_parcours(ParcoursLicenceILEPS6())
|
||||
|
||||
|
||||
class ParcoursUCAC(TypeParcours):
|
||||
"""Règles de validation UCAC"""
|
||||
|
||||
|
@ -536,7 +617,7 @@ class ParcoursUCAC(TypeParcours):
|
|||
class ParcoursLicenceUCAC3(ParcoursUCAC):
|
||||
"""UCAC: Licence en 3 sessions d'un an"""
|
||||
|
||||
TYPE_PARCOURS = 501
|
||||
TYPE_PARCOURS = CodesParcours.LicenceUCAC3
|
||||
NAME = "Licence UCAC en 3 sessions d'un an"
|
||||
NB_SEM = 3
|
||||
|
||||
|
@ -547,7 +628,7 @@ register_parcours(ParcoursLicenceUCAC3())
|
|||
class ParcoursMasterUCAC2(ParcoursUCAC):
|
||||
"""UCAC: Master en 2 sessions d'un an"""
|
||||
|
||||
TYPE_PARCOURS = 502
|
||||
TYPE_PARCOURS = CodesParcours.MasterUCAC2
|
||||
NAME = "Master UCAC en 2 sessions d'un an"
|
||||
NB_SEM = 2
|
||||
|
||||
|
@ -558,7 +639,7 @@ register_parcours(ParcoursMasterUCAC2())
|
|||
class ParcoursMonoUCAC(ParcoursUCAC):
|
||||
"""UCAC: Formation en 1 session de durée variable"""
|
||||
|
||||
TYPE_PARCOURS = 503
|
||||
TYPE_PARCOURS = CodesParcours.MonoUCAC
|
||||
NAME = "Formation UCAC en 1 session de durée variable"
|
||||
NB_SEM = 1
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
@ -570,7 +651,7 @@ register_parcours(ParcoursMonoUCAC())
|
|||
class Parcours6Sem(TypeParcours):
|
||||
"""Parcours générique en 6 semestres"""
|
||||
|
||||
TYPE_PARCOURS = 600
|
||||
TYPE_PARCOURS = CodesParcours.GEN_6_SEM
|
||||
NAME = "Formation en 6 semestres"
|
||||
NB_SEM = 6
|
||||
COMPENSATION_UE = True
|
||||
|
@ -592,7 +673,7 @@ register_parcours(Parcours6Sem())
|
|||
class ParcoursMasterLMD(TypeParcours):
|
||||
"""Master générique en 4 semestres dans le LMD"""
|
||||
|
||||
TYPE_PARCOURS = 402
|
||||
TYPE_PARCOURS = CodesParcours.MasterLMD
|
||||
NAME = "Master LMD"
|
||||
NB_SEM = 4
|
||||
COMPENSATION_UE = True # variabale inutilisée
|
||||
|
@ -605,7 +686,7 @@ register_parcours(ParcoursMasterLMD())
|
|||
class ParcoursMasterIG(ParcoursMasterLMD):
|
||||
"""Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
|
||||
|
||||
TYPE_PARCOURS = 403
|
||||
TYPE_PARCOURS = CodesParcours.MasterIG
|
||||
NAME = "Master IG P13"
|
||||
BARRE_MOY = 10.0
|
||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||
|
@ -672,4 +753,9 @@ FORMATION_PARCOURS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_PARCOUR
|
|||
|
||||
|
||||
def get_parcours_from_code(code_parcours):
|
||||
return TYPES_PARCOURS[code_parcours]
|
||||
parcours = TYPES_PARCOURS.get(code_parcours)
|
||||
if parcours is None:
|
||||
log(f"Warning: invalid code_parcours: {code_parcours}")
|
||||
# default to legacy
|
||||
parcours = TYPES_PARCOURS.get(0)
|
||||
return parcours
|
||||
|
|
|
@ -34,6 +34,7 @@ from flask import url_for, g
|
|||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_utils import (
|
||||
ModuleType,
|
||||
NOTES_ATTENTE,
|
||||
NOTES_NEUTRALISE,
|
||||
EVALUATION_NORMALE,
|
||||
|
@ -44,7 +45,7 @@ from app.scodoc.sco_exceptions import ScoValueError
|
|||
from app import log
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_formulas
|
||||
|
@ -103,8 +104,9 @@ formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor.
|
|||
formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit
|
||||
|
||||
|
||||
def get_ue_expression(formsemestre_id, ue_id, cnx, html_quote=False):
|
||||
def get_ue_expression(formsemestre_id, ue_id, html_quote=False):
|
||||
"""Returns UE expression (formula), or None if no expression has been defined"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
el = formsemestre_ue_computation_expr_list(
|
||||
cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
|
||||
)
|
||||
|
@ -160,7 +162,7 @@ def compute_user_formula(
|
|||
# log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug
|
||||
user_moy = sco_formulas.eval_user_expression(formula, variables)
|
||||
# log('user_moy=%s' % user_moy)
|
||||
if user_moy != "NA0" and user_moy != "NA":
|
||||
if user_moy != "NA":
|
||||
user_moy = float(user_moy)
|
||||
if (user_moy > 20) or (user_moy < 0):
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
|
@ -203,7 +205,7 @@ def compute_moduleimpl_moyennes(nt, modimpl):
|
|||
"""
|
||||
diag_info = {} # message d'erreur formule
|
||||
moduleimpl_id = modimpl["moduleimpl_id"]
|
||||
is_malus = modimpl["module"]["module_type"] == scu.MODULE_MALUS
|
||||
is_malus = modimpl["module"]["module_type"] == ModuleType.MALUS
|
||||
sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
|
||||
etudids = sco_moduleimpl.moduleimpl_listeetuds(
|
||||
moduleimpl_id
|
||||
|
@ -230,7 +232,7 @@ def compute_moduleimpl_moyennes(nt, modimpl):
|
|||
eval_rattr = None
|
||||
for e in evals:
|
||||
e["nb_inscrits"] = e["etat"]["nb_inscrits"]
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
e["evaluation_id"]
|
||||
) # toutes, y compris demissions
|
||||
# restreint aux étudiants encore inscrits à ce module
|
||||
|
@ -295,15 +297,17 @@ def compute_moduleimpl_moyennes(nt, modimpl):
|
|||
# il manque une note ! (si publish_incomplete, cela peut arriver, on ignore)
|
||||
if e["coefficient"] > 0 and not e["publish_incomplete"]:
|
||||
nb_missing += 1
|
||||
# ne devrait pas arriver ?
|
||||
log("\nXXX SCM298\n")
|
||||
if nb_missing == 0 and sum_coefs > 0:
|
||||
if sum_coefs > 0:
|
||||
R[etudid] = sum_notes / sum_coefs
|
||||
moy_valid = True
|
||||
else:
|
||||
R[etudid] = "na"
|
||||
R[etudid] = "NA"
|
||||
moy_valid = False
|
||||
else:
|
||||
R[etudid] = "NA%d" % nb_missing
|
||||
R[etudid] = "NA"
|
||||
moy_valid = False
|
||||
|
||||
if user_expr:
|
||||
|
@ -361,7 +365,7 @@ def compute_moduleimpl_moyennes(nt, modimpl):
|
|||
if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE:
|
||||
# rattrapage classique: prend la meilleure note entre moyenne
|
||||
# module et note eval rattrapage
|
||||
if (R[etudid] == "NA0") or (note_sur_20 > R[etudid]):
|
||||
if (R[etudid] == "NA") or (note_sur_20 > R[etudid]):
|
||||
# log('note_sur_20=%s' % note_sur_20)
|
||||
R[etudid] = note_sur_20
|
||||
elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2:
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc.sco_logos import write_logo, find_logo, delete_logo
|
||||
import app
|
||||
from flask import current_app
|
||||
|
||||
|
||||
class Action:
|
||||
"""Base class for all classes describing an action from from config form."""
|
||||
|
||||
def __init__(self, message, parameters):
|
||||
self.message = message
|
||||
self.parameters = parameters
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters, stream=None):
|
||||
"""Check (from parameters) if some action has to be done and
|
||||
then return list of action (or else return empty list)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def display(self):
|
||||
"""return a str describing the action to be done"""
|
||||
return self.message.format_map(self.parameters)
|
||||
|
||||
def execute(self):
|
||||
"""Executes the action"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
GLOBAL = "_"
|
||||
|
||||
|
||||
class LogoUpdate(Action):
|
||||
"""Action: change a logo
|
||||
dept_id: dept_id or '_',
|
||||
logo_id: logo_id,
|
||||
upload: image file replacement
|
||||
"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Modification du logo {parameters['logo_id']} pour le département {parameters['dept_id']}",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
dept_id = parameters["dept_key"]
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
parameters["dept_id"] = dept_id
|
||||
if parameters["upload"] is not None:
|
||||
return LogoUpdate(parameters)
|
||||
return None
|
||||
|
||||
def execute(self):
|
||||
current_app.logger.info(self.message)
|
||||
write_logo(
|
||||
stream=self.parameters["upload"],
|
||||
dept_id=self.parameters["dept_id"],
|
||||
name=self.parameters["logo_id"],
|
||||
)
|
||||
|
||||
|
||||
class LogoDelete(Action):
|
||||
"""Action: Delete an existing logo
|
||||
dept_id: dept_id or '_',
|
||||
logo_id: logo_id
|
||||
"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Suppression du logo {parameters['logo_id']} pour le département {parameters['dept_id'] or 'tous'}.",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
parameters["dept_id"] = parameters["dept_key"]
|
||||
if parameters["dept_key"] == GLOBAL:
|
||||
parameters["dept_id"] = None
|
||||
if parameters["do_delete"]:
|
||||
return LogoDelete(parameters)
|
||||
return None
|
||||
|
||||
def execute(self):
|
||||
current_app.logger.info(self.message)
|
||||
delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"])
|
||||
|
||||
|
||||
class LogoInsert(Action):
|
||||
"""Action: add a new logo
|
||||
dept_key: dept_id or '_',
|
||||
logo_id: logo_id,
|
||||
upload: image file replacement
|
||||
"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Ajout du logo {parameters['name']} pour le département {parameters['dept_key']} ({parameters['upload'].filename}).",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
if parameters["dept_key"] == GLOBAL:
|
||||
parameters["dept_id"] = None
|
||||
if parameters["upload"] and parameters["name"]:
|
||||
logo = find_logo(
|
||||
logoname=parameters["name"], dept_id=parameters["dept_key"]
|
||||
)
|
||||
if logo is None:
|
||||
return LogoInsert(parameters)
|
||||
return None
|
||||
|
||||
def execute(self):
|
||||
dept_id = self.parameters["dept_key"]
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
current_app.logger.info(self.message)
|
||||
write_logo(
|
||||
stream=self.parameters["upload"],
|
||||
name=self.parameters["name"],
|
||||
dept_id=dept_id,
|
||||
)
|
||||
|
||||
|
||||
class BonusSportUpdate(Action):
|
||||
"""Action: Change bonus_sport_function_name.
|
||||
bonus_sport_function_name: the new value"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Changement du calcul de bonus sport pour ({parameters['bonus_sport_func_name']}).",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
if (
|
||||
parameters["bonus_sport_func_name"]
|
||||
!= ScoDocSiteConfig.get_bonus_sport_func_name()
|
||||
):
|
||||
return [BonusSportUpdate(parameters)]
|
||||
return []
|
||||
|
||||
def execute(self):
|
||||
current_app.logger.info(self.message)
|
||||
ScoDocSiteConfig.set_bonus_sport_func(self.parameters["bonus_sport_func_name"])
|
||||
app.clear_scodoc_cache()
|
|
@ -0,0 +1,402 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaires configuration logos
|
||||
|
||||
Contrib @jmp, dec 21
|
||||
"""
|
||||
import re
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
|
||||
from wtforms.fields.simple import StringField, HiddenField
|
||||
|
||||
from app import AccessDenied
|
||||
from app.models import Departement
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc import sco_logos, html_sco_header
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_config_actions import (
|
||||
LogoDelete,
|
||||
LogoUpdate,
|
||||
LogoInsert,
|
||||
BonusSportUpdate,
|
||||
)
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
|
||||
|
||||
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||
|
||||
# class ItemForm(FlaskForm):
|
||||
# """Unused Generic class to document common behavior for classes
|
||||
# * ScoConfigurationForm
|
||||
# * DeptForm
|
||||
# * LogoForm
|
||||
# Some or all of these implements:
|
||||
# * Composite design pattern (ScoConfigurationForm and DeptForm)
|
||||
# - a FieldList(FormField(ItemForm))
|
||||
# - FieldListItem are created by browsing the model
|
||||
# - index dictionnary to provide direct access to a SubItemForm
|
||||
# - the direct access method (get_form)
|
||||
# * have some information added to be displayed
|
||||
# - information are collected from a model object
|
||||
# Common methods:
|
||||
# * build(model) (not for LogoForm who has no child)
|
||||
# for each child:
|
||||
# * create en entry in the FieldList for each subitem found
|
||||
# * update self.index
|
||||
# * fill_in additional information into the form
|
||||
# * recursively calls build for each chid
|
||||
# some spécific information may be added after standard processing
|
||||
# (typically header/footer description)
|
||||
# * preview(data)
|
||||
# check the data from a post and build a list of operations that has to be done.
|
||||
# for a two phase process:
|
||||
# * phase 1 (list all opérations)
|
||||
# * phase 2 (may be confirmation and execure)
|
||||
# - if no op found: return to the form with a message 'Aucune modification trouvée'
|
||||
# - only one operation found: execute and go to main page
|
||||
# - more than 1 operation found. asked form confirmation (and execution if confirmed)
|
||||
#
|
||||
# Someday we'll have time to refactor as abstract classes but Abstract FieldList makes this a bit complicated
|
||||
# """
|
||||
|
||||
# Terminology:
|
||||
# dept_id : identifies a dept in modele (= list_logos()). None designates globals logos
|
||||
# dept_key : identifies a dept in this form only (..index[dept_key], and fields 'dept_key').
|
||||
# 'GLOBAL' designates globals logos (we need a string value to set up HiddenField
|
||||
GLOBAL = "_"
|
||||
|
||||
|
||||
def dept_id_to_key(dept_id):
|
||||
if dept_id is None:
|
||||
return GLOBAL
|
||||
return dept_id
|
||||
|
||||
|
||||
def dept_key_to_id(dept_key):
|
||||
if dept_key == GLOBAL:
|
||||
return None
|
||||
return dept_key
|
||||
|
||||
|
||||
class AddLogoForm(FlaskForm):
|
||||
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
|
||||
|
||||
dept_key = HiddenField()
|
||||
name = StringField(
|
||||
label="Nom",
|
||||
validators=[
|
||||
validators.regexp(
|
||||
r"^[a-zA-Z0-9-]*$",
|
||||
re.IGNORECASE,
|
||||
"Ne doit comporter que lettres, chiffres ou -",
|
||||
),
|
||||
validators.Length(
|
||||
max=20, message="Un nom ne doit pas dépasser 20 caractères"
|
||||
),
|
||||
validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"),
|
||||
],
|
||||
)
|
||||
upload = FileField(
|
||||
label="Sélectionner l'image",
|
||||
validators=[
|
||||
FileAllowed(
|
||||
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
||||
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
|
||||
),
|
||||
validators.DataRequired("Fichier image manquant"),
|
||||
],
|
||||
)
|
||||
do_insert = SubmitField("ajouter une image")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate_name(self, name):
|
||||
dept_id = dept_key_to_id(self.dept_key.data)
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
if find_logo(logoname=name.data, dept_id=dept_id) is not None:
|
||||
raise validators.ValidationError("Un logo de même nom existe déjà")
|
||||
|
||||
def select_action(self):
|
||||
if self.data["do_insert"]:
|
||||
if self.validate():
|
||||
return LogoInsert.build_action(self.data)
|
||||
return None
|
||||
|
||||
|
||||
class LogoForm(FlaskForm):
|
||||
"""Embed both presentation of a logo (cf. template file configuration.html)
|
||||
and all its data and UI action (change, delete)"""
|
||||
|
||||
dept_key = HiddenField()
|
||||
logo_id = HiddenField()
|
||||
upload = FileField(
|
||||
label="Remplacer l'image",
|
||||
validators=[
|
||||
FileAllowed(
|
||||
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
||||
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
|
||||
)
|
||||
],
|
||||
)
|
||||
do_delete = SubmitField("Supprimer l'image")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
self.logo = find_logo(
|
||||
logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data)
|
||||
).select()
|
||||
self.description = None
|
||||
self.titre = None
|
||||
self.can_delete = True
|
||||
if self.dept_key.data == GLOBAL:
|
||||
if self.logo_id.data == "header":
|
||||
self.can_delete = False
|
||||
self.description = ""
|
||||
self.titre = "Logo en-tête"
|
||||
if self.logo_id.data == "footer":
|
||||
self.can_delete = False
|
||||
self.titre = "Logo pied de page"
|
||||
self.description = ""
|
||||
else:
|
||||
if self.logo_id.data == "header":
|
||||
self.description = "Se substitue au header défini au niveau global"
|
||||
self.titre = "Logo en-tête"
|
||||
if self.logo_id.data == "footer":
|
||||
self.description = "Se substitue au footer défini au niveau global"
|
||||
self.titre = "Logo pied de page"
|
||||
|
||||
def select_action(self):
|
||||
if self.do_delete.data and self.can_delete:
|
||||
return LogoDelete.build_action(self.data)
|
||||
if self.upload.data and self.validate():
|
||||
return LogoUpdate.build_action(self.data)
|
||||
return None
|
||||
|
||||
|
||||
class DeptForm(FlaskForm):
|
||||
dept_key = HiddenField()
|
||||
dept_name = HiddenField()
|
||||
add_logo = FormField(AddLogoForm)
|
||||
logos = FieldList(FormField(LogoForm))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def is_local(self):
|
||||
if self.dept_key.data == GLOBAL:
|
||||
return None
|
||||
return True
|
||||
|
||||
def select_action(self):
|
||||
action = self.add_logo.form.select_action()
|
||||
if action:
|
||||
return action
|
||||
for logo_entry in self.logos.entries:
|
||||
logo_form = logo_entry.form
|
||||
action = logo_form.select_action()
|
||||
if action:
|
||||
return action
|
||||
return None
|
||||
|
||||
def get_form(self, logoname=None):
|
||||
"""Retourne le formulaire associé à un logo. None si pas trouvé"""
|
||||
if logoname is None: # recherche de département
|
||||
return self
|
||||
return self.index.get(logoname, None)
|
||||
|
||||
|
||||
def _make_dept_id_name():
|
||||
"""Cette section assute que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
|
||||
et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département)
|
||||
-> [ (None, None), (dept_id, dept_name)... ]"""
|
||||
depts = [(None, GLOBAL)]
|
||||
for dept in (
|
||||
Departement.query.filter_by(visible=True).order_by(Departement.acronym).all()
|
||||
):
|
||||
depts.append((dept.id, dept.acronym))
|
||||
return depts
|
||||
|
||||
|
||||
def _ordered_logos(modele):
|
||||
"""sort logoname alphabetically but header and footer moved at start. (since there is no space in logoname)"""
|
||||
|
||||
def sort(name):
|
||||
if name == "header":
|
||||
return " 0"
|
||||
if name == "footer":
|
||||
return " 1"
|
||||
return name
|
||||
|
||||
order = sorted(modele.keys(), key=sort)
|
||||
return order
|
||||
|
||||
|
||||
def _make_dept_data(dept_id, dept_name, modele):
|
||||
dept_key = dept_id_to_key(dept_id)
|
||||
data = {
|
||||
"dept_key": dept_key,
|
||||
"dept_name": dept_name,
|
||||
"add_logo": {"dept_key": dept_key},
|
||||
}
|
||||
logos = []
|
||||
if modele is not None:
|
||||
for name in _ordered_logos(modele):
|
||||
logos.append({"dept_key": dept_key, "logo_id": name})
|
||||
data["logos"] = logos
|
||||
return data
|
||||
|
||||
|
||||
def _make_depts_data(modele):
|
||||
data = []
|
||||
for dept_id, dept_name in _make_dept_id_name():
|
||||
data.append(
|
||||
_make_dept_data(
|
||||
dept_id=dept_id, dept_name=dept_name, modele=modele.get(dept_id, None)
|
||||
)
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def _make_data(bonus_sport, modele):
|
||||
data = {
|
||||
"bonus_sport_func_name": bonus_sport,
|
||||
"depts": _make_depts_data(modele=modele),
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration général"
|
||||
bonus_sport_func_name = SelectField(
|
||||
label="Fonction de calcul des bonus sport&culture",
|
||||
choices=[
|
||||
(x, x if x else "Aucune")
|
||||
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
|
||||
],
|
||||
)
|
||||
depts = FieldList(FormField(DeptForm))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# def _set_global_logos_infos(self):
|
||||
# "specific processing for globals items"
|
||||
# global_header = self.get_form(logoname="header")
|
||||
# global_header.description = (
|
||||
# "image placée en haut de certains documents documents PDF."
|
||||
# )
|
||||
# global_header.titre = "Logo en-tête"
|
||||
# global_header.can_delete = False
|
||||
# global_footer = self.get_form(logoname="footer")
|
||||
# global_footer.description = (
|
||||
# "image placée en pied de page de certains documents documents PDF."
|
||||
# )
|
||||
# global_footer.titre = "Logo pied de page"
|
||||
# global_footer.can_delete = False
|
||||
|
||||
# def _build_dept(self, dept_id, dept_name, modele):
|
||||
# dept_key = dept_id or GLOBAL
|
||||
# data = {"dept_key": dept_key}
|
||||
# entry = self.depts.append_entry(data)
|
||||
# entry.form.build(dept_name, modele.get(dept_id, {}))
|
||||
# self.index[str(dept_key)] = entry.form
|
||||
|
||||
# def build(self, modele):
|
||||
# "Build the Form hierachy (DeptForm, LogoForm) and add extra data (from modele)"
|
||||
# # if entries already initialized (POST). keep subforms
|
||||
# self.index = {}
|
||||
# # create entries in FieldList (one entry per dept
|
||||
# for dept_id, dept_name in self.dept_id_name:
|
||||
# self._build_dept(dept_id=dept_id, dept_name=dept_name, modele=modele)
|
||||
# self._set_global_logos_infos()
|
||||
|
||||
def get_form(self, dept_key=GLOBAL, logoname=None):
|
||||
"""Retourne un formulaire:
|
||||
* pour un département (get_form(dept_id)) ou à un logo (get_form(dept_id, logname))
|
||||
* propre à un département (get_form(dept_id, logoname) ou global (get_form(logoname))
|
||||
retourne None si le formulaire cherché ne peut être trouvé
|
||||
"""
|
||||
dept_form = self.index.get(dept_key, None)
|
||||
if dept_form is None: # département non trouvé
|
||||
return None
|
||||
return dept_form.get_form(logoname)
|
||||
|
||||
def select_action(self):
|
||||
if (
|
||||
self.data["bonus_sport_func_name"]
|
||||
!= ScoDocSiteConfig.get_bonus_sport_func_name()
|
||||
):
|
||||
return BonusSportUpdate(self.data)
|
||||
for dept_entry in self.depts:
|
||||
dept_form = dept_entry.form
|
||||
action = dept_form.select_action()
|
||||
if action:
|
||||
return action
|
||||
return None
|
||||
|
||||
|
||||
def configuration():
|
||||
"""Panneau de configuration général"""
|
||||
auth_name = str(current_user)
|
||||
if not current_user.is_administrator():
|
||||
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
|
||||
form = ScoDocConfigurationForm(
|
||||
data=_make_data(
|
||||
bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(),
|
||||
modele=sco_logos.list_logos(),
|
||||
)
|
||||
)
|
||||
if form.is_submitted():
|
||||
action = form.select_action()
|
||||
if action:
|
||||
action.execute()
|
||||
flash(action.message)
|
||||
return redirect(
|
||||
url_for(
|
||||
"scodoc.configuration",
|
||||
)
|
||||
)
|
||||
return render_template(
|
||||
"configuration.html",
|
||||
scodoc_dept=None,
|
||||
title="Configuration ScoDoc",
|
||||
form=form,
|
||||
)
|
|
@ -0,0 +1,173 @@
|
|||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Édition formation APC (BUT)
|
||||
"""
|
||||
import flask
|
||||
from flask import url_for
|
||||
from flask.templating import render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl
|
||||
from app.models.notes import ScolarFormSemestreValidation
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
def html_edit_formation_apc(
|
||||
formation,
|
||||
semestre_idx=None,
|
||||
editable=True,
|
||||
tag_editable=True,
|
||||
):
|
||||
"""Formulaire html pour visualisation ou édition d'une formation APC.
|
||||
- Les UEs
|
||||
- Les ressources
|
||||
- Les SAÉs
|
||||
"""
|
||||
parcours = formation.get_parcours()
|
||||
assert parcours.APC_SAE
|
||||
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
|
||||
Module.semestre_id, Module.numero, Module.code
|
||||
)
|
||||
saes = formation.modules.filter_by(module_type=ModuleType.SAE).order_by(
|
||||
Module.semestre_id, Module.numero, Module.code
|
||||
)
|
||||
if semestre_idx is None:
|
||||
semestre_ids = range(1, parcours.NB_SEM + 1)
|
||||
else:
|
||||
semestre_ids = [semestre_idx]
|
||||
other_modules = formation.modules.filter(
|
||||
Module.module_type != ModuleType.SAE, Module.module_type != ModuleType.RESSOURCE
|
||||
).order_by(
|
||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||
)
|
||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||
|
||||
icons = {
|
||||
"arrow_up": arrow_up,
|
||||
"arrow_down": arrow_down,
|
||||
"arrow_none": arrow_none,
|
||||
"delete": scu.icontag(
|
||||
"delete_small_img",
|
||||
title="Supprimer (module inutilisé)",
|
||||
alt="supprimer",
|
||||
),
|
||||
"delete_disabled": scu.icontag(
|
||||
"delete_small_dis_img", title="Suppression impossible (module utilisé)"
|
||||
),
|
||||
}
|
||||
|
||||
H = [
|
||||
render_template(
|
||||
"pn/form_ues.html",
|
||||
formation=formation,
|
||||
semestre_ids=semestre_ids,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
UniteEns=UniteEns,
|
||||
),
|
||||
]
|
||||
for semestre_idx in semestre_ids:
|
||||
ressources_in_sem = ressources.filter_by(semestre_id=semestre_idx)
|
||||
saes_in_sem = saes.filter_by(semestre_id=semestre_idx)
|
||||
other_modules_in_sem = other_modules.filter_by(semestre_id=semestre_idx)
|
||||
H += [
|
||||
render_template(
|
||||
"pn/form_mods.html",
|
||||
formation=formation,
|
||||
titre=f"Ressources du S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle ressource",
|
||||
modules=ressources_in_sem,
|
||||
module_type=ModuleType.RESSOURCE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
),
|
||||
render_template(
|
||||
"pn/form_mods.html",
|
||||
formation=formation,
|
||||
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle SAÉ",
|
||||
modules=saes_in_sem,
|
||||
module_type=ModuleType.SAE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
),
|
||||
render_template(
|
||||
"pn/form_mods.html",
|
||||
formation=formation,
|
||||
titre=f"Autres modules (non BUT) du S{semestre_idx}",
|
||||
create_element_msg="créer un nouveau module",
|
||||
modules=other_modules_in_sem,
|
||||
module_type=ModuleType.STANDARD,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
),
|
||||
]
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def html_ue_infos(ue):
|
||||
"""page d'information sur une UE"""
|
||||
from app.views import ScoData
|
||||
|
||||
formsemestres = (
|
||||
db.session.query(FormSemestre)
|
||||
.filter(
|
||||
ue.id == Module.ue_id,
|
||||
Module.id == ModuleImpl.module_id,
|
||||
FormSemestre.id == ModuleImpl.formsemestre_id,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
nb_etuds_valid_ue = ScolarFormSemestreValidation.query.filter_by(
|
||||
ue_id=ue.id
|
||||
).count()
|
||||
can_safely_be_suppressed = (
|
||||
(nb_etuds_valid_ue == 0)
|
||||
and (len(formsemestres) == 0)
|
||||
and ue.modules.count() == 0
|
||||
and ue.matieres.count() == 0
|
||||
)
|
||||
return render_template(
|
||||
"pn/ue_infos.html",
|
||||
# "pn/tmp.html",
|
||||
titre=f"UE {ue.acronyme} {ue.titre}",
|
||||
ue=ue,
|
||||
formsemestres=formsemestres,
|
||||
nb_etuds_valid_ue=nb_etuds_valid_ue,
|
||||
can_safely_be_suppressed=can_safely_be_suppressed,
|
||||
sco=ScoData(),
|
||||
)
|
|
@ -31,9 +31,13 @@
|
|||
import flask
|
||||
from flask import g, url_for, request
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models.formations import Formation
|
||||
from app.models.modules import Module
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
|
||||
|
||||
|
@ -298,52 +302,51 @@ def do_formation_edit(args):
|
|||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
sco_formations._formationEditor.edit(cnx, args)
|
||||
invalidate_sems_in_formation(args["formation_id"])
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
formation.invalidate_cached_sems()
|
||||
formation.force_semestre_modules_aux_ues()
|
||||
|
||||
|
||||
def invalidate_sems_in_formation(formation_id):
|
||||
"Invalide les semestres utilisant cette formation"
|
||||
for sem in sco_formsemestre.do_formsemestre_list(
|
||||
args={"formation_id": formation_id}
|
||||
):
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=sem["formsemestre_id"]
|
||||
) # > formation modif.
|
||||
|
||||
|
||||
def module_move(module_id, after=0, redirect=1):
|
||||
def module_move(module_id, after=0, redirect=True):
|
||||
"""Move before/after previous one (decrement/increment numero)"""
|
||||
module = sco_edit_module.module_list({"module_id": module_id})[0]
|
||||
redirect = int(redirect)
|
||||
redirect = bool(redirect)
|
||||
module = Module.query.get_or_404(module_id)
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
formation_id = module["formation_id"]
|
||||
others = sco_edit_module.module_list({"matiere_id": module["matiere_id"]})
|
||||
# log('others=%s' % others)
|
||||
if len(others) > 1:
|
||||
idx = [p["module_id"] for p in others].index(module_id)
|
||||
# log('module_move: after=%s idx=%s' % (after, idx))
|
||||
raise ValueError(f'invalid value for "after" ({after})')
|
||||
if module.formation.is_apc():
|
||||
# pas de matières, mais on prend tous les modules de même type de la formation
|
||||
query = Module.query.filter_by(
|
||||
semestre_id=module.semestre_id,
|
||||
formation=module.formation,
|
||||
module_type=module.module_type,
|
||||
)
|
||||
else:
|
||||
query = Module.query.filter_by(matiere=module.matiere)
|
||||
modules = query.order_by(Module.numero, Module.code).all()
|
||||
if len({o.numero for o in modules}) != len(modules):
|
||||
# il y a des numeros identiques !
|
||||
scu.objects_renumber(db, modules)
|
||||
if len(modules) > 1:
|
||||
idx = [m.id for m in modules].index(module.id)
|
||||
neigh = None # object to swap with
|
||||
if after == 0 and idx > 0:
|
||||
neigh = others[idx - 1]
|
||||
elif after == 1 and idx < len(others) - 1:
|
||||
neigh = others[idx + 1]
|
||||
if neigh: #
|
||||
# swap numero between partition and its neighbor
|
||||
# log('moving module %s' % module_id)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
module["numero"], neigh["numero"] = neigh["numero"], module["numero"]
|
||||
if module["numero"] == neigh["numero"]:
|
||||
neigh["numero"] -= 2 * after - 1
|
||||
sco_edit_module._moduleEditor.edit(cnx, module)
|
||||
sco_edit_module._moduleEditor.edit(cnx, neigh)
|
||||
|
||||
neigh = modules[idx - 1]
|
||||
elif after == 1 and idx < len(modules) - 1:
|
||||
neigh = modules[idx + 1]
|
||||
if neigh: # échange les numéros
|
||||
module.numero, neigh.numero = neigh.numero, module.numero
|
||||
db.session.add(module)
|
||||
db.session.add(neigh)
|
||||
db.session.commit()
|
||||
# redirect to ue_list page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=module.formation.id,
|
||||
semestre_idx=module.ue.semestre_idx,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -381,5 +384,6 @@ def ue_move(ue_id, after=0, redirect=1):
|
|||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=o["formation_id"],
|
||||
semestre_idx=o["semestre_idx"],
|
||||
)
|
||||
)
|
||||
|
|
|
@ -34,6 +34,7 @@ from flask import g, url_for, request
|
|||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Formation
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
|
||||
from app.scodoc import html_sco_header
|
||||
|
@ -66,7 +67,7 @@ def do_matiere_edit(*args, **kw):
|
|||
# edit
|
||||
_matiereEditor.edit(cnx, *args, **kw)
|
||||
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
||||
sco_edit_formation.invalidate_sems_in_formation(formation_id)
|
||||
Formation.query.get(formation_id).invalidate_cached_sems()
|
||||
|
||||
|
||||
def do_matiere_create(args):
|
||||
|
|
|
@ -29,12 +29,17 @@
|
|||
(portage from DTML)
|
||||
"""
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
from flask import url_for, render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
from app.models import Matiere, Module, UniteEns
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models import Formation
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError
|
||||
|
@ -44,22 +49,6 @@ from app.scodoc import sco_edit_matiere
|
|||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
|
||||
_MODULE_HELP = """<p class="help">
|
||||
Les modules sont décrits dans le programme pédagogique. Un module est pour ce
|
||||
logiciel l'unité pédagogique élémentaire. On va lui associer une note
|
||||
à travers des <em>évaluations</em>. <br/>
|
||||
Cette note (moyenne de module) sera utilisée pour calculer la moyenne
|
||||
générale (et la moyenne de l'UE à laquelle appartient le module). Pour
|
||||
cela, on utilisera le <em>coefficient</em> associé au module.
|
||||
</p>
|
||||
|
||||
<p class="help">Un module possède un enseignant responsable
|
||||
(typiquement celui qui dispense le cours magistral). On peut associer
|
||||
au module une liste d'enseignants (typiquement les chargés de TD).
|
||||
Tous ces enseignants, plus le responsable du semestre, pourront
|
||||
saisir et modifier les notes de ce module.
|
||||
</p> """
|
||||
|
||||
_moduleEditor = ndb.EditableTable(
|
||||
"notes_modules",
|
||||
"module_id",
|
||||
|
@ -101,7 +90,7 @@ def module_list(*args, **kw):
|
|||
|
||||
|
||||
def do_module_create(args) -> int:
|
||||
"create a module"
|
||||
"Create a module. Returns id of new object."
|
||||
# create
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
|
@ -119,77 +108,164 @@ def do_module_create(args) -> int:
|
|||
return r
|
||||
|
||||
|
||||
def module_create(matiere_id=None):
|
||||
"""Creation d'un module"""
|
||||
def module_create(matiere_id=None, module_type=None, semestre_id=None):
|
||||
"""Création d'un module"""
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
if matiere_id is None:
|
||||
matiere = Matiere.query.get_or_404(matiere_id)
|
||||
if matiere is None:
|
||||
raise ScoValueError("invalid matiere !")
|
||||
M = sco_edit_matiere.matiere_list(args={"matiere_id": matiere_id})[0]
|
||||
UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0]
|
||||
Fo = sco_formations.formation_list(args={"formation_id": UE["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Création d'un module"),
|
||||
"""<h2>Création d'un module dans la matière %(titre)s""" % M,
|
||||
""" (UE %(acronyme)s)</h2>""" % UE,
|
||||
_MODULE_HELP,
|
||||
]
|
||||
# cherche le numero adequat (pour placer le module en fin de liste)
|
||||
Mods = module_list(args={"matiere_id": matiere_id})
|
||||
if Mods:
|
||||
default_num = max([m["numero"] for m in Mods]) + 10
|
||||
ue = matiere.ue
|
||||
parcours = ue.formation.get_parcours()
|
||||
is_apc = parcours.APC_SAE
|
||||
ues = ue.formation.ues.order_by(
|
||||
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
|
||||
).all()
|
||||
# cherche le numero adéquat (pour placer le module en fin de liste)
|
||||
modules = matiere.ue.formation.modules.all()
|
||||
if modules:
|
||||
default_num = max([m.numero or 0 for m in modules]) + 10
|
||||
else:
|
||||
default_num = 10
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
|
||||
if is_apc and module_type is not None:
|
||||
object_name = scu.MODULE_TYPE_NAMES[module_type]
|
||||
else:
|
||||
object_name = "Module"
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title=f"Création {object_name}"),
|
||||
]
|
||||
if is_apc:
|
||||
H += [
|
||||
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}</h2>"""
|
||||
]
|
||||
else:
|
||||
H += [
|
||||
f"""<h2>Création {object_name} dans la matière {matiere.titre},
|
||||
(UE {ue.acronyme})</h2>
|
||||
"""
|
||||
]
|
||||
|
||||
H += [
|
||||
render_template(
|
||||
"scodoc/help/modules.html",
|
||||
is_apc=is_apc,
|
||||
ue=ue,
|
||||
semestre_id=semestre_id,
|
||||
)
|
||||
]
|
||||
|
||||
descr = [
|
||||
(
|
||||
"code",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
|
||||
val, field, formation_id
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"titre",
|
||||
{
|
||||
"size": 30,
|
||||
"explanation": "nom du module. Exemple: <em>Introduction à la démarche ergonomique</em>",
|
||||
},
|
||||
),
|
||||
(
|
||||
"abbrev",
|
||||
{
|
||||
"size": 20,
|
||||
"explanation": "nom abrégé (pour les bulletins). Exemple: <em>Intro. à l'ergonomie</em>",
|
||||
},
|
||||
),
|
||||
]
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
if is_apc: # BUT: choix de l'UE de rattachement (qui donnera le semestre)
|
||||
descr += [
|
||||
(
|
||||
"code",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module (doit être unique dans la formation)",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=Fo[
|
||||
"formation_id"
|
||||
]: check_module_code_unicity(val, field, formation_id),
|
||||
},
|
||||
),
|
||||
("titre", {"size": 30, "explanation": "nom du module"}),
|
||||
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
|
||||
(
|
||||
"module_type",
|
||||
"ue_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": ("Standard", "Malus"),
|
||||
"allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)),
|
||||
"type": "int",
|
||||
"title": "UE de rattachement",
|
||||
"explanation": "utilisée pour la présentation dans certains documents",
|
||||
"labels": [f"{u.acronyme} {u.titre}" for u in ues],
|
||||
"allowed_values": [u.id for u in ues],
|
||||
},
|
||||
),
|
||||
]
|
||||
else:
|
||||
# Formations classiques: choix du semestre
|
||||
descr += [
|
||||
(
|
||||
"heures_cours",
|
||||
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
"semestre_id",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés",
|
||||
"input_type": "menu",
|
||||
"type": "int",
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": "%s du module" % parcours.SESSION_NAME,
|
||||
"labels": [str(x) for x in semestres_indices],
|
||||
"allowed_values": semestres_indices,
|
||||
},
|
||||
),
|
||||
]
|
||||
descr += [
|
||||
(
|
||||
"module_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": [x.name.capitalize() for x in scu.ModuleType],
|
||||
"allowed_values": [str(int(x)) for x in scu.ModuleType],
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_cours",
|
||||
{
|
||||
"title": "Heures de cours",
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de cours (optionnel)",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
{
|
||||
"title": "Heures de TD",
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_tp",
|
||||
{
|
||||
"title": "Heures de TP",
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
descr += [
|
||||
(
|
||||
"heures_tp",
|
||||
"sep_ue_coefs",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques",
|
||||
"input_type": "separator",
|
||||
"title": """
|
||||
<div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
|
||||
</div>""",
|
||||
},
|
||||
),
|
||||
]
|
||||
else:
|
||||
descr += [
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
|
@ -199,51 +275,56 @@ def module_create(matiere_id=None):
|
|||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
|
||||
("formation_id", {"default": UE["formation_id"], "input_type": "hidden"}),
|
||||
("ue_id", {"default": M["ue_id"], "input_type": "hidden"}),
|
||||
("matiere_id", {"default": M["matiere_id"], "input_type": "hidden"}),
|
||||
(
|
||||
"semestre_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"type": "int",
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": "%s de début du module dans la formation standard"
|
||||
% parcours.SESSION_NAME,
|
||||
"labels": [str(x) for x in semestres_indices],
|
||||
"allowed_values": semestres_indices,
|
||||
},
|
||||
),
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
"default": default_num,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
descr += [
|
||||
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
|
||||
("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
|
||||
("ue_id", {"default": ue.id, "input_type": "hidden"}),
|
||||
("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
"default": default_num,
|
||||
},
|
||||
),
|
||||
]
|
||||
args = scu.get_request_args()
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
args,
|
||||
descr,
|
||||
submitlabel="Créer ce module",
|
||||
)
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
else:
|
||||
do_module_create(tf[2])
|
||||
if is_apc:
|
||||
# BUT: l'UE indique le semestre
|
||||
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
if selected_ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
||||
|
||||
_ = do_module_create(tf[2])
|
||||
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=UE["formation_id"],
|
||||
formation_id=ue.formation_id,
|
||||
semestre_idx=tf[2]["semestre_id"],
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -331,7 +412,7 @@ def do_module_edit(val):
|
|||
# edit
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_moduleEditor.edit(cnx, val)
|
||||
sco_edit_formation.invalidate_sems_in_formation(mod["formation_id"])
|
||||
Formation.query.get(mod["formation_id"]).invalidate_cached_sems()
|
||||
|
||||
|
||||
def check_module_code_unicity(code, field, formation_id, module_id=None):
|
||||
|
@ -350,35 +431,34 @@ def module_edit(module_id=None):
|
|||
|
||||
if not module_id:
|
||||
raise ScoValueError("invalid module !")
|
||||
Mod = module_list(args={"module_id": module_id})
|
||||
if not Mod:
|
||||
modules = module_list(args={"module_id": module_id})
|
||||
if not modules:
|
||||
raise ScoValueError("invalid module !")
|
||||
Mod = Mod[0]
|
||||
module = modules[0]
|
||||
a_module = models.Module.query.get(module_id)
|
||||
unlocked = not module_is_locked(module_id)
|
||||
Fo = sco_formations.formation_list(args={"formation_id": Mod["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
|
||||
M = ndb.SimpleDictFetch(
|
||||
formation_id = module["formation_id"]
|
||||
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
|
||||
is_apc = parcours.APC_SAE
|
||||
ues_matieres = ndb.SimpleDictFetch(
|
||||
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
||||
FROM notes_matieres mat, notes_ue ue
|
||||
WHERE mat.ue_id = ue.id
|
||||
AND ue.formation_id = %(formation_id)s
|
||||
ORDER BY ue.numero, mat.numero
|
||||
""",
|
||||
{"formation_id": Mod["formation_id"]},
|
||||
{"formation_id": formation_id},
|
||||
)
|
||||
Mnames = ["%s / %s" % (x["acronyme"], x["titre"]) for x in M]
|
||||
Mids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in M]
|
||||
Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"])
|
||||
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
|
||||
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
|
||||
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
|
||||
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
dest_url = url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=str(Mod["formation_id"]),
|
||||
)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Modification du module %(titre)s" % Mod,
|
||||
page_title="Modification du module %(titre)s" % module,
|
||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||
javascripts=[
|
||||
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
|
||||
|
@ -386,65 +466,80 @@ def module_edit(module_id=None):
|
|||
"js/module_tag_editor.js",
|
||||
],
|
||||
),
|
||||
"""<h2>Modification du module %(titre)s""" % Mod,
|
||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % Fo,
|
||||
_MODULE_HELP,
|
||||
"""<h2>Modification du module %(titre)s""" % module,
|
||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
|
||||
render_template("scodoc/help/modules.html", is_apc=is_apc),
|
||||
]
|
||||
if not unlocked:
|
||||
H.append(
|
||||
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
|
||||
)
|
||||
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
descr = [
|
||||
(
|
||||
"code",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module (doit être unique dans la formation)",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
|
||||
val, field, formation_id, module_id=module_id
|
||||
),
|
||||
},
|
||||
),
|
||||
("titre", {"size": 30, "explanation": "nom du module"}),
|
||||
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
|
||||
(
|
||||
"module_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": [x.name.capitalize() for x in scu.ModuleType],
|
||||
"allowed_values": [str(int(x)) for x in scu.ModuleType],
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_cours",
|
||||
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_tp",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques",
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
coefs_descr = a_module.ue_coefs_descr()
|
||||
if coefs_descr:
|
||||
coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr])
|
||||
else:
|
||||
coefs_descr_txt = """<span class="missing_value">non définis</span>"""
|
||||
descr += [
|
||||
(
|
||||
"code",
|
||||
"ue_coefs",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module (doit être unique dans la formation)",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=Mod[
|
||||
"formation_id"
|
||||
]: check_module_code_unicity(
|
||||
val, field, formation_id, module_id=module_id
|
||||
),
|
||||
"readonly": True,
|
||||
"title": "Coefficients vers les UE",
|
||||
"default": coefs_descr_txt,
|
||||
"explanation": "passer par la page d'édition de la formation pour modifier les coefficients",
|
||||
},
|
||||
),
|
||||
("titre", {"size": 30, "explanation": "nom du module"}),
|
||||
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
|
||||
(
|
||||
"module_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": ("Standard", "Malus"),
|
||||
"allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)),
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_cours",
|
||||
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_tp",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques",
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
else: # Module classique avec coef scalaire:
|
||||
descr += [
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
|
@ -455,21 +550,39 @@ def module_edit(module_id=None):
|
|||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS', 'enabled' : unlocked }),
|
||||
("formation_id", {"input_type": "hidden"}),
|
||||
("ue_id", {"input_type": "hidden"}),
|
||||
("module_id", {"input_type": "hidden"}),
|
||||
]
|
||||
descr += [
|
||||
("formation_id", {"input_type": "hidden"}),
|
||||
("ue_id", {"input_type": "hidden"}),
|
||||
("module_id", {"input_type": "hidden"}),
|
||||
(
|
||||
"ue_matiere_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Rattachement :" if is_apc else "Matière :",
|
||||
"explanation": "UE de rattachement, utilisée pour la présentation"
|
||||
if is_apc
|
||||
else "un module appartient à une seule matière.",
|
||||
"labels": mat_names,
|
||||
"allowed_values": ue_mat_ids,
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
# le semestre du module est toujours celui de son UE
|
||||
descr += [
|
||||
(
|
||||
"ue_matiere_id",
|
||||
"semestre_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Matière",
|
||||
"explanation": "un module appartient à une seule matière.",
|
||||
"labels": Mnames,
|
||||
"allowed_values": Mids,
|
||||
"enabled": unlocked,
|
||||
"input_type": "hidden",
|
||||
"type": "int",
|
||||
"readonly": True,
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
else:
|
||||
descr += [
|
||||
(
|
||||
"semestre_id",
|
||||
{
|
||||
|
@ -482,49 +595,79 @@ def module_edit(module_id=None):
|
|||
"allowed_values": semestres_indices,
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
descr += [
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
},
|
||||
),
|
||||
]
|
||||
# force module semestre_idx to its UE
|
||||
if a_module.ue.semestre_idx:
|
||||
module["semestre_id"] = a_module.ue.semestre_idx
|
||||
# Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
|
||||
if not module["semestre_id"]:
|
||||
module["semestre_id"] = 1
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
descr,
|
||||
html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format(
|
||||
module_id, ",".join(sco_tag_module.module_tag_list(module_id))
|
||||
),
|
||||
initvalues=Mod,
|
||||
initvalues=module,
|
||||
submitlabel="Modifier ce module",
|
||||
)
|
||||
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id,
|
||||
semestre_idx=module["semestre_id"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# l'UE peut changer
|
||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||
# En APC, force le semestre égal à celui de l'UE
|
||||
if is_apc:
|
||||
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
if selected_ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
||||
# Check unicité code module dans la formation
|
||||
|
||||
do_module_edit(tf[2])
|
||||
return flask.redirect(dest_url)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id,
|
||||
semestre_idx=tf[2]["semestre_id"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Edition en ligne du code Apogee
|
||||
def edit_module_set_code_apogee(id=None, value=None):
|
||||
"Set UE code apogee"
|
||||
module_id = id
|
||||
value = value.strip("-_ \t")
|
||||
value = str(value).strip("-_ \t")
|
||||
log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
|
||||
|
||||
modules = module_list(args={"module_id": module_id})
|
||||
|
@ -602,7 +745,7 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
|
|||
[
|
||||
mod
|
||||
for mod in module_list(args={"ue_id": ue["ue_id"]})
|
||||
if mod["module_type"] == scu.MODULE_MALUS
|
||||
if mod["module_type"] == ModuleType.MALUS
|
||||
]
|
||||
)
|
||||
if nb_mod_malus == 0:
|
||||
|
@ -654,7 +797,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None):
|
|||
"matiere_id": matiere_id,
|
||||
"formation_id": ue["formation_id"],
|
||||
"semestre_id": semestre_id,
|
||||
"module_type": scu.MODULE_MALUS,
|
||||
"module_type": ModuleType.MALUS,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -29,12 +29,14 @@
|
|||
|
||||
"""
|
||||
import flask
|
||||
from flask import g, url_for, request
|
||||
from flask import url_for, render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.models.formations import NotesUE
|
||||
from app.models import Formation, UniteEns, ModuleImpl, Module
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app import log
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
|
@ -44,6 +46,7 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
|
|||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_apc
|
||||
from app.scodoc import sco_edit_formation
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
|
@ -65,6 +68,7 @@ _ueEditor = ndb.EditableTable(
|
|||
"acronyme",
|
||||
"numero",
|
||||
"titre",
|
||||
"semestre_idx",
|
||||
"type",
|
||||
"ue_code",
|
||||
"ects",
|
||||
|
@ -81,6 +85,7 @@ _ueEditor = ndb.EditableTable(
|
|||
"numero": ndb.int_null_is_zero,
|
||||
"ects": ndb.float_null_is_null,
|
||||
"coefficient": ndb.float_null_is_zero,
|
||||
"semestre_idx": ndb.int_null_is_null,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -99,10 +104,20 @@ def do_ue_create(args):
|
|||
# check duplicates
|
||||
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
|
||||
if ues:
|
||||
raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
|
||||
raise ScoValueError(
|
||||
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
||||
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
||||
)
|
||||
# create
|
||||
r = _ueEditor.create(cnx, args)
|
||||
ue_id = _ueEditor.create(cnx, args)
|
||||
|
||||
# Invalidate cache: vire les poids de toutes les évals de la formation
|
||||
for modimpl in ModuleImpl.query.filter(
|
||||
ModuleImpl.module_id == Module.id, Module.formation_id == args["formation_id"]
|
||||
):
|
||||
modimpl.invalidate_evaluations_poids()
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
formation.invalidate_module_coefs()
|
||||
# news
|
||||
F = sco_formations.formation_list(args={"formation_id": args["formation_id"]})[0]
|
||||
sco_news.add(
|
||||
|
@ -111,7 +126,7 @@ def do_ue_create(args):
|
|||
text="Modification de la formation %(acronyme)s" % F,
|
||||
max_frequency=3,
|
||||
)
|
||||
return r
|
||||
return ue_id
|
||||
|
||||
|
||||
def do_ue_delete(ue_id, delete_validations=False, force=False):
|
||||
|
@ -194,7 +209,7 @@ def ue_create(formation_id=None):
|
|||
|
||||
|
||||
def ue_edit(ue_id=None, create=False, formation_id=None):
|
||||
"""Modification ou creation d'une UE"""
|
||||
"""Modification ou création d'une UE"""
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
create = int(create)
|
||||
|
@ -211,25 +226,25 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
title = "Création d'une UE"
|
||||
initvalues = {}
|
||||
submitlabel = "Créer cette UE"
|
||||
Fol = sco_formations.formation_list(args={"formation_id": formation_id})
|
||||
if not Fol:
|
||||
raise ScoValueError(
|
||||
"Formation %s inexistante ! (si vous avez suivi un lien valide, merci de signaler le problème)"
|
||||
% formation_id
|
||||
)
|
||||
Fo = Fol[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
|
||||
|
||||
formation = Formation.query.get(formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
|
||||
parcours = formation.get_parcours()
|
||||
is_apc = parcours.APC_SAE
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"]),
|
||||
"<h2>" + title,
|
||||
" (formation %(acronyme)s, version %(version)s)</h2>" % Fo,
|
||||
f" (formation {formation.acronyme}, version {formation.version})</h2>",
|
||||
"""
|
||||
<p class="help">Les UE sont des groupes de modules dans une formation donnée, utilisés pour l'évaluation (on calcule des moyennes par UE et applique des seuils ("barres")).
|
||||
</p>
|
||||
<p class="help">Les UE sont des groupes de modules dans une formation donnée,
|
||||
utilisés pour la validation (on calcule des moyennes par UE et applique des
|
||||
seuils ("barres")).
|
||||
</p>
|
||||
|
||||
<p class="help">Note: L'UE n'a pas de coefficient associé. Seuls les <em>modules</em> ont des coefficients.
|
||||
</p>""",
|
||||
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
||||
Seuls les <em>modules</em> ont des coefficients.
|
||||
</p>""",
|
||||
]
|
||||
|
||||
ue_types = parcours.ALLOWED_UE_TYPES
|
||||
|
@ -251,6 +266,18 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
"type": "int",
|
||||
},
|
||||
),
|
||||
(
|
||||
"semestre_idx",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"type": "int",
|
||||
"allow_null": False,
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": "%s de l'UE dans la formation" % parcours.SESSION_NAME,
|
||||
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
|
||||
"allowed_values": [""] + semestres_indices,
|
||||
},
|
||||
),
|
||||
(
|
||||
"type",
|
||||
{
|
||||
|
@ -275,11 +302,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
"size": 4,
|
||||
"type": "float",
|
||||
"title": "Coefficient",
|
||||
"explanation": """les coefficients d'UE ne sont utilisés que lorsque
|
||||
l'option <em>Utiliser les coefficients d'UE pour calculer la moyenne générale</em>
|
||||
est activée. Par défaut, le coefficient d'une UE est simplement la somme des
|
||||
coefficients des modules dans lesquels l'étudiant a des notes.
|
||||
""",
|
||||
"explanation": """les coefficients d'UE ne sont utilisés que
|
||||
lorsque l'option <em>Utiliser les coefficients d'UE pour calculer
|
||||
la moyenne générale</em> est activée. Par défaut, le coefficient
|
||||
d'une UE est simplement la somme des coefficients des modules dans
|
||||
lesquels l'étudiant a des notes.
|
||||
""",
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -307,30 +335,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
},
|
||||
),
|
||||
]
|
||||
if parcours.UE_IS_MODULE:
|
||||
# demande le semestre pour creer le module immediatement:
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
fw.append(
|
||||
(
|
||||
"semestre_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"type": "int",
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": "%s de début du module dans la formation"
|
||||
% parcours.SESSION_NAME,
|
||||
"labels": [str(x) for x in semestres_indices],
|
||||
"allowed_values": semestres_indices,
|
||||
},
|
||||
)
|
||||
)
|
||||
if create and not parcours.UE_IS_MODULE:
|
||||
if create and not parcours.UE_IS_MODULE and not is_apc:
|
||||
fw.append(
|
||||
(
|
||||
"create_matiere",
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"default": False,
|
||||
"default": True,
|
||||
"title": "Créer matière identique",
|
||||
"explanation": "créer immédiatement une matière dans cette UE (utile si on n'utilise pas de matières)",
|
||||
},
|
||||
|
@ -352,15 +363,14 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
if not tf[2]["ue_code"]:
|
||||
del tf[2]["ue_code"]
|
||||
if not tf[2]["numero"]:
|
||||
if not "semestre_id" in tf[2]:
|
||||
tf[2]["semestre_id"] = 0
|
||||
# numero regroupant par semestre ou année:
|
||||
tf[2]["numero"] = next_ue_numero(
|
||||
formation_id, int(tf[2]["semestre_id"] or 0)
|
||||
formation_id, int(tf[2]["semestre_idx"])
|
||||
)
|
||||
|
||||
ue_id = do_ue_create(tf[2])
|
||||
if parcours.UE_IS_MODULE or tf[2]["create_matiere"]:
|
||||
if is_apc or parcours.UE_IS_MODULE or tf[2]["create_matiere"]:
|
||||
# rappel: en APC, toutes les UE ont une matière, créée ici
|
||||
# (inutilisée mais à laquelle les modules sont rattachés)
|
||||
matiere_id = sco_edit_matiere.do_matiere_create(
|
||||
{"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1},
|
||||
)
|
||||
|
@ -374,35 +384,47 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
"ue_id": ue_id,
|
||||
"matiere_id": matiere_id,
|
||||
"formation_id": formation_id,
|
||||
"semestre_id": tf[2]["semestre_id"],
|
||||
"semestre_id": tf[2]["semestre_idx"],
|
||||
},
|
||||
)
|
||||
else:
|
||||
do_ue_edit(tf[2])
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id,
|
||||
semestre_idx=tf[2]["semestre_idx"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _add_ue_semestre_id(ues):
|
||||
"""ajoute semestre_id dans les ue, en regardant le premier module de chacune.
|
||||
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
|
||||
def _add_ue_semestre_id(ues: list[dict], is_apc):
|
||||
"""ajoute semestre_id dans les ue, en regardant
|
||||
semestre_idx ou à défaut, pour les formations non APC, le premier module
|
||||
de chacune.
|
||||
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
|
||||
qui les place à la fin de la liste.
|
||||
"""
|
||||
for ue in ues:
|
||||
Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
|
||||
if Modlist:
|
||||
ue["semestre_id"] = Modlist[0]["semestre_id"]
|
||||
if ue["semestre_idx"] is not None:
|
||||
ue["semestre_id"] = ue["semestre_idx"]
|
||||
elif is_apc:
|
||||
ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT
|
||||
else:
|
||||
ue["semestre_id"] = 1000000
|
||||
# était le comportement ScoDoc7
|
||||
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
|
||||
if modules:
|
||||
ue["semestre_id"] = modules[0]["semestre_id"]
|
||||
else:
|
||||
ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT
|
||||
|
||||
|
||||
def next_ue_numero(formation_id, semestre_id=None):
|
||||
"""Numero d'une nouvelle UE dans cette formation.
|
||||
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
|
||||
"""
|
||||
formation = Formation.query.get(formation_id)
|
||||
ues = ue_list(args={"formation_id": formation_id})
|
||||
if not ues:
|
||||
return 0
|
||||
|
@ -410,7 +432,7 @@ def next_ue_numero(formation_id, semestre_id=None):
|
|||
return ues[-1]["numero"] + 1000
|
||||
else:
|
||||
# Avec semestre: (prend le semestre du 1er module de l'UE)
|
||||
_add_ue_semestre_id(ues)
|
||||
_add_ue_semestre_id(ues, formation.get_parcours().APC_SAE)
|
||||
ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
|
||||
if ue_list_semestre:
|
||||
return ue_list_semestre[-1]["numero"] + 10
|
||||
|
@ -440,33 +462,49 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
|||
return do_ue_delete(ue_id, delete_validations=delete_validations)
|
||||
|
||||
|
||||
def ue_table(formation_id=None, msg=""): # was ue_list
|
||||
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
"""Liste des matières et modules d'une formation, avec liens pour
|
||||
éditer (si non verrouillée).
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre_validation
|
||||
|
||||
F = sco_formations.formation_list(args={"formation_id": formation_id})
|
||||
if not F:
|
||||
formation = Formation.query.get(formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError("invalid formation_id")
|
||||
F = F[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
||||
locked = sco_formations.formation_has_locked_sems(formation_id)
|
||||
parcours = formation.get_parcours()
|
||||
is_apc = parcours.APC_SAE
|
||||
locked = formation.has_locked_sems()
|
||||
if semestre_idx == "all":
|
||||
semestre_idx = None
|
||||
else:
|
||||
semestre_idx = int(semestre_idx)
|
||||
semestre_ids = range(1, parcours.NB_SEM + 1)
|
||||
# transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
|
||||
# basées sur des dicts
|
||||
ues_obj = UniteEns.query.filter_by(formation_id=formation_id, is_external=False)
|
||||
ues_externes_obj = UniteEns.query.filter_by(
|
||||
formation_id=formation_id, is_external=True
|
||||
)
|
||||
if is_apc:
|
||||
# pour faciliter la transition des anciens programmes non APC
|
||||
for ue in ues_obj:
|
||||
ue.guess_semestre_idx()
|
||||
ues = [ue.to_dict() for ue in ues_obj]
|
||||
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
|
||||
|
||||
ues = ue_list(args={"formation_id": formation_id, "is_external": False})
|
||||
ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True})
|
||||
# tri par semestre et numero:
|
||||
_add_ue_semestre_id(ues)
|
||||
_add_ue_semestre_id(ues_externes)
|
||||
_add_ue_semestre_id(ues, is_apc)
|
||||
_add_ue_semestre_id(ues_externes, is_apc)
|
||||
ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
|
||||
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
|
||||
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues)
|
||||
|
||||
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
|
||||
# editable = (not locked) and has_perm_change
|
||||
# On autorise maintanant la modification des formations qui ont des semestres verrouillés,
|
||||
# sauf si cela affect les notes passées (verrouillées):
|
||||
# On autorise maintenant la modification des formations qui ont
|
||||
# des semestres verrouillés, sauf si cela affect les notes passées
|
||||
# (verrouillées):
|
||||
# - pas de modif des modules utilisés dans des semestres verrouillés
|
||||
# - pas de changement des codes d'UE utilisés dans des semestres verrouillés
|
||||
editable = has_perm_change
|
||||
|
@ -495,17 +533,18 @@ def ue_table(formation_id=None, msg=""): # was ue_list
|
|||
"libjs/jQuery-tagEditor/jquery.caret.min.js",
|
||||
"js/module_tag_editor.js",
|
||||
],
|
||||
page_title="Programme %s" % F["acronyme"],
|
||||
page_title=f"Programme {formation.acronyme}",
|
||||
),
|
||||
"""<h2>Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s"""
|
||||
% F,
|
||||
lockicon,
|
||||
"</h2>",
|
||||
f"""<h2>Formation {formation.titre} ({formation.acronyme})
|
||||
[version {formation.version}] code {formation.formation_code}
|
||||
{lockicon}
|
||||
</h2>
|
||||
""",
|
||||
]
|
||||
if locked:
|
||||
H.append(
|
||||
f"""<p class="help">Cette formation est verrouillée car
|
||||
{len(locked)} semestres verrouillés s'y réferent.
|
||||
des semestres verrouillés s'y réferent.
|
||||
Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module),
|
||||
vous devez:
|
||||
</p>
|
||||
|
@ -530,85 +569,69 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
)
|
||||
|
||||
# Description de la formation
|
||||
H.append('<div class="formation_descr">')
|
||||
H.append(
|
||||
'<div class="fd_d"><span class="fd_t">Titre:</span><span class="fd_v">%(titre)s</span></div>'
|
||||
% F
|
||||
)
|
||||
H.append(
|
||||
'<div class="fd_d"><span class="fd_t">Titre officiel:</span><span class="fd_v">%(titre_officiel)s</span></div>'
|
||||
% F
|
||||
)
|
||||
H.append(
|
||||
'<div class="fd_d"><span class="fd_t">Acronyme:</span><span class="fd_v">%(acronyme)s</span></div>'
|
||||
% F
|
||||
)
|
||||
H.append(
|
||||
'<div class="fd_d"><span class="fd_t">Code:</span><span class="fd_v">%(formation_code)s</span></div>'
|
||||
% F
|
||||
)
|
||||
H.append(
|
||||
'<div class="fd_d"><span class="fd_t">Version:</span><span class="fd_v">%(version)s</span></div>'
|
||||
% F
|
||||
)
|
||||
H.append(
|
||||
'<div class="fd_d"><span class="fd_t">Type parcours:</span><span class="fd_v">%s</span></div>'
|
||||
% parcours.__doc__
|
||||
)
|
||||
if parcours.UE_IS_MODULE:
|
||||
H.append(
|
||||
'<div class="fd_d"><span class="fd_t"> </span><span class="fd_n">(Chaque module est une UE)</span></div>'
|
||||
render_template(
|
||||
"pn/form_descr.html",
|
||||
formation=formation,
|
||||
parcours=parcours,
|
||||
editable=editable,
|
||||
)
|
||||
)
|
||||
|
||||
if editable:
|
||||
# Formation APC (BUT) ?
|
||||
if is_apc:
|
||||
H.append(
|
||||
'<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>'
|
||||
% F
|
||||
f"""<div class="formation_apc_infos">
|
||||
<div class="ue_list_tit">Formation par compétences (BUT)
|
||||
- Semestre {_html_select_semestre_idx(formation_id, semestre_ids, semestre_idx)}
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
if formation.referentiel_competence is None:
|
||||
descr_refcomp = ""
|
||||
msg_refcomp = "associer à un référentiel de compétences"
|
||||
else:
|
||||
descr_refcomp = f"{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}"
|
||||
msg_refcomp = "changer"
|
||||
H.append(
|
||||
f"""
|
||||
<ul>
|
||||
<li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
||||
}">{msg_refcomp}</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
|
||||
}">éditer les coefficients des ressources et SAÉs</a>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>")
|
||||
|
||||
# Description des UE/matières/modules
|
||||
H.append('<div class="formation_ue_list">')
|
||||
H.append('<div class="ue_list_tit">Programme pédagogique:</div>')
|
||||
|
||||
H.append(
|
||||
'<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>'
|
||||
"""
|
||||
<div class="formation_ue_list">
|
||||
<div class="ue_list_tit">Programme pédagogique:</div>
|
||||
<form>
|
||||
<input type="checkbox" class="sco_tag_checkbox">montrer les tags</input>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
_ue_table_ues(
|
||||
parcours,
|
||||
ues,
|
||||
editable,
|
||||
tag_editable,
|
||||
has_perm_change,
|
||||
arrow_up,
|
||||
arrow_down,
|
||||
arrow_none,
|
||||
delete_icon,
|
||||
delete_disabled_icon,
|
||||
)
|
||||
)
|
||||
if editable:
|
||||
if is_apc:
|
||||
H.append(
|
||||
'<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>'
|
||||
% formation_id
|
||||
)
|
||||
H.append(
|
||||
'<li><a href="formation_add_malus_modules?formation_id=%(formation_id)s" class="stdlink">Ajouter des modules de malus dans chaque UE</a></li></ul>'
|
||||
% F
|
||||
)
|
||||
H.append("</div>") # formation_ue_list
|
||||
|
||||
if ues_externes:
|
||||
H.append('<div class="formation_ue_list formation_ue_list_externes">')
|
||||
H.append(
|
||||
'<div class="ue_list_tit">UE externes déclarées (pour information):</div>'
|
||||
sco_edit_apc.html_edit_formation_apc(
|
||||
formation,
|
||||
semestre_idx=semestre_idx,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
)
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
_ue_table_ues(
|
||||
parcours,
|
||||
ues_externes,
|
||||
ues,
|
||||
editable,
|
||||
tag_editable,
|
||||
has_perm_change,
|
||||
|
@ -619,28 +642,84 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
delete_disabled_icon,
|
||||
)
|
||||
)
|
||||
H.append("</div>") # formation_ue_list
|
||||
if editable:
|
||||
H.append(
|
||||
f"""<ul>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
||||
}">Ajouter une UE</a>
|
||||
</li>
|
||||
<li><a href="{
|
||||
url_for('notes.formation_add_malus_modules',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
||||
}" class="stdlink">Ajouter des modules de malus dans chaque UE</a>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>") # formation_ue_list
|
||||
|
||||
if ues_externes:
|
||||
H.append(
|
||||
f"""
|
||||
<div class="formation_ue_list formation_ue_list_externes">
|
||||
<div class="ue_list_tit">UE externes déclarées (pour information):
|
||||
</div>
|
||||
{_ue_table_ues(
|
||||
parcours,
|
||||
ues_externes,
|
||||
editable,
|
||||
tag_editable,
|
||||
has_perm_change,
|
||||
arrow_up,
|
||||
arrow_down,
|
||||
arrow_none,
|
||||
delete_icon,
|
||||
delete_disabled_icon,
|
||||
)}
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
H.append("<p><ul>")
|
||||
if editable:
|
||||
H.append(
|
||||
"""
|
||||
<li><a class="stdlink" href="formation_create_new_version?formation_id=%(formation_id)s">Créer une nouvelle version (non verrouillée)</a></li>
|
||||
"""
|
||||
% F
|
||||
f"""
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formation_create_new_version',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
||||
)
|
||||
}">Créer une nouvelle version (non verrouillée)</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
"""
|
||||
<li><a class="stdlink" href="formation_table_recap?formation_id=%(formation_id)s">Table récapitulative de la formation</a></li>
|
||||
|
||||
<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=xml">Export XML de la formation</a> (permet de la sauvegarder pour l'échanger avec un autre site)</li>
|
||||
f"""
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formation_table_recap', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id)
|
||||
}">Table récapitulative de la formation</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id, format='xml')
|
||||
}">Export XML de la formation</a>
|
||||
(permet de la sauvegarder pour l'échanger avec un autre site)
|
||||
</li>
|
||||
|
||||
<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=json">Export JSON de la formation</a></li>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id, format='json')
|
||||
}">Export JSON de la formation</a>
|
||||
</li>
|
||||
|
||||
<li><a class="stdlink" href="module_list?formation_id=%(formation_id)s">Liste détaillée des modules de la formation</a> (debug) </li>
|
||||
</ul>
|
||||
</p>"""
|
||||
% F
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.module_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id)
|
||||
}">Liste détaillée des modules de la formation</a> (debug)
|
||||
</li>
|
||||
</ul>
|
||||
</p>"""
|
||||
)
|
||||
if has_perm_change:
|
||||
H.append(
|
||||
|
@ -667,12 +746,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
|
||||
if current_user.has_permission(Permission.ScoImplement):
|
||||
H.append(
|
||||
"""<ul>
|
||||
<li><a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">Mettre en place un nouveau semestre de formation %(acronyme)s</a>
|
||||
</li>
|
||||
|
||||
</ul>"""
|
||||
% F
|
||||
f"""<ul>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id, semestre_id=1)
|
||||
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a>
|
||||
</li>
|
||||
</ul>"""
|
||||
)
|
||||
# <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li>
|
||||
|
||||
|
@ -683,6 +763,30 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
return "".join(H)
|
||||
|
||||
|
||||
def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
|
||||
htm = """<form method="get">Semestre:
|
||||
<select onchange="this.form.submit()" name="semestre_idx" id="semestre_idx" >
|
||||
"""
|
||||
for i in list(semestre_ids) + ["all"]:
|
||||
if i == "all":
|
||||
label = "tous"
|
||||
else:
|
||||
label = f"S{i}"
|
||||
htm += f"""<option value="{i}" {
|
||||
'selected'
|
||||
if (semestre_idx == i)
|
||||
or (i == "all" and semestre_idx is None)
|
||||
else ''
|
||||
}>{label}</option>
|
||||
"""
|
||||
|
||||
htm += f"""
|
||||
</select>
|
||||
<input type="hidden" name="formation_id" value="{formation_id}"></input>
|
||||
</form>"""
|
||||
return htm
|
||||
|
||||
|
||||
def _ue_table_ues(
|
||||
parcours,
|
||||
ues,
|
||||
|
@ -695,7 +799,9 @@ def _ue_table_ues(
|
|||
delete_icon,
|
||||
delete_disabled_icon,
|
||||
):
|
||||
"""Édition de programme: liste des UEs (avec leurs matières et modules)."""
|
||||
"""Édition de programme: liste des UEs (avec leurs matières et modules).
|
||||
Pour les formations classiques (non APC/BUT)
|
||||
"""
|
||||
H = []
|
||||
cur_ue_semestre_id = None
|
||||
iue = 0
|
||||
|
@ -837,6 +943,8 @@ def _ue_table_matieres(
|
|||
delete_disabled_icon,
|
||||
)
|
||||
)
|
||||
if not parcours.UE_IS_MODULE:
|
||||
H.append("</li>")
|
||||
if not matieres:
|
||||
H.append("<li>Aucune matière dans cette UE ! ")
|
||||
if editable:
|
||||
|
@ -866,6 +974,10 @@ def _ue_table_modules(
|
|||
arrow_none,
|
||||
delete_icon,
|
||||
delete_disabled_icon,
|
||||
unit_name="matière",
|
||||
add_suppress_link=True, # lien "supprimer cette matière"
|
||||
empty_list_msg="Aucun élément dans cette matière",
|
||||
create_element_msg="créer un module",
|
||||
):
|
||||
"""Édition de programme: liste des modules d'une matière d'une UE"""
|
||||
H = ['<ul class="notes_module_list">']
|
||||
|
@ -875,7 +987,7 @@ def _ue_table_modules(
|
|||
mod["module_id"]
|
||||
)
|
||||
klass = "notes_module_list"
|
||||
if mod["module_type"] == scu.MODULE_MALUS:
|
||||
if mod["module_type"] == ModuleType.MALUS:
|
||||
klass += " module_malus"
|
||||
H.append('<li class="%s">' % klass)
|
||||
|
||||
|
@ -948,8 +1060,8 @@ def _ue_table_modules(
|
|||
)
|
||||
H.append("</li>")
|
||||
if not modules:
|
||||
H.append("<li>Aucun module dans cette matière ! ")
|
||||
if editable:
|
||||
H.append(f"<li>{empty_list_msg} ! ")
|
||||
if editable and add_suppress_link:
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{
|
||||
url_for("notes.matiere_delete",
|
||||
|
@ -963,11 +1075,10 @@ def _ue_table_modules(
|
|||
f"""<li> <a class="stdlink" href="{
|
||||
url_for("notes.module_create",
|
||||
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
|
||||
>créer un module</a></li>
|
||||
>{create_element_msg}</a></li>
|
||||
"""
|
||||
)
|
||||
H.append("</ul>")
|
||||
H.append("</li>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
|
@ -987,20 +1098,20 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
|
|||
formation_code = F["formation_code"]
|
||||
# UE du même code, code formation et departement:
|
||||
q_ues = (
|
||||
NotesUE.query.filter_by(ue_code=ue_code)
|
||||
.join(NotesUE.formation, aliased=True)
|
||||
UniteEns.query.filter_by(ue_code=ue_code)
|
||||
.join(UniteEns.formation, aliased=True)
|
||||
.filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
|
||||
)
|
||||
else:
|
||||
# Toutes les UE du departement avec ce code:
|
||||
q_ues = (
|
||||
NotesUE.query.filter_by(ue_code=ue_code)
|
||||
.join(NotesUE.formation, aliased=True)
|
||||
UniteEns.query.filter_by(ue_code=ue_code)
|
||||
.join(UniteEns.formation, aliased=True)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
if hide_ue_id: # enlève l'ue de depart
|
||||
q_ues = q_ues.filter(NotesUE.id != hide_ue_id)
|
||||
q_ues = q_ues.filter(UniteEns.id != hide_ue_id)
|
||||
|
||||
ues = q_ues.all()
|
||||
if not ues:
|
||||
|
@ -1038,7 +1149,10 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
|||
new_acro = args["acronyme"]
|
||||
ues = ue_list({"formation_id": ue["formation_id"], "acronyme": new_acro})
|
||||
if ues and ues[0]["ue_id"] != ue_id:
|
||||
raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
|
||||
raise ScoValueError(
|
||||
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
||||
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
||||
)
|
||||
|
||||
# On ne peut pas supprimer le code UE:
|
||||
if "ue_code" in args and not args["ue_code"]:
|
||||
|
@ -1047,9 +1161,11 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
|||
cnx = ndb.GetDBConnexion()
|
||||
_ueEditor.edit(cnx, args)
|
||||
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
if not dont_invalidate_cache:
|
||||
# Invalide les semestres utilisant cette formation:
|
||||
sco_edit_formation.invalidate_sems_in_formation(ue["formation_id"])
|
||||
formation.invalidate_cached_sems()
|
||||
formation.force_semestre_modules_aux_ues()
|
||||
|
||||
|
||||
# essai edition en ligne:
|
||||
|
@ -1182,5 +1298,5 @@ def ue_list_semestre_ids(ue):
|
|||
Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
|
||||
aussi ScoDoc laisse le choix.
|
||||
"""
|
||||
Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
|
||||
return sorted(list(set([mod["semestre_id"] for mod in Modlist])))
|
||||
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
|
||||
return sorted(list(set([mod["semestre_id"] for mod in modules])))
|
||||
|
|
|
@ -47,36 +47,6 @@ from app.scodoc import sco_preferences
|
|||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
|
||||
MONTH_NAMES_ABBREV = [
|
||||
"Jan ",
|
||||
"Fév ",
|
||||
"Mars",
|
||||
"Avr ",
|
||||
"Mai ",
|
||||
"Juin",
|
||||
"Jul ",
|
||||
"Août",
|
||||
"Sept",
|
||||
"Oct ",
|
||||
"Nov ",
|
||||
"Déc ",
|
||||
]
|
||||
|
||||
MONTH_NAMES = [
|
||||
"janvier",
|
||||
"février",
|
||||
"mars",
|
||||
"avril",
|
||||
"mai",
|
||||
"juin",
|
||||
"juillet",
|
||||
"août",
|
||||
"septembre",
|
||||
"octobre",
|
||||
"novembre",
|
||||
"décembre",
|
||||
]
|
||||
|
||||
|
||||
def format_etud_ident(etud):
|
||||
"""Format identite de l'étudiant (modifié en place)
|
||||
|
@ -657,8 +627,8 @@ def log_unknown_etud():
|
|||
|
||||
def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
|
||||
"""infos sur un etudiant (API). If not found, returns empty list.
|
||||
On peut specifier etudid ou code_nip
|
||||
ou bien cherche dans les argumenst de la requête courante:
|
||||
On peut spécifier etudid ou code_nip
|
||||
ou bien cherche dans les arguments de la requête courante:
|
||||
etudid, code_nip, code_ine (dans cet ordre).
|
||||
"""
|
||||
if etudid is None:
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Vérification des abasneces à une évaluation
|
||||
"""
|
||||
from flask import url_for, g
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
|
||||
# matin et/ou après-midi ?
|
||||
def _eval_demijournee(E):
|
||||
"1 si matin, 0 si apres midi, 2 si toute la journee"
|
||||
am, pm = False, False
|
||||
if E["heure_debut"] < "13:00":
|
||||
am = True
|
||||
if E["heure_fin"] > "13:00":
|
||||
pm = True
|
||||
if am and pm:
|
||||
demijournee = 2
|
||||
elif am:
|
||||
demijournee = 1
|
||||
else:
|
||||
demijournee = 0
|
||||
pm = True
|
||||
return am, pm, demijournee
|
||||
|
||||
|
||||
def evaluation_check_absences(evaluation_id):
|
||||
"""Vérifie les absences au moment de cette évaluation.
|
||||
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
|
||||
note et absent
|
||||
ABS et pas noté absent
|
||||
ABS et absent justifié
|
||||
EXC et pas noté absent
|
||||
EXC et pas justifie
|
||||
Ramene 3 listes d'etudid
|
||||
"""
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
if not E["jour"]:
|
||||
return [], [], [], [], [] # evaluation sans date
|
||||
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
|
||||
# Liste les absences à ce moment:
|
||||
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
|
||||
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
|
||||
Just = sco_abs.list_abs_jour(
|
||||
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
|
||||
)
|
||||
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
|
||||
|
||||
# Les notes:
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
ValButAbs = [] # une note mais noté absent
|
||||
AbsNonSignalee = [] # note ABS mais pas noté absent
|
||||
ExcNonSignalee = [] # note EXC mais pas noté absent
|
||||
ExcNonJust = [] # note EXC mais absent non justifie
|
||||
AbsButExc = [] # note ABS mais justifié
|
||||
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True
|
||||
):
|
||||
if etudid in NotesDB:
|
||||
val = NotesDB[etudid]["value"]
|
||||
if (
|
||||
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
|
||||
) and etudid in As:
|
||||
# note valide et absent
|
||||
ValButAbs.append(etudid)
|
||||
if val is None and not etudid in As:
|
||||
# absent mais pas signale comme tel
|
||||
AbsNonSignalee.append(etudid)
|
||||
if val == scu.NOTES_NEUTRALISE and not etudid in As:
|
||||
# Neutralisé mais pas signale absent
|
||||
ExcNonSignalee.append(etudid)
|
||||
if val == scu.NOTES_NEUTRALISE and etudid in NJs:
|
||||
# EXC mais pas justifié
|
||||
ExcNonJust.append(etudid)
|
||||
if val is None and etudid in Justs:
|
||||
# ABS mais justificatif
|
||||
AbsButExc.append(etudid)
|
||||
|
||||
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
|
||||
|
||||
|
||||
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
|
||||
"""Affiche état vérification absences d'une évaluation"""
|
||||
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
|
||||
(
|
||||
ValButAbs,
|
||||
AbsNonSignalee,
|
||||
ExcNonSignalee,
|
||||
ExcNonJust,
|
||||
AbsButExc,
|
||||
) = evaluation_check_absences(evaluation_id)
|
||||
|
||||
if with_header:
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
|
||||
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
|
||||
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
|
||||
]
|
||||
else:
|
||||
# pas de header, mais un titre
|
||||
H = [
|
||||
"""<h2 class="eval_check_absences">%s du %s """
|
||||
% (E["description"], E["jour"])
|
||||
]
|
||||
if (
|
||||
not ValButAbs
|
||||
and not AbsNonSignalee
|
||||
and not ExcNonSignalee
|
||||
and not ExcNonJust
|
||||
):
|
||||
H.append(': <span class="eval_check_absences_ok">ok</span>')
|
||||
H.append("</h2>")
|
||||
|
||||
def etudlist(etudids, linkabs=False):
|
||||
H.append("<ul>")
|
||||
if not etudids and show_ok:
|
||||
H.append("<li>aucun</li>")
|
||||
for etudid in etudids:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
H.append(
|
||||
'<li><a class="discretelink" href="%s">'
|
||||
% url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
+ "%(nomprenom)s</a>" % etud
|
||||
)
|
||||
if linkabs:
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{url_for(
|
||||
'absences.doSignaleAbsence',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
datedebut=E["jour"],
|
||||
datefin=E["jour"],
|
||||
demijournee=demijournee,
|
||||
moduleimpl_id=E["moduleimpl_id"],
|
||||
)
|
||||
}">signaler cette absence</a>"""
|
||||
)
|
||||
H.append("</li>")
|
||||
H.append("</ul>")
|
||||
|
||||
if ValButAbs or show_ok:
|
||||
H.append(
|
||||
"<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>"
|
||||
)
|
||||
etudlist(ValButAbs)
|
||||
|
||||
if AbsNonSignalee or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
|
||||
)
|
||||
etudlist(AbsNonSignalee, linkabs=True)
|
||||
|
||||
if ExcNonSignalee or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
|
||||
)
|
||||
etudlist(ExcNonSignalee)
|
||||
|
||||
if ExcNonJust or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>"""
|
||||
)
|
||||
etudlist(ExcNonJust)
|
||||
|
||||
if AbsButExc or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
|
||||
)
|
||||
etudlist(AbsButExc)
|
||||
|
||||
if with_header:
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def formsemestre_check_absences_html(formsemestre_id):
|
||||
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Vérification absences aux évaluations de ce semestre",
|
||||
sem,
|
||||
),
|
||||
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
|
||||
Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:
|
||||
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
|
||||
</p>""",
|
||||
]
|
||||
# Modules, dans l'ordre
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
for M in Mlist:
|
||||
evals = sco_evaluation_db.do_evaluation_list(
|
||||
{"moduleimpl_id": M["moduleimpl_id"]}
|
||||
)
|
||||
if evals:
|
||||
H.append(
|
||||
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
|
||||
% (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"])
|
||||
)
|
||||
for E in evals:
|
||||
H.append(
|
||||
evaluation_check_absences_html(
|
||||
E["evaluation_id"],
|
||||
with_header=False,
|
||||
show_ok=False,
|
||||
)
|
||||
)
|
||||
if evals:
|
||||
H.append("</div>")
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
|
@ -0,0 +1,482 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@gmail.com
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import pprint
|
||||
|
||||
import flask
|
||||
from flask import url_for, g
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_permissions_check
|
||||
|
||||
|
||||
_evaluationEditor = ndb.EditableTable(
|
||||
"notes_evaluation",
|
||||
"evaluation_id",
|
||||
(
|
||||
"evaluation_id",
|
||||
"moduleimpl_id",
|
||||
"jour",
|
||||
"heure_debut",
|
||||
"heure_fin",
|
||||
"description",
|
||||
"note_max",
|
||||
"coefficient",
|
||||
"visibulletin",
|
||||
"publish_incomplete",
|
||||
"evaluation_type",
|
||||
"numero",
|
||||
),
|
||||
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord
|
||||
output_formators={
|
||||
"jour": ndb.DateISOtoDMY,
|
||||
"numero": ndb.int_null_is_zero,
|
||||
},
|
||||
input_formators={
|
||||
"jour": ndb.DateDMYtoISO,
|
||||
"heure_debut": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
||||
"heure_fin": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
||||
"visibulletin": bool,
|
||||
"publish_incomplete": bool,
|
||||
"evaluation_type": int,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def evaluation_enrich_dict(e):
|
||||
"""add or convert some fileds in an evaluation dict"""
|
||||
# For ScoDoc7 compat
|
||||
heure_debut_dt = e["heure_debut"] or datetime.time(
|
||||
8, 00
|
||||
) # au cas ou pas d'heure (note externe?)
|
||||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
||||
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
|
||||
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d is not None:
|
||||
m = d % 60
|
||||
e["duree"] = "%dh" % (d / 60)
|
||||
if m != 0:
|
||||
e["duree"] += "%02d" % m
|
||||
else:
|
||||
e["duree"] = ""
|
||||
if heure_debut and (not heure_fin or heure_fin == heure_debut):
|
||||
e["descrheure"] = " à " + heure_debut
|
||||
elif heure_debut and heure_fin:
|
||||
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
|
||||
else:
|
||||
e["descrheure"] = ""
|
||||
# matin, apresmidi: utile pour se referer aux absences:
|
||||
if heure_debut_dt < datetime.time(12, 00):
|
||||
e["matin"] = 1
|
||||
else:
|
||||
e["matin"] = 0
|
||||
if heure_fin_dt > datetime.time(12, 00):
|
||||
e["apresmidi"] = 1
|
||||
else:
|
||||
e["apresmidi"] = 0
|
||||
return e
|
||||
|
||||
|
||||
def do_evaluation_list(args, sortkey=None):
|
||||
"""List evaluations, sorted by numero (or most recent date first).
|
||||
|
||||
Ajoute les champs:
|
||||
'duree' : '2h30'
|
||||
'matin' : 1 (commence avant 12:00) ou 0
|
||||
'apresmidi' : 1 (termine après 12:00) ou 0
|
||||
'descrheure' : ' de 15h00 à 16h30'
|
||||
"""
|
||||
# Attention: transformation fonction ScoDc7 en SQLAlchemy
|
||||
cnx = ndb.GetDBConnexion()
|
||||
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
|
||||
# calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
|
||||
for e in evals:
|
||||
evaluation_enrich_dict(e)
|
||||
|
||||
return evals
|
||||
|
||||
|
||||
def do_evaluation_list_in_formsemestre(formsemestre_id):
|
||||
"list evaluations in this formsemestre"
|
||||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
evals = []
|
||||
for modimpl in mods:
|
||||
evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
|
||||
return evals
|
||||
|
||||
|
||||
def _check_evaluation_args(args):
|
||||
"Check coefficient, dates and duration, raises exception if invalid"
|
||||
moduleimpl_id = args["moduleimpl_id"]
|
||||
# check bareme
|
||||
note_max = args.get("note_max", None)
|
||||
if note_max is None:
|
||||
raise ScoValueError("missing note_max")
|
||||
try:
|
||||
note_max = float(note_max)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid note_max value")
|
||||
if note_max < 0:
|
||||
raise ScoValueError("Invalid note_max value (must be positive or null)")
|
||||
# check coefficient
|
||||
coef = args.get("coefficient", None)
|
||||
if coef is None:
|
||||
raise ScoValueError("missing coefficient")
|
||||
try:
|
||||
coef = float(coef)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid coefficient value")
|
||||
if coef < 0:
|
||||
raise ScoValueError("Invalid coefficient value (must be positive or null)")
|
||||
# check date
|
||||
jour = args.get("jour", None)
|
||||
args["jour"] = jour
|
||||
if jour:
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
||||
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
|
||||
date_debut = datetime.date(y, m, d)
|
||||
d, m, y = [int(x) for x in sem["date_fin"].split("/")]
|
||||
date_fin = datetime.date(y, m, d)
|
||||
# passe par ndb.DateDMYtoISO pour avoir date pivot
|
||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||||
jour = datetime.date(y, m, d)
|
||||
if (jour > date_fin) or (jour < date_debut):
|
||||
raise ScoValueError(
|
||||
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
|
||||
% (d, m, y)
|
||||
)
|
||||
heure_debut = args.get("heure_debut", None)
|
||||
args["heure_debut"] = heure_debut
|
||||
heure_fin = args.get("heure_fin", None)
|
||||
args["heure_fin"] = heure_fin
|
||||
if jour and ((not heure_debut) or (not heure_fin)):
|
||||
raise ScoValueError("Les heures doivent être précisées")
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d and ((d < 0) or (d > 60 * 12)):
|
||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||
|
||||
|
||||
def do_evaluation_create(
|
||||
moduleimpl_id=None,
|
||||
jour=None,
|
||||
heure_debut=None,
|
||||
heure_fin=None,
|
||||
description=None,
|
||||
note_max=None,
|
||||
coefficient=None,
|
||||
visibulletin=None,
|
||||
publish_incomplete=None,
|
||||
evaluation_type=None,
|
||||
numero=None,
|
||||
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8
|
||||
):
|
||||
"""Create an evaluation"""
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
args = locals()
|
||||
log("do_evaluation_create: args=" + str(args))
|
||||
_check_evaluation_args(args)
|
||||
# Check numeros
|
||||
module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
|
||||
if not "numero" in args or args["numero"] is None:
|
||||
n = None
|
||||
# determine le numero avec la date
|
||||
# Liste des eval existantes triees par date, la plus ancienne en tete
|
||||
mod_evals = do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id},
|
||||
sortkey="jour asc, heure_debut asc",
|
||||
)
|
||||
if args["jour"]:
|
||||
next_eval = None
|
||||
t = (
|
||||
ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
|
||||
ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
|
||||
)
|
||||
for e in mod_evals:
|
||||
if (
|
||||
ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
||||
ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
|
||||
) > t:
|
||||
next_eval = e
|
||||
break
|
||||
if next_eval:
|
||||
n = module_evaluation_insert_before(mod_evals, next_eval)
|
||||
else:
|
||||
n = None # a placer en fin
|
||||
if n is None: # pas de date ou en fin:
|
||||
if mod_evals:
|
||||
log(pprint.pformat(mod_evals[-1]))
|
||||
n = mod_evals[-1]["numero"] + 1
|
||||
else:
|
||||
n = 0 # the only one
|
||||
# log("creating with numero n=%d" % n)
|
||||
args["numero"] = n
|
||||
|
||||
#
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _evaluationEditor.create(cnx, args)
|
||||
|
||||
# news
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def do_evaluation_edit(args):
|
||||
"edit an evaluation"
|
||||
evaluation_id = args["evaluation_id"]
|
||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not the_evals:
|
||||
raise ValueError("evaluation inexistante !")
|
||||
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
args["moduleimpl_id"] = moduleimpl_id
|
||||
_check_evaluation_args(args)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_evaluationEditor.edit(cnx, args)
|
||||
# inval cache pour ce semestre
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||
|
||||
|
||||
def do_evaluation_delete(evaluation_id):
|
||||
"delete evaluation"
|
||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not the_evals:
|
||||
raise ValueError("evaluation inexistante !")
|
||||
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
|
||||
notes = [x["value"] for x in NotesDB.values()]
|
||||
if notes:
|
||||
raise ScoValueError(
|
||||
"Impossible de supprimer cette évaluation: il reste des notes"
|
||||
)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
||||
_evaluationEditor.delete(cnx, evaluation_id)
|
||||
# inval cache pour ce semestre
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||
# news
|
||||
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = (
|
||||
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
||||
|
||||
# ancien _notes_getall
|
||||
def do_evaluation_get_all_notes(
|
||||
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
||||
):
|
||||
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
||||
"""
|
||||
do_cache = (
|
||||
filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||
if do_cache:
|
||||
r = sco_cache.EvaluationCache.get(evaluation_id)
|
||||
if r != None:
|
||||
return r
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cond = " where evaluation_id=%(evaluation_id)s"
|
||||
if by_uid:
|
||||
cond += " and uid=%(by_uid)s"
|
||||
|
||||
cursor.execute(
|
||||
"select * from " + table + cond,
|
||||
{"evaluation_id": evaluation_id, "by_uid": by_uid},
|
||||
)
|
||||
res = cursor.dictfetchall()
|
||||
d = {}
|
||||
if filter_suppressed:
|
||||
for x in res:
|
||||
if x["value"] != scu.NOTES_SUPPRESS:
|
||||
d[x["etudid"]] = x
|
||||
else:
|
||||
for x in res:
|
||||
d[x["etudid"]] = x
|
||||
if do_cache:
|
||||
status = sco_cache.EvaluationCache.set(evaluation_id, d)
|
||||
if not status:
|
||||
log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}")
|
||||
return d
|
||||
|
||||
|
||||
def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
|
||||
"""Renumber evaluations in this module, according to their date. (numero=0: oldest one)
|
||||
Needed because previous versions of ScoDoc did not have eval numeros
|
||||
Note: existing numeros are ignored
|
||||
"""
|
||||
redirect = int(redirect)
|
||||
# log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
|
||||
# List sorted according to date/heure, ignoring numeros:
|
||||
# (note that we place evaluations with NULL date at the end)
|
||||
mod_evals = do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id},
|
||||
sortkey="jour asc, heure_debut asc",
|
||||
)
|
||||
|
||||
all_numbered = False not in [x["numero"] > 0 for x in mod_evals]
|
||||
if all_numbered and only_if_unumbered:
|
||||
return # all ok
|
||||
|
||||
# Reset all numeros:
|
||||
i = 1
|
||||
for e in mod_evals:
|
||||
e["numero"] = i
|
||||
do_evaluation_edit(e)
|
||||
i += 1
|
||||
|
||||
# If requested, redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def module_evaluation_insert_before(mod_evals, next_eval):
|
||||
"""Renumber evals such that an evaluation with can be inserted before next_eval
|
||||
Returns numero suitable for the inserted evaluation
|
||||
"""
|
||||
if next_eval:
|
||||
n = next_eval["numero"]
|
||||
if not n:
|
||||
log("renumbering old evals")
|
||||
module_evaluation_renumber(next_eval["moduleimpl_id"])
|
||||
next_eval = do_evaluation_list(
|
||||
args={"evaluation_id": next_eval["evaluation_id"]}
|
||||
)[0]
|
||||
n = next_eval["numero"]
|
||||
else:
|
||||
n = 1
|
||||
# log('inserting at position numero %s' % n )
|
||||
# all numeros >= n are incremented
|
||||
for e in mod_evals:
|
||||
if e["numero"] >= n:
|
||||
e["numero"] += 1
|
||||
# log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
|
||||
do_evaluation_edit(e)
|
||||
|
||||
return n
|
||||
|
||||
|
||||
def module_evaluation_move(evaluation_id, after=0, redirect=1):
|
||||
"""Move before/after previous one (decrement/increment numero)
|
||||
(published)
|
||||
"""
|
||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
redirect = int(redirect)
|
||||
# access: can change eval ?
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
|
||||
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True)
|
||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
|
||||
if len(mod_evals) > 1:
|
||||
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
||||
neigh = None # object to swap with
|
||||
if after == 0 and idx > 0:
|
||||
neigh = mod_evals[idx - 1]
|
||||
elif after == 1 and idx < len(mod_evals) - 1:
|
||||
neigh = mod_evals[idx + 1]
|
||||
if neigh: #
|
||||
if neigh["numero"] == e["numero"]:
|
||||
log("Warning: module_evaluation_move: forcing renumber")
|
||||
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=False)
|
||||
else:
|
||||
# swap numero with neighbor
|
||||
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
|
||||
do_evaluation_edit(e)
|
||||
do_evaluation_edit(neigh)
|
||||
# redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e["moduleimpl_id"],
|
||||
)
|
||||
)
|
|
@ -0,0 +1,339 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@gmail.com
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Formulaire ajout/édition d'une évaluation
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import flask
|
||||
from flask import url_for, render_template
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
|
||||
|
||||
def evaluation_create_form(
|
||||
moduleimpl_id=None,
|
||||
evaluation_id=None,
|
||||
edit=False,
|
||||
page_title="Évaluation",
|
||||
):
|
||||
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
||||
if evaluation_id is not None:
|
||||
evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
moduleimpl_id = evaluation.moduleimpl_id
|
||||
#
|
||||
modimpl_o = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[
|
||||
0
|
||||
]
|
||||
mod = modimpl_o["module"]
|
||||
formsemestre_id = modimpl_o["formsemestre_id"]
|
||||
sem = FormSemestre.query.get(formsemestre_id)
|
||||
sem_ues = sem.query_ues(with_sport=False).all()
|
||||
is_malus = mod["module_type"] == ModuleType.MALUS
|
||||
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
|
||||
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
||||
#
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
return (
|
||||
html_sco_header.sco_header()
|
||||
+ "<h2>Opération non autorisée</h2><p>"
|
||||
+ "Modification évaluation impossible pour %s"
|
||||
% current_user.get_nomplogin()
|
||||
+ "</p>"
|
||||
+ '<p><a href="moduleimpl_status?moduleimpl_id=%s">Revenir</a></p>'
|
||||
% (moduleimpl_id,)
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
if not edit:
|
||||
# création nouvel
|
||||
if moduleimpl_id is None:
|
||||
raise ValueError("missing moduleimpl_id parameter")
|
||||
initvalues = {
|
||||
"note_max": 20,
|
||||
"jour": time.strftime("%d/%m/%Y", time.localtime()),
|
||||
"publish_incomplete": is_malus,
|
||||
}
|
||||
submitlabel = "Créer cette évaluation"
|
||||
action = "Création d'une évaluation"
|
||||
link = ""
|
||||
else:
|
||||
# édition données existantes
|
||||
# setup form init values
|
||||
if evaluation_id is None:
|
||||
raise ValueError("missing evaluation_id parameter")
|
||||
initvalues = evaluation.to_dict()
|
||||
moduleimpl_id = initvalues["moduleimpl_id"]
|
||||
submitlabel = "Modifier les données"
|
||||
action = "Modification d'une évaluation"
|
||||
link = ""
|
||||
# Note maximale actuelle dans cette éval ?
|
||||
etat = sco_evaluations.do_evaluation_etat(evaluation_id)
|
||||
if etat["maxi_num"] is not None:
|
||||
min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"])
|
||||
else:
|
||||
min_note_max = scu.NOTES_PRECISION
|
||||
#
|
||||
if min_note_max > scu.NOTES_PRECISION:
|
||||
min_note_max_str = scu.fmt_note(min_note_max)
|
||||
else:
|
||||
min_note_max_str = "0"
|
||||
#
|
||||
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
|
||||
moduleimpl_id,
|
||||
mod["code"],
|
||||
mod["titre"],
|
||||
link,
|
||||
)
|
||||
H = [
|
||||
f"""<h3>{action} en
|
||||
{scu.MODULE_TYPE_NAMES[mod["module_type"]]} {mod_descr}</h3>
|
||||
"""
|
||||
]
|
||||
|
||||
heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)]
|
||||
#
|
||||
initvalues["visibulletin"] = initvalues.get("visibulletin", True)
|
||||
if initvalues["visibulletin"]:
|
||||
initvalues["visibulletinlist"] = ["X"]
|
||||
else:
|
||||
initvalues["visibulletinlist"] = []
|
||||
vals = scu.get_request_args()
|
||||
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
|
||||
vals["visibulletinlist"] = []
|
||||
#
|
||||
if is_apc: # BUT: poids vers les UE
|
||||
ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict()
|
||||
for ue in sem_ues:
|
||||
if edit:
|
||||
existing_poids = models.EvaluationUEPoids.query.filter_by(
|
||||
ue=ue, evaluation=evaluation
|
||||
).first()
|
||||
else:
|
||||
existing_poids = None
|
||||
if existing_poids:
|
||||
poids = existing_poids.poids
|
||||
else:
|
||||
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
|
||||
if coef_ue > 0:
|
||||
poids = 1.0 # par defaut au départ
|
||||
else:
|
||||
poids = 0.0
|
||||
initvalues[f"poids_{ue.id}"] = poids
|
||||
#
|
||||
form = [
|
||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
|
||||
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
|
||||
# ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
|
||||
(
|
||||
"jour",
|
||||
{
|
||||
"input_type": "date",
|
||||
"title": "Date",
|
||||
"size": 12,
|
||||
"explanation": "date de l'examen, devoir ou contrôle",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heure_debut",
|
||||
{
|
||||
"title": "Heure de début",
|
||||
"explanation": "heure du début de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
},
|
||||
),
|
||||
(
|
||||
"heure_fin",
|
||||
{
|
||||
"title": "Heure de fin",
|
||||
"explanation": "heure de fin de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_malus: # pas de coefficient
|
||||
form.append(("coefficient", {"input_type": "hidden", "default": "1."}))
|
||||
elif not is_apc: # modules standard hors BUT
|
||||
form.append(
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
"size": 6,
|
||||
"type": "float",
|
||||
"explanation": "coef. dans le module (choisi librement par l'enseignant)",
|
||||
"allow_null": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
form += [
|
||||
(
|
||||
"note_max",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"title": "Notes de 0 à",
|
||||
"explanation": "barème (note max actuelle: %s)" % min_note_max_str,
|
||||
"allow_null": False,
|
||||
"max_value": scu.NOTES_MAX,
|
||||
"min_value": min_note_max,
|
||||
},
|
||||
),
|
||||
(
|
||||
"description",
|
||||
{
|
||||
"size": 36,
|
||||
"type": "text",
|
||||
"explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".',
|
||||
},
|
||||
),
|
||||
(
|
||||
"visibulletinlist",
|
||||
{
|
||||
"input_type": "checkbox",
|
||||
"allowed_values": ["X"],
|
||||
"labels": [""],
|
||||
"title": "Visible sur bulletins",
|
||||
"explanation": "(pour les bulletins en version intermédiaire)",
|
||||
},
|
||||
),
|
||||
(
|
||||
"publish_incomplete",
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "Prise en compte immédiate",
|
||||
"explanation": "notes utilisées même si incomplètes",
|
||||
},
|
||||
),
|
||||
(
|
||||
"evaluation_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Modalité",
|
||||
"allowed_values": (
|
||||
scu.EVALUATION_NORMALE,
|
||||
scu.EVALUATION_RATTRAPAGE,
|
||||
scu.EVALUATION_SESSION2,
|
||||
),
|
||||
"type": "int",
|
||||
"labels": (
|
||||
"Normale",
|
||||
"Rattrapage (remplace si meilleure note)",
|
||||
"Deuxième session (remplace toujours)",
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc: # ressources et SAÉs
|
||||
form += [
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
"size": 6,
|
||||
"type": "float",
|
||||
"explanation": "importance de l'évaluation (multiplie les poids ci-dessous)",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
# Liste des UE utilisées dans des modules de ce semestre:
|
||||
for ue in sem_ues:
|
||||
form.append(
|
||||
(
|
||||
f"poids_{ue.id}",
|
||||
{
|
||||
"title": f"Poids {ue.acronyme}",
|
||||
"size": 2,
|
||||
"type": "float",
|
||||
"explanation": f"{ue.titre}",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
)
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
vals,
|
||||
form,
|
||||
cancelbutton="Annuler",
|
||||
submitlabel=submitlabel,
|
||||
initvalues=initvalues,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
dest_url = "moduleimpl_status?moduleimpl_id=%s" % modimpl_o["moduleimpl_id"]
|
||||
if tf[0] == 0:
|
||||
head = html_sco_header.sco_header(page_title=page_title)
|
||||
return (
|
||||
head
|
||||
+ "\n".join(H)
|
||||
+ "\n"
|
||||
+ tf[1]
|
||||
+ render_template("scodoc/help/evaluations.html", is_apc=is_apc)
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# form submission
|
||||
if tf[2]["visibulletinlist"]:
|
||||
tf[2]["visibulletin"] = True
|
||||
else:
|
||||
tf[2]["visibulletin"] = False
|
||||
if edit:
|
||||
sco_evaluation_db.do_evaluation_edit(tf[2])
|
||||
else:
|
||||
# creation d'une evaluation
|
||||
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
||||
if is_apc:
|
||||
# Set poids
|
||||
evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
for ue in sem_ues:
|
||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
return flask.redirect(dest_url)
|
|
@ -29,9 +29,7 @@
|
|||
"""
|
||||
import datetime
|
||||
import operator
|
||||
import pprint
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import flask
|
||||
from flask import url_for
|
||||
|
@ -41,12 +39,13 @@ from flask import request
|
|||
|
||||
from app import log
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
|
@ -96,285 +95,10 @@ def ListMedian(L):
|
|||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
_evaluationEditor = ndb.EditableTable(
|
||||
"notes_evaluation",
|
||||
"evaluation_id",
|
||||
(
|
||||
"evaluation_id",
|
||||
"moduleimpl_id",
|
||||
"jour",
|
||||
"heure_debut",
|
||||
"heure_fin",
|
||||
"description",
|
||||
"note_max",
|
||||
"coefficient",
|
||||
"visibulletin",
|
||||
"publish_incomplete",
|
||||
"evaluation_type",
|
||||
"numero",
|
||||
),
|
||||
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord
|
||||
output_formators={
|
||||
"jour": ndb.DateISOtoDMY,
|
||||
"numero": ndb.int_null_is_zero,
|
||||
},
|
||||
input_formators={
|
||||
"jour": ndb.DateDMYtoISO,
|
||||
"heure_debut": ndb.TimetoISO8601, # converti par do_evaluation_list
|
||||
"heure_fin": ndb.TimetoISO8601, # converti par do_evaluation_list
|
||||
"visibulletin": bool,
|
||||
"publish_incomplete": bool,
|
||||
"evaluation_type": int,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def do_evaluation_list(args, sortkey=None):
|
||||
"""List evaluations, sorted by numero (or most recent date first).
|
||||
|
||||
Ajoute les champs:
|
||||
'duree' : '2h30'
|
||||
'matin' : 1 (commence avant 12:00) ou 0
|
||||
'apresmidi' : 1 (termine après 12:00) ou 0
|
||||
'descrheure' : ' de 15h00 à 16h30'
|
||||
"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
|
||||
# calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
|
||||
for e in evals:
|
||||
heure_debut_dt = e["heure_debut"] or datetime.time(
|
||||
8, 00
|
||||
) # au cas ou pas d'heure (note externe?)
|
||||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
||||
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
|
||||
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d is not None:
|
||||
m = d % 60
|
||||
e["duree"] = "%dh" % (d / 60)
|
||||
if m != 0:
|
||||
e["duree"] += "%02d" % m
|
||||
else:
|
||||
e["duree"] = ""
|
||||
if heure_debut and (not heure_fin or heure_fin == heure_debut):
|
||||
e["descrheure"] = " à " + heure_debut
|
||||
elif heure_debut and heure_fin:
|
||||
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
|
||||
else:
|
||||
e["descrheure"] = ""
|
||||
# matin, apresmidi: utile pour se referer aux absences:
|
||||
if heure_debut_dt < datetime.time(12, 00):
|
||||
e["matin"] = 1
|
||||
else:
|
||||
e["matin"] = 0
|
||||
if heure_fin_dt > datetime.time(12, 00):
|
||||
e["apresmidi"] = 1
|
||||
else:
|
||||
e["apresmidi"] = 0
|
||||
|
||||
return evals
|
||||
|
||||
|
||||
def do_evaluation_list_in_formsemestre(formsemestre_id):
|
||||
"list evaluations in this formsemestre"
|
||||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
evals = []
|
||||
for mod in mods:
|
||||
evals += do_evaluation_list(args={"moduleimpl_id": mod["moduleimpl_id"]})
|
||||
return evals
|
||||
|
||||
|
||||
def _check_evaluation_args(args):
|
||||
"Check coefficient, dates and duration, raises exception if invalid"
|
||||
moduleimpl_id = args["moduleimpl_id"]
|
||||
# check bareme
|
||||
note_max = args.get("note_max", None)
|
||||
if note_max is None:
|
||||
raise ScoValueError("missing note_max")
|
||||
try:
|
||||
note_max = float(note_max)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid note_max value")
|
||||
if note_max < 0:
|
||||
raise ScoValueError("Invalid note_max value (must be positive or null)")
|
||||
# check coefficient
|
||||
coef = args.get("coefficient", None)
|
||||
if coef is None:
|
||||
raise ScoValueError("missing coefficient")
|
||||
try:
|
||||
coef = float(coef)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid coefficient value")
|
||||
if coef < 0:
|
||||
raise ScoValueError("Invalid coefficient value (must be positive or null)")
|
||||
# check date
|
||||
jour = args.get("jour", None)
|
||||
args["jour"] = jour
|
||||
if jour:
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
||||
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
|
||||
date_debut = datetime.date(y, m, d)
|
||||
d, m, y = [int(x) for x in sem["date_fin"].split("/")]
|
||||
date_fin = datetime.date(y, m, d)
|
||||
# passe par ndb.DateDMYtoISO pour avoir date pivot
|
||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||||
jour = datetime.date(y, m, d)
|
||||
if (jour > date_fin) or (jour < date_debut):
|
||||
raise ScoValueError(
|
||||
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
|
||||
% (d, m, y)
|
||||
)
|
||||
heure_debut = args.get("heure_debut", None)
|
||||
args["heure_debut"] = heure_debut
|
||||
heure_fin = args.get("heure_fin", None)
|
||||
args["heure_fin"] = heure_fin
|
||||
if jour and ((not heure_debut) or (not heure_fin)):
|
||||
raise ScoValueError("Les heures doivent être précisées")
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d and ((d < 0) or (d > 60 * 12)):
|
||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||
|
||||
|
||||
def do_evaluation_create(
|
||||
moduleimpl_id=None,
|
||||
jour=None,
|
||||
heure_debut=None,
|
||||
heure_fin=None,
|
||||
description=None,
|
||||
note_max=None,
|
||||
coefficient=None,
|
||||
visibulletin=None,
|
||||
publish_incomplete=None,
|
||||
evaluation_type=None,
|
||||
numero=None,
|
||||
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8
|
||||
):
|
||||
"""Create an evaluation"""
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
args = locals()
|
||||
log("do_evaluation_create: args=" + str(args))
|
||||
_check_evaluation_args(args)
|
||||
# Check numeros
|
||||
module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
|
||||
if not "numero" in args or args["numero"] is None:
|
||||
n = None
|
||||
# determine le numero avec la date
|
||||
# Liste des eval existantes triees par date, la plus ancienne en tete
|
||||
ModEvals = do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id},
|
||||
sortkey="jour asc, heure_debut asc",
|
||||
)
|
||||
if args["jour"]:
|
||||
next_eval = None
|
||||
t = (
|
||||
ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
|
||||
ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
|
||||
)
|
||||
for e in ModEvals:
|
||||
if (
|
||||
ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
||||
ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
|
||||
) > t:
|
||||
next_eval = e
|
||||
break
|
||||
if next_eval:
|
||||
n = module_evaluation_insert_before(ModEvals, next_eval)
|
||||
else:
|
||||
n = None # a placer en fin
|
||||
if n is None: # pas de date ou en fin:
|
||||
if ModEvals:
|
||||
log(pprint.pformat(ModEvals[-1]))
|
||||
n = ModEvals[-1]["numero"] + 1
|
||||
else:
|
||||
n = 0 # the only one
|
||||
# log("creating with numero n=%d" % n)
|
||||
args["numero"] = n
|
||||
|
||||
#
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _evaluationEditor.create(cnx, args)
|
||||
|
||||
# news
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def do_evaluation_edit(args):
|
||||
"edit an evaluation"
|
||||
evaluation_id = args["evaluation_id"]
|
||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not the_evals:
|
||||
raise ValueError("evaluation inexistante !")
|
||||
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
args["moduleimpl_id"] = moduleimpl_id
|
||||
_check_evaluation_args(args)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_evaluationEditor.edit(cnx, args)
|
||||
# inval cache pour ce semestre
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||
|
||||
|
||||
def do_evaluation_delete(evaluation_id):
|
||||
"delete evaluation"
|
||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not the_evals:
|
||||
raise ValueError("evaluation inexistante !")
|
||||
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
|
||||
notes = [x["value"] for x in NotesDB.values()]
|
||||
if notes:
|
||||
raise ScoValueError(
|
||||
"Impossible de supprimer cette évaluation: il reste des notes"
|
||||
)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
||||
_evaluationEditor.delete(cnx, evaluation_id)
|
||||
# inval cache pour ce semestre
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||
# news
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = (
|
||||
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
||||
|
||||
def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=False):
|
||||
"""donne infos sur l'etat du evaluation
|
||||
"""donne infos sur l'état de l'évaluation
|
||||
{ nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att,
|
||||
moyenne, mediane, mini, maxi,
|
||||
date_last_modif, gr_complets, gr_incomplets, evalcomplete }
|
||||
|
@ -385,33 +109,15 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
|
|||
nb_inscrits = len(
|
||||
sco_groups.do_evaluation_listeetuds_groups(evaluation_id, getallstudents=True)
|
||||
)
|
||||
NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
|
||||
notes = [x["value"] for x in NotesDB.values()]
|
||||
nb_abs = len([x for x in notes if x is None])
|
||||
nb_neutre = len([x for x in notes if x == scu.NOTES_NEUTRALISE])
|
||||
nb_att = len([x for x in notes if x == scu.NOTES_ATTENTE])
|
||||
moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes)
|
||||
if moy_num is None:
|
||||
median, moy = "", ""
|
||||
median_num, moy_num = None, None
|
||||
mini, maxi = "", ""
|
||||
mini_num, maxi_num = None, None
|
||||
else:
|
||||
median = scu.fmt_note(median_num)
|
||||
moy = scu.fmt_note(moy_num)
|
||||
mini = scu.fmt_note(mini_num)
|
||||
maxi = scu.fmt_note(maxi_num)
|
||||
# cherche date derniere modif note
|
||||
if len(NotesDB):
|
||||
t = [x["date"] for x in NotesDB.values()]
|
||||
last_modif = max(t)
|
||||
else:
|
||||
last_modif = None
|
||||
etuds_notes_dict = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
evaluation_id
|
||||
) # { etudid : note }
|
||||
|
||||
# ---- Liste des groupes complets et incomplets
|
||||
E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
is_malus = Mod["module_type"] == scu.MODULE_MALUS # True si module de malus
|
||||
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
# Si partition_id is None, prend 'all' ou bien la premiere:
|
||||
if partition_id is None:
|
||||
|
@ -437,8 +143,32 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
|
|||
|
||||
# Nombre de notes valides d'étudiants inscrits au module
|
||||
# (car il peut y avoir des notes d'étudiants désinscrits depuis l'évaluation)
|
||||
nb_notes = len(insmodset.intersection(NotesDB))
|
||||
nb_notes_total = len(NotesDB)
|
||||
etudids_avec_note = insmodset.intersection(etuds_notes_dict)
|
||||
nb_notes = len(etudids_avec_note)
|
||||
# toutes saisies, y compris chez des non-inscrits:
|
||||
nb_notes_total = len(etuds_notes_dict)
|
||||
|
||||
notes = [etuds_notes_dict[etudid]["value"] for etudid in etudids_avec_note]
|
||||
nb_abs = len([x for x in notes if x is None])
|
||||
nb_neutre = len([x for x in notes if x == scu.NOTES_NEUTRALISE])
|
||||
nb_att = len([x for x in notes if x == scu.NOTES_ATTENTE])
|
||||
moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes)
|
||||
if moy_num is None:
|
||||
median, moy = "", ""
|
||||
median_num, moy_num = None, None
|
||||
mini, maxi = "", ""
|
||||
mini_num, maxi_num = None, None
|
||||
else:
|
||||
median = scu.fmt_note(median_num)
|
||||
moy = scu.fmt_note(moy_num)
|
||||
mini = scu.fmt_note(mini_num)
|
||||
maxi = scu.fmt_note(maxi_num)
|
||||
# cherche date derniere modif note
|
||||
if len(etuds_notes_dict):
|
||||
t = [x["date"] for x in etuds_notes_dict.values()]
|
||||
last_modif = max(t)
|
||||
else:
|
||||
last_modif = None
|
||||
|
||||
# On considere une note "manquante" lorsqu'elle n'existe pas
|
||||
# ou qu'elle est en attente (ATT)
|
||||
|
@ -455,8 +185,8 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
|
|||
groups[group["group_id"]] = group
|
||||
#
|
||||
isMissing = False
|
||||
if i["etudid"] in NotesDB:
|
||||
val = NotesDB[i["etudid"]]["value"]
|
||||
if i["etudid"] in etuds_notes_dict:
|
||||
val = etuds_notes_dict[i["etudid"]]["value"]
|
||||
if val == scu.NOTES_ATTENTE:
|
||||
isMissing = True
|
||||
TotalNbAtt += 1
|
||||
|
@ -611,46 +341,6 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
|||
return res
|
||||
|
||||
|
||||
# ancien _notes_getall
|
||||
def do_evaluation_get_all_notes(
|
||||
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
||||
):
|
||||
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
||||
"""
|
||||
do_cache = (
|
||||
filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||
if do_cache:
|
||||
r = sco_cache.EvaluationCache.get(evaluation_id)
|
||||
if r != None:
|
||||
return r
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cond = " where evaluation_id=%(evaluation_id)s"
|
||||
if by_uid:
|
||||
cond += " and uid=%(by_uid)s"
|
||||
|
||||
cursor.execute(
|
||||
"select * from " + table + cond,
|
||||
{"evaluation_id": evaluation_id, "by_uid": by_uid},
|
||||
)
|
||||
res = cursor.dictfetchall()
|
||||
d = {}
|
||||
if filter_suppressed:
|
||||
for x in res:
|
||||
if x["value"] != scu.NOTES_SUPPRESS:
|
||||
d[x["etudid"]] = x
|
||||
else:
|
||||
for x in res:
|
||||
d[x["etudid"]] = x
|
||||
if do_cache:
|
||||
status = sco_cache.EvaluationCache.set(evaluation_id, d)
|
||||
if not status:
|
||||
log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}")
|
||||
return d
|
||||
|
||||
|
||||
def _eval_etat(evals):
|
||||
"""evals: list of mappings (etats)
|
||||
-> nb_eval_completes, nb_evals_en_cours,
|
||||
|
@ -818,10 +508,12 @@ def evaluation_date_first_completion(evaluation_id):
|
|||
# ins = [i for i in insem if i["etudid"] in insmodset]
|
||||
|
||||
notes = list(
|
||||
do_evaluation_get_all_notes(evaluation_id, filter_suppressed=False).values()
|
||||
sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
evaluation_id, filter_suppressed=False
|
||||
).values()
|
||||
)
|
||||
notes_log = list(
|
||||
do_evaluation_get_all_notes(
|
||||
sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
evaluation_id, filter_suppressed=False, table="notes_notes_log"
|
||||
).values()
|
||||
)
|
||||
|
@ -854,7 +546,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
|||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or (
|
||||
Mod["module_type"] == scu.MODULE_MALUS
|
||||
Mod["module_type"] == ModuleType.MALUS
|
||||
):
|
||||
continue
|
||||
e["date_first_complete"] = evaluation_date_first_completion(e["evaluation_id"])
|
||||
|
@ -917,112 +609,6 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
|||
return tab.make_page(format=format)
|
||||
|
||||
|
||||
def module_evaluation_insert_before(ModEvals, next_eval):
|
||||
"""Renumber evals such that an evaluation with can be inserted before next_eval
|
||||
Returns numero suitable for the inserted evaluation
|
||||
"""
|
||||
if next_eval:
|
||||
n = next_eval["numero"]
|
||||
if not n:
|
||||
log("renumbering old evals")
|
||||
module_evaluation_renumber(next_eval["moduleimpl_id"])
|
||||
next_eval = do_evaluation_list(
|
||||
args={"evaluation_id": next_eval["evaluation_id"]}
|
||||
)[0]
|
||||
n = next_eval["numero"]
|
||||
else:
|
||||
n = 1
|
||||
# log('inserting at position numero %s' % n )
|
||||
# all numeros >= n are incremented
|
||||
for e in ModEvals:
|
||||
if e["numero"] >= n:
|
||||
e["numero"] += 1
|
||||
# log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
|
||||
do_evaluation_edit(e)
|
||||
|
||||
return n
|
||||
|
||||
|
||||
def module_evaluation_move(evaluation_id, after=0, redirect=1):
|
||||
"""Move before/after previous one (decrement/increment numero)
|
||||
(published)
|
||||
"""
|
||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
redirect = int(redirect)
|
||||
# access: can change eval ?
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
|
||||
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True)
|
||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
ModEvals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
|
||||
# log('ModEvals=%s' % [ x['evaluation_id'] for x in ModEvals] )
|
||||
if len(ModEvals) > 1:
|
||||
idx = [p["evaluation_id"] for p in ModEvals].index(evaluation_id)
|
||||
neigh = None # object to swap with
|
||||
if after == 0 and idx > 0:
|
||||
neigh = ModEvals[idx - 1]
|
||||
elif after == 1 and idx < len(ModEvals) - 1:
|
||||
neigh = ModEvals[idx + 1]
|
||||
if neigh: #
|
||||
# swap numero with neighbor
|
||||
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
|
||||
do_evaluation_edit(e)
|
||||
do_evaluation_edit(neigh)
|
||||
# redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e["moduleimpl_id"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
|
||||
"""Renumber evaluations in this module, according to their date. (numero=0: oldest one)
|
||||
Needed because previous versions of ScoDoc did not have eval numeros
|
||||
Note: existing numeros are ignored
|
||||
"""
|
||||
redirect = int(redirect)
|
||||
# log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
|
||||
# List sorted according to date/heure, ignoring numeros:
|
||||
# (note that we place evaluations with NULL date at the end)
|
||||
ModEvals = do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id},
|
||||
sortkey="jour asc, heure_debut asc",
|
||||
)
|
||||
|
||||
all_numbered = False not in [x["numero"] > 0 for x in ModEvals]
|
||||
if all_numbered and only_if_unumbered:
|
||||
return # all ok
|
||||
|
||||
# log('module_evaluation_renumber')
|
||||
# Reset all numeros:
|
||||
i = 1
|
||||
for e in ModEvals:
|
||||
e["numero"] = i
|
||||
do_evaluation_edit(e)
|
||||
i += 1
|
||||
|
||||
# If requested, redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -------------- VIEWS
|
||||
def evaluation_describe(evaluation_id="", edit_in_place=True):
|
||||
"""HTML description of evaluation, for page headers
|
||||
|
@ -1030,7 +616,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
|
|||
"""
|
||||
from app.scodoc import sco_saisie_notes
|
||||
|
||||
E = do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
moduleimpl_id = E["moduleimpl_id"]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
|
@ -1054,13 +640,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
|
|||
etit = E["description"] or ""
|
||||
if etit:
|
||||
etit = ' "' + etit + '"'
|
||||
if Mod["module_type"] == scu.MODULE_MALUS:
|
||||
if Mod["module_type"] == ModuleType.MALUS:
|
||||
etit += ' <span class="eval_malus">(points de malus)</span>'
|
||||
H = [
|
||||
'<span class="eval_title">Evaluation%s</span><p><b>Module : %s</b></p>'
|
||||
% (etit, mod_descr)
|
||||
]
|
||||
if Mod["module_type"] == scu.MODULE_MALUS:
|
||||
if Mod["module_type"] == ModuleType.MALUS:
|
||||
# Indique l'UE
|
||||
ue = sco_edit_ue.ue_list(args={"ue_id": Mod["ue_id"]})[0]
|
||||
H.append("<p><b>UE : %(acronyme)s</b></p>" % ue)
|
||||
|
@ -1070,21 +656,25 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
|
|||
)
|
||||
else:
|
||||
# date et absences (pas pour evals de malus)
|
||||
jour = E["jour"] or "<em>pas de date</em>"
|
||||
H.append(
|
||||
"<p>Réalisée le <b>%s</b> de %s à %s "
|
||||
% (jour, E["heure_debut"], E["heure_fin"])
|
||||
)
|
||||
if E["jour"]:
|
||||
jour = E["jour"]
|
||||
H.append("<p>Réalisée le <b>%s</b> " % (jour))
|
||||
if E["heure_debut"] != E["heure_fin"]:
|
||||
H.append("de %s à %s " % (E["heure_debut"], E["heure_fin"]))
|
||||
group_id = sco_groups.get_default_group(formsemestre_id)
|
||||
H.append(
|
||||
'<span class="noprint"><a href="%s/Absences/EtatAbsencesDate?group_ids=%s&date=%s">(absences ce jour)</a></span>'
|
||||
% (
|
||||
scu.ScoURL(),
|
||||
group_id,
|
||||
urllib.parse.quote(E["jour"], safe=""),
|
||||
)
|
||||
f"""<span class="noprint"><a href="{url_for(
|
||||
'absences.EtatAbsencesDate',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
group_ids=group_id,
|
||||
date=E["jour"]
|
||||
)
|
||||
}">(absences ce jour)</a></span>"""
|
||||
)
|
||||
else:
|
||||
jour = "<em>pas de date</em>"
|
||||
H.append("<p>Réalisée le <b>%s</b> " % (jour))
|
||||
|
||||
H.append(
|
||||
'</p><p>Coefficient dans le module: <b>%s</b>, notes sur <span id="eval_note_max">%g</span> '
|
||||
% (E["coefficient"], E["note_max"])
|
||||
|
@ -1092,275 +682,16 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
|
|||
H.append('<span id="eval_note_min" class="sco-hidden">0.</span>')
|
||||
if can_edit:
|
||||
H.append(
|
||||
'<a href="evaluation_edit?evaluation_id=%s">(modifier l\'évaluation)</a>'
|
||||
% evaluation_id
|
||||
f"""
|
||||
<a class="stdlink" href="{url_for(
|
||||
"notes.evaluation_edit", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
||||
}">modifier l'évaluation</a>
|
||||
|
||||
<a class="stdlink" href="{url_for(
|
||||
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
||||
}">saisie des notes</a>
|
||||
"""
|
||||
)
|
||||
H.append("</p>")
|
||||
|
||||
return '<div class="eval_description">' + "\n".join(H) + "</div>"
|
||||
|
||||
|
||||
def evaluation_create_form(
|
||||
moduleimpl_id=None,
|
||||
evaluation_id=None,
|
||||
edit=False,
|
||||
readonly=False,
|
||||
page_title="Evaluation",
|
||||
):
|
||||
"formulaire creation/edition des evaluations (pas des notes)"
|
||||
if evaluation_id != None:
|
||||
the_eval = do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
moduleimpl_id = the_eval["moduleimpl_id"]
|
||||
#
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
is_malus = M["module"]["module_type"] == scu.MODULE_MALUS # True si module de malus
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
||||
if not readonly:
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
return (
|
||||
html_sco_header.sco_header()
|
||||
+ "<h2>Opération non autorisée</h2><p>"
|
||||
+ "Modification évaluation impossible pour %s"
|
||||
% current_user.get_nomplogin()
|
||||
+ "</p>"
|
||||
+ '<p><a href="moduleimpl_status?moduleimpl_id=%s">Revenir</a></p>'
|
||||
% (moduleimpl_id,)
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
if readonly:
|
||||
edit = True # montre les donnees existantes
|
||||
if not edit:
|
||||
# creation nouvel
|
||||
if moduleimpl_id is None:
|
||||
raise ValueError("missing moduleimpl_id parameter")
|
||||
initvalues = {
|
||||
"note_max": 20,
|
||||
"jour": time.strftime("%d/%m/%Y", time.localtime()),
|
||||
"publish_incomplete": is_malus,
|
||||
}
|
||||
submitlabel = "Créer cette évaluation"
|
||||
action = "Création d'une é"
|
||||
link = ""
|
||||
else:
|
||||
# edition donnees existantes
|
||||
# setup form init values
|
||||
if evaluation_id is None:
|
||||
raise ValueError("missing evaluation_id parameter")
|
||||
initvalues = the_eval
|
||||
moduleimpl_id = initvalues["moduleimpl_id"]
|
||||
submitlabel = "Modifier les données"
|
||||
if readonly:
|
||||
action = "E"
|
||||
link = (
|
||||
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
|
||||
% M["moduleimpl_id"]
|
||||
)
|
||||
else:
|
||||
action = "Modification d'une é"
|
||||
link = ""
|
||||
# Note maximale actuelle dans cette eval ?
|
||||
etat = do_evaluation_etat(evaluation_id)
|
||||
if etat["maxi_num"] is not None:
|
||||
min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"])
|
||||
else:
|
||||
min_note_max = scu.NOTES_PRECISION
|
||||
#
|
||||
if min_note_max > scu.NOTES_PRECISION:
|
||||
min_note_max_str = scu.fmt_note(min_note_max)
|
||||
else:
|
||||
min_note_max_str = "0"
|
||||
#
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
#
|
||||
help = """<div class="help"><p class="help">
|
||||
Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module.
|
||||
Il est fixé librement par l'enseignant pour refléter l'importance de ses différentes notes
|
||||
(examens, projets, travaux pratiques...). Ce coefficient est utilisé pour calculer la note
|
||||
moyenne de chaque étudiant dans ce module.
|
||||
</p><p class="help">
|
||||
Ne pas confondre ce coefficient avec le coefficient du module, qui est lui fixé par le programme
|
||||
pédagogique (le PPN pour les DUT) et pondère les moyennes de chaque module pour obtenir
|
||||
les moyennes d'UE et la moyenne générale.
|
||||
</p><p class="help">
|
||||
L'option <em>Visible sur bulletins</em> indique que la note sera reportée sur les bulletins
|
||||
en version dite "intermédiaire" (dans cette version, on peut ne faire apparaitre que certaines
|
||||
notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur
|
||||
les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).
|
||||
</p><p class="help">
|
||||
Les modalités "rattrapage" et "deuxième session" définissent des évaluations prises en compte de
|
||||
façon spéciale: </p>
|
||||
<ul>
|
||||
<li>les notes d'une évaluation de "rattrapage" remplaceront les moyennes du module
|
||||
<em>si elles sont meilleures que celles calculées</em>.</li>
|
||||
<li>les notes de "deuxième session" remplacent, lorsqu'elles sont saisies, la moyenne de l'étudiant
|
||||
à ce module, même si la note de deuxième session est plus faible.</li>
|
||||
</ul>
|
||||
<p class="help">
|
||||
Dans ces deux cas, le coefficient est ignoré, et toutes les notes n'ont
|
||||
pas besoin d'être rentrées.
|
||||
</p>
|
||||
<p class="help">
|
||||
Par ailleurs, les évaluations des modules de type "malus" sont toujours spéciales: le coefficient n'est pas utilisé.
|
||||
Les notes de malus sont toujours comprises entre -20 et 20. Les points sont soustraits à la moyenne
|
||||
de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée).
|
||||
</p>
|
||||
"""
|
||||
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
|
||||
moduleimpl_id,
|
||||
Mod["code"],
|
||||
Mod["titre"],
|
||||
link,
|
||||
)
|
||||
if not readonly:
|
||||
H = ["<h3>%svaluation en %s</h3>" % (action, mod_descr)]
|
||||
else:
|
||||
return evaluation_describe(evaluation_id)
|
||||
|
||||
heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)]
|
||||
#
|
||||
initvalues["visibulletin"] = initvalues.get("visibulletin", True)
|
||||
if initvalues["visibulletin"]:
|
||||
initvalues["visibulletinlist"] = ["X"]
|
||||
else:
|
||||
initvalues["visibulletinlist"] = []
|
||||
vals = scu.get_request_args()
|
||||
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
|
||||
vals["visibulletinlist"] = []
|
||||
#
|
||||
form = [
|
||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
|
||||
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
|
||||
# ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
|
||||
(
|
||||
"jour",
|
||||
{
|
||||
"input_type": "date",
|
||||
"title": "Date",
|
||||
"size": 12,
|
||||
"explanation": "date de l'examen, devoir ou contrôle",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heure_debut",
|
||||
{
|
||||
"title": "Heure de début",
|
||||
"explanation": "heure du début de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
},
|
||||
),
|
||||
(
|
||||
"heure_fin",
|
||||
{
|
||||
"title": "Heure de fin",
|
||||
"explanation": "heure de fin de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_malus: # pas de coefficient
|
||||
form.append(("coefficient", {"input_type": "hidden", "default": "1."}))
|
||||
else:
|
||||
form.append(
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
"size": 10,
|
||||
"type": "float",
|
||||
"explanation": "coef. dans le module (choisi librement par l'enseignant)",
|
||||
"allow_null": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
form += [
|
||||
(
|
||||
"note_max",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"title": "Notes de 0 à",
|
||||
"explanation": "barème (note max actuelle: %s)" % min_note_max_str,
|
||||
"allow_null": False,
|
||||
"max_value": scu.NOTES_MAX,
|
||||
"min_value": min_note_max,
|
||||
},
|
||||
),
|
||||
(
|
||||
"description",
|
||||
{
|
||||
"size": 36,
|
||||
"type": "text",
|
||||
"explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".',
|
||||
},
|
||||
),
|
||||
(
|
||||
"visibulletinlist",
|
||||
{
|
||||
"input_type": "checkbox",
|
||||
"allowed_values": ["X"],
|
||||
"labels": [""],
|
||||
"title": "Visible sur bulletins",
|
||||
"explanation": "(pour les bulletins en version intermédiaire)",
|
||||
},
|
||||
),
|
||||
(
|
||||
"publish_incomplete",
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "Prise en compte immédiate",
|
||||
"explanation": "notes utilisées même si incomplètes",
|
||||
},
|
||||
),
|
||||
(
|
||||
"evaluation_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Modalité",
|
||||
"allowed_values": (
|
||||
scu.EVALUATION_NORMALE,
|
||||
scu.EVALUATION_RATTRAPAGE,
|
||||
scu.EVALUATION_SESSION2,
|
||||
),
|
||||
"type": "int",
|
||||
"labels": (
|
||||
"Normale",
|
||||
"Rattrapage (remplace si meilleure note)",
|
||||
"Deuxième session (remplace toujours)",
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
vals,
|
||||
form,
|
||||
cancelbutton="Annuler",
|
||||
submitlabel=submitlabel,
|
||||
initvalues=initvalues,
|
||||
readonly=readonly,
|
||||
)
|
||||
|
||||
dest_url = "moduleimpl_status?moduleimpl_id=%s" % M["moduleimpl_id"]
|
||||
if tf[0] == 0:
|
||||
head = html_sco_header.sco_header(page_title=page_title)
|
||||
return head + "\n".join(H) + "\n" + tf[1] + help + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# form submission
|
||||
if tf[2]["visibulletinlist"]:
|
||||
tf[2]["visibulletin"] = True
|
||||
else:
|
||||
tf[2]["visibulletin"] = False
|
||||
if not edit:
|
||||
# creation d'une evaluation
|
||||
evaluation_id = do_evaluation_create(**tf[2])
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
do_evaluation_edit(tf[2])
|
||||
return flask.redirect(dest_url)
|
||||
|
|
|
@ -56,7 +56,7 @@ class ScoValueError(ScoException):
|
|||
self.dest_url = dest_url
|
||||
|
||||
|
||||
class FormatError(ScoValueError):
|
||||
class ScoFormatError(ScoValueError):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -35,9 +35,10 @@ from flask import g, url_for, request
|
|||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import Formation, Module
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
|
@ -49,7 +50,7 @@ from app.scodoc import sco_tag_module
|
|||
from app.scodoc import sco_xml
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
_formationEditor = ndb.EditableTable(
|
||||
|
@ -85,12 +86,10 @@ def formation_list(formation_id=None, args={}):
|
|||
return r
|
||||
|
||||
|
||||
def formation_has_locked_sems(formation_id):
|
||||
"True if there is a locked formsemestre in this formation"
|
||||
sems = sco_formsemestre.do_formsemestre_list(
|
||||
args={"formation_id": formation_id, "etat": False}
|
||||
)
|
||||
return sems
|
||||
def formation_has_locked_sems(formation_id): # XXX to remove
|
||||
"backward compat: True if there is a locked formsemestre in this formation"
|
||||
formation = Formation.query.get(formation_id)
|
||||
return formation.has_locked_sems()
|
||||
|
||||
|
||||
def formation_export(
|
||||
|
@ -103,7 +102,8 @@ def formation_export(
|
|||
"""Get a formation, with UE, matieres, modules
|
||||
in desired format
|
||||
"""
|
||||
F = formation_list(args={"formation_id": formation_id})[0]
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
F = formation.to_dict()
|
||||
selector = {"formation_id": formation_id}
|
||||
if not export_external_ues:
|
||||
selector["is_external"] = False
|
||||
|
@ -111,7 +111,9 @@ def formation_export(
|
|||
F["ue"] = ues
|
||||
for ue in ues:
|
||||
ue_id = ue["ue_id"]
|
||||
ue["reference"] = ue_id # pour les coefficients
|
||||
if not export_ids:
|
||||
del ue["id"]
|
||||
del ue["ue_id"]
|
||||
del ue["formation_id"]
|
||||
if ue["ects"] is None:
|
||||
|
@ -121,17 +123,27 @@ def formation_export(
|
|||
for mat in mats:
|
||||
matiere_id = mat["matiere_id"]
|
||||
if not export_ids:
|
||||
del mat["id"]
|
||||
del mat["matiere_id"]
|
||||
del mat["ue_id"]
|
||||
mods = sco_edit_module.module_list({"matiere_id": matiere_id})
|
||||
mat["module"] = mods
|
||||
for mod in mods:
|
||||
module_id = mod["module_id"]
|
||||
if export_tags:
|
||||
# mod['tags'] = sco_tag_module.module_tag_list( module_id=mod['module_id'])
|
||||
tags = sco_tag_module.module_tag_list(module_id=mod["module_id"])
|
||||
if tags:
|
||||
mod["tags"] = [{"name": x} for x in tags]
|
||||
#
|
||||
module = Module.query.get(module_id)
|
||||
if module.is_apc():
|
||||
# Exporte les coefficients
|
||||
mod["coefficients"] = [
|
||||
{"ue_reference": str(ue_id), "coef": str(coef)}
|
||||
for (ue_id, coef) in module.get_ue_coef_dict().items()
|
||||
]
|
||||
if not export_ids:
|
||||
del mod["id"]
|
||||
del mod["ue_id"]
|
||||
del mod["matiere_id"]
|
||||
del mod["module_id"]
|
||||
|
@ -166,8 +178,14 @@ def formation_import_xml(doc: str, import_tags=True):
|
|||
log("formation_import_xml: invalid XML data")
|
||||
raise ScoValueError("Fichier XML invalide")
|
||||
|
||||
f = dom.getElementsByTagName("formation")[0] # or dom.documentElement
|
||||
D = sco_xml.xml_to_dicts(f)
|
||||
try:
|
||||
f = dom.getElementsByTagName("formation")[0] # or dom.documentElement
|
||||
D = sco_xml.xml_to_dicts(f)
|
||||
except:
|
||||
raise ScoFormatError(
|
||||
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc.
|
||||
(élément 'formation' inexistant par exemple)."""
|
||||
)
|
||||
assert D[0] == "formation"
|
||||
F = D[1]
|
||||
# F_quoted = F.copy()
|
||||
|
@ -197,6 +215,9 @@ def formation_import_xml(doc: str, import_tags=True):
|
|||
ues_old2new = {} # xml ue_id : new ue_id
|
||||
modules_old2new = {} # xml module_id : new module_id
|
||||
# (nb: mecanisme utilise pour cloner semestres seulement, pas pour I/O XML)
|
||||
|
||||
ue_reference_to_id = {} # pour les coefs APC (map reference -> ue_id)
|
||||
modules_a_coefficienter = [] # Liste des modules avec coefs APC
|
||||
# -- create UEs
|
||||
for ue_info in D[2]:
|
||||
assert ue_info[0] == "ue"
|
||||
|
@ -209,6 +230,10 @@ def formation_import_xml(doc: str, import_tags=True):
|
|||
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
|
||||
if xml_ue_id:
|
||||
ues_old2new[xml_ue_id] = ue_id
|
||||
# élément optionnel présent dans les exports BUT:
|
||||
ue_reference = ue_info[1].get("reference")
|
||||
if ue_reference:
|
||||
ue_reference_to_id[int(ue_reference)] = ue_id
|
||||
# -- create matieres
|
||||
for mat_info in ue_info[2]:
|
||||
assert mat_info[0] == "matiere"
|
||||
|
@ -228,11 +253,27 @@ def formation_import_xml(doc: str, import_tags=True):
|
|||
mod_id = sco_edit_module.do_module_create(mod_info[1])
|
||||
if xml_module_id:
|
||||
modules_old2new[int(xml_module_id)] = mod_id
|
||||
if import_tags:
|
||||
if len(mod_info) > 2:
|
||||
tag_names = [t[1]["name"] for t in mod_info[2]]
|
||||
if len(mod_info) > 2:
|
||||
module = Module.query.get(mod_id)
|
||||
tag_names = []
|
||||
ue_coef_dict = {}
|
||||
for child in mod_info[2]:
|
||||
if child[0] == "tags" and import_tags:
|
||||
tag_names.append(child[1]["name"])
|
||||
elif child[0] == "coefficients":
|
||||
ue_reference = int(child[1]["ue_reference"])
|
||||
coef = float(child[1]["coef"])
|
||||
ue_coef_dict[ue_reference] = coef
|
||||
if import_tags and tag_names:
|
||||
sco_tag_module.module_tag_set(mod_id, tag_names)
|
||||
|
||||
if module.is_apc() and ue_coef_dict:
|
||||
modules_a_coefficienter.append((module, ue_coef_dict))
|
||||
# Fixe les coefs APC (à la fin pour que les UE soient crées)
|
||||
for module, ue_coef_dict_ref in modules_a_coefficienter:
|
||||
# remap ue ids:
|
||||
ue_coef_dict = {ue_reference_to_id[k]: v for (k, v) in ue_coef_dict_ref.items()}
|
||||
module.set_ue_coef_dict(ue_coef_dict)
|
||||
db.session.commit()
|
||||
return formation_id, modules_old2new, ues_old2new
|
||||
|
||||
|
||||
|
|
|
@ -39,7 +39,6 @@ from app.scodoc import sco_codes_parcours
|
|||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app import log
|
||||
from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID
|
||||
|
@ -195,7 +194,7 @@ def _formsemestre_enrich(sem):
|
|||
sem["titreannee"] += "-" + annee_fin
|
||||
sem["annee"] += "-" + annee_fin
|
||||
# et les dates sous la forme "oct 2007 - fev 2008"
|
||||
months = sco_etud.MONTH_NAMES_ABBREV
|
||||
months = scu.MONTH_NAMES_ABBREV
|
||||
if mois_debut:
|
||||
mois_debut = months[int(mois_debut) - 1]
|
||||
if mois_fin:
|
||||
|
@ -369,7 +368,7 @@ def _write_formsemestre_aux(sem, fieldname, valuename):
|
|||
# uniquify
|
||||
values = set([str(x) for x in sem[fieldname]])
|
||||
|
||||
cnx = ndb.GetDBConnexion(autocommit=False)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
tablename = "notes_formsemestre_" + fieldname
|
||||
try:
|
||||
|
@ -398,6 +397,8 @@ def _write_formsemestre_aux(sem, fieldname, valuename):
|
|||
|
||||
def sem_set_responsable_name(sem):
|
||||
"ajoute champs responsable_name"
|
||||
from app.scodoc import sco_users
|
||||
|
||||
sem["responsable_name"] = ", ".join(
|
||||
[
|
||||
sco_users.user_info(responsable_id)["nomprenom"]
|
||||
|
@ -469,7 +470,7 @@ def sem_une_annee(sem):
|
|||
return debut == fin
|
||||
|
||||
|
||||
def sem_est_courant(sem):
|
||||
def sem_est_courant(sem): # -> FormSemestre.est_courant
|
||||
"""Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)"""
|
||||
now = time.strftime("%Y-%m-%d")
|
||||
debut = ndb.DateDMYtoISO(sem["date_debut"])
|
||||
|
|
|
@ -30,8 +30,10 @@
|
|||
import flask
|
||||
from flask import url_for, g, request
|
||||
from flask_login import current_user
|
||||
from app.auth.models import User
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_cache
|
||||
|
@ -49,6 +51,7 @@ from app.scodoc import sco_edit_module
|
|||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups_copy
|
||||
|
@ -851,7 +854,7 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
|
|||
)[0]["moduleimpl_id"]
|
||||
mod = sco_edit_module.module_list({"module_id": module_id})[0]
|
||||
# Evaluations dans ce module ?
|
||||
evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
||||
evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
||||
if evals:
|
||||
msg += [
|
||||
'<b>impossible de supprimer %s (%s) car il y a %d évaluations définies (<a href="moduleimpl_status?moduleimpl_id=%s" class="stdlink">supprimer les d\'abord</a>)</b>'
|
||||
|
@ -1030,14 +1033,15 @@ def do_formsemestre_clone(
|
|||
sco_moduleimpl.do_ens_create(args)
|
||||
# optionally, copy evaluations
|
||||
if clone_evaluations:
|
||||
evals = sco_evaluations.do_evaluation_list(
|
||||
args={"moduleimpl_id": mod_orig["moduleimpl_id"]}
|
||||
)
|
||||
for e in evals:
|
||||
args = e.copy()
|
||||
del args["jour"] # erase date
|
||||
args["moduleimpl_id"] = mid
|
||||
_ = sco_evaluations.do_evaluation_create(**args)
|
||||
for e in Evaluation.query.filter_by(
|
||||
moduleimpl_id=mod_orig["moduleimpl_id"]
|
||||
):
|
||||
# copie en enlevant la date
|
||||
new_eval = e.clone(not_copying=("jour", "moduleimpl_id"))
|
||||
new_eval.moduleimpl_id = mid
|
||||
# Copie les poids APC de l'évaluation
|
||||
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
|
||||
db.session.commit()
|
||||
|
||||
# 3- copy uecoefs
|
||||
objs = sco_formsemestre.formsemestre_uecoef_list(
|
||||
|
@ -1105,7 +1109,7 @@ def formsemestre_associate_new_version(
|
|||
"etat": "1",
|
||||
},
|
||||
)
|
||||
of = []
|
||||
H = []
|
||||
for s in othersems:
|
||||
if (
|
||||
s["formsemestre_id"] == formsemestre_id
|
||||
|
@ -1118,9 +1122,10 @@ def formsemestre_associate_new_version(
|
|||
disabled = 'disabled="1"'
|
||||
else:
|
||||
disabled = ""
|
||||
of.append(
|
||||
'<div><input type="checkbox" name="other_formsemestre_ids:list" value="%s" %s %s>%s</input></div>'
|
||||
% (s["formsemestre_id"], checked, disabled, s["titremois"])
|
||||
H.append(
|
||||
f"""<div><input type="checkbox" name="other_formsemestre_ids:list"
|
||||
value="{s['formsemestre_id']}" {checked} {disabled}
|
||||
>{s['titremois']}</input></div>"""
|
||||
)
|
||||
|
||||
return scu.confirm_dialog(
|
||||
|
@ -1129,7 +1134,7 @@ def formsemestre_associate_new_version(
|
|||
<p>Veillez à ne pas abuser de cette possibilité, car créer trop de versions de formations va vous compliquer la gestion (à vous de garder trace des différences et à ne pas vous tromper par la suite...).
|
||||
</p>
|
||||
<div class="othersemlist"><p>Si vous voulez associer aussi d'autres semestres à la nouvelle version, cochez-les:</p>"""
|
||||
+ "".join(of)
|
||||
+ "".join(H)
|
||||
+ "</div>",
|
||||
OK="Associer ces semestres à une nouvelle version",
|
||||
dest_url="",
|
||||
|
@ -1195,6 +1200,17 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
|
|||
for mod in modimpls:
|
||||
mod["module_id"] = modules_old2new[mod["module_id"]]
|
||||
sco_moduleimpl.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id)
|
||||
# Update poids des évaluations
|
||||
# les poids associent les évaluations aux UE (qui ont changé d'id)
|
||||
for poids in EvaluationUEPoids.query.filter(
|
||||
EvaluationUEPoids.evaluation_id == Evaluation.id,
|
||||
Evaluation.moduleimpl_id == ModuleImpl.id,
|
||||
ModuleImpl.formsemestre_id == formsemestre_id,
|
||||
):
|
||||
poids.ue_id = ues_old2new[poids.ue_id]
|
||||
db.session.add(poids)
|
||||
db.session.commit()
|
||||
|
||||
# update decisions:
|
||||
events = sco_etud.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id})
|
||||
for e in events:
|
||||
|
@ -1229,7 +1245,7 @@ def formsemestre_delete(formsemestre_id):
|
|||
</ol></div>""",
|
||||
]
|
||||
|
||||
evals = sco_evaluations.do_evaluation_list_in_formsemestre(formsemestre_id)
|
||||
evals = sco_evaluation_db.do_evaluation_list_in_formsemestre(formsemestre_id)
|
||||
if evals:
|
||||
H.append(
|
||||
"""<p class="warning">Attention: il y a %d évaluations dans ce semestre (sa suppression entrainera l'effacement définif des notes) !</p>"""
|
||||
|
@ -1311,7 +1327,7 @@ def do_formsemestre_delete(formsemestre_id):
|
|||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
for mod in mods:
|
||||
# evaluations
|
||||
evals = sco_evaluations.do_evaluation_list(
|
||||
evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": mod["moduleimpl_id"]}
|
||||
)
|
||||
for e in evals:
|
||||
|
@ -1620,28 +1636,14 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
|||
# ----- identification externe des sessions (pour SOJA et autres logiciels)
|
||||
def get_formsemestre_session_id(sem, F, parcours):
|
||||
"""Identifiant de session pour ce semestre
|
||||
Exemple: RT-DUT-FI-S1-ANNEE
|
||||
|
||||
DEPT-TYPE-MODALITE+-S?|SPECIALITE
|
||||
|
||||
TYPE=DUT|LP*|M*
|
||||
MODALITE=FC|FI|FA (si plusieurs, en inverse alpha)
|
||||
|
||||
SPECIALITE=[A-Z]+ EON,ASSUR, ... (si pas Sn ou SnD)
|
||||
|
||||
ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013)
|
||||
|
||||
Obsolete: vooir FormSemestre.session_id() #sco7
|
||||
"""
|
||||
# sem = sco_formsemestre.get_formsemestre( formsemestre_id)
|
||||
# F = sco_formations.formation_list( args={ 'formation_id' : sem['formation_id'] } )[0]
|
||||
# parcours = sco_codes_parcours.get_parcours_from_code(F['type_parcours'])
|
||||
|
||||
ImputationDept = sco_preferences.get_preference(
|
||||
imputation_dept = sco_preferences.get_preference(
|
||||
"ImputationDept", sem["formsemestre_id"]
|
||||
)
|
||||
if not ImputationDept:
|
||||
ImputationDept = sco_preferences.get_preference("DeptName")
|
||||
ImputationDept = ImputationDept.upper()
|
||||
if not imputation_dept:
|
||||
imputation_dept = sco_preferences.get_preference("DeptName")
|
||||
imputation_dept = imputation_dept.upper()
|
||||
parcours_type = parcours.NAME
|
||||
modalite = sem["modalite"]
|
||||
modalite = (
|
||||
|
@ -1655,5 +1657,5 @@ def get_formsemestre_session_id(sem, F, parcours):
|
|||
annee_sco = str(scu.annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"]))
|
||||
|
||||
return scu.sanitize_string(
|
||||
"-".join((ImputationDept, parcours_type, modalite, semestre_id, annee_sco))
|
||||
"-".join((imputation_dept, parcours_type, modalite, semestre_id, annee_sco))
|
||||
)
|
||||
|
|
|
@ -35,7 +35,7 @@ from flask import url_for, g, request
|
|||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_codes_parcours import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -66,7 +66,9 @@ def do_formsemestre_inscription_list(*args, **kw):
|
|||
|
||||
|
||||
def do_formsemestre_inscription_listinscrits(formsemestre_id):
|
||||
"""Liste les inscrits (état I) à ce semestre et cache le résultat"""
|
||||
"""Liste les inscrits (état I) à ce semestre et cache le résultat.
|
||||
Result: [ { "etudid":, "formsemestre_id": , "etat": , "etape": }]
|
||||
"""
|
||||
r = sco_cache.SemInscriptionsCache.get(formsemestre_id)
|
||||
if r is None:
|
||||
# retreive list
|
||||
|
@ -127,6 +129,42 @@ def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
|
|||
) # > desinscription du semestre
|
||||
|
||||
|
||||
def do_formsemestre_demission(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
event_date=None,
|
||||
etat_new="D", # 'D' or DEF
|
||||
operation_method="demEtudiant",
|
||||
event_type="DEMISSION",
|
||||
):
|
||||
"Démission ou défaillance d'un étudiant"
|
||||
# marque 'D' ou DEF dans l'inscription au semestre et ajoute
|
||||
# un "evenement" scolarite
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check lock
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if not sem["etat"]:
|
||||
raise ScoValueError("Modification impossible: semestre verrouille")
|
||||
#
|
||||
ins = do_formsemestre_inscription_list(
|
||||
{"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
)[0]
|
||||
if not ins:
|
||||
raise ScoException("etudiant non inscrit ?!")
|
||||
ins["etat"] = etat_new
|
||||
do_formsemestre_inscription_edit(args=ins, formsemestre_id=formsemestre_id)
|
||||
logdb(cnx, method=operation_method, etudid=etudid)
|
||||
sco_etud.scolar_events_create(
|
||||
cnx,
|
||||
args={
|
||||
"etudid": etudid,
|
||||
"event_date": event_date,
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"event_type": event_type,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def do_formsemestre_inscription_edit(args=None, formsemestre_id=None):
|
||||
"edit a formsemestre_inscription"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
|
|
@ -35,11 +35,12 @@ from flask import url_for
|
|||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app.models import Module
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError
|
||||
import sco_version
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_abs
|
||||
|
@ -50,6 +51,7 @@ from app.scodoc import sco_compute_moy
|
|||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_edit
|
||||
|
@ -61,6 +63,7 @@ from app.scodoc import sco_preferences
|
|||
from app.scodoc import sco_users
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
|
||||
import sco_version
|
||||
|
||||
|
||||
def _build_menu_stats(formsemestre_id):
|
||||
|
@ -459,7 +462,9 @@ def retreive_formsemestre_from_request() -> int:
|
|||
modimpl = modimpl[0]
|
||||
formsemestre_id = modimpl["formsemestre_id"]
|
||||
elif "evaluation_id" in args:
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": args["evaluation_id"]})
|
||||
E = sco_evaluation_db.do_evaluation_list(
|
||||
{"evaluation_id": args["evaluation_id"]}
|
||||
)
|
||||
if not E:
|
||||
return None # evaluation suppressed ?
|
||||
E = E[0]
|
||||
|
@ -593,7 +598,9 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
formsemestre_id=formsemestre_id, sort_by_ue=True
|
||||
)
|
||||
|
||||
R = []
|
||||
sum_coef = 0
|
||||
|
@ -837,7 +844,6 @@ def _make_listes_sem(sem, with_absences=True):
|
|||
url_for("scolar.groups_view",
|
||||
curtab="tab-photos",
|
||||
group_ids=group["group_id"],
|
||||
etat="I",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
}">Photos</a>
|
||||
|
@ -980,13 +986,22 @@ def formsemestre_status(formsemestre_id=None):
|
|||
# porté du DTML
|
||||
cnx = ndb.GetDBConnexion()
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
# inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
# args={"formsemestre_id": formsemestre_id}
|
||||
# )
|
||||
prev_ue_id = None
|
||||
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
formsemestre_id=formsemestre_id
|
||||
)
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
# Construit la liste de tous les enseignants de ce semestre:
|
||||
mails_enseignants = set(
|
||||
[sco_users.user_info(ens_id)["email"] for ens_id in sem["responsables"]]
|
||||
)
|
||||
for modimpl in modimpls:
|
||||
mails_enseignants.add(sco_users.user_info(modimpl["responsable_id"])["email"])
|
||||
mails_enseignants |= set(
|
||||
[sco_users.user_info(m["ens_id"])["email"] for m in modimpl["ens"]]
|
||||
)
|
||||
|
||||
can_edit = sco_formsemestre_edit.can_edit_sem(formsemestre_id, sem=sem)
|
||||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Semestre %s" % sem["titreannee"]),
|
||||
|
@ -996,158 +1011,69 @@ def formsemestre_status(formsemestre_id=None):
|
|||
),
|
||||
"""<p><b style="font-size: 130%">Tableau de bord: </b><span class="help">cliquez sur un module pour saisir des notes</span></p>""",
|
||||
]
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
|
||||
if nt.expr_diagnostics:
|
||||
H.append(html_expr_diagnostic(nt.expr_diagnostics))
|
||||
H.append(
|
||||
"""
|
||||
<p>
|
||||
<table class="formsemestre_status">
|
||||
<tr>
|
||||
<th class="formsemestre_status">Code</th>
|
||||
<th class="formsemestre_status">Module</th>
|
||||
<th class="formsemestre_status">Inscrits</th>
|
||||
<th class="resp">Responsable</th>
|
||||
<th class="evals">Evaluations</th></tr>"""
|
||||
)
|
||||
mails_enseignants = set(
|
||||
[sco_users.user_info(ens_id)["email"] for ens_id in sem["responsables"]]
|
||||
) # adr. mail des enseignants
|
||||
for M in Mlist:
|
||||
Mod = M["module"]
|
||||
ModDescr = (
|
||||
"Module "
|
||||
+ M["module"]["titre"]
|
||||
+ ", coef. "
|
||||
+ str(M["module"]["coefficient"])
|
||||
)
|
||||
ModEns = sco_users.user_info(M["responsable_id"])["nomcomplet"]
|
||||
if M["ens"]:
|
||||
ModEns += " (resp.), " + ", ".join(
|
||||
[sco_users.user_info(e["ens_id"])["nomcomplet"] for e in M["ens"]]
|
||||
)
|
||||
ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=M["moduleimpl_id"]
|
||||
)
|
||||
mails_enseignants.add(sco_users.user_info(M["responsable_id"])["email"])
|
||||
mails_enseignants |= set(
|
||||
[sco_users.user_info(m["ens_id"])["email"] for m in M["ens"]]
|
||||
)
|
||||
ue = M["ue"]
|
||||
if prev_ue_id != ue["ue_id"]:
|
||||
prev_ue_id = ue["ue_id"]
|
||||
acronyme = ue["acronyme"]
|
||||
titre = ue["titre"]
|
||||
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
|
||||
titre += " <b>(coef. %s)</b>" % (ue["coefficient"] or 0.0)
|
||||
H.append(
|
||||
"""<tr class="formsemestre_status_ue"><td colspan="4">
|
||||
<span class="status_ue_acro">%s</span>
|
||||
<span class="status_ue_title">%s</span>
|
||||
</td><td>"""
|
||||
% (acronyme, titre)
|
||||
)
|
||||
|
||||
expr = sco_compute_moy.get_ue_expression(
|
||||
formsemestre_id, ue["ue_id"], cnx, html_quote=True
|
||||
)
|
||||
|
||||
if can_edit:
|
||||
H.append(
|
||||
' <a href="edit_ue_expr?formsemestre_id=%s&ue_id=%s">'
|
||||
% (formsemestre_id, ue["ue_id"])
|
||||
)
|
||||
H.append(
|
||||
scu.icontag(
|
||||
"formula",
|
||||
title="Mode calcul moyenne d'UE",
|
||||
style="vertical-align:middle",
|
||||
)
|
||||
)
|
||||
if can_edit:
|
||||
H.append("</a>")
|
||||
if expr:
|
||||
H.append(
|
||||
""" <span class="formula" title="mode de calcul de la moyenne d'UE">%s</span>"""
|
||||
% expr
|
||||
)
|
||||
|
||||
H.append("</td></tr>")
|
||||
|
||||
if M["ue"]["type"] != sco_codes_parcours.UE_STANDARD:
|
||||
fontorange = " fontorange" # style css additionnel
|
||||
else:
|
||||
fontorange = ""
|
||||
|
||||
etat = sco_evaluations.do_evaluation_etat_in_mod(nt, M["moduleimpl_id"])
|
||||
if (
|
||||
etat["nb_evals_completes"] > 0
|
||||
and etat["nb_evals_en_cours"] == 0
|
||||
and etat["nb_evals_vides"] == 0
|
||||
):
|
||||
H.append('<tr class="formsemestre_status_green%s">' % fontorange)
|
||||
else:
|
||||
H.append('<tr class="formsemestre_status%s">' % fontorange)
|
||||
|
||||
H.append(
|
||||
'<td class="formsemestre_status_code"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="stdlink">%s</a></td>'
|
||||
% (M["moduleimpl_id"], ModDescr, Mod["code"])
|
||||
)
|
||||
H.append(
|
||||
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
|
||||
% (M["moduleimpl_id"], ModDescr, Mod["abbrev"] or Mod["titre"])
|
||||
)
|
||||
H.append('<td class="formsemestre_status_inscrits">%s</td>' % len(ModInscrits))
|
||||
H.append(
|
||||
'<td class="resp scotext"><a class="discretelink" href="moduleimpl_status?moduleimpl_id=%s" title="%s">%s</a></td>'
|
||||
% (
|
||||
M["moduleimpl_id"],
|
||||
ModEns,
|
||||
sco_users.user_info(M["responsable_id"])["prenomnom"],
|
||||
)
|
||||
)
|
||||
|
||||
if Mod["module_type"] == scu.MODULE_STANDARD:
|
||||
H.append('<td class="evals">')
|
||||
nb_evals = (
|
||||
etat["nb_evals_completes"]
|
||||
+ etat["nb_evals_en_cours"]
|
||||
+ etat["nb_evals_vides"]
|
||||
)
|
||||
if nb_evals != 0:
|
||||
H.append(
|
||||
'<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">%s prévues, %s ok</a>'
|
||||
% (M["moduleimpl_id"], nb_evals, etat["nb_evals_completes"])
|
||||
)
|
||||
if etat["nb_evals_en_cours"] > 0:
|
||||
H.append(
|
||||
', <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il manque des notes">%s en cours</a></span>'
|
||||
% (M["moduleimpl_id"], etat["nb_evals_en_cours"])
|
||||
)
|
||||
if etat["attente"]:
|
||||
H.append(
|
||||
' <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il y a des notes en attente">[en attente]</a></span>'
|
||||
% M["moduleimpl_id"]
|
||||
)
|
||||
elif Mod["module_type"] == scu.MODULE_MALUS:
|
||||
nb_malus_notes = sum(
|
||||
[
|
||||
e["etat"]["nb_notes"]
|
||||
for e in nt.get_mod_evaluation_etat_list(M["moduleimpl_id"])
|
||||
]
|
||||
)
|
||||
H.append(
|
||||
"""<td class="malus">
|
||||
<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">malus (%d notes)</a>
|
||||
if nt.parcours.APC_SAE:
|
||||
# BUT: tableau ressources puis SAE
|
||||
ressources = [
|
||||
m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE
|
||||
]
|
||||
saes = [m for m in modimpls if m["module"]["module_type"] == ModuleType.SAE]
|
||||
autres = [
|
||||
m
|
||||
for m in modimpls
|
||||
if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
]
|
||||
H += [
|
||||
"""
|
||||
% (M["moduleimpl_id"], nb_malus_notes)
|
||||
)
|
||||
else:
|
||||
raise ValueError("Invalid module_type") # a bug
|
||||
<div class="tableau_modules">
|
||||
""",
|
||||
_TABLEAU_MODULES_HEAD,
|
||||
f"""<tr class="formsemestre_status_cat">
|
||||
<td colspan="5">
|
||||
<span class="status_module_cat">Ressources</span>
|
||||
</td></tr>""",
|
||||
formsemestre_tableau_modules(
|
||||
ressources, nt, formsemestre_id, can_edit=can_edit, show_ues=False
|
||||
),
|
||||
f"""<tr class="formsemestre_status_cat">
|
||||
<td colspan="5">
|
||||
<span class="status_module_cat">SAÉs</span>
|
||||
</td></tr>""",
|
||||
formsemestre_tableau_modules(
|
||||
saes, nt, formsemestre_id, can_edit=can_edit, show_ues=False
|
||||
),
|
||||
]
|
||||
if autres:
|
||||
H += [
|
||||
f"""<tr class="formsemestre_status_cat">
|
||||
<td colspan="5">
|
||||
<span class="status_module_cat">Autres modules</span>
|
||||
</td></tr>""",
|
||||
formsemestre_tableau_modules(
|
||||
autres, nt, formsemestre_id, can_edit=can_edit, show_ues=False
|
||||
),
|
||||
]
|
||||
H += [_TABLEAU_MODULES_FOOT, "</div>"]
|
||||
else:
|
||||
# formations classiques: groupe par UE
|
||||
H += [
|
||||
"<p>",
|
||||
_TABLEAU_MODULES_HEAD,
|
||||
formsemestre_tableau_modules(
|
||||
modimpls,
|
||||
nt,
|
||||
formsemestre_id,
|
||||
can_edit=can_edit,
|
||||
use_ue_coefs=use_ue_coefs,
|
||||
),
|
||||
_TABLEAU_MODULES_FOOT,
|
||||
"</p>",
|
||||
]
|
||||
|
||||
H.append("</td></tr>")
|
||||
H.append("</table></p>")
|
||||
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
|
||||
if use_ue_coefs:
|
||||
H.append(
|
||||
"""
|
||||
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
|
||||
|
@ -1167,3 +1093,164 @@ def formsemestre_status(formsemestre_id=None):
|
|||
% (",".join(adrlist), len(adrlist))
|
||||
)
|
||||
return "".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
_TABLEAU_MODULES_HEAD = """
|
||||
<table class="formsemestre_status">
|
||||
<tr>
|
||||
<th class="formsemestre_status">Code</th>
|
||||
<th class="formsemestre_status">Module</th>
|
||||
<th class="formsemestre_status">Inscrits</th>
|
||||
<th class="resp">Responsable</th>
|
||||
<th class="evals">Évaluations</th>
|
||||
</tr>
|
||||
"""
|
||||
_TABLEAU_MODULES_FOOT = """</table>"""
|
||||
|
||||
|
||||
def formsemestre_tableau_modules(
|
||||
modimpls, nt, formsemestre_id, can_edit=True, show_ues=True, use_ue_coefs=False
|
||||
) -> str:
|
||||
"Lignes table HTML avec modules du semestre"
|
||||
H = []
|
||||
prev_ue_id = None
|
||||
for modimpl in modimpls:
|
||||
mod = Module.query.get(modimpl["module_id"])
|
||||
mod_descr = "Module " + (mod.titre or "")
|
||||
if mod.is_apc():
|
||||
coef_descr = ", ".join(
|
||||
[f"{ue_acro}: {co}" for ue_acro, co in mod.ue_coefs_descr()]
|
||||
)
|
||||
if coef_descr:
|
||||
mod_descr += " Coefs: " + coef_descr
|
||||
else:
|
||||
mod_descr += " (pas de coefficients) "
|
||||
else:
|
||||
mod_descr += ", coef. " + str(mod.coefficient)
|
||||
mod_ens = sco_users.user_info(modimpl["responsable_id"])["nomcomplet"]
|
||||
if modimpl["ens"]:
|
||||
mod_ens += " (resp.), " + ", ".join(
|
||||
[sco_users.user_info(e["ens_id"])["nomcomplet"] for e in modimpl["ens"]]
|
||||
)
|
||||
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||
)
|
||||
|
||||
ue = modimpl["ue"]
|
||||
if show_ues and (prev_ue_id != ue["ue_id"]):
|
||||
prev_ue_id = ue["ue_id"]
|
||||
titre = ue["titre"]
|
||||
if use_ue_coefs:
|
||||
titre += " <b>(coef. %s)</b>" % (ue["coefficient"] or 0.0)
|
||||
H.append(
|
||||
f"""<tr class="formsemestre_status_ue"><td colspan="4">
|
||||
<span class="status_ue_acro">{ue["acronyme"]}</span>
|
||||
<span class="status_ue_title">{titre}</span>
|
||||
</td><td>"""
|
||||
)
|
||||
if can_edit:
|
||||
H.append(
|
||||
' <a href="edit_ue_expr?formsemestre_id=%s&ue_id=%s">'
|
||||
% (formsemestre_id, ue["ue_id"])
|
||||
)
|
||||
H.append(
|
||||
scu.icontag(
|
||||
"formula",
|
||||
title="Mode calcul moyenne d'UE",
|
||||
style="vertical-align:middle",
|
||||
)
|
||||
)
|
||||
if can_edit:
|
||||
H.append("</a>")
|
||||
|
||||
expr = sco_compute_moy.get_ue_expression(
|
||||
formsemestre_id, ue["ue_id"], html_quote=True
|
||||
)
|
||||
if expr:
|
||||
H.append(
|
||||
""" <span class="formula" title="mode de calcul de la moyenne d'UE">%s</span>"""
|
||||
% expr
|
||||
)
|
||||
|
||||
H.append("</td></tr>")
|
||||
|
||||
if modimpl["ue"]["type"] != sco_codes_parcours.UE_STANDARD:
|
||||
fontorange = " fontorange" # style css additionnel
|
||||
else:
|
||||
fontorange = ""
|
||||
|
||||
etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl["moduleimpl_id"])
|
||||
# if nt.parcours.APC_SAE:
|
||||
# tbd style si module non conforme
|
||||
if (
|
||||
etat["nb_evals_completes"] > 0
|
||||
and etat["nb_evals_en_cours"] == 0
|
||||
and etat["nb_evals_vides"] == 0
|
||||
):
|
||||
H.append('<tr class="formsemestre_status_green%s">' % fontorange)
|
||||
else:
|
||||
H.append('<tr class="formsemestre_status%s">' % fontorange)
|
||||
|
||||
H.append(
|
||||
'<td class="formsemestre_status_code"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="stdlink">%s</a></td>'
|
||||
% (modimpl["moduleimpl_id"], mod_descr, mod.code)
|
||||
)
|
||||
H.append(
|
||||
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
|
||||
% (modimpl["moduleimpl_id"], mod_descr, mod.abbrev or mod.titre)
|
||||
)
|
||||
H.append('<td class="formsemestre_status_inscrits">%s</td>' % len(mod_inscrits))
|
||||
H.append(
|
||||
'<td class="resp scotext"><a class="discretelink" href="moduleimpl_status?moduleimpl_id=%s" title="%s">%s</a></td>'
|
||||
% (
|
||||
modimpl["moduleimpl_id"],
|
||||
mod_ens,
|
||||
sco_users.user_info(modimpl["responsable_id"])["prenomnom"],
|
||||
)
|
||||
)
|
||||
|
||||
if mod.module_type in (
|
||||
None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs
|
||||
ModuleType.STANDARD,
|
||||
ModuleType.RESSOURCE,
|
||||
ModuleType.SAE,
|
||||
):
|
||||
H.append('<td class="evals">')
|
||||
nb_evals = (
|
||||
etat["nb_evals_completes"]
|
||||
+ etat["nb_evals_en_cours"]
|
||||
+ etat["nb_evals_vides"]
|
||||
)
|
||||
if nb_evals != 0:
|
||||
H.append(
|
||||
'<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">%s prévues, %s ok</a>'
|
||||
% (modimpl["moduleimpl_id"], nb_evals, etat["nb_evals_completes"])
|
||||
)
|
||||
if etat["nb_evals_en_cours"] > 0:
|
||||
H.append(
|
||||
', <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il manque des notes">%s en cours</a></span>'
|
||||
% (modimpl["moduleimpl_id"], etat["nb_evals_en_cours"])
|
||||
)
|
||||
if etat["attente"]:
|
||||
H.append(
|
||||
' <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il y a des notes en attente">[en attente]</a></span>'
|
||||
% modimpl["moduleimpl_id"]
|
||||
)
|
||||
elif mod.module_type == ModuleType.MALUS:
|
||||
nb_malus_notes = sum(
|
||||
[
|
||||
e["etat"]["nb_notes"]
|
||||
for e in nt.get_mod_evaluation_etat_list(modimpl["moduleimpl_id"])
|
||||
]
|
||||
)
|
||||
H.append(
|
||||
"""<td class="malus">
|
||||
<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">malus (%d notes)</a>
|
||||
"""
|
||||
% (modimpl["moduleimpl_id"], nb_malus_notes)
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid module_type {mod.module_type}") # a bug
|
||||
|
||||
H.append("</td></tr>")
|
||||
return "\n".join(H)
|
||||
|
|
|
@ -956,7 +956,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
|||
def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
|
||||
"""Suppression des decisions de jury pour un etudiant."""
|
||||
log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid))
|
||||
cnx = ndb.GetDBConnexion(autocommit=False)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
try:
|
||||
|
@ -1123,7 +1123,7 @@ def do_formsemestre_validate_previous_ue(
|
|||
cette UE (utile seulement pour les semestres extérieurs).
|
||||
"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
cnx = ndb.GetDBConnexion(autocommit=False)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etud_ue_status
|
||||
if ue_coefficient != None:
|
||||
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
|
||||
|
|
|
@ -35,7 +35,6 @@ Optimisation possible:
|
|||
"""
|
||||
import collections
|
||||
import operator
|
||||
import re
|
||||
import time
|
||||
|
||||
from xml.etree import ElementTree
|
||||
|
@ -45,6 +44,8 @@ import flask
|
|||
from flask import g, request
|
||||
from flask import url_for, make_response
|
||||
|
||||
from app import db
|
||||
from app.models.groups import Partition
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log, cache
|
||||
|
@ -377,11 +378,18 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
|
|||
{ etudid : { partition_id : group_name }} (attr=group_name or group_id)
|
||||
"""
|
||||
infos = ndb.SimpleDictFetch(
|
||||
"""SELECT i.id AS etudid, p.id AS partition_id,
|
||||
gd.group_name, gd.id AS group_id
|
||||
FROM notes_formsemestre_inscription i, partition p,
|
||||
group_descr gd, group_membership gm
|
||||
WHERE i.formsemestre_id=%(formsemestre_id)s
|
||||
"""SELECT
|
||||
i.etudid AS etudid,
|
||||
p.id AS partition_id,
|
||||
gd.group_name,
|
||||
gd.id AS group_id
|
||||
FROM
|
||||
notes_formsemestre_inscription i,
|
||||
partition p,
|
||||
group_descr gd,
|
||||
group_membership gm
|
||||
WHERE
|
||||
i.formsemestre_id=%(formsemestre_id)s
|
||||
and i.formsemestre_id = p.formsemestre_id
|
||||
and p.id = gd.partition_id
|
||||
and gm.etudid = i.etudid
|
||||
|
@ -1052,6 +1060,16 @@ def partition_move(partition_id, after=0, redirect=1):
|
|||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
others = get_partitions_list(formsemestre_id)
|
||||
|
||||
objs = (
|
||||
Partition.query.filter_by(formsemestre_id=formsemestre_id)
|
||||
.order_by(Partition.numero, Partition.partition_name)
|
||||
.all()
|
||||
)
|
||||
if len({o.numero for o in objs}) != len(objs):
|
||||
# il y a des numeros identiques !
|
||||
scu.objects_renumber(db, objs)
|
||||
|
||||
if len(others) > 1:
|
||||
pidx = [p["partition_id"] for p in others].index(partition_id)
|
||||
# log("partition_move: after=%s pidx=%s" % (after, pidx))
|
||||
|
@ -1417,6 +1435,8 @@ def do_evaluation_listeetuds_groups(
|
|||
evaluation.
|
||||
Si include_dems, compte aussi les etudiants démissionnaires
|
||||
(sinon, par défaut, seulement les 'I')
|
||||
|
||||
Résultat: [ (etudid, etat) ], où etat='I', 'D', 'DEF'
|
||||
"""
|
||||
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et include_dems faux
|
||||
fromtables = [
|
||||
|
@ -1441,7 +1461,7 @@ def do_evaluation_listeetuds_groups(
|
|||
|
||||
# requete complete
|
||||
req = (
|
||||
"SELECT distinct Im.etudid FROM "
|
||||
"SELECT distinct Im.etudid, Isem.etat FROM "
|
||||
+ ", ".join(fromtables)
|
||||
+ """ WHERE Isem.etudid = Im.etudid
|
||||
and Im.moduleimpl_id = M.id
|
||||
|
@ -1456,7 +1476,7 @@ def do_evaluation_listeetuds_groups(
|
|||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor()
|
||||
cursor.execute(req, {"evaluation_id": evaluation_id})
|
||||
return [x[0] for x in cursor]
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
def do_evaluation_listegroupes(evaluation_id, include_default=False):
|
||||
|
|
|
@ -47,7 +47,7 @@ from app.scodoc.sco_formsemestre_inscriptions import (
|
|||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
FormatError,
|
||||
ScoFormatError,
|
||||
ScoException,
|
||||
ScoValueError,
|
||||
ScoInvalidDateError,
|
||||
|
@ -262,7 +262,7 @@ def scolars_import_excel_file(
|
|||
et les inscrit dans le semestre indiqué (et à TOUS ses modules)
|
||||
"""
|
||||
log("scolars_import_excel_file: formsemestre_id=%s" % formsemestre_id)
|
||||
cnx = ndb.GetDBConnexion(autocommit=False)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
annee_courante = time.localtime()[0]
|
||||
always_require_ine = sco_preferences.get_preference("always_require_ine")
|
||||
|
@ -640,10 +640,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
|||
if (idx_nom is None) or (idx_prenom is None):
|
||||
log("fields indices=" + ", ".join([str(x) for x in fields]))
|
||||
log("fields titles =" + ", ".join([fields[x][0] for x in fields]))
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"scolars_import_admission: colonnes nom et prenom requises",
|
||||
dest_url=url_for(
|
||||
"notes.form_students_import_infos_admissions",
|
||||
"scolar.form_students_import_infos_admissions",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
|
@ -672,11 +672,11 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
|||
try:
|
||||
val = convertor(line[idx])
|
||||
except ValueError:
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
|
||||
% (nline, field_name, line[idx]),
|
||||
dest_url=url_for(
|
||||
"notes.form_students_import_infos_admissions",
|
||||
"scolar.form_students_import_infos_admissions",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
|
@ -758,11 +758,11 @@ def adm_get_fields(titles, formsemestre_id):
|
|||
convertor = adm_convert_text
|
||||
# doublons ?
|
||||
if k in [x[0] for x in fields.values()]:
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
'scolars_import_admission: titre "%s" en double (ligne 1)'
|
||||
% (title),
|
||||
dest_url=url_for(
|
||||
"notes.form_students_import_infos_admissions_apb",
|
||||
"scolar.form_students_import_infos_admissions_apb",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
|
|
|
@ -288,10 +288,11 @@ def formsemestre_inscr_passage(
|
|||
footer = html_sco_header.sco_footer()
|
||||
H = [header]
|
||||
if isinstance(etuds, str):
|
||||
etuds = etuds.split(",") # vient du form de confirmation
|
||||
# list de strings, vient du form de confirmation
|
||||
etuds = [int(x) for x in etuds.split(",") if x]
|
||||
elif isinstance(etuds, int):
|
||||
etuds = [etuds]
|
||||
etuds = [int(x) for x in etuds]
|
||||
|
||||
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem)
|
||||
etuds_set = set(etuds)
|
||||
candidats_set = set(candidats)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
|
@ -27,25 +27,23 @@
|
|||
|
||||
"""Liste des notes d'une évaluation
|
||||
"""
|
||||
from operator import itemgetter
|
||||
import urllib
|
||||
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
|
||||
from app import models
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.comp import moy_mod
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
|
@ -56,35 +54,34 @@ from app.scodoc.gen_tables import GenTable
|
|||
from app.scodoc.htmlutils import histogram_notes
|
||||
|
||||
|
||||
def do_evaluation_listenotes():
|
||||
def do_evaluation_listenotes(
|
||||
evaluation_id=None, moduleimpl_id=None, format="html"
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Affichage des notes d'une évaluation
|
||||
|
||||
args: evaluation_id ou moduleimpl_id
|
||||
(si moduleimpl_id, affiche toutes les évaluations du module)
|
||||
Affichage des notes d'une évaluation (si evaluation_id)
|
||||
ou de toutes les évaluations d'un module (si moduleimpl_id)
|
||||
"""
|
||||
mode = None
|
||||
vals = scu.get_request_args()
|
||||
if "evaluation_id" in vals:
|
||||
evaluation_id = int(vals["evaluation_id"])
|
||||
mode = "eval"
|
||||
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if "moduleimpl_id" in vals and vals["moduleimpl_id"]:
|
||||
moduleimpl_id = int(vals["moduleimpl_id"])
|
||||
if moduleimpl_id:
|
||||
mode = "module"
|
||||
evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
||||
if not mode:
|
||||
evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
||||
elif evaluation_id:
|
||||
mode = "eval"
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
else:
|
||||
raise ValueError("missing argument: evaluation or module")
|
||||
if not evals:
|
||||
return "<p>Aucune évaluation !</p>"
|
||||
return "<p>Aucune évaluation !</p>", f"ScoDoc"
|
||||
|
||||
format = vals.get("format", "html")
|
||||
E = evals[0] # il y a au moins une evaluation
|
||||
modimpl = ModuleImpl.query.get(E["moduleimpl_id"])
|
||||
# description de l'evaluation
|
||||
if mode == "eval":
|
||||
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
|
||||
page_title = f"Notes {E['description'] or modimpl.module.code}"
|
||||
else:
|
||||
H = []
|
||||
page_title = f"Notes {modimpl.module.code}"
|
||||
# groupes
|
||||
groups = sco_groups.do_evaluation_listegroupes(
|
||||
E["evaluation_id"], include_default=True
|
||||
|
@ -190,26 +187,33 @@ def do_evaluation_listenotes():
|
|||
is_submitted=True, # toujours "soumis" (démarre avec liste complète)
|
||||
)
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + "\n" + tf[1]
|
||||
return "\n".join(H) + "\n" + tf[1], page_title
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
"%s/Notes/moduleimpl_status?moduleimpl_id=%s"
|
||||
% (scu.ScoURL(), E["moduleimpl_id"])
|
||||
return (
|
||||
flask.redirect(
|
||||
"%s/Notes/moduleimpl_status?moduleimpl_id=%s"
|
||||
% (scu.ScoURL(), E["moduleimpl_id"])
|
||||
),
|
||||
"",
|
||||
)
|
||||
else:
|
||||
anonymous_listing = tf[2]["anonymous_listing"]
|
||||
note_sur_20 = tf[2]["note_sur_20"]
|
||||
hide_groups = tf[2]["hide_groups"]
|
||||
with_emails = tf[2]["with_emails"]
|
||||
return _make_table_notes(
|
||||
tf[1],
|
||||
evals,
|
||||
format=format,
|
||||
note_sur_20=note_sur_20,
|
||||
anonymous_listing=anonymous_listing,
|
||||
group_ids=tf[2]["group_ids"],
|
||||
hide_groups=hide_groups,
|
||||
with_emails=with_emails,
|
||||
return (
|
||||
_make_table_notes(
|
||||
tf[1],
|
||||
evals,
|
||||
format=format,
|
||||
note_sur_20=note_sur_20,
|
||||
anonymous_listing=anonymous_listing,
|
||||
group_ids=tf[2]["group_ids"],
|
||||
hide_groups=hide_groups,
|
||||
with_emails=with_emails,
|
||||
mode=mode,
|
||||
),
|
||||
page_title,
|
||||
)
|
||||
|
||||
|
||||
|
@ -222,15 +226,27 @@ def _make_table_notes(
|
|||
hide_groups=False,
|
||||
with_emails=False,
|
||||
group_ids=[],
|
||||
mode="module", # "eval" or "module"
|
||||
):
|
||||
"""Generate table for evaluations marks"""
|
||||
"""Table liste notes (une seule évaluation ou toutes celles d'un module)"""
|
||||
# Code à ré-écrire !
|
||||
if not evals:
|
||||
return "<p>Aucune évaluation !</p>"
|
||||
E = evals[0]
|
||||
moduleimpl_id = E["moduleimpl_id"]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
||||
modimpl_o = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
module = models.Module.query.get(modimpl_o["module_id"])
|
||||
is_apc = module.formation.get_parcours().APC_SAE
|
||||
if is_apc:
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
is_conforme = modimpl.check_apc_conformity()
|
||||
evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id)
|
||||
if not ues:
|
||||
is_apc = False
|
||||
else:
|
||||
evals_poids, ues = None, None
|
||||
is_conforme = True
|
||||
sem = sco_formsemestre.get_formsemestre(modimpl_o["formsemestre_id"])
|
||||
# (debug) check that all evals are in same module:
|
||||
for e in evals:
|
||||
if e["moduleimpl_id"] != moduleimpl_id:
|
||||
|
@ -242,16 +258,12 @@ def _make_table_notes(
|
|||
keep_numeric = False
|
||||
# Si pas de groupe, affiche tout
|
||||
if not group_ids:
|
||||
group_ids = [sco_groups.get_default_group(M["formsemestre_id"])]
|
||||
group_ids = [sco_groups.get_default_group(modimpl_o["formsemestre_id"])]
|
||||
groups = sco_groups.listgroups(group_ids)
|
||||
|
||||
gr_title = sco_groups.listgroups_abbrev(groups)
|
||||
gr_title_filename = sco_groups.listgroups_filename(groups)
|
||||
|
||||
etudids = sco_groups.do_evaluation_listeetuds_groups(
|
||||
E["evaluation_id"], groups, include_dems=True
|
||||
)
|
||||
|
||||
if anonymous_listing:
|
||||
columns_ids = ["code"] # cols in table
|
||||
else:
|
||||
|
@ -271,11 +283,11 @@ def _make_table_notes(
|
|||
"expl_key": "Rem.",
|
||||
"email": "e-mail",
|
||||
"emailperso": "e-mail perso",
|
||||
"signatures": "Signatures",
|
||||
}
|
||||
|
||||
rows = []
|
||||
|
||||
class keymgr(dict): # comment : key (pour regrouper les comments a la fin)
|
||||
class KeyManager(dict): # comment : key (pour regrouper les comments a la fin)
|
||||
def __init__(self):
|
||||
self.lastkey = 1
|
||||
|
||||
|
@ -285,31 +297,35 @@ def _make_table_notes(
|
|||
# self.lastkey = chr(ord(self.lastkey)+1)
|
||||
return str(r)
|
||||
|
||||
K = keymgr()
|
||||
for etudid in etudids:
|
||||
key_mgr = KeyManager()
|
||||
|
||||
# code pour listings anonyme, à la place du nom
|
||||
if sco_preferences.get_preference("anonymous_lst_code") == "INE":
|
||||
anonymous_lst_key = "code_ine"
|
||||
elif sco_preferences.get_preference("anonymous_lst_code") == "NIP":
|
||||
anonymous_lst_key = "code_nip"
|
||||
else:
|
||||
anonymous_lst_key = "etudid"
|
||||
|
||||
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
|
||||
E["evaluation_id"], groups, include_dems=True
|
||||
)
|
||||
for etudid, etat in etudid_etats:
|
||||
css_row_class = None
|
||||
# infos identite etudiant
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
# infos inscription
|
||||
inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
{"etudid": etudid, "formsemestre_id": M["formsemestre_id"]}
|
||||
)[0]
|
||||
|
||||
if inscr["etat"] == "I": # si inscrit, indique groupe
|
||||
if etat == "I": # si inscrit, indique groupe
|
||||
groups = sco_groups.get_etud_groups(etudid, sem)
|
||||
grc = sco_groups.listgroups_abbrev(groups)
|
||||
else:
|
||||
if inscr["etat"] == "D":
|
||||
if etat == "D":
|
||||
grc = "DEM" # attention: ce code est re-ecrit plus bas, ne pas le changer (?)
|
||||
css_row_class = "etuddem"
|
||||
else:
|
||||
grc = inscr["etat"]
|
||||
grc = etat
|
||||
|
||||
code = "" # code pour listings anonyme, à la place du nom
|
||||
if sco_preferences.get_preference("anonymous_lst_code") == "INE":
|
||||
code = etud["code_ine"]
|
||||
elif sco_preferences.get_preference("anonymous_lst_code") == "NIP":
|
||||
code = etud["code_nip"]
|
||||
code = etud.get(anonymous_lst_key)
|
||||
if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid
|
||||
code = etudid
|
||||
|
||||
|
@ -320,7 +336,7 @@ def _make_table_notes(
|
|||
"etudid": etudid,
|
||||
"nom": etud["nom"].upper(),
|
||||
"_nomprenom_target": "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
|
||||
% (M["formsemestre_id"], etudid),
|
||||
% (modimpl_o["formsemestre_id"], etudid),
|
||||
"_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
|
||||
"prenom": etud["prenom"].lower().capitalize(),
|
||||
"nomprenom": etud["nomprenom"],
|
||||
|
@ -332,7 +348,7 @@ def _make_table_notes(
|
|||
)
|
||||
|
||||
# Lignes en tête:
|
||||
coefs = {
|
||||
row_coefs = {
|
||||
"nom": "",
|
||||
"prenom": "",
|
||||
"nomprenom": "",
|
||||
|
@ -341,7 +357,16 @@ def _make_table_notes(
|
|||
"_css_row_class": "sorttop fontitalic",
|
||||
"_table_part": "head",
|
||||
}
|
||||
note_max = {
|
||||
row_poids = {
|
||||
"nom": "",
|
||||
"prenom": "",
|
||||
"nomprenom": "",
|
||||
"group": "",
|
||||
"code": "",
|
||||
"_css_row_class": "sorttop poids",
|
||||
"_table_part": "head",
|
||||
}
|
||||
row_note_max = {
|
||||
"nom": "",
|
||||
"prenom": "",
|
||||
"nomprenom": "",
|
||||
|
@ -350,7 +375,7 @@ def _make_table_notes(
|
|||
"_css_row_class": "sorttop fontitalic",
|
||||
"_table_part": "head",
|
||||
}
|
||||
moys = {
|
||||
row_moys = {
|
||||
"_css_row_class": "moyenne sortbottom",
|
||||
"_table_part": "foot",
|
||||
#'_nomprenom_td_attrs' : 'colspan="2" ',
|
||||
|
@ -362,14 +387,19 @@ def _make_table_notes(
|
|||
e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
|
||||
notes, nb_abs, nb_att = _add_eval_columns(
|
||||
e,
|
||||
evals_poids,
|
||||
ues,
|
||||
rows,
|
||||
titles,
|
||||
coefs,
|
||||
note_max,
|
||||
moys,
|
||||
K,
|
||||
row_coefs,
|
||||
row_poids,
|
||||
row_note_max,
|
||||
row_moys,
|
||||
is_apc,
|
||||
key_mgr,
|
||||
note_sur_20,
|
||||
keep_numeric,
|
||||
format=format,
|
||||
)
|
||||
columns_ids.append(e["evaluation_id"])
|
||||
#
|
||||
|
@ -380,28 +410,52 @@ def _make_table_notes(
|
|||
key=lambda x: (x["nom"] or "", x["prenom"] or "")
|
||||
) # sort by nom, prenom
|
||||
|
||||
# Si module, ajoute moyenne du module:
|
||||
if len(evals) > 1:
|
||||
_add_moymod_column(
|
||||
sem["formsemestre_id"],
|
||||
e,
|
||||
rows,
|
||||
titles,
|
||||
coefs,
|
||||
note_max,
|
||||
moys,
|
||||
note_sur_20,
|
||||
keep_numeric,
|
||||
)
|
||||
columns_ids.append("moymod")
|
||||
# Si module, ajoute la (les) "moyenne(s) du module:
|
||||
if mode == "module":
|
||||
if len(evals) > 1:
|
||||
# Moyenne de l'étudiant dans le module
|
||||
# Affichée même en APC à titre indicatif
|
||||
_add_moymod_column(
|
||||
sem["formsemestre_id"],
|
||||
moduleimpl_id,
|
||||
rows,
|
||||
columns_ids,
|
||||
titles,
|
||||
row_coefs,
|
||||
row_poids,
|
||||
row_note_max,
|
||||
row_moys,
|
||||
is_apc,
|
||||
keep_numeric,
|
||||
)
|
||||
if is_apc:
|
||||
# Ajoute une colonne par UE
|
||||
_add_apc_columns(
|
||||
moduleimpl_id,
|
||||
evals_poids,
|
||||
ues,
|
||||
rows,
|
||||
columns_ids,
|
||||
titles,
|
||||
is_conforme,
|
||||
row_coefs,
|
||||
row_poids,
|
||||
row_note_max,
|
||||
row_moys,
|
||||
keep_numeric,
|
||||
)
|
||||
|
||||
# Ajoute colonnes emails tout à droite:
|
||||
if with_emails:
|
||||
columns_ids += ["email", "emailperso"]
|
||||
# Ajoute lignes en tête et moyennes
|
||||
if len(evals) > 0:
|
||||
rows = [coefs, note_max] + rows
|
||||
rows.append(moys)
|
||||
if len(evals) > 0 and format != "bordereau":
|
||||
rows_head = [row_coefs]
|
||||
if is_apc:
|
||||
rows_head.append(row_poids)
|
||||
rows_head.append(row_note_max)
|
||||
rows = rows_head + rows
|
||||
rows.append(row_moys)
|
||||
# ajout liens HTMl vers affichage une evaluation:
|
||||
if format == "html" and len(evals) > 1:
|
||||
rlinks = {"_table_part": "head"}
|
||||
|
@ -423,6 +477,8 @@ def _make_table_notes(
|
|||
columns_ids.append("expl_key")
|
||||
elif format == "xls" or format == "xml":
|
||||
columns_ids.append("comment")
|
||||
elif format == "bordereau":
|
||||
columns_ids.append("signatures")
|
||||
|
||||
# titres divers:
|
||||
gl = "".join(["&group_ids%3Alist=" + str(g) for g in group_ids])
|
||||
|
@ -435,11 +491,41 @@ def _make_table_notes(
|
|||
if with_emails:
|
||||
gl = "&with_emails%3Alist=yes" + gl
|
||||
if len(evals) == 1:
|
||||
evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(E["jour"]))
|
||||
hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudids))
|
||||
evalname = "%s-%s" % (module.code, ndb.DateDMYtoISO(E["jour"]))
|
||||
hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudid_etats))
|
||||
filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename))
|
||||
|
||||
if format == "bordereau":
|
||||
hh = " %d étudiants" % (len(etudid_etats))
|
||||
hh += " %d absent" % (nb_abs)
|
||||
if nb_abs > 1:
|
||||
hh += "s"
|
||||
hh += ", %d en attente." % (nb_att)
|
||||
|
||||
pdf_title = "<br/> BORDEREAU DE SIGNATURES"
|
||||
pdf_title += "<br/><br/>%(titre)s" % sem
|
||||
pdf_title += "<br/>(%(mois_debut)s - %(mois_fin)s)" % sem
|
||||
pdf_title += " semestre %s %s" % (
|
||||
sem["semestre_id"],
|
||||
sem.get("modalite", ""),
|
||||
)
|
||||
pdf_title += f"<br/>Notes du module {module.code} - {module.titre}"
|
||||
pdf_title += "<br/>Evaluation : %(description)s " % e
|
||||
if len(e["jour"]) > 0:
|
||||
pdf_title += " (%(jour)s)" % e
|
||||
pdf_title += "(noté sur %(note_max)s )<br/><br/>" % e
|
||||
else:
|
||||
hh = " %s, %s (%d étudiants)" % (
|
||||
E["description"],
|
||||
gr_title,
|
||||
len(etudid_etats),
|
||||
)
|
||||
if len(e["jour"]) > 0:
|
||||
pdf_title = "%(description)s (%(jour)s)" % e
|
||||
else:
|
||||
pdf_title = "%(description)s " % e
|
||||
|
||||
caption = hh
|
||||
pdf_title = "%(description)s (%(jour)s)" % e
|
||||
html_title = ""
|
||||
base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl
|
||||
html_next_section = (
|
||||
|
@ -447,20 +533,25 @@ def _make_table_notes(
|
|||
% (nb_abs, nb_att)
|
||||
)
|
||||
else:
|
||||
filename = scu.make_filename("notes_%s_%s" % (Mod["code"], gr_title_filename))
|
||||
title = "Notes du module %(code)s %(titre)s" % Mod
|
||||
filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename))
|
||||
title = f"Notes {module.type_name()} {module.code} {module.titre}"
|
||||
title += " semestre %(titremois)s" % sem
|
||||
if gr_title and gr_title != "tous":
|
||||
title += " %s" % gr_title
|
||||
caption = title
|
||||
html_next_section = ""
|
||||
if format == "pdf":
|
||||
if format == "pdf" or format == "bordereau":
|
||||
caption = "" # same as pdf_title
|
||||
pdf_title = title
|
||||
html_title = (
|
||||
"""<h2 class="formsemestre">Notes du module <a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a></h2>"""
|
||||
% (moduleimpl_id, Mod["code"], Mod["titre"])
|
||||
)
|
||||
html_title = f"""<h2 class="formsemestre">Notes {module.type_name()} <a href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
}">{module.code} {module.titre}</a></h2>
|
||||
"""
|
||||
if not is_conforme:
|
||||
html_title += (
|
||||
"""<div class="warning">Poids des évaluations non conformes !</div>"""
|
||||
)
|
||||
base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl
|
||||
# display
|
||||
tab = GenTable(
|
||||
|
@ -479,10 +570,11 @@ def _make_table_notes(
|
|||
html_title=html_title,
|
||||
pdf_title=pdf_title,
|
||||
html_class="table_leftalign notes_evaluation",
|
||||
preferences=sco_preferences.SemPreferences(M["formsemestre_id"]),
|
||||
preferences=sco_preferences.SemPreferences(modimpl_o["formsemestre_id"]),
|
||||
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete
|
||||
)
|
||||
|
||||
if format == "bordereau":
|
||||
format = "pdf"
|
||||
t = tab.make_page(format=format, with_html_headers=False)
|
||||
if format != "html":
|
||||
return t
|
||||
|
@ -502,12 +594,13 @@ def _make_table_notes(
|
|||
histo = histogram_notes(notes)
|
||||
# 2 colonnes: histo, comments
|
||||
C = [
|
||||
f'<br><a class="stdlink" href="{base_url}&format=bordereau">Bordereau de Signatures (version PDF)</a>',
|
||||
"<table><tr><td><div><h4>Répartition des notes:</h4>"
|
||||
+ histo
|
||||
+ "</div></td>\n",
|
||||
'<td style="padding-left: 50px; vertical-align: top;"><p>',
|
||||
]
|
||||
commentkeys = list(K.items()) # [ (comment, key), ... ]
|
||||
commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
|
||||
commentkeys.sort(key=lambda x: int(x[1]))
|
||||
for (comment, key) in commentkeys:
|
||||
C.append(
|
||||
|
@ -536,7 +629,20 @@ def _make_table_notes(
|
|||
|
||||
|
||||
def _add_eval_columns(
|
||||
e, rows, titles, coefs, note_max, moys, K, note_sur_20, keep_numeric
|
||||
e,
|
||||
evals_poids,
|
||||
ues,
|
||||
rows,
|
||||
titles,
|
||||
row_coefs,
|
||||
row_poids,
|
||||
row_note_max,
|
||||
row_moys,
|
||||
is_apc,
|
||||
K,
|
||||
note_sur_20,
|
||||
keep_numeric,
|
||||
format="html",
|
||||
):
|
||||
"""Add eval e"""
|
||||
nb_notes = 0
|
||||
|
@ -545,7 +651,8 @@ def _add_eval_columns(
|
|||
sum_notes = 0
|
||||
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
||||
evaluation_id = e["evaluation_id"]
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id)
|
||||
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
for row in rows:
|
||||
etudid = row["etudid"]
|
||||
if etudid in NotesDB:
|
||||
|
@ -604,19 +711,28 @@ def _add_eval_columns(
|
|||
}
|
||||
)
|
||||
|
||||
coefs[evaluation_id] = "coef. %s" % e["coefficient"]
|
||||
row_coefs[evaluation_id] = "coef. %s" % e["coefficient"]
|
||||
if is_apc:
|
||||
if format == "html":
|
||||
row_poids[evaluation_id] = _mini_table_eval_ue_poids(
|
||||
evaluation_id, evals_poids, ues
|
||||
)
|
||||
else:
|
||||
row_poids[evaluation_id] = e_o.get_ue_poids_str()
|
||||
if note_sur_20:
|
||||
nmax = 20.0
|
||||
else:
|
||||
nmax = e["note_max"]
|
||||
if keep_numeric:
|
||||
note_max[evaluation_id] = nmax
|
||||
row_note_max[evaluation_id] = nmax
|
||||
else:
|
||||
note_max[evaluation_id] = "/ %s" % nmax
|
||||
row_note_max[evaluation_id] = "/ %s" % nmax
|
||||
|
||||
if nb_notes > 0:
|
||||
moys[evaluation_id] = "%.3g" % (sum_notes / nb_notes)
|
||||
moys[
|
||||
row_moys[evaluation_id] = scu.fmt_note(
|
||||
sum_notes / nb_notes, keep_numeric=keep_numeric
|
||||
)
|
||||
row_moys[
|
||||
"_" + str(evaluation_id) + "_help"
|
||||
] = "moyenne sur %d notes (%s le %s)" % (
|
||||
nb_notes,
|
||||
|
@ -624,9 +740,12 @@ def _add_eval_columns(
|
|||
e["jour"],
|
||||
)
|
||||
else:
|
||||
moys[evaluation_id] = ""
|
||||
row_moys[evaluation_id] = ""
|
||||
|
||||
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
|
||||
if len(e["jour"]) > 0:
|
||||
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
|
||||
else:
|
||||
titles[evaluation_id] = "%(description)s " % e
|
||||
|
||||
if e["eval_state"]["evalcomplete"]:
|
||||
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_complete"'
|
||||
|
@ -638,15 +757,29 @@ def _add_eval_columns(
|
|||
return notes, nb_abs, nb_att # pour histogramme
|
||||
|
||||
|
||||
def _mini_table_eval_ue_poids(evaluation_id, evals_poids, ues):
|
||||
"contenu de la cellule: poids"
|
||||
return (
|
||||
"""<table class="eval_poids" title="poids vers les UE"><tr><td>"""
|
||||
+ "</td><td>".join([f"{ue.acronyme}" for ue in ues])
|
||||
+ "</td></tr>"
|
||||
+ "<tr><td>"
|
||||
+ "</td><td>".join([f"{evals_poids[ue.id][evaluation_id]}" for ue in ues])
|
||||
+ "</td></tr></table>"
|
||||
)
|
||||
|
||||
|
||||
def _add_moymod_column(
|
||||
formsemestre_id,
|
||||
e,
|
||||
moduleimpl_id,
|
||||
rows,
|
||||
columns_ids,
|
||||
titles,
|
||||
coefs,
|
||||
note_max,
|
||||
moys,
|
||||
note_sur_20,
|
||||
row_coefs,
|
||||
row_poids,
|
||||
row_note_max,
|
||||
row_moys,
|
||||
is_apc,
|
||||
keep_numeric,
|
||||
):
|
||||
"""Ajoute la colonne moymod à rows"""
|
||||
|
@ -657,240 +790,74 @@ def _add_moymod_column(
|
|||
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
||||
for row in rows:
|
||||
etudid = row["etudid"]
|
||||
val = nt.get_etud_mod_moy(
|
||||
e["moduleimpl_id"], etudid
|
||||
) # note sur 20, ou 'NA','NI'
|
||||
val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI'
|
||||
row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
|
||||
row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
|
||||
if not isinstance(val, str):
|
||||
notes.append(val)
|
||||
nb_notes = nb_notes + 1
|
||||
sum_notes += val
|
||||
coefs[col_id] = ""
|
||||
row_coefs[col_id] = "(avec abs)"
|
||||
if is_apc:
|
||||
row_poids[col_id] = "à titre indicatif"
|
||||
if keep_numeric:
|
||||
note_max[col_id] = 20.0
|
||||
row_note_max[col_id] = 20.0
|
||||
else:
|
||||
note_max[col_id] = "/ 20"
|
||||
row_note_max[col_id] = "/ 20"
|
||||
titles[col_id] = "Moyenne module"
|
||||
columns_ids.append(col_id)
|
||||
if nb_notes > 0:
|
||||
moys[col_id] = "%.3g" % (sum_notes / nb_notes)
|
||||
moys["_" + col_id + "_help"] = "moyenne des moyennes"
|
||||
row_moys[col_id] = "%.3g" % (sum_notes / nb_notes)
|
||||
row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
|
||||
else:
|
||||
moys[col_id] = ""
|
||||
row_moys[col_id] = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# matin et/ou après-midi ?
|
||||
def _eval_demijournee(E):
|
||||
"1 si matin, 0 si apres midi, 2 si toute la journee"
|
||||
am, pm = False, False
|
||||
if E["heure_debut"] < "13:00":
|
||||
am = True
|
||||
if E["heure_fin"] > "13:00":
|
||||
pm = True
|
||||
if am and pm:
|
||||
demijournee = 2
|
||||
elif am:
|
||||
demijournee = 1
|
||||
else:
|
||||
demijournee = 0
|
||||
pm = True
|
||||
return am, pm, demijournee
|
||||
|
||||
|
||||
def evaluation_check_absences(evaluation_id):
|
||||
"""Vérifie les absences au moment de cette évaluation.
|
||||
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
|
||||
note et absent
|
||||
ABS et pas noté absent
|
||||
ABS et absent justifié
|
||||
EXC et pas noté absent
|
||||
EXC et pas justifie
|
||||
Ramene 3 listes d'etudid
|
||||
"""
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
if not E["jour"]:
|
||||
return [], [], [], [], [] # evaluation sans date
|
||||
|
||||
etudids = sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True
|
||||
def _add_apc_columns(
|
||||
moduleimpl_id,
|
||||
evals_poids,
|
||||
ues,
|
||||
rows,
|
||||
columns_ids,
|
||||
titles,
|
||||
is_conforme: bool,
|
||||
row_coefs,
|
||||
row_poids,
|
||||
row_note_max,
|
||||
row_moys,
|
||||
keep_numeric,
|
||||
):
|
||||
"""Ajoute les colonnes moyennes vers les UE"""
|
||||
# On raccorde ici les nouveaux calculs de notes (BUT 2021)
|
||||
# sur l'ancien code ScoDoc
|
||||
# => On recharge tout dans les nouveaux modèles
|
||||
# rows est une liste de dict avec une clé "etudid"
|
||||
# on va y ajouter une clé par UE du semestre
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
|
||||
moduleimpl_id
|
||||
)
|
||||
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
|
||||
# Liste les absences à ce moment:
|
||||
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
|
||||
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
|
||||
Just = sco_abs.list_abs_jour(
|
||||
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
|
||||
etuds_moy_module = moy_mod.compute_module_moy(
|
||||
evals_notes, evals_poids, evaluations, evaluations_completes
|
||||
)
|
||||
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
|
||||
|
||||
# Les notes:
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id)
|
||||
ValButAbs = [] # une note mais noté absent
|
||||
AbsNonSignalee = [] # note ABS mais pas noté absent
|
||||
ExcNonSignalee = [] # note EXC mais pas noté absent
|
||||
ExcNonJust = [] # note EXC mais absent non justifie
|
||||
AbsButExc = [] # note ABS mais justifié
|
||||
for etudid in etudids:
|
||||
if etudid in NotesDB:
|
||||
val = NotesDB[etudid]["value"]
|
||||
if (
|
||||
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
|
||||
) and etudid in As:
|
||||
# note valide et absent
|
||||
ValButAbs.append(etudid)
|
||||
if val is None and not etudid in As:
|
||||
# absent mais pas signale comme tel
|
||||
AbsNonSignalee.append(etudid)
|
||||
if val == scu.NOTES_NEUTRALISE and not etudid in As:
|
||||
# Neutralisé mais pas signale absent
|
||||
ExcNonSignalee.append(etudid)
|
||||
if val == scu.NOTES_NEUTRALISE and etudid in NJs:
|
||||
# EXC mais pas justifié
|
||||
ExcNonJust.append(etudid)
|
||||
if val is None and etudid in Justs:
|
||||
# ABS mais justificatif
|
||||
AbsButExc.append(etudid)
|
||||
|
||||
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
|
||||
|
||||
|
||||
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
|
||||
"""Affiche etat verification absences d'une evaluation"""
|
||||
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
|
||||
(
|
||||
ValButAbs,
|
||||
AbsNonSignalee,
|
||||
ExcNonSignalee,
|
||||
ExcNonJust,
|
||||
AbsButExc,
|
||||
) = evaluation_check_absences(evaluation_id)
|
||||
|
||||
if with_header:
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
|
||||
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
|
||||
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
|
||||
]
|
||||
if is_conforme:
|
||||
# valeur des moyennes vers les UEs:
|
||||
for row in rows:
|
||||
for ue in ues:
|
||||
moy_ue = etuds_moy_module[ue.id].get(row["etudid"], "?")
|
||||
row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
|
||||
row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
|
||||
# Nom et coefs des UE (lignes titres):
|
||||
ue_coefs = modimpl.module.ue_coefs
|
||||
if is_conforme:
|
||||
coef_class = "coef_mod_ue"
|
||||
else:
|
||||
# pas de header, mais un titre
|
||||
H = [
|
||||
"""<h2 class="eval_check_absences">%s du %s """
|
||||
% (E["description"], E["jour"])
|
||||
]
|
||||
if (
|
||||
not ValButAbs
|
||||
and not AbsNonSignalee
|
||||
and not ExcNonSignalee
|
||||
and not ExcNonJust
|
||||
):
|
||||
H.append(': <span class="eval_check_absences_ok">ok</span>')
|
||||
H.append("</h2>")
|
||||
|
||||
def etudlist(etudids, linkabs=False):
|
||||
H.append("<ul>")
|
||||
if not etudids and show_ok:
|
||||
H.append("<li>aucun</li>")
|
||||
for etudid in etudids:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
H.append(
|
||||
'<li><a class="discretelink" href="%s">'
|
||||
% url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
+ "%(nomprenom)s</a>" % etud
|
||||
)
|
||||
if linkabs:
|
||||
H.append(
|
||||
'<a class="stdlink" href="Absences/doSignaleAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s&moduleimpl_id=%s">signaler cette absence</a>'
|
||||
% (
|
||||
etud["etudid"],
|
||||
urllib.parse.quote(E["jour"]),
|
||||
urllib.parse.quote(E["jour"]),
|
||||
demijournee,
|
||||
E["moduleimpl_id"],
|
||||
)
|
||||
)
|
||||
H.append("</li>")
|
||||
H.append("</ul>")
|
||||
|
||||
if ValButAbs or show_ok:
|
||||
H.append(
|
||||
"<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>"
|
||||
)
|
||||
etudlist(ValButAbs)
|
||||
|
||||
if AbsNonSignalee or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
|
||||
)
|
||||
etudlist(AbsNonSignalee, linkabs=True)
|
||||
|
||||
if ExcNonSignalee or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
|
||||
)
|
||||
etudlist(ExcNonSignalee)
|
||||
|
||||
if ExcNonJust or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>"""
|
||||
)
|
||||
etudlist(ExcNonJust)
|
||||
|
||||
if AbsButExc or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
|
||||
)
|
||||
etudlist(AbsButExc)
|
||||
|
||||
if with_header:
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def formsemestre_check_absences_html(formsemestre_id):
|
||||
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Vérification absences aux évaluations de ce semestre",
|
||||
sem,
|
||||
),
|
||||
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
|
||||
Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:
|
||||
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
|
||||
</p>""",
|
||||
]
|
||||
# Modules, dans l'ordre
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
for M in Mlist:
|
||||
evals = sco_evaluations.do_evaluation_list(
|
||||
{"moduleimpl_id": M["moduleimpl_id"]}
|
||||
)
|
||||
if evals:
|
||||
H.append(
|
||||
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
|
||||
% (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"])
|
||||
)
|
||||
for E in evals:
|
||||
H.append(
|
||||
evaluation_check_absences_html(
|
||||
E["evaluation_id"],
|
||||
with_header=False,
|
||||
show_ok=False,
|
||||
)
|
||||
)
|
||||
if evals:
|
||||
H.append("</div>")
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
coef_class = "coef_mod_ue_non_conforme"
|
||||
for ue in ues:
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
titles[col_id] = ue.acronyme
|
||||
columns_ids.append(col_id)
|
||||
coefs = [uc for uc in ue_coefs if uc.ue_id == ue.id]
|
||||
if coefs:
|
||||
row_coefs[f"moy_ue_{ue.id}"] = coefs[0].coef
|
||||
row_coefs[f"_moy_ue_{ue.id}_td_attrs"] = f' class="{coef_class}" '
|
||||
|
|
|
@ -25,39 +25,263 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Gestion des images logos (nouveau ScoDoc 9)
|
||||
"""Gestion des images logos (nouveau ScoDoc 9.1)
|
||||
|
||||
Les logos sont `logo_header.<ext>` et `logo_footer.<ext>`
|
||||
avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png)
|
||||
|
||||
SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
|
||||
"""
|
||||
import glob
|
||||
import imghdr
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from flask import abort, current_app
|
||||
from flask import abort, current_app, url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app import Departement, ScoValueError
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from PIL import Image as PILImage
|
||||
|
||||
GLOBAL = "_" # category for server level logos
|
||||
|
||||
|
||||
def get_logo_filename(logo_type: str, scodoc_dept: str) -> str:
|
||||
"""return full filename for this logo, or "" if not found
|
||||
an existing file with extension.
|
||||
logo_type: "header" or "footer"
|
||||
scodoc-dept: acronym
|
||||
def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX):
|
||||
"""
|
||||
# Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_<dept>),
|
||||
# then in config dir /opt/scodoc-data/config/logos/
|
||||
for image_dir in (
|
||||
scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept,
|
||||
scu.SCODOC_LOGOS_DIR, # global logos
|
||||
):
|
||||
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
||||
filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}")
|
||||
if os.path.isfile(filename) and os.access(filename, os.R_OK):
|
||||
return filename
|
||||
"Recherche un logo 'name' existant.
|
||||
Deux strategies:
|
||||
si strict:
|
||||
reherche uniquement dans le département puis si non trouvé au niveau global
|
||||
sinon
|
||||
On recherche en local au dept d'abord puis si pas trouvé recherche globale
|
||||
quelque soit la stratégie, retourne None si pas trouvé
|
||||
:param logoname: le nom recherche
|
||||
:param dept_id: l'id du département dans lequel se fait la recherche (None si global)
|
||||
:param strict: stratégie de recherche (strict = False => dept ou global)
|
||||
:param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX)
|
||||
:return: un objet Logo désignant le fichier image trouvé (ou None)
|
||||
"""
|
||||
logo = Logo(logoname, dept_id, prefix).select()
|
||||
if logo is None and not strict:
|
||||
logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select()
|
||||
return logo
|
||||
|
||||
return ""
|
||||
|
||||
def delete_logo(name, dept_id=None):
|
||||
"""Delete all files matching logo (dept_id, name) (including all allowed extensions)
|
||||
Args:
|
||||
name: The name of the logo
|
||||
dept_id: the dept_id (if local). Use None to destroy globals logos
|
||||
"""
|
||||
logo = find_logo(logoname=name, dept_id=dept_id)
|
||||
while logo is not None:
|
||||
os.unlink(logo.select().filepath)
|
||||
logo = find_logo(logoname=name, dept_id=dept_id)
|
||||
|
||||
|
||||
def write_logo(stream, name, dept_id=None):
|
||||
"""Crée le fichier logo sur le serveur.
|
||||
Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream"""
|
||||
Logo(logoname=name, dept_id=dept_id).create(stream)
|
||||
|
||||
|
||||
def list_logos():
|
||||
"""Crée l'inventaire de tous les logos existants.
|
||||
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
|
||||
[None][name] pour les logos globaux
|
||||
[dept_id][name] pour les logos propres à un département (attention id numérique du dept)
|
||||
Les départements sans logos sont absents du résultat
|
||||
"""
|
||||
inventory = {None: _list_dept_logos()} # logos globaux (header / footer)
|
||||
for dept in Departement.query.filter_by(visible=True).all():
|
||||
logos_dept = _list_dept_logos(dept_id=dept.id)
|
||||
if logos_dept:
|
||||
inventory[dept.id] = _list_dept_logos(dept.id)
|
||||
return inventory
|
||||
|
||||
|
||||
def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
|
||||
"""Inventorie toutes les images existantes pour un niveau (GLOBAL ou un département).
|
||||
retourne un dictionnaire de Logo [logoname] -> Logo
|
||||
les noms des fichiers concernés doivent être de la forme: <rep>/<prefix><name>.<suffixe>
|
||||
<rep> : répertoire de recherche (déduit du dept_id)
|
||||
<prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
|
||||
<suffix>: un des suffixes autorisés
|
||||
:param dept_id: l'id du departement concerné (si None -> global)
|
||||
:param prefix: le préfixe utilisé
|
||||
:return: le résultat de la recherche ou None si aucune image trouvée
|
||||
"""
|
||||
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
|
||||
filename_parser = re.compile(f"{prefix}([^.]*).({allowed_ext})")
|
||||
logos = {}
|
||||
path_dir = Path(scu.SCODOC_LOGOS_DIR)
|
||||
if dept_id:
|
||||
path_dir = Path(
|
||||
os.path.sep.join(
|
||||
[scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)]
|
||||
)
|
||||
)
|
||||
if path_dir.exists():
|
||||
for entry in path_dir.iterdir():
|
||||
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
|
||||
result = filename_parser.match(entry.name)
|
||||
if result:
|
||||
logoname = result.group(1)
|
||||
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
|
||||
return logos if len(logos.keys()) > 0 else None
|
||||
|
||||
|
||||
class Logo:
|
||||
"""Responsable des opérations (select, create), du calcul des chemins et url
|
||||
ainsi que de la récupération des informations sur un logo.
|
||||
Usage:
|
||||
logo existant: Logo(<name>, <dept_id>, ...).select() (retourne None si fichier non trouvé)
|
||||
logo en création: Logo(<name>, <dept_id>, ...).create(stream)
|
||||
Les attributs filename, filepath, get_url() ne devraient pas être utilisés avant les opérations
|
||||
select ou save (le format n'est pas encore connu à ce moement là)
|
||||
"""
|
||||
|
||||
def __init__(self, logoname, dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
|
||||
"""Initialisation des noms et département des logos.
|
||||
if prefix = None on recherche simplement une image 'logoname.*'
|
||||
Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
|
||||
"""
|
||||
self.logoname = secure_filename(logoname)
|
||||
self.scodoc_dept_id = dept_id
|
||||
self.prefix = prefix or ""
|
||||
if self.scodoc_dept_id:
|
||||
self.dirpath = os.path.sep.join(
|
||||
[
|
||||
scu.SCODOC_LOGOS_DIR,
|
||||
scu.LOGOS_DIR_PREFIX + secure_filename(str(dept_id)),
|
||||
]
|
||||
)
|
||||
else:
|
||||
self.dirpath = scu.SCODOC_LOGOS_DIR
|
||||
self.basepath = os.path.sep.join(
|
||||
[self.dirpath, self.prefix + secure_filename(self.logoname)]
|
||||
)
|
||||
# next attributes are computed by the select function
|
||||
self.suffix = (
|
||||
"Not initialized: call the select or create function before access"
|
||||
)
|
||||
self.filepath = (
|
||||
"Not initialized: call the select or create function before access"
|
||||
)
|
||||
self.filename = (
|
||||
"Not initialized: call the select or create function before access"
|
||||
)
|
||||
self.size = "Not initialized: call the select or create function before access"
|
||||
self.aspect_ratio = (
|
||||
"Not initialized: call the select or create function before access"
|
||||
)
|
||||
self.density = (
|
||||
"Not initialized: call the select or create function before access"
|
||||
)
|
||||
self.mm = "Not initialized: call the select or create function before access"
|
||||
|
||||
def _set_format(self, fmt):
|
||||
self.suffix = fmt
|
||||
self.filepath = self.basepath + "." + fmt
|
||||
self.filename = self.logoname + "." + fmt
|
||||
|
||||
def _ensure_directory_exists(self):
|
||||
"create enclosing directory if necessary"
|
||||
if not Path(self.dirpath).exists():
|
||||
current_app.logger.info("sco_logos creating directory %s", self.dirpath)
|
||||
os.mkdir(self.dirpath)
|
||||
|
||||
def create(self, stream):
|
||||
img_type = guess_image_type(stream)
|
||||
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
||||
raise ScoValueError("type d'image invalide")
|
||||
self._set_format(img_type)
|
||||
self._ensure_directory_exists()
|
||||
filename = self.basepath + "." + self.suffix
|
||||
with open(filename, "wb") as f:
|
||||
f.write(stream.read())
|
||||
current_app.logger.info("sco_logos.store_image %s", self.filename)
|
||||
# erase other formats if they exists
|
||||
for suffix in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
|
||||
try:
|
||||
os.unlink(self.basepath + "." + suffix)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def _read_info(self, img):
|
||||
"""computes some properties from the real image
|
||||
aspect_ratio assumes that x_density and y_density are equals
|
||||
"""
|
||||
x_size, y_size = img.size
|
||||
self.density = img.info.get("dpi", None)
|
||||
unit = 1
|
||||
if self.density is None: # no dpi found try jfif infos
|
||||
self.density = img.info.get("jfif_density", None)
|
||||
unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = mm
|
||||
if self.density is not None:
|
||||
x_density, y_density = self.density
|
||||
if unit != 0:
|
||||
unit2mm = [0, 1 / 0.254, 0.1][unit]
|
||||
x_mm = round(x_size * unit2mm / x_density, 2)
|
||||
y_mm = round(y_size * unit2mm / y_density, 2)
|
||||
self.mm = (x_mm, y_mm)
|
||||
else:
|
||||
self.mm = None
|
||||
else:
|
||||
self.mm = None
|
||||
|
||||
self.size = (x_size, y_size)
|
||||
self.aspect_ratio = round(float(x_size) / y_size, 2)
|
||||
|
||||
def select(self):
|
||||
"""
|
||||
Récupération des données pour un logo existant
|
||||
il doit exister un et un seul fichier image parmi de suffixe/types autorisés
|
||||
(sinon on prend le premier trouvé)
|
||||
cette opération permet d'affiner le format d'un logo de format inconnu
|
||||
"""
|
||||
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
||||
path = Path(self.basepath + "." + suffix)
|
||||
if path.exists():
|
||||
self._set_format(suffix)
|
||||
with open(self.filepath, "rb") as f:
|
||||
img = PILImage.open(f)
|
||||
self._read_info(img)
|
||||
return self
|
||||
return None
|
||||
|
||||
def get_url(self):
|
||||
"""Retourne l'URL permettant d'obtenir l'image du logo"""
|
||||
return url_for(
|
||||
"scodoc.get_logo",
|
||||
dept_id=self.scodoc_dept_id,
|
||||
name=self.logoname,
|
||||
global_if_not_found=False,
|
||||
)
|
||||
|
||||
def get_url_small(self):
|
||||
"""Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature"""
|
||||
return url_for(
|
||||
"scodoc.get_logo_small",
|
||||
dept_id=self.scodoc_dept_id,
|
||||
name=self.logoname,
|
||||
global_if_not_found=False,
|
||||
)
|
||||
|
||||
def get_usage(self):
|
||||
if self.mm is None:
|
||||
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
|
||||
else:
|
||||
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm"">'
|
||||
|
||||
def last_modified(self):
|
||||
path = Path(self.filepath)
|
||||
dt = path.stat().st_mtime
|
||||
return path.stat().st_mtime
|
||||
|
||||
|
||||
def guess_image_type(stream) -> str:
|
||||
|
@ -70,26 +294,33 @@ def guess_image_type(stream) -> str:
|
|||
return fmt if fmt != "jpeg" else "jpg"
|
||||
|
||||
|
||||
def _ensure_directory_exists(filename):
|
||||
"create enclosing directory if necessary"
|
||||
directory = os.path.split(filename)[0]
|
||||
if not os.path.exists(directory):
|
||||
current_app.logger.info(f"sco_logos creating directory %s", directory)
|
||||
os.mkdir(directory)
|
||||
|
||||
|
||||
def store_image(stream, basename):
|
||||
img_type = guess_image_type(stream)
|
||||
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
||||
abort(400, "type d'image invalide")
|
||||
filename = basename + "." + img_type
|
||||
_ensure_directory_exists(filename)
|
||||
with open(filename, "wb") as f:
|
||||
f.write(stream.read())
|
||||
current_app.logger.info(f"sco_logos.store_image %s", filename)
|
||||
# erase other formats if they exists
|
||||
for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
|
||||
try:
|
||||
os.unlink(basename + "." + extension)
|
||||
except IOError:
|
||||
pass
|
||||
def make_logo_local(logoname, dept_name):
|
||||
depts = Departement.query.filter_by(acronym=dept_name).all()
|
||||
if len(depts) == 0:
|
||||
print(f"no dept {dept_name} found. aborting")
|
||||
return
|
||||
if len(depts) > 1:
|
||||
print(f"several depts {dept_name} found. aborting")
|
||||
return
|
||||
dept = depts[0]
|
||||
print(f"Move logo {logoname}' from global to {dept.acronym}")
|
||||
old_path_wild = f"/opt/scodoc-data/config/logos/logo_{logoname}.*"
|
||||
new_dir = f"/opt/scodoc-data/config/logos/logos_{dept.id}"
|
||||
logos = glob.glob(old_path_wild)
|
||||
# checks that there is non local already present
|
||||
for logo in logos:
|
||||
filename = os.path.split(logo)[1]
|
||||
new_name = os.path.sep.join([new_dir, filename])
|
||||
if os.path.exists(new_name):
|
||||
print("local version of global logo already exists. aborting")
|
||||
return
|
||||
# create new__dir if necessary
|
||||
if not os.path.exists(new_dir):
|
||||
print(f"- create {new_dir} directory")
|
||||
os.mkdir(new_dir)
|
||||
# move global logo (all suffixes) to local dir note: pre existent file (logo_XXX.*) in local dir does not
|
||||
# prevent operation if there is no conflict with moved files
|
||||
# At this point everything is ok so we can do files manipulation
|
||||
for logo in logos:
|
||||
shutil.move(logo, new_dir)
|
||||
# print(f"moved {n_moves}/{n} etuds")
|
||||
|
|
|
@ -35,6 +35,7 @@ import app.scodoc.notesdb as ndb
|
|||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
||||
from app import log
|
||||
from app import models
|
||||
from app.scodoc import scolog
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_cache
|
||||
|
@ -123,11 +124,13 @@ def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None):
|
|||
|
||||
|
||||
def moduleimpl_withmodule_list(
|
||||
moduleimpl_id=None, formsemestre_id=None, module_id=None
|
||||
):
|
||||
moduleimpl_id=None, formsemestre_id=None, module_id=None, sort_by_ue=False
|
||||
) -> list:
|
||||
"""Liste les moduleimpls et ajoute dans chacun
|
||||
l'UE, la matière et le module auxquels ils appartiennent.
|
||||
Tri la liste par semestre/UE/numero_matiere/numero_module.
|
||||
Tri la liste par:
|
||||
- pour les formations classiques: semestre/UE/numero_matiere/numero_module;
|
||||
- pour le BUT: ignore UEs sauf si sort_by_ue et matières dans le tri.
|
||||
|
||||
Attention: Cette fonction fait partie de l'API ScoDoc 7 et est publiée.
|
||||
"""
|
||||
|
@ -142,6 +145,8 @@ def moduleimpl_withmodule_list(
|
|||
"module_id": module_id,
|
||||
}
|
||||
)
|
||||
if not modimpls:
|
||||
return []
|
||||
ues = {}
|
||||
matieres = {}
|
||||
modules = {}
|
||||
|
@ -163,17 +168,42 @@ def moduleimpl_withmodule_list(
|
|||
)[0]
|
||||
mi["matiere"] = matieres[matiere_id]
|
||||
|
||||
# tri par semestre/UE/numero_matiere/numero_module
|
||||
modimpls.sort(
|
||||
key=lambda x: (
|
||||
x["ue"]["numero"],
|
||||
x["ue"]["ue_id"],
|
||||
x["matiere"]["numero"],
|
||||
x["matiere"]["matiere_id"],
|
||||
x["module"]["numero"],
|
||||
x["module"]["code"],
|
||||
mod = modimpls[0]["module"]
|
||||
formation = models.Formation.query.get(mod["formation_id"])
|
||||
|
||||
if formation.is_apc():
|
||||
# tri par numero_module
|
||||
if sort_by_ue:
|
||||
modimpls.sort(
|
||||
key=lambda x: (
|
||||
x["ue"]["numero"],
|
||||
x["ue"]["ue_id"],
|
||||
x["module"]["module_type"],
|
||||
x["module"]["numero"],
|
||||
x["module"]["code"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
modimpls.sort(
|
||||
key=lambda x: (
|
||||
x["module"]["module_type"],
|
||||
x["module"]["numero"],
|
||||
x["module"]["code"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Formations classiques, avec matières:
|
||||
# tri par semestre/UE/numero_matiere/numero_module
|
||||
modimpls.sort(
|
||||
key=lambda x: (
|
||||
x["ue"]["numero"],
|
||||
x["ue"]["ue_id"],
|
||||
x["matiere"]["numero"],
|
||||
x["matiere"]["matiere_id"],
|
||||
x["module"]["numero"],
|
||||
x["module"]["code"],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return modimpls
|
||||
|
||||
|
|
|
@ -263,7 +263,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
|||
can_change = authuser.has_permission(Permission.ScoEtudInscrit) and sem["etat"]
|
||||
|
||||
# Liste des modules
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
formsemestre_id=formsemestre_id, sort_by_ue=True
|
||||
)
|
||||
# Decrit les inscriptions aux modules:
|
||||
commons = [] # modules communs a tous les etuds du semestre
|
||||
options = [] # modules ou seuls quelques etudiants sont inscrits
|
||||
|
|
|
@ -28,12 +28,13 @@
|
|||
"""Tableau de bord module
|
||||
"""
|
||||
import time
|
||||
import urllib
|
||||
|
||||
from flask import g, url_for
|
||||
from flask_login import current_user
|
||||
from app.auth.models import User
|
||||
|
||||
from app.auth.models import User
|
||||
from app.models import ModuleImpl
|
||||
from app.models.evaluations import Evaluation
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
@ -44,6 +45,7 @@ from app.scodoc import sco_compute_moy
|
|||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_status
|
||||
|
@ -57,7 +59,7 @@ from app.scodoc import sco_users
|
|||
# menu evaluation dans moduleimpl
|
||||
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0):
|
||||
"Menu avec actions sur une evaluation"
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
|
||||
group_id = sco_groups.get_default_group(modimpl["formsemestre_id"])
|
||||
|
@ -136,7 +138,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0):
|
|||
"title": "Absences ce jour",
|
||||
"endpoint": "absences.EtatAbsencesDate",
|
||||
"args": {
|
||||
"date": urllib.parse.quote(E["jour"], safe=""),
|
||||
"date": E["jour"],
|
||||
"group_ids": group_id,
|
||||
},
|
||||
"enabled": E["jour"],
|
||||
|
@ -154,20 +156,46 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0):
|
|||
return htmlutils.make_menu("actions", menuEval, alone=True)
|
||||
|
||||
|
||||
def _ue_coefs_html(coefs_descr) -> str:
|
||||
""" """
|
||||
max_coef = max([x[1] for x in coefs_descr]) if coefs_descr else 1.0
|
||||
H = """
|
||||
<div id="modimpl_coefs">
|
||||
<div>Coefficients vers les UE</div>
|
||||
"""
|
||||
if coefs_descr:
|
||||
H += f"""
|
||||
<div class="coefs_histo" style="--max:{max_coef}">
|
||||
""" + "\n".join(
|
||||
[
|
||||
f"""<div style="--coef:{coef}"><div>{coef}</div>{ue_acronyme}</div>"""
|
||||
for ue_acronyme, coef in coefs_descr
|
||||
]
|
||||
)
|
||||
else:
|
||||
H += """<div class="missing_value">non définis</span>"""
|
||||
H += """
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return H
|
||||
|
||||
|
||||
def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
"""Tableau de bord module (liste des evaluations etc)"""
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||
M = modimpl.to_dict()
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=M["moduleimpl_id"]
|
||||
)
|
||||
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
ModEvals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
||||
ModEvals.sort(
|
||||
mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
||||
mod_evals.sort(
|
||||
key=lambda x: (x["numero"], x["jour"], x["heure_debut"]), reverse=True
|
||||
) # la plus RECENTE en tête
|
||||
|
||||
|
@ -179,15 +207,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||
#
|
||||
module_resp = User.query.get(M["responsable_id"])
|
||||
mod_type_name = scu.MODULE_TYPE_NAMES[Mod["module_type"]]
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Module %(titre)s" % Mod),
|
||||
"""<h2 class="formsemestre">Module <tt>%(code)s</tt> %(titre)s</h2>""" % Mod,
|
||||
"""<div class="moduleimpl_tableaubord">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="fichetitre2">Responsable: </td><td class="redboldtext">""",
|
||||
module_resp.get_nomcomplet(), # sco_users.user_info(M["responsable_id"])["nomprenom"],
|
||||
f"""<span class="blacktt">({module_resp.user_name})</span>""",
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}"
|
||||
),
|
||||
f"""<h2 class="formsemestre">{mod_type_name}
|
||||
<tt>{Mod['code']}</tt> {Mod['titre']}</h2>
|
||||
<div class="moduleimpl_tableaubord moduleimpl_type_{
|
||||
scu.ModuleType(Mod['module_type']).name.lower()}">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="fichetitre2">Responsable: </td><td class="redboldtext">
|
||||
{module_resp.get_nomcomplet()}
|
||||
<span class="blacktt">({module_resp.user_name})</span>
|
||||
""",
|
||||
]
|
||||
try:
|
||||
sco_moduleimpl.can_change_module_resp(moduleimpl_id)
|
||||
|
@ -220,10 +254,12 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
H.append("""</td><td>""")
|
||||
if not sem["etat"]:
|
||||
H.append(scu.icontag("lock32_img", title="verrouillé"))
|
||||
H.append(
|
||||
"""</td><td class="fichetitre2">Coef dans le semestre: %(coefficient)s</td><td></td></tr>"""
|
||||
% Mod
|
||||
)
|
||||
H.append("""</td><td class="fichetitre2">""")
|
||||
if modimpl.module.is_apc():
|
||||
H.append(_ue_coefs_html(modimpl.module.ue_coefs_descr()))
|
||||
else:
|
||||
H.append(f"Coef. dans le semestre: {modimpl.module.coefficient}")
|
||||
H.append("""</td><td></td></tr>""")
|
||||
# 3ieme ligne: Formation
|
||||
H.append(
|
||||
"""<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>""" % F
|
||||
|
@ -231,7 +267,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
# Ligne: Inscrits
|
||||
H.append(
|
||||
"""<tr><td class="fichetitre2">Inscrits: </td><td> %d étudiants"""
|
||||
% len(ModInscrits)
|
||||
% len(mod_inscrits)
|
||||
)
|
||||
if current_user.has_permission(Permission.ScoEtudInscrit):
|
||||
H.append(
|
||||
|
@ -285,6 +321,13 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
|
||||
H.append("</td></tr></table>")
|
||||
#
|
||||
if not modimpl.check_apc_conformity():
|
||||
H.append(
|
||||
"""<ul class="tf-msg"><li class="tf-msg warning conformite">Les poids des évaluations de ce module ne sont pas encore conformes au PN.
|
||||
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
|
||||
</li></ul>"""
|
||||
)
|
||||
#
|
||||
if has_expression and nt.expr_diagnostics:
|
||||
H.append(sco_formsemestre_status.html_expr_diagnostic(nt.expr_diagnostics))
|
||||
#
|
||||
|
@ -297,7 +340,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
"""<p><form name="f"><span style="font-size:120%%; font-weight: bold;">%d évaluations :</span>
|
||||
<span style="padding-left: 30px;">
|
||||
<input type="hidden" name="moduleimpl_id" value="%s"/>"""
|
||||
% (len(ModEvals), moduleimpl_id)
|
||||
% (len(mod_evals), moduleimpl_id)
|
||||
)
|
||||
#
|
||||
# Liste les noms de partitions
|
||||
|
@ -341,16 +384,17 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
"""
|
||||
% M
|
||||
)
|
||||
if ModEvals:
|
||||
if mod_evals:
|
||||
H.append(
|
||||
'<div class="moduleimpl_evaluations_top_links">'
|
||||
+ top_table_links
|
||||
+ "</div>"
|
||||
)
|
||||
H.append("""<table class="moduleimpl_evaluations">""")
|
||||
eval_index = len(ModEvals) - 1
|
||||
first = True
|
||||
for eval in ModEvals:
|
||||
eval_index = len(mod_evals) - 1
|
||||
first_eval = True
|
||||
for eval in mod_evals:
|
||||
evaluation = Evaluation.query.get(eval["evaluation_id"]) # TODO unifier
|
||||
etat = sco_evaluations.do_evaluation_etat(
|
||||
eval["evaluation_id"],
|
||||
partition_id=partition_id,
|
||||
|
@ -364,9 +408,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
else:
|
||||
tr_class = "mievr"
|
||||
tr_class_1 = "mievr"
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
if not first_eval:
|
||||
H.append("""<tr><td colspan="8"> </td></tr>""")
|
||||
tr_class_1 += " mievr_spaced"
|
||||
H.append("""<tr class="%s"><td class="mievr_tit" colspan="8">""" % tr_class_1)
|
||||
|
@ -399,7 +441,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
)
|
||||
# Fleches:
|
||||
H.append('<span class="eval_arrows_chld">')
|
||||
if eval_index != (len(ModEvals) - 1) and caneditevals:
|
||||
if eval_index != (len(mod_evals) - 1) and caneditevals:
|
||||
H.append(
|
||||
'<a href="module_evaluation_move?evaluation_id=%s&after=0" class="aud">%s</a>'
|
||||
% (eval["evaluation_id"], arrow_up)
|
||||
|
@ -544,19 +586,35 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
H.append("""</td></tr>""")
|
||||
#
|
||||
if etat["nb_notes"] == 0:
|
||||
H.append("""<tr class="%s"><td colspan="8"> """ % tr_class)
|
||||
H.append("""</td></tr>""")
|
||||
H.append("""<tr class="%s"><td></td>""" % tr_class)
|
||||
if modimpl.module.is_apc():
|
||||
H.append(
|
||||
f"""<td colspan="7" class="eval_poids">{
|
||||
evaluation.get_ue_poids_str()}</td>"""
|
||||
)
|
||||
else:
|
||||
H.append('<td colspan="7"></td>')
|
||||
H.append("""</tr>""")
|
||||
else: # il y a deja des notes saisies
|
||||
gr_moyennes = etat["gr_moyennes"]
|
||||
first_group = True
|
||||
for gr_moyenne in gr_moyennes:
|
||||
H.append("""<tr class="%s">""" % tr_class)
|
||||
H.append("""<td colspan="2"> </td>""")
|
||||
H.append("""<td> </td>""")
|
||||
if first_group and modimpl.module.is_apc():
|
||||
H.append(
|
||||
f"""<td class="eval_poids" colspan="3">{
|
||||
evaluation.get_ue_poids_str()}</td>"""
|
||||
)
|
||||
else:
|
||||
H.append("""<td colspan="3"></td>""")
|
||||
first_group = False
|
||||
if gr_moyenne["group_name"] is None:
|
||||
name = "Tous" # tous
|
||||
else:
|
||||
name = "Groupe %s" % gr_moyenne["group_name"]
|
||||
H.append(
|
||||
"""<td colspan="5" class="mievr_grtit">%s </td><td>""" % name
|
||||
"""<td colspan="3" class="mievr_grtit">%s </td><td>""" % name
|
||||
)
|
||||
if gr_moyenne["gr_nb_notes"] > 0:
|
||||
H.append("%(gr_moy)s" % gr_moyenne)
|
||||
|
@ -595,6 +653,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
H.append("""</a>""")
|
||||
H.append("</span>")
|
||||
H.append("""</td></tr>""")
|
||||
first_eval = False
|
||||
|
||||
#
|
||||
if caneditevals or not sem["etat"]:
|
||||
|
|
|
@ -42,7 +42,6 @@ from app import log
|
|||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
from app import email
|
||||
|
||||
|
||||
|
@ -82,6 +81,8 @@ def add(typ, object=None, text="", url=None, max_frequency=False):
|
|||
Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency
|
||||
secondes d'intervalle.
|
||||
"""
|
||||
from app.scodoc import sco_users
|
||||
|
||||
authuser_name = current_user.user_name
|
||||
cnx = ndb.GetDBConnexion()
|
||||
args = {
|
||||
|
@ -112,6 +113,7 @@ def scolar_news_summary(n=5):
|
|||
News are "compressed", ie redondant events are joined.
|
||||
"""
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_users
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
|
@ -149,7 +151,7 @@ def scolar_news_summary(n=5):
|
|||
n[k] = _scolar_news_editor.output_formators[k](n[k])
|
||||
# date resumee
|
||||
j, m = n["date"].split("/")[:2]
|
||||
mois = sco_etud.MONTH_NAMES_ABBREV[int(m) - 1]
|
||||
mois = scu.MONTH_NAMES_ABBREV[int(m) - 1]
|
||||
n["formatted_date"] = "%s %s %s" % (j, mois, n["hm"])
|
||||
# indication semestre si ajout notes:
|
||||
infos = _get_formsemestre_infos_from_news(n)
|
||||
|
|
|
@ -149,6 +149,10 @@ def ficheEtud(etudid=None):
|
|||
authuser = current_user
|
||||
cnx = ndb.GetDBConnexion()
|
||||
if etudid:
|
||||
try: # pour les bookmarks avec d'anciens ids...
|
||||
etudid = int(etudid)
|
||||
except ValueError:
|
||||
raise ScoValueError("id invalide !")
|
||||
# la sidebar est differente s'il y a ou pas un etudid
|
||||
# voir html_sidebar.sidebar()
|
||||
g.etudid = etudid
|
||||
|
|
|
@ -539,7 +539,7 @@ class SituationEtudParcoursGeneric(object):
|
|||
"""Enregistre la decision (instance de DecisionSem)
|
||||
Enregistre codes semestre et UE, et autorisations inscription.
|
||||
"""
|
||||
cnx = ndb.GetDBConnexion(autocommit=False)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# -- check
|
||||
if decision.code_etat in self.parcours.UNUSED_CODES:
|
||||
raise ScoValueError("code decision invalide dans ce parcours")
|
||||
|
@ -902,7 +902,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
|
|||
Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ).
|
||||
"""
|
||||
valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False)
|
||||
cnx = ndb.GetDBConnexion(autocommit=False)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_ues, get_etud_ue_status
|
||||
ue_ids = [x["ue_id"] for x in nt.get_ues(etudid=etudid, filter_sport=True)]
|
||||
for ue_id in ue_ids:
|
||||
|
|
|
@ -60,11 +60,7 @@ from reportlab.lib.pagesizes import letter, A4, landscape
|
|||
from flask import g
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import (
|
||||
CONFIG,
|
||||
SCODOC_LOGOS_DIR,
|
||||
LOGOS_IMAGES_ALLOWED_TYPES,
|
||||
)
|
||||
from app.scodoc.sco_utils import CONFIG
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
import sco_version
|
||||
|
@ -196,6 +192,10 @@ class ScolarsPageTemplate(PageTemplate):
|
|||
preferences=None, # dictionnary with preferences, required
|
||||
):
|
||||
"""Initialise our page template."""
|
||||
from app.scodoc.sco_logos import (
|
||||
find_logo,
|
||||
) # defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
|
||||
|
||||
self.preferences = preferences
|
||||
self.pagesbookmarks = pagesbookmarks
|
||||
self.pdfmeta_author = author
|
||||
|
@ -219,20 +219,16 @@ class ScolarsPageTemplate(PageTemplate):
|
|||
)
|
||||
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
|
||||
self.logo = None
|
||||
# XXX COPIED from sco_pvpdf, to be refactored (no time now)
|
||||
# Search background in dept specific dir, then in global config dir
|
||||
for image_dir in (
|
||||
SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/",
|
||||
SCODOC_LOGOS_DIR + "/", # global logos
|
||||
):
|
||||
for suffix in LOGOS_IMAGES_ALLOWED_TYPES:
|
||||
fn = image_dir + "/bul_pdf_background" + "." + suffix
|
||||
if not self.background_image_filename and os.path.exists(fn):
|
||||
self.background_image_filename = fn
|
||||
# Also try to use PV background
|
||||
fn = image_dir + "/letter_background" + "." + suffix
|
||||
if not self.background_image_filename and os.path.exists(fn):
|
||||
self.background_image_filename = fn
|
||||
logo = find_logo(
|
||||
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None
|
||||
)
|
||||
if logo is None:
|
||||
# Also try to use PV background
|
||||
logo = find_logo(
|
||||
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None
|
||||
)
|
||||
if logo is not None:
|
||||
self.background_image_filename = logo.filepath
|
||||
|
||||
def beforeDrawPage(self, canvas, doc):
|
||||
"""Draws (optional) background, logo and contribution message on each page.
|
||||
|
|
|
@ -42,9 +42,6 @@ Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx
|
|||
- support for legacy ZODB removed in v1909.
|
||||
|
||||
"""
|
||||
|
||||
from flask.helpers import make_response
|
||||
from app.scodoc.sco_exceptions import ScoGenError
|
||||
import datetime
|
||||
import glob
|
||||
import io
|
||||
|
@ -52,24 +49,26 @@ import os
|
|||
import random
|
||||
import requests
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import PIL
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from flask import request, g
|
||||
from flask.helpers import make_response, url_for
|
||||
|
||||
from config import Config
|
||||
|
||||
from app import log
|
||||
from app import db
|
||||
from app.models import Identite
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_portal_apogee
|
||||
from app.scodoc import sco_preferences
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoGenError
|
||||
from app.scodoc.scolog import logdb
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from config import Config
|
||||
|
||||
# Full paths on server's filesystem. Something like "/opt/scodoc/var/scodoc/photos"
|
||||
# Full paths on server's filesystem. Something like "/opt/scodoc-data/photos"
|
||||
PHOTO_DIR = os.path.join(Config.SCODOC_VAR_DIR, "photos")
|
||||
ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons")
|
||||
UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg")
|
||||
|
@ -91,17 +90,21 @@ def photo_portal_url(etud):
|
|||
return None
|
||||
|
||||
|
||||
def etud_photo_url(etud, size="small", fast=False):
|
||||
def get_etud_photo_url(etudid, size="small"):
|
||||
return url_for(
|
||||
"scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid=etudid, size=size
|
||||
)
|
||||
|
||||
|
||||
def etud_photo_url(etud: dict, size="small", fast=False) -> str:
|
||||
"""url to the image of the student, in "small" size or "orig" size.
|
||||
If ScoDoc doesn't have an image and a portal is configured, link to it.
|
||||
|
||||
"""
|
||||
photo_url = scu.ScoURL() + "/get_photo_image?etudid=%s&size=%s" % (
|
||||
etud["etudid"],
|
||||
size,
|
||||
)
|
||||
photo_url = get_etud_photo_url(etud["etudid"], size=size)
|
||||
if fast:
|
||||
return photo_url
|
||||
path = photo_pathname(etud, size=size)
|
||||
path = photo_pathname(etud["photo_filename"], size=size)
|
||||
if not path:
|
||||
# Portail ?
|
||||
ext_url = photo_portal_url(etud)
|
||||
|
@ -128,8 +131,8 @@ def get_photo_image(etudid=None, size="small"):
|
|||
if not etudid:
|
||||
filename = UNKNOWN_IMAGE_PATH
|
||||
else:
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
filename = photo_pathname(etud, size=size)
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
filename = photo_pathname(etud.photo_filename, size=size)
|
||||
if not filename:
|
||||
filename = UNKNOWN_IMAGE_PATH
|
||||
return _http_jpeg_file(filename)
|
||||
|
@ -168,8 +171,8 @@ def _http_jpeg_file(filename):
|
|||
return response
|
||||
|
||||
|
||||
def etud_photo_is_local(etud, size="small"):
|
||||
return photo_pathname(etud, size=size)
|
||||
def etud_photo_is_local(etud: dict, size="small"):
|
||||
return photo_pathname(etud["photo_filename"], size=size)
|
||||
|
||||
|
||||
def etud_photo_html(etud=None, etudid=None, title=None, size="small"):
|
||||
|
@ -212,9 +215,12 @@ def etud_photo_orig_html(etud=None, etudid=None, title=None):
|
|||
return etud_photo_html(etud=etud, etudid=etudid, title=title, size="orig")
|
||||
|
||||
|
||||
def photo_pathname(etud, size="orig"):
|
||||
"""Returns full path of image file if etud has a photo (in the filesystem), or False.
|
||||
def photo_pathname(photo_filename: str, size="orig"):
|
||||
"""Returns full path of image file if etud has a photo (in the filesystem),
|
||||
or False.
|
||||
Do not distinguish the cases: no photo, or file missing.
|
||||
Argument: photo_filename (Identite attribute)
|
||||
Resultat: False or str
|
||||
"""
|
||||
if size == "small":
|
||||
version = H90
|
||||
|
@ -222,9 +228,9 @@ def photo_pathname(etud, size="orig"):
|
|||
version = ""
|
||||
else:
|
||||
raise ValueError("invalid size parameter for photo")
|
||||
if not etud["photo_filename"]:
|
||||
if not photo_filename:
|
||||
return False
|
||||
path = os.path.join(PHOTO_DIR, etud["photo_filename"]) + version + IMAGE_EXT
|
||||
path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
else:
|
||||
|
@ -261,15 +267,14 @@ def store_photo(etud, data):
|
|||
return 1, "ok"
|
||||
|
||||
|
||||
def suppress_photo(etud):
|
||||
def suppress_photo(etud: Identite) -> None:
|
||||
"""Suppress a photo"""
|
||||
log("suppress_photo etudid=%s" % etud["etudid"])
|
||||
rel_path = photo_pathname(etud)
|
||||
log("suppress_photo etudid=%s" % etud.id)
|
||||
rel_path = photo_pathname(etud.photo_filename)
|
||||
# 1- remove ref. from database
|
||||
etud["photo_filename"] = None
|
||||
cnx = ndb.GetDBConnexion()
|
||||
sco_etud.identite_edit_nocheck(cnx, etud)
|
||||
cnx.commit()
|
||||
etud.photo_filename = None
|
||||
db.session.add(etud)
|
||||
|
||||
# 2- erase images files
|
||||
if rel_path:
|
||||
# remove extension and glob
|
||||
|
@ -278,8 +283,10 @@ def suppress_photo(etud):
|
|||
for filename in filenames:
|
||||
log("removing file %s" % filename)
|
||||
os.remove(filename)
|
||||
db.session.commit()
|
||||
# 3- log
|
||||
logdb(cnx, method="changePhoto", msg="suppression", etudid=etud["etudid"])
|
||||
cnx = ndb.GetDBConnexion()
|
||||
logdb(cnx, method="changePhoto", msg="suppression", etudid=etud.id)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
@ -370,6 +377,9 @@ def copy_portal_photo_to_fs(etud):
|
|||
log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
|
||||
if status == 1:
|
||||
log("copy_portal_photo_to_fs: copied %s" % url)
|
||||
return photo_pathname(etud), "%s: photo chargée" % etud["nomprenom"]
|
||||
return (
|
||||
photo_pathname(etud["photo_filename"]),
|
||||
f"{etud['nomprenom']}: photo chargée",
|
||||
)
|
||||
else:
|
||||
return None, "%s: <b>%s</b>" % (etud["nomprenom"], diag)
|
||||
|
|
|
@ -54,6 +54,7 @@ from app import ScoValueError
|
|||
from app.scodoc import html_sco_header, sco_preferences
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc.sco_excel import ScoExcelBook, COLORS
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
@ -137,7 +138,9 @@ class PlacementForm(FlaskForm):
|
|||
|
||||
def set_evaluation_infos(self, evaluation_id):
|
||||
"""Initialise les données du formulaire avec les données de l'évaluation."""
|
||||
eval_data = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
eval_data = sco_evaluation_db.do_evaluation_list(
|
||||
{"evaluation_id": evaluation_id}
|
||||
)
|
||||
if not eval_data:
|
||||
raise ScoValueError("invalid evaluation_id")
|
||||
self.groups_tree, self.has_groups, self.nb_groups = _get_group_info(
|
||||
|
@ -209,7 +212,7 @@ def placement_eval_selectetuds(evaluation_id):
|
|||
)
|
||||
return runner.exec_placement() # calcul et generation du fichier
|
||||
htmls = [
|
||||
html_sco_header.sco_header(init_jquery_ui=True),
|
||||
html_sco_header.sco_header(),
|
||||
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
|
||||
"<h3>Placement et émargement des étudiants</h3>",
|
||||
render_template("scodoc/forms/placement.html", form=form),
|
||||
|
@ -236,7 +239,7 @@ class PlacementRunner:
|
|||
self.groups_ids = [
|
||||
gid if gid != TOUS else form.tous_id for gid in form["groups"].data
|
||||
]
|
||||
self.eval_data = sco_evaluations.do_evaluation_list(
|
||||
self.eval_data = sco_evaluation_db.do_evaluation_list(
|
||||
{"evaluation_id": self.evaluation_id}
|
||||
)[0]
|
||||
self.groups = sco_groups.listgroups(self.groups_ids)
|
||||
|
@ -300,24 +303,17 @@ class PlacementRunner:
|
|||
get_all_students = None in [
|
||||
g["group_name"] for g in self.groups
|
||||
] # tous les etudiants
|
||||
etudids = sco_groups.do_evaluation_listeetuds_groups(
|
||||
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
|
||||
self.evaluation_id,
|
||||
self.groups,
|
||||
getallstudents=get_all_students,
|
||||
include_dems=True,
|
||||
)
|
||||
listetud = [] # liste de couples (nom,prenom)
|
||||
for etudid in etudids:
|
||||
for etudid, etat in etudid_etats:
|
||||
# infos identite etudiant (xxx sous-optimal: 1/select par etudiant)
|
||||
ident = sco_etud.etudident_list(ndb.GetDBConnexion(), {"etudid": etudid})[0]
|
||||
# infos inscription
|
||||
inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
{
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": self.moduleimpl_data["formsemestre_id"],
|
||||
}
|
||||
)[0]
|
||||
if inscr["etat"] != "D":
|
||||
if etat != "D":
|
||||
nom = ident["nom"].upper()
|
||||
prenom = ident["prenom"].lower().capitalize()
|
||||
etudid = ident["etudid"]
|
||||
|
|
|
@ -1894,21 +1894,9 @@ class BasePreferences(object):
|
|||
"""Returns preference value.
|
||||
when no value defined for this semestre, returns global value.
|
||||
"""
|
||||
params = {
|
||||
"dept_id": self.dept_id,
|
||||
"name": name,
|
||||
"formsemestre_id": formsemestre_id,
|
||||
}
|
||||
cnx = ndb.GetDBConnexion()
|
||||
plist = self._editor.list(cnx, params)
|
||||
if not plist:
|
||||
params["formsemestre_id"] = None
|
||||
plist = self._editor.list(cnx, params)
|
||||
if not plist:
|
||||
return self.default[name]
|
||||
p = plist[0]
|
||||
_convert_pref_type(p, self.prefs_dict[name])
|
||||
return p["value"]
|
||||
if formsemestre_id in self.prefs:
|
||||
return self.prefs[formsemestre_id].get(name, self.prefs[None][name])
|
||||
return self.prefs[None][name]
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.prefs[None]
|
||||
|
@ -2029,10 +2017,10 @@ class BasePreferences(object):
|
|||
H = [
|
||||
html_sco_header.sco_header(page_title="Préférences"),
|
||||
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
|
||||
f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept)
|
||||
}">modification des logos du département (pour documents pdf)</a></p>"""
|
||||
if current_user.is_administrator()
|
||||
else "",
|
||||
# f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept)
|
||||
# }">modification des logos du département (pour documents pdf)</a></p>"""
|
||||
# if current_user.is_administrator()
|
||||
# else "",
|
||||
"""<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p>
|
||||
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
|
||||
""",
|
||||
|
|
|
@ -236,7 +236,7 @@ def feuille_preparation_jury(formsemestre_id):
|
|||
if sco_preferences.get_preference("prepa_jury_nip"):
|
||||
cells.append(ws.make_cell(etud["code_nip"]))
|
||||
if sco_preferences.get_preference("prepa_jury_ine"):
|
||||
cells.append(ws.make_cell(["code_ine"]))
|
||||
cells.append(ws.make_cell(etud["code_ine"]))
|
||||
cells += ws.make_row(
|
||||
[
|
||||
etudid,
|
||||
|
|
|
@ -52,6 +52,7 @@ from app.scodoc import sco_pdf
|
|||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
import sco_version
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
from app.scodoc.sco_pdf import PDFLOCK
|
||||
from app.scodoc.sco_pdf import SU
|
||||
|
||||
|
@ -201,33 +202,36 @@ class CourrierIndividuelTemplate(PageTemplate):
|
|||
self.logo_footer = None
|
||||
self.logo_header = None
|
||||
# Search logos in dept specific dir, then in global scu.CONFIG dir
|
||||
for image_dir in (
|
||||
scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept,
|
||||
scu.SCODOC_LOGOS_DIR, # global logos
|
||||
):
|
||||
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
||||
if template_name == "PVJuryTemplate":
|
||||
fn = image_dir + "/pvjury_background" + "." + suffix
|
||||
else:
|
||||
fn = image_dir + "/letter_background" + "." + suffix
|
||||
if not self.background_image_filename and os.path.exists(fn):
|
||||
self.background_image_filename = fn
|
||||
if template_name == "PVJuryTemplate":
|
||||
background = find_logo(
|
||||
logoname="pvjury_background",
|
||||
dept_id=g.scodoc_dept_id,
|
||||
prefix="",
|
||||
)
|
||||
else:
|
||||
background = find_logo(
|
||||
logoname="letter_background",
|
||||
dept_id=g.scodoc_dept_id,
|
||||
prefix="",
|
||||
)
|
||||
if not self.background_image_filename and background is not None:
|
||||
self.background_image_filename = background.filepath
|
||||
|
||||
fn = image_dir + "/logo_footer" + "." + suffix
|
||||
if not self.logo_footer and os.path.exists(fn):
|
||||
self.logo_footer = Image(
|
||||
fn,
|
||||
height=LOGO_FOOTER_HEIGHT,
|
||||
width=LOGO_FOOTER_WIDTH,
|
||||
)
|
||||
footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id)
|
||||
if footer is not None:
|
||||
self.logo_footer = Image(
|
||||
footer.filepath,
|
||||
height=LOGO_FOOTER_HEIGHT,
|
||||
width=LOGO_FOOTER_WIDTH,
|
||||
)
|
||||
|
||||
fn = image_dir + "/logo_header" + "." + suffix
|
||||
if not self.logo_header and os.path.exists(fn):
|
||||
self.logo_header = Image(
|
||||
fn,
|
||||
height=LOGO_HEADER_HEIGHT,
|
||||
width=LOGO_HEADER_WIDTH,
|
||||
)
|
||||
header = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
|
||||
if header is not None:
|
||||
self.logo_header = Image(
|
||||
header.filepath,
|
||||
height=LOGO_HEADER_HEIGHT,
|
||||
width=LOGO_HEADER_WIDTH,
|
||||
)
|
||||
|
||||
def beforeDrawPage(self, canvas, doc):
|
||||
"""Draws a logo and an contribution message on each page."""
|
||||
|
|
|
@ -35,8 +35,12 @@ from xml.etree import ElementTree
|
|||
from flask import request
|
||||
from flask import make_response
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.but import bulletin_but
|
||||
from app.models import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_bac
|
||||
from app.scodoc import sco_bulletins_json
|
||||
|
@ -45,11 +49,11 @@ from app.scodoc import sco_bulletins, sco_excel
|
|||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_status
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_permissions
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
|
@ -78,6 +82,10 @@ def formsemestre_recapcomplet(
|
|||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
||||
# Options pour APC BUT: cache les modules par défaut car moyenne n'a pas de sens
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.formation.is_apc():
|
||||
hidemodules = True
|
||||
# traduit du DTML
|
||||
modejury = int(modejury)
|
||||
hidemodules = (
|
||||
|
@ -289,6 +297,8 @@ def make_formsemestre_recapcomplet(
|
|||
"apb_classement_gr",
|
||||
]
|
||||
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
# A ré-écrire XXX
|
||||
sem = sco_formsemestre.do_formsemestre_list(
|
||||
args={"formsemestre_id": formsemestre_id}
|
||||
)[0]
|
||||
|
@ -298,6 +308,9 @@ def make_formsemestre_recapcomplet(
|
|||
modimpls = nt.get_modimpls()
|
||||
ues = nt.get_ues() # incluant le(s) UE de sport
|
||||
#
|
||||
if formsemestre.formation.is_apc():
|
||||
nt.apc_recompute_moyennes()
|
||||
#
|
||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
||||
formsemestre_id
|
||||
)
|
||||
|
@ -433,7 +446,8 @@ def make_formsemestre_recapcomplet(
|
|||
else:
|
||||
l.append(gr_name) # groupe
|
||||
|
||||
l.append(fmtnum(scu.fmt_note(t[0], keep_numeric=keep_numeric))) # moy_gen
|
||||
# Moyenne générale
|
||||
l.append(fmtnum(scu.fmt_note(t[0], keep_numeric=keep_numeric)))
|
||||
# Ajoute rangs dans groupes seulement si CSV ou XLS
|
||||
if format[:3] == "xls" or format == "csv":
|
||||
rang_gr, _, gr_name = sco_bulletins.get_etud_rangs_groups(
|
||||
|
@ -442,9 +456,8 @@ def make_formsemestre_recapcomplet(
|
|||
|
||||
for partition in partitions:
|
||||
l.append(rang_gr[partition["partition_id"]])
|
||||
i = 0
|
||||
for ue in ues:
|
||||
i += 1
|
||||
|
||||
for i, ue in enumerate(ues, start=1):
|
||||
if ue["type"] != UE_SPORT:
|
||||
l.append(
|
||||
fmtnum(scu.fmt_note(t[i], keep_numeric=keep_numeric))
|
||||
|
@ -668,11 +681,11 @@ def make_formsemestre_recapcomplet(
|
|||
else:
|
||||
cells = '<tr class="recap_row_odd" id="etudid%s">' % etudid
|
||||
ir += 1
|
||||
# XXX nsn = [ x.replace('NA0', '-') for x in l[:-2] ]
|
||||
# notes sans le NA0:
|
||||
# XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ]
|
||||
# notes sans le NA:
|
||||
nsn = l[:-2] # copy
|
||||
for i in range(len(nsn)):
|
||||
if nsn[i] == "NA0":
|
||||
if nsn[i] == "NA":
|
||||
nsn[i] = "-"
|
||||
cells += '<td class="recap_col">%s</td>' % nsn[0] # rang
|
||||
cells += '<td class="recap_col">%s</td>' % el # nom etud (lien)
|
||||
|
@ -818,9 +831,9 @@ def _list_notes_evals(evals, etudid):
|
|||
or e["etat"]["evalattente"]
|
||||
or e["publish_incomplete"]
|
||||
):
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(e["evaluation_id"])
|
||||
if etudid in NotesDB:
|
||||
val = NotesDB[etudid]["value"]
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e["evaluation_id"])
|
||||
if etudid in notes_db:
|
||||
val = notes_db[etudid]["value"]
|
||||
else:
|
||||
# Note manquante mais prise en compte immédiate: affiche ATT
|
||||
val = scu.NOTES_ATTENTE
|
||||
|
@ -925,6 +938,9 @@ def _formsemestre_recapcomplet_json(
|
|||
:param force_publishing: donne les bulletins même si non "publiés sur portail"
|
||||
:returns: dict, "", "json"
|
||||
"""
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
is_apc = formsemestre.formation.is_apc()
|
||||
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
|
@ -946,14 +962,18 @@ def _formsemestre_recapcomplet_json(
|
|||
T = nt.get_table_moyennes_triees()
|
||||
for t in T:
|
||||
etudid = t[-1]
|
||||
bulletins.append(
|
||||
sco_bulletins_json.formsemestre_bulletinetud_published_dict(
|
||||
if is_apc:
|
||||
etud = Identite.query.get(etudid)
|
||||
r = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||
bul = r.bulletin_etud(etud, formsemestre)
|
||||
else:
|
||||
bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
force_publishing=force_publishing,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
)
|
||||
)
|
||||
bulletins.append(bul)
|
||||
return J, "", "json"
|
||||
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ from flask import url_for, g, request
|
|||
import pydot
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models import NotesFormModalite
|
||||
from app.models import FormationModalite
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
@ -1340,7 +1340,7 @@ def graph_parcours(
|
|||
log("n=%s" % n)
|
||||
log("get=%s" % g.get_node(sem_node_name(s)))
|
||||
log("nodes names = %s" % [x.get_name() for x in g.get_node_list()])
|
||||
if s["modalite"] and s["modalite"] != NotesFormModalite.DEFAULT_MODALITE:
|
||||
if s["modalite"] and s["modalite"] != FormationModalite.DEFAULT_MODALITE:
|
||||
modalite = " " + s["modalite"]
|
||||
else:
|
||||
modalite = ""
|
||||
|
|
|
@ -39,6 +39,7 @@ from flask import g, url_for, request
|
|||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import (
|
||||
|
@ -56,6 +57,7 @@ from app.scodoc import sco_abs
|
|||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
|
@ -133,9 +135,13 @@ def _check_notes(notes, evaluation, mod):
|
|||
and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
|
||||
"""
|
||||
note_max = evaluation["note_max"]
|
||||
if mod["module_type"] == scu.MODULE_STANDARD:
|
||||
if mod["module_type"] in (
|
||||
scu.ModuleType.STANDARD,
|
||||
scu.ModuleType.RESSOURCE,
|
||||
scu.ModuleType.SAE,
|
||||
):
|
||||
note_min = scu.NOTES_MIN
|
||||
elif mod["module_type"] == scu.MODULE_MALUS:
|
||||
elif mod["module_type"] == ModuleType.MALUS:
|
||||
note_min = -20.0
|
||||
else:
|
||||
raise ValueError("Invalid module type") # bug
|
||||
|
@ -176,7 +182,7 @@ def do_evaluation_upload_xls():
|
|||
vals = scu.get_request_args()
|
||||
evaluation_id = int(vals["evaluation_id"])
|
||||
comment = vals["comment"]
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
# Check access
|
||||
# (admin, respformation, and responsable_id)
|
||||
|
@ -202,11 +208,14 @@ def do_evaluation_upload_xls():
|
|||
diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)")
|
||||
raise InvalidNoteValue()
|
||||
|
||||
eval_id = int(lines[i][0].strip()[1:])
|
||||
eval_id_str = lines[i][0].strip()[1:]
|
||||
try:
|
||||
eval_id = int(eval_id_str)
|
||||
except ValueError:
|
||||
eval_id = None
|
||||
if eval_id != evaluation_id:
|
||||
diag.append(
|
||||
"Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('%s' != '%s')"
|
||||
% (eval_id, evaluation_id)
|
||||
f"Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{eval_id_str}' != '{evaluation_id}')"
|
||||
)
|
||||
raise InvalidNoteValue()
|
||||
# --- get notes -> list (etudid, value)
|
||||
|
@ -246,11 +255,13 @@ def do_evaluation_upload_xls():
|
|||
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
|
||||
raise InvalidNoteValue()
|
||||
else:
|
||||
nb_changed, nb_suppress, existing_decisions = _notes_add(
|
||||
nb_changed, nb_suppress, existing_decisions = notes_add(
|
||||
authuser, evaluation_id, L, comment
|
||||
)
|
||||
# news
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[
|
||||
0
|
||||
]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
|
@ -289,7 +300,7 @@ def do_evaluation_upload_xls():
|
|||
|
||||
def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
|
||||
"""Initialisation des notes manquantes"""
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
# Check access
|
||||
# (admin, respformation, and responsable_id)
|
||||
|
@ -297,12 +308,12 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
|
|||
# XXX imaginer un redirect + msg erreur
|
||||
raise AccessDenied("Modification des notes impossible pour %s" % current_user)
|
||||
#
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id)
|
||||
etudids = sco_groups.do_evaluation_listeetuds_groups(
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True, include_dems=False
|
||||
)
|
||||
notes = []
|
||||
for etudid in etudids: # pour tous les inscrits
|
||||
for etudid, _ in etudid_etats: # pour tous les inscrits
|
||||
if etudid not in NotesDB: # pas de note
|
||||
notes.append((etudid, value))
|
||||
# Check value
|
||||
|
@ -334,7 +345,7 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
|
|||
)
|
||||
# ok
|
||||
comment = "Initialisation notes manquantes"
|
||||
nb_changed, _, _ = _notes_add(current_user, evaluation_id, L, comment)
|
||||
nb_changed, _, _ = notes_add(current_user, evaluation_id, L, comment)
|
||||
# news
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
|
@ -375,19 +386,19 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
|
|||
|
||||
def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
||||
"suppress all notes in this eval"
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
|
||||
if sco_permissions_check.can_edit_notes(
|
||||
current_user, E["moduleimpl_id"], allow_ens=False
|
||||
):
|
||||
# On a le droit de modifier toutes les notes
|
||||
# recupere les etuds ayant une note
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id)
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
elif sco_permissions_check.can_edit_notes(
|
||||
current_user, E["moduleimpl_id"], allow_ens=True
|
||||
):
|
||||
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
evaluation_id, by_uid=current_user.id
|
||||
)
|
||||
else:
|
||||
|
@ -396,8 +407,8 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in NotesDB.keys()]
|
||||
|
||||
if not dialog_confirmed:
|
||||
nb_changed, nb_suppress, existing_decisions = _notes_add(
|
||||
current_user, evaluation_id, notes, do_it=False
|
||||
nb_changed, nb_suppress, existing_decisions = notes_add(
|
||||
current_user, evaluation_id, notes, do_it=False, check_inscription=False
|
||||
)
|
||||
msg = (
|
||||
"<p>Confirmer la suppression des %d notes ? <em>(peut affecter plusieurs groupes)</em></p>"
|
||||
|
@ -414,8 +425,12 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||
)
|
||||
|
||||
# modif
|
||||
nb_changed, nb_suppress, existing_decisions = _notes_add(
|
||||
current_user, evaluation_id, notes, comment="effacer tout"
|
||||
nb_changed, nb_suppress, existing_decisions = notes_add(
|
||||
current_user,
|
||||
evaluation_id,
|
||||
notes,
|
||||
comment="effacer tout",
|
||||
check_inscription=False,
|
||||
)
|
||||
assert nb_changed == nb_suppress
|
||||
H = ["<p>%s notes supprimées</p>" % nb_suppress]
|
||||
|
@ -443,7 +458,14 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
|
||||
def notes_add(
|
||||
user,
|
||||
evaluation_id: int,
|
||||
notes: list,
|
||||
comment=None,
|
||||
do_it=True,
|
||||
check_inscription=True,
|
||||
) -> tuple:
|
||||
"""
|
||||
Insert or update notes
|
||||
notes is a list of tuples (etudid,value)
|
||||
|
@ -451,30 +473,33 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
|
|||
WOULD be changed or suppressed.
|
||||
Nota:
|
||||
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
|
||||
Return number of changed notes
|
||||
Return tuple (nb_changed, nb_suppress, existing_decisions)
|
||||
"""
|
||||
now = psycopg2.Timestamp(
|
||||
*time.localtime()[:6]
|
||||
) # datetime.datetime.now().isoformat()
|
||||
# Verifie inscription et valeur note
|
||||
_ = {}.fromkeys(
|
||||
sco_groups.do_evaluation_listeetuds_groups(
|
||||
inscrits = {
|
||||
x[0]
|
||||
for x in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True, include_dems=True
|
||||
)
|
||||
)
|
||||
}
|
||||
for (etudid, value) in notes:
|
||||
if check_inscription and (etudid not in inscrits):
|
||||
raise NoteProcessError("etudiant non inscrit dans ce module")
|
||||
if not ((value is None) or (type(value) == type(1.0))):
|
||||
raise NoteProcessError(
|
||||
"etudiant %s: valeur de note invalide (%s)" % (etudid, value)
|
||||
)
|
||||
# Recherche notes existantes
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id)
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
# Met a jour la base
|
||||
cnx = ndb.GetDBConnexion(autocommit=False)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
nb_changed = 0
|
||||
nb_suppress = 0
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
existing_decisions = (
|
||||
[]
|
||||
|
@ -549,7 +574,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
|
|||
else: # suppression ancienne note
|
||||
if do_it:
|
||||
log(
|
||||
"_notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
|
||||
"notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
|
||||
% (evaluation_id, etudid, oldval)
|
||||
)
|
||||
cursor.execute(
|
||||
|
@ -573,7 +598,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
|
|||
if has_existing_decision(M, E, etudid):
|
||||
existing_decisions.append(etudid)
|
||||
except:
|
||||
log("*** exception in _notes_add")
|
||||
log("*** exception in notes_add")
|
||||
if do_it:
|
||||
cnx.rollback() # abort
|
||||
# inval cache
|
||||
|
@ -593,7 +618,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
|
|||
|
||||
def saisie_notes_tableur(evaluation_id, group_ids=()):
|
||||
"""Saisie des notes via un fichier Excel"""
|
||||
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not evals:
|
||||
raise ScoValueError("invalid evaluation_id")
|
||||
E = evals[0]
|
||||
|
@ -764,7 +789,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
|
|||
|
||||
def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
||||
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
|
||||
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not evals:
|
||||
raise ScoValueError("invalid evaluation_id")
|
||||
E = evals[0]
|
||||
|
@ -805,9 +830,12 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
|||
gr_title_filename = "tous"
|
||||
else:
|
||||
getallstudents = False
|
||||
etudids = sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, groups, getallstudents=getallstudents, include_dems=True
|
||||
)
|
||||
etudids = [
|
||||
x[0]
|
||||
for x in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, groups, getallstudents=getallstudents, include_dems=True
|
||||
)
|
||||
]
|
||||
|
||||
# une liste de liste de chaines: lignes de la feuille de calcul
|
||||
L = []
|
||||
|
@ -863,7 +891,7 @@ def has_existing_decision(M, E, etudid):
|
|||
def saisie_notes(evaluation_id, group_ids=[]):
|
||||
"""Formulaire saisie notes d'une évaluation pour un groupe"""
|
||||
group_ids = [int(group_id) for group_id in group_ids]
|
||||
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not evals:
|
||||
raise ScoValueError("invalid evaluation_id")
|
||||
E = evals[0]
|
||||
|
@ -974,7 +1002,7 @@ def saisie_notes(evaluation_id, group_ids=[]):
|
|||
|
||||
def _get_sorted_etuds(E, etudids, formsemestre_id):
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
E["evaluation_id"]
|
||||
) # Notes existantes
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
@ -1014,14 +1042,14 @@ def _get_sorted_etuds(E, etudids, formsemestre_id):
|
|||
e["absinfo"] = '<span class="sn_abs">' + " ".join(warn_abs_lst) + "</span> "
|
||||
|
||||
# Note actuelle de l'étudiant:
|
||||
if etudid in NotesDB:
|
||||
e["val"] = _displayNote(NotesDB[etudid]["value"])
|
||||
comment = NotesDB[etudid]["comment"]
|
||||
if etudid in notes_db:
|
||||
e["val"] = _displayNote(notes_db[etudid]["value"])
|
||||
comment = notes_db[etudid]["comment"]
|
||||
if comment is None:
|
||||
comment = ""
|
||||
e["explanation"] = "%s (%s) %s" % (
|
||||
NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
|
||||
NotesDB[etudid]["uid"],
|
||||
notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
|
||||
notes_db[etudid]["uid"],
|
||||
comment,
|
||||
)
|
||||
else:
|
||||
|
@ -1048,9 +1076,12 @@ def _form_saisie_notes(E, M, group_ids, destination=""):
|
|||
evaluation_id = E["evaluation_id"]
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
|
||||
etudids = sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True, include_dems=True
|
||||
)
|
||||
etudids = [
|
||||
x[0]
|
||||
for x in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True, include_dems=True
|
||||
)
|
||||
]
|
||||
if not etudids:
|
||||
return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'
|
||||
|
||||
|
@ -1070,7 +1101,11 @@ def _form_saisie_notes(E, M, group_ids, destination=""):
|
|||
("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}),
|
||||
("changed", {"default": "0", "input_type": "hidden"}), # changed in JS
|
||||
]
|
||||
if M["module"]["module_type"] == scu.MODULE_STANDARD:
|
||||
if M["module"]["module_type"] in (
|
||||
ModuleType.STANDARD,
|
||||
ModuleType.RESSOURCE,
|
||||
ModuleType.SAE,
|
||||
):
|
||||
descr.append(
|
||||
(
|
||||
"s3",
|
||||
|
@ -1083,7 +1118,7 @@ def _form_saisie_notes(E, M, group_ids, destination=""):
|
|||
},
|
||||
)
|
||||
)
|
||||
elif M["module"]["module_type"] == scu.MODULE_MALUS:
|
||||
elif M["module"]["module_type"] == ModuleType.MALUS:
|
||||
descr.append(
|
||||
(
|
||||
"s3",
|
||||
|
@ -1226,7 +1261,7 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
|
|||
"save_note: evaluation_id=%s etudid=%s uid=%s value=%s"
|
||||
% (evaluation_id, etudid, authuser, value)
|
||||
)
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
Mod["url"] = url_for(
|
||||
|
@ -1241,7 +1276,7 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
|
|||
else:
|
||||
L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
|
||||
if L:
|
||||
nbchanged, _, existing_decisions = _notes_add(
|
||||
nbchanged, _, existing_decisions = notes_add(
|
||||
authuser, evaluation_id, L, comment=comment, do_it=True
|
||||
)
|
||||
sco_news.add(
|
||||
|
|
|
@ -742,7 +742,7 @@ def do_import_etud_admission(
|
|||
sco_etud.admission_edit(cnx, e)
|
||||
# Traite cas particulier de la date de naissance pour anciens
|
||||
# etudiants IUTV
|
||||
if import_naissance:
|
||||
if import_naissance and "naissance" in etud:
|
||||
date_naissance = etud["naissance"].strip()
|
||||
if date_naissance:
|
||||
sco_etud.identite_edit_nocheck(
|
||||
|
|
|
@ -280,7 +280,7 @@ def get_etud_tagged_modules(etudid, tagname):
|
|||
R.append(
|
||||
{
|
||||
"sem": sem,
|
||||
"moy": moy, # valeur réelle, ou NI (non inscrit au module ou NA0 (pas de note)
|
||||
"moy": moy, # valeur réelle, ou NI (non inscrit au module ou NA (pas de note)
|
||||
"moduleimpl": modimpl,
|
||||
"tags": tags,
|
||||
}
|
||||
|
|
|
@ -169,6 +169,7 @@ def trombino_html(groups_infos):
|
|||
+ sco_etud.format_prenom(t["prenom"])
|
||||
+ '</span><span class="trombi_nom">'
|
||||
+ sco_etud.format_nom(t["nom"])
|
||||
+ (" <i>(dem.)</i>" if t["etat"] == "D" else "")
|
||||
)
|
||||
H.append("</span></span></span>")
|
||||
i += 1
|
||||
|
@ -182,10 +183,11 @@ def trombino_html(groups_infos):
|
|||
|
||||
|
||||
def check_local_photos_availability(groups_infos, format=""):
|
||||
"""Verifie que toutes les photos (des gropupes indiqués) sont copiées localement
|
||||
dans ScoDoc (seules les photos dont nous disposons localement peuvent être exportées
|
||||
en pdf ou en zip).
|
||||
Si toutes ne sont pas dispo, retourne un dialogue d'avertissement pour l'utilisateur.
|
||||
"""Vérifie que toutes les photos (des groupes indiqués) sont copiées
|
||||
localement dans ScoDoc (seules les photos dont nous disposons localement
|
||||
peuvent être exportées en pdf ou en zip).
|
||||
Si toutes ne sont pas dispo, retourne un dialogue d'avertissement
|
||||
pour l'utilisateur.
|
||||
"""
|
||||
nb_missing = 0
|
||||
for t in groups_infos.members:
|
||||
|
@ -220,7 +222,7 @@ def _trombino_zip(groups_infos):
|
|||
# assume we have the photos (or the user acknowledged the fact)
|
||||
# Archive originals (not reduced) images, in JPEG
|
||||
for t in groups_infos.members:
|
||||
im_path = sco_photos.photo_pathname(t, size="orig")
|
||||
im_path = sco_photos.photo_pathname(t["photo_filename"], size="orig")
|
||||
if not im_path:
|
||||
continue
|
||||
img = open(im_path, "rb").read()
|
||||
|
@ -228,7 +230,7 @@ def _trombino_zip(groups_infos):
|
|||
if code_nip:
|
||||
filename = code_nip + ".jpg"
|
||||
else:
|
||||
filename = t["nom"] + "_" + t["prenom"] + "_" + t["etudid"] + ".jpg"
|
||||
filename = f'{t["nom"]}_{t["prenom"]}_{t["etudid"]}.jpg'
|
||||
Z.writestr(filename, img)
|
||||
Z.close()
|
||||
size = data.tell()
|
||||
|
@ -291,9 +293,9 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
|
|||
|
||||
|
||||
def _get_etud_platypus_image(t, image_width=2 * cm):
|
||||
"""Returns aplatypus object for the photo of student t"""
|
||||
"""Returns a platypus object for the photo of student t"""
|
||||
try:
|
||||
path = sco_photos.photo_pathname(t, size="small")
|
||||
path = sco_photos.photo_pathname(t["photo_filename"], size="small")
|
||||
if not path:
|
||||
# log('> unknown')
|
||||
path = sco_photos.UNKNOWN_IMAGE_PATH
|
||||
|
@ -492,6 +494,8 @@ def photos_generate_excel_sample(group_ids=[]):
|
|||
|
||||
def photos_import_files_form(group_ids=[]):
|
||||
"""Formulaire pour importation photos"""
|
||||
if not group_ids:
|
||||
raise ScoValueError("paramètre manquant !")
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||
back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
"""Fonction de gestion des UE "externes" (effectuees dans un cursus exterieur)
|
||||
|
||||
On rapatrie (saisit) les notes (et crédits ECTS).
|
||||
On rapatrie (saisie) les notes (et crédits ECTS).
|
||||
|
||||
Cas d'usage: les étudiants d'une formation gérée par ScoDoc peuvent
|
||||
suivre un certain nombre d'UE à l'extérieur. L'établissement a reconnu
|
||||
|
@ -66,6 +66,7 @@ from app.scodoc import sco_edit_matiere
|
|||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_moduleimpl
|
||||
|
@ -148,13 +149,15 @@ def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds):
|
|||
)
|
||||
|
||||
# Création d'une évaluation si il n'y en a pas déjà:
|
||||
ModEvals = sco_evaluations.do_evaluation_list(args={"moduleimpl_id": moduleimpl_id})
|
||||
if len(ModEvals):
|
||||
mod_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id}
|
||||
)
|
||||
if len(mod_evals):
|
||||
# met la note dans le première évaluation existante:
|
||||
evaluation_id = ModEvals[0]["evaluation_id"]
|
||||
evaluation_id = mod_evals[0]["evaluation_id"]
|
||||
else:
|
||||
# crée une évaluation:
|
||||
evaluation_id = sco_evaluations.do_evaluation_create(
|
||||
evaluation_id = sco_evaluation_db.do_evaluation_create(
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
note_max=20.0,
|
||||
coefficient=1.0,
|
||||
|
@ -164,7 +167,7 @@ def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds):
|
|||
description="note externe",
|
||||
)
|
||||
# Saisie des notes
|
||||
_, _, _ = sco_saisie_notes._notes_add(
|
||||
_, _, _ = sco_saisie_notes.notes_add(
|
||||
current_user,
|
||||
evaluation_id,
|
||||
list(notes_etuds.items()),
|
||||
|
|
|
@ -53,6 +53,7 @@ from app.scodoc.intervals import intervalmap
|
|||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
|
@ -105,12 +106,12 @@ class NotesOperation(dict):
|
|||
def list_operations(evaluation_id):
|
||||
"""returns list of NotesOperation for this evaluation"""
|
||||
notes = list(
|
||||
sco_evaluations.do_evaluation_get_all_notes(
|
||||
sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
evaluation_id, filter_suppressed=False
|
||||
).values()
|
||||
)
|
||||
notes_log = list(
|
||||
sco_evaluations.do_evaluation_get_all_notes(
|
||||
sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
evaluation_id, filter_suppressed=False, table="notes_notes_log"
|
||||
).values()
|
||||
)
|
||||
|
@ -132,11 +133,13 @@ def list_operations(evaluation_id):
|
|||
|
||||
Ops = []
|
||||
for uid in NotesDates.keys():
|
||||
user_name = "{prenomnom} ({user_name})".format(**sco_users.user_info(uid))
|
||||
for (t0, _), notes in NotesDates[uid].items():
|
||||
Op = NotesOperation(
|
||||
evaluation_id=evaluation_id,
|
||||
date=t0,
|
||||
uid=uid,
|
||||
user_name=user_name,
|
||||
notes=NotesDates[uid][t0],
|
||||
current_notes_by_etud=current_notes_by_etud,
|
||||
)
|
||||
|
@ -148,15 +151,15 @@ def list_operations(evaluation_id):
|
|||
|
||||
def evaluation_list_operations(evaluation_id):
|
||||
"""Page listing operations on evaluation"""
|
||||
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
|
||||
Ops = list_operations(evaluation_id)
|
||||
|
||||
columns_ids = ("datestr", "uid", "nb_notes", "comment")
|
||||
columns_ids = ("datestr", "user_name", "nb_notes", "comment")
|
||||
titles = {
|
||||
"datestr": "Date",
|
||||
"uid": "Enseignant",
|
||||
"user_name": "Enseignant",
|
||||
"nb_notes": "Nb de notes",
|
||||
"comment": "Commentaire",
|
||||
}
|
||||
|
@ -176,15 +179,16 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||
"""Table listant toutes les opérations de saisies de notes, dans toutes
|
||||
les évaluations du semestre.
|
||||
"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT i.nom, n.*, mod.titre, e.description, e.jour
|
||||
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name
|
||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
||||
notes_modules mod, identite i
|
||||
notes_modules mod, identite i, "user" u
|
||||
WHERE mi.id = e.moduleimpl_id
|
||||
and mi.module_id = mod.id
|
||||
and e.id = n.evaluation_id
|
||||
and i.id = n.etudid
|
||||
and u.id = n.uid
|
||||
and mi.formsemestre_id = %(formsemestre_id)s
|
||||
ORDER BY date desc
|
||||
""",
|
||||
|
@ -192,20 +196,24 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||
)
|
||||
columns_ids = (
|
||||
"date",
|
||||
"code_nip",
|
||||
"nom",
|
||||
"prenom",
|
||||
"value",
|
||||
"uid",
|
||||
"user_name",
|
||||
"titre",
|
||||
"description",
|
||||
"jour",
|
||||
"comment",
|
||||
)
|
||||
titles = {
|
||||
"nom": "Etudiant",
|
||||
"code_nip": "NIP",
|
||||
"nom": "nom",
|
||||
"prenom": "prenom",
|
||||
"date": "Date",
|
||||
"value": "Note",
|
||||
"comment": "Remarque",
|
||||
"uid": "Enseignant",
|
||||
"user_name": "Enseignant",
|
||||
"titre": "Module",
|
||||
"description": "Evaluation",
|
||||
"jour": "Date éval.",
|
||||
|
|
|
@ -46,7 +46,7 @@ from app.scodoc import sco_etud
|
|||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app import log
|
||||
from app import log, cache
|
||||
from app.scodoc.scolog import logdb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
@ -226,6 +226,7 @@ def _user_list(user_name):
|
|||
return None
|
||||
|
||||
|
||||
@cache.memoize(timeout=50) # seconds
|
||||
def user_info(user_name_or_id=None, user=None):
|
||||
"""Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base).
|
||||
Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance
|
||||
|
|
|
@ -32,23 +32,25 @@ import base64
|
|||
import bisect
|
||||
import copy
|
||||
import datetime
|
||||
from enum import IntEnum
|
||||
import json
|
||||
from hashlib import md5
|
||||
import numbers
|
||||
import os
|
||||
import pydot
|
||||
import re
|
||||
import requests
|
||||
import _thread
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib
|
||||
from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image as PILImage
|
||||
import pydot
|
||||
import requests
|
||||
|
||||
from flask import g, request
|
||||
from flask import url_for, make_response
|
||||
from flask import url_for, make_response, jsonify
|
||||
|
||||
from config import Config
|
||||
from app import log
|
||||
|
@ -64,14 +66,34 @@ import sco_version
|
|||
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
|
||||
NOTES_MIN = 0.0 # valeur minimale admise pour une note (sauf malus, dans [-20, 20])
|
||||
NOTES_MAX = 1000.0
|
||||
NOTES_ABSENCE = -999.0 # absences dans les DataFrames, NULL en base
|
||||
NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes
|
||||
NOTES_SUPPRESS = -1001.0 # note a supprimer
|
||||
NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee)
|
||||
|
||||
# ---- CODES INSCRIPTION AUX SEMESTRES
|
||||
# (champ etat de FormSemestreInscription)
|
||||
INSCRIT = "I"
|
||||
DEMISSION = "D"
|
||||
DEF = "DEF"
|
||||
|
||||
# Types de modules
|
||||
MODULE_STANDARD = 0
|
||||
MODULE_MALUS = 1
|
||||
class ModuleType(IntEnum):
|
||||
"""Code des types de module."""
|
||||
|
||||
# Stockés en BD dans Module.module_type: ne pas modifier ces valeurs
|
||||
STANDARD = 0
|
||||
MALUS = 1
|
||||
RESSOURCE = 2 # BUT
|
||||
SAE = 3 # BUT
|
||||
|
||||
|
||||
MODULE_TYPE_NAMES = {
|
||||
ModuleType.STANDARD: "Module",
|
||||
ModuleType.MALUS: "Malus",
|
||||
ModuleType.RESSOURCE: "Ressource",
|
||||
ModuleType.SAE: "SAÉ",
|
||||
}
|
||||
|
||||
MALUS_MAX = 20.0
|
||||
MALUS_MIN = -20.0
|
||||
|
@ -111,18 +133,50 @@ EVALUATION_NORMALE = 0
|
|||
EVALUATION_RATTRAPAGE = 1
|
||||
EVALUATION_SESSION2 = 2
|
||||
|
||||
MONTH_NAMES_ABBREV = (
|
||||
"Jan ",
|
||||
"Fév ",
|
||||
"Mars",
|
||||
"Avr ",
|
||||
"Mai ",
|
||||
"Juin",
|
||||
"Jul ",
|
||||
"Août",
|
||||
"Sept",
|
||||
"Oct ",
|
||||
"Nov ",
|
||||
"Déc ",
|
||||
)
|
||||
|
||||
MONTH_NAMES = (
|
||||
"janvier",
|
||||
"février",
|
||||
"mars",
|
||||
"avril",
|
||||
"mai",
|
||||
"juin",
|
||||
"juillet",
|
||||
"août",
|
||||
"septembre",
|
||||
"octobre",
|
||||
"novembre",
|
||||
"décembre",
|
||||
)
|
||||
|
||||
|
||||
def fmt_note(val, note_max=None, keep_numeric=False):
|
||||
"""conversion note en str pour affichage dans tables HTML ou PDF.
|
||||
Si keep_numeric, laisse les valeur numeriques telles quelles (pour export Excel)
|
||||
"""
|
||||
if val is None:
|
||||
if val is None or val == NOTES_ABSENCE:
|
||||
return "ABS"
|
||||
if val == NOTES_NEUTRALISE:
|
||||
return "EXC" # excuse, note neutralise
|
||||
if val == NOTES_ATTENTE:
|
||||
return "ATT" # attente, note neutralisee
|
||||
if isinstance(val, float) or isinstance(val, int):
|
||||
if np.isnan(val):
|
||||
return "~"
|
||||
if note_max != None and note_max > 0:
|
||||
val = val * 20.0 / note_max
|
||||
if keep_numeric:
|
||||
|
@ -132,7 +186,7 @@ def fmt_note(val, note_max=None, keep_numeric=False):
|
|||
s = "0" * (5 - len(s)) + s # padding: 0 à gauche pour longueur 5: "12.34"
|
||||
return s
|
||||
else:
|
||||
return val.replace("NA0", "-") # notes sans le NA0
|
||||
return val.replace("NA", "-")
|
||||
|
||||
|
||||
def fmt_coef(val):
|
||||
|
@ -154,6 +208,13 @@ def isnumber(x):
|
|||
return isinstance(x, numbers.Number)
|
||||
|
||||
|
||||
def jsnan(x):
|
||||
"if x is NaN, returns None"
|
||||
if isinstance(x, numbers.Number) and np.isnan(x):
|
||||
return None
|
||||
return x
|
||||
|
||||
|
||||
def join_words(*words):
|
||||
words = [str(w).strip() for w in words if w is not None]
|
||||
return " ".join([w for w in words if w])
|
||||
|
@ -228,7 +289,12 @@ if not os.path.exists(SCO_TMP_DIR) and os.path.exists(Config.SCODOC_VAR_DIR):
|
|||
# ----- Les logos: /opt/scodoc-data/config/logos
|
||||
SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos")
|
||||
LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "jpeg", "png") # remind that PIL does not read pdf
|
||||
LOGOS_DIR_PREFIX = "logos_"
|
||||
LOGO_FILE_PREFIX = "logo_"
|
||||
|
||||
# forme générale des noms des fichiers logos/background:
|
||||
# SCODOC_LOGO_DIR/LOGO_FILE_PREFIX<name>.<suffix> (fichier global) ou
|
||||
# SCODOC_LOGO_DIR/LOGOS_DIR_PREFIX<dept_id>/LOGO_FILE_PREFIX<name>.<suffix> (fichier départemental)
|
||||
|
||||
# ----- Les outils distribués
|
||||
SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools")
|
||||
|
@ -655,6 +721,17 @@ def get_request_args():
|
|||
return vals
|
||||
|
||||
|
||||
def json_error(message, success=False, status=404):
|
||||
"""Simple JSON response, for errors"""
|
||||
response = {
|
||||
"success": success,
|
||||
"status": status,
|
||||
"message": message,
|
||||
}
|
||||
log(f"Error: {response}")
|
||||
return jsonify(response), status
|
||||
|
||||
|
||||
def get_scodoc_version():
|
||||
"return a string identifying ScoDoc version"
|
||||
return sco_version.SCOVERSION
|
||||
|
@ -913,3 +990,13 @@ def confirm_dialog(
|
|||
)
|
||||
else:
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def objects_renumber(db, obj_list) -> None:
|
||||
"""fixe les numeros des objets d'une liste de modèles
|
||||
pour ne pas changer son ordre"""
|
||||
log(f"objects_renumber {obj_list}")
|
||||
for i, obj in enumerate(obj_list):
|
||||
obj.numero = i
|
||||
db.session.add(obj)
|
||||
db.session.commit()
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
/* Bulletin BUT, Seb. L. 2021-12-06 */
|
||||
/*******************/
|
||||
/* Styles généraux */
|
||||
/*******************/
|
||||
.wait{
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
margin: auto;
|
||||
background: #424242; /* la réponse à tout */
|
||||
animation: wait .4s infinite alternate;
|
||||
}
|
||||
@keyframes wait{
|
||||
100%{transform: translateY(40px) rotate(1turn);}
|
||||
}
|
||||
main{
|
||||
--couleurPrincipale: rgb(240,250,255);
|
||||
--couleurFondTitresUE: rgb(206,255,235);
|
||||
--couleurFondTitresRes: rgb(125, 170, 255);
|
||||
--couleurFondTitresSAE: rgb(211, 255, 255);
|
||||
--couleurSecondaire: #fec;
|
||||
--couleurIntense: #c09;
|
||||
--couleurSurlignage: rgba(232, 255, 132, 0.47);
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
display: none;
|
||||
}
|
||||
.ready .wait{display: none;}
|
||||
.ready main{display: block;}
|
||||
h2{
|
||||
margin: 0;
|
||||
color: black;
|
||||
}
|
||||
section{
|
||||
background: #FFF;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #AAA;
|
||||
padding: 16px 32px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
section>div:nth-child(1){
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.CTA_Liste{
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
background: var(--couleurIntense);
|
||||
color: #FFF;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 2px rgba(0,0,0,0.26);
|
||||
cursor: pointer;
|
||||
}
|
||||
.CTA_Liste>svg{
|
||||
transition: 0.2s;
|
||||
}
|
||||
.CTA_Liste:hover{
|
||||
outline: 2px solid #424242;
|
||||
}
|
||||
.listeOff svg{
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.listeOff .syntheseModule,
|
||||
.listeOff .eval{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moduleOnOff>.syntheseModule,
|
||||
.moduleOnOff>.eval{
|
||||
display: none;
|
||||
}
|
||||
.listeOff .moduleOnOff>.syntheseModule,
|
||||
.listeOff .moduleOnOff>.eval{
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/***********************/
|
||||
/* Options d'affichage */
|
||||
/***********************/
|
||||
.hide_abs .absences,
|
||||
.hide_abs_modules .module>.absences,
|
||||
.hide_coef .synthese em,
|
||||
.hide_coef .eval>em,
|
||||
.hide_date_inscr .dateInscription,
|
||||
.hide_ects .ects{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.module>.absences,
|
||||
.module .moyenne,
|
||||
.module .info{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/************/
|
||||
/* Etudiant */
|
||||
/************/
|
||||
.etudiant{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border-color: var(--couleurPrincipale);
|
||||
background: var(--couleurPrincipale);
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
.civilite{
|
||||
font-weight: bold;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
/************/
|
||||
/* Semestre */
|
||||
/************/
|
||||
.infoSemestre{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.infoSemestre>div{
|
||||
border: 1px solid var(--couleurIntense);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
column-gap: 4px;
|
||||
}
|
||||
.infoSemestre>div:nth-child(1){
|
||||
margin-right: auto;
|
||||
}
|
||||
.infoSemestre>div>div:nth-child(even){
|
||||
text-align: right;
|
||||
}
|
||||
.rang{
|
||||
text-decoration: underline var(--couleurIntense);
|
||||
}
|
||||
|
||||
.enteteSemestre{
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/***************/
|
||||
/* Synthèse */
|
||||
/***************/
|
||||
.synthese .ue,
|
||||
.synthese h3{
|
||||
background: var(--couleurFondTitresUE);
|
||||
}
|
||||
.synthese em,
|
||||
.eval em{
|
||||
opacity: 0.6;
|
||||
min-width: 80px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/***************/
|
||||
/* Evaluations */
|
||||
/***************/
|
||||
.module, .ue {
|
||||
background: var(--couleurSecondaire);
|
||||
color: #000;
|
||||
padding: 4px 32px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 4px 0 2px 0;
|
||||
overflow: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
h3{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 auto 0 0;
|
||||
position: sticky;
|
||||
left: -32px;
|
||||
z-index: 1;
|
||||
font-size: 16px;
|
||||
background: var(--couleurSecondaire);
|
||||
}
|
||||
.sae .module, .sae h3{
|
||||
background: var(--couleurFondTitresSAE);
|
||||
}
|
||||
|
||||
.moyenne{
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
.info{
|
||||
opacity: 0.9;
|
||||
}
|
||||
.eval, .syntheseModule{
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 0 28px;
|
||||
padding: 0px 4px;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
.eval>div, .syntheseModule>div{
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.eval:hover, .syntheseModule:hover{
|
||||
background: var(--couleurSurlignage);
|
||||
/* color: #FFF; */
|
||||
}
|
||||
.complement{
|
||||
pointer-events:none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
border: 1px solid var(--couleurIntense);
|
||||
opacity: 0;
|
||||
display: grid !important;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 0 !important;
|
||||
column-gap: 4px !important;
|
||||
}
|
||||
.eval:hover .complement{
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
.complement>div:nth-child(even){
|
||||
text-align: right;
|
||||
}
|
||||
.complement>div:nth-child(1),
|
||||
.complement>div:nth-child(2){
|
||||
font-weight: bold;
|
||||
}
|
||||
.complement>div:nth-child(1),
|
||||
.complement>div:nth-child(7){
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.absences{
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
column-gap: 4px;
|
||||
text-align: right;
|
||||
border-left: 1px solid;
|
||||
padding-left: 16px;
|
||||
}
|
||||
.absences>div:nth-child(1),
|
||||
.absences>div:nth-child(2){
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/* # -*- mode: css -*-
|
||||
ScoDoc, (c) Emmanuel Viennet 1998 - 2020
|
||||
ScoDoc, (c) Emmanuel Viennet 1998 - 2021
|
||||
*/
|
||||
|
||||
html,body {
|
||||
|
@ -31,6 +31,20 @@ div#gtrcontent {
|
|||
margin-bottom: 4ex;
|
||||
}
|
||||
|
||||
.gtrcontent {
|
||||
margin-left: 140px;
|
||||
height: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.gtrcontent a, .gtrcontent a:visited {
|
||||
color: rgb(4,16,159);
|
||||
text-decoration: none;
|
||||
}
|
||||
.gtrcontent a:hover {
|
||||
color: rgb(153,51,51);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.scotext {
|
||||
font-family : TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif;
|
||||
}
|
||||
|
@ -161,6 +175,9 @@ p.footer {
|
|||
margin-top: 15px;
|
||||
border-top: 1px solid rgb(60,60,60);
|
||||
}
|
||||
div.part2 {
|
||||
margin-top: 3ex;
|
||||
}
|
||||
|
||||
/* ---- (left) SIDEBAR ----- */
|
||||
|
||||
|
@ -177,15 +194,15 @@ div.sidebar {
|
|||
display:none;
|
||||
}
|
||||
}
|
||||
a.sidebar:link {
|
||||
a.sidebar:link, .sidebar a:link {
|
||||
color: rgb(4,16,159);
|
||||
text-decoration:none;
|
||||
}
|
||||
a.sidebar:visited {
|
||||
a.sidebar:visited, .sidebar a:visited {
|
||||
color: rgb(4,16,159);
|
||||
text-decoration: none;
|
||||
}
|
||||
a.sidebar:hover {
|
||||
a.sidebar:hover, .sidebar a:hover {
|
||||
color: rgb(153,51,51);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -262,8 +279,10 @@ div.logo-logo {
|
|||
text-align: center ;
|
||||
}
|
||||
div.logo-logo img {
|
||||
box-sizing: content-box;
|
||||
margin-top: 20px;
|
||||
width: 100px;
|
||||
width: 55px; /* 100px */
|
||||
padding-right: 50px;
|
||||
}
|
||||
div.sidebar-bottom {
|
||||
margin-top: 10px;
|
||||
|
@ -377,24 +396,24 @@ table.semlist tr td.modalite {
|
|||
text-align: left;
|
||||
padding-right: 1em;
|
||||
}
|
||||
div.gtrcontent table.semlist tr.css_S-1 {
|
||||
div#gtrcontent table.semlist tr.css_S-1 {
|
||||
background-color: rgb(251, 250, 216);
|
||||
}
|
||||
|
||||
div.gtrcontent table.semlist tr.css_S1 {
|
||||
div#gtrcontent table.semlist tr.css_S1 {
|
||||
background-color: rgb(92%,95%,94%);
|
||||
}
|
||||
div.gtrcontent table.semlist tr.css_S2 {
|
||||
div#gtrcontent table.semlist tr.css_S2 {
|
||||
background-color: rgb(214, 223, 236);
|
||||
}
|
||||
div.gtrcontent table.semlist tr.css_S3 {
|
||||
div#gtrcontent table.semlist tr.css_S3 {
|
||||
background-color: rgb(167, 216, 201);
|
||||
}
|
||||
div.gtrcontent table.semlist tr.css_S4 {
|
||||
div#gtrcontent table.semlist tr.css_S4 {
|
||||
background-color: rgb(131, 225, 140);
|
||||
}
|
||||
|
||||
div.gtrcontent table.semlist tr.css_MEXT {
|
||||
div#gtrcontent table.semlist tr.css_MEXT {
|
||||
color: #0b6e08;
|
||||
}
|
||||
|
||||
|
@ -848,6 +867,9 @@ div.sco_help {
|
|||
|
||||
span.wtf-field ul.errors li {
|
||||
color: red;
|
||||
}
|
||||
.configuration_logo div.img {
|
||||
|
||||
}
|
||||
.configuration_logo div.img-container {
|
||||
width: 256px;
|
||||
|
@ -855,6 +877,20 @@ span.wtf-field ul.errors li {
|
|||
.configuration_logo div.img-container img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.configuration_logo div.img-data {
|
||||
vertical-align: top;
|
||||
}
|
||||
.configuration_logo logo-edit titre {
|
||||
background-color:lightblue;
|
||||
}
|
||||
.configuration_logo logo-edit nom {
|
||||
float: left;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.configuration_logo logo-edit description {
|
||||
float:right;
|
||||
vertical-align:baseline;
|
||||
}
|
||||
|
||||
p.indent {
|
||||
padding-left: 2em;
|
||||
|
@ -917,13 +953,13 @@ tr.moyenne td {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
table.notes_evaluation th.eval_complete a.sortheader {
|
||||
color: green;
|
||||
table.notes_evaluation th.eval_complete {
|
||||
color: rgb(6, 90, 6);
|
||||
}
|
||||
table.notes_evaluation th.eval_incomplete a.sortheader {
|
||||
table.notes_evaluation th.eval_incomplete {
|
||||
color: red;
|
||||
}
|
||||
table.notes_evaluation th.eval_attente a.sortheader {
|
||||
table.notes_evaluation th.eval_attente {
|
||||
color: rgb(215, 90, 0);;
|
||||
}
|
||||
table.notes_evaluation tr td a.discretelink:hover {
|
||||
|
@ -1039,7 +1075,28 @@ td.colcomment, span.colcomment {
|
|||
color: rgb(80,100,80);
|
||||
}
|
||||
|
||||
h2.formsemestre, .gtrcontent h2 {
|
||||
table.notes_evaluation table.eval_poids {
|
||||
font-size: 50%;
|
||||
}
|
||||
table.notes_evaluation td.moy_ue {
|
||||
font-weight: bold;
|
||||
color:rgb(1, 116, 96);
|
||||
}
|
||||
|
||||
td.coef_mod_ue {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
color: rgb(1, 116, 96);
|
||||
}
|
||||
|
||||
td.coef_mod_ue_non_conforme {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
h2.formsemestre, #gtrcontent h2 {
|
||||
margin-top: 2px;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
@ -1147,6 +1204,9 @@ h2.formsemestre, .gtrcontent h2 {
|
|||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
#sco_menu > li > a.ui-menu-item, #sco_menu > li > a.ui-menu-item:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#sco_menu ul .ui-menu {
|
||||
width : 200px;
|
||||
|
@ -1192,6 +1252,7 @@ table.formsemestre_status {
|
|||
tr.formsemestre_status { background-color: rgb(90%,90%,90%); }
|
||||
tr.formsemestre_status_green { background-color: #EFF7F2; }
|
||||
tr.formsemestre_status_ue { background-color: rgb(90%,90%,90%); }
|
||||
tr.formsemestre_status_cat td { padding-top: 2ex;}
|
||||
table.formsemestre_status td {
|
||||
border-top: 1px solid rgb(80%,80%,80%);
|
||||
border-bottom: 1px solid rgb(80%,80%,80%);
|
||||
|
@ -1236,6 +1297,7 @@ td.formsemestre_status_cell {
|
|||
|
||||
span.status_ue_acro { font-weight: bold; }
|
||||
span.status_ue_title { font-style: italic; padding-left: 1cm;}
|
||||
span.status_module_cat { font-weight: bold; }
|
||||
|
||||
table.formsemestre_inscr td {
|
||||
padding-right: 1.25em;
|
||||
|
@ -1268,11 +1330,42 @@ ul.ue_inscr_list li.etud {
|
|||
border-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Modules */
|
||||
/* Tableau de bord module */
|
||||
div.moduleimpl_tableaubord {
|
||||
padding: 7px;
|
||||
border: 2px solid gray;
|
||||
}
|
||||
div.moduleimpl_type_sae {
|
||||
background-color:#cfeccf;
|
||||
}
|
||||
div.moduleimpl_type_ressource {
|
||||
background-color:#f5e9d2;
|
||||
}
|
||||
|
||||
div#modimpl_coefs {
|
||||
position: absolute;
|
||||
}
|
||||
.coefs_histo{
|
||||
height: 32px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
color: rgb(0, 0, 0);
|
||||
text-align: center;
|
||||
align-items: flex-end;
|
||||
font-weight: normal;
|
||||
font-size: 60%;
|
||||
}
|
||||
.coefs_histo>div{
|
||||
--height: calc(32px * var(--coef) / var(--max));
|
||||
height: var(--height);
|
||||
padding: var(--height) 4px 0 4px;
|
||||
background: #09c;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.coefs_histo>div:nth-child(odd){
|
||||
background-color: #9c0;
|
||||
}
|
||||
|
||||
span.moduleimpl_abs_link {
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
@ -1383,6 +1476,9 @@ span.evalindex {
|
|||
margin-left: 3px;
|
||||
}
|
||||
|
||||
table.moduleimpl_evaluations td.eval_poids {
|
||||
color:rgb(0, 0, 255);
|
||||
}
|
||||
|
||||
/* Formulaire edition des partitions */
|
||||
form#editpart table {
|
||||
|
@ -1471,6 +1567,54 @@ div.formation_ue_list {
|
|||
margin-right: 12px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
div.formation_list_ues_titre {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.formation_list_modules, div.formation_list_ues {
|
||||
border-radius: 18px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
div.formation_list_ues {
|
||||
background-color: #b7d2fa;
|
||||
margin-top: 20px
|
||||
}
|
||||
div.formation_list_modules {
|
||||
margin-top: 20px;
|
||||
}
|
||||
div.formation_list_modules_RESSOURCE {
|
||||
background-color: #f8c844;
|
||||
}
|
||||
div.formation_list_modules_SAE {
|
||||
background-color: #c6ffab;
|
||||
}
|
||||
div.formation_list_modules_STANDARD {
|
||||
background-color: #afafc2;
|
||||
}
|
||||
div.formation_list_modules_titre {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
div.formation_list_ues ul.notes_module_list {
|
||||
margin-top: 0px;
|
||||
margin-bottom: -1px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
div.formation_list_modules ul.notes_module_list {
|
||||
margin-top: 0px;
|
||||
margin-bottom: -1px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
li.module_malus span.formation_module_tit {
|
||||
color: red;
|
||||
|
@ -1478,6 +1622,9 @@ li.module_malus span.formation_module_tit {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
span.formation_module_ue {
|
||||
background-color: #b7d2fa;
|
||||
}
|
||||
span.notes_module_list_buts {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
@ -1487,15 +1634,23 @@ div.ue_list_tit {
|
|||
margin-top: 5px;
|
||||
}
|
||||
|
||||
ul.apc_ue_list {
|
||||
background-color: rgba(180, 189, 191, 0.14);
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
ul.notes_ue_list {
|
||||
background-color: rgb(240,240,240);
|
||||
font-weight: bold;
|
||||
margin-top: 4px;
|
||||
margin-right: 1em;
|
||||
margin-left: 1em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
li.notes_ue_list {
|
||||
margin-top: 9px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
span.ue_code {
|
||||
|
@ -1510,7 +1665,10 @@ span.ue_type {
|
|||
margin-left: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
ul.notes_module_list span.ue_coefs_list {
|
||||
color: blue;
|
||||
font-size: 70%;
|
||||
}
|
||||
div.formation_ue_list_externes {
|
||||
background-color: #98cc98;
|
||||
}
|
||||
|
@ -1540,7 +1698,7 @@ ul.notes_module_list {
|
|||
font-style: normal;
|
||||
}
|
||||
|
||||
.notes_ue_list a.discretelink, .notes_ue_list a.stdlink {
|
||||
.notes_ue_list a.stdlink {
|
||||
color: #001084;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -1563,7 +1721,7 @@ div#ue_list_code {
|
|||
}
|
||||
|
||||
ul.notes_module_list {
|
||||
list-style-type: none;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
div#ue_list_etud_validations {
|
||||
|
@ -1597,6 +1755,11 @@ div.ue_warning span {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.missing_value {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* tableau recap notes */
|
||||
table.notes_recapcomplet {
|
||||
border: 2px solid blue;
|
||||
|
@ -1930,7 +2093,10 @@ td.present {
|
|||
span.capstr {
|
||||
color: red;
|
||||
}
|
||||
|
||||
b.etuddem {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
tr.row_1 {
|
||||
background-color: white;
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/* table_editor, par Sébastien L.
|
||||
*/
|
||||
form.semestre_selector {
|
||||
margin-top: 2ex;
|
||||
}
|
||||
/***************************/
|
||||
/* Le tableau */
|
||||
/***************************/
|
||||
.tableau{
|
||||
display: grid;
|
||||
grid-auto-rows: minmax(24px, auto);
|
||||
grid-template-columns: fit-content(50px);
|
||||
gap: 2px;
|
||||
margin-top: 5px;
|
||||
background: #fffefa;
|
||||
margin: 10px;
|
||||
}
|
||||
.entete{
|
||||
background: #09c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tableau>div{
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #999;
|
||||
grid-column: var(--x) / span var(--nbX);
|
||||
grid-row: var(--y) / span var(--nbY);
|
||||
}
|
||||
|
||||
[data-editable="true"]{
|
||||
cursor: pointer;
|
||||
}
|
||||
/*****************/
|
||||
/* Styles ScoDoc */
|
||||
/*****************/
|
||||
div.title_ue {
|
||||
background-color: #b7d2fa;
|
||||
}
|
||||
.tableau>div.title_mod {
|
||||
|
||||
}
|
||||
div.title_RESSOURCE {
|
||||
background-color: #f8c844;
|
||||
}
|
||||
div.title_SAE {
|
||||
background-color: #c6ffab;
|
||||
}
|
||||
div.title_STANDARD, .champs_STANDARD {
|
||||
background-color: #fefefe;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='32' viewBox='0 0 16 32'%3E%3Cg fill='%239C92AC' fill-opacity='0.21'%3E%3Cpath fill-rule='evenodd' d='M0 24h4v2H0v-2zm0 4h6v2H0v-2zm0-8h2v2H0v-2zM0 0h4v2H0V0zm0 4h2v2H0V4zm16 20h-6v2h6v-2zm0 4H8v2h8v-2zm0-8h-4v2h4v-2zm0-20h-6v2h6V0zm0 4h-4v2h4V4zm-2 12h2v2h-2v-2zm0-8h2v2h-2V8zM2 8h10v2H2V8zm0 8h10v2H2v-2zm-2-4h14v2H0v-2zm4-8h6v2H4V4zm0 16h6v2H4v-2zM6 0h2v2H6V0zm0 24h2v2H6v-2z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
div.title_MALUS {
|
||||
background-color: #ff4700;
|
||||
}
|
||||
/***************************/
|
||||
/* Statut des cellules */
|
||||
/***************************/
|
||||
.selected{ outline: 1px solid #c09; }
|
||||
.modifying{ outline: 2px dashed #c09; }
|
||||
.wait{ outline: 2px solid #c90; }
|
||||
.good{ outline: 2px solid #9c0; }
|
||||
.modified { font-weight: bold; color:indigo}
|
||||
/***************************/
|
||||
/* Message */
|
||||
/***************************/
|
||||
.message{
|
||||
position: fixed;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
padding: 20px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
background: #ec7068;
|
||||
background: #90c;
|
||||
color: #FFF;
|
||||
font-size: 24px;
|
||||
animation: message 3s;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
@keyframes message{
|
||||
20%{transform: translate(-50%, 100%)}
|
||||
80%{transform: translate(-50%, 100%)}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 7.8 KiB |
|
@ -0,0 +1,6 @@
|
|||
function submit_form() {
|
||||
$("#configuration_form").submit();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue