This commit is contained in:
leonard_montalbano 2021-12-22 08:20:05 +01:00
commit 224bb2d281
186 changed files with 13471 additions and 3049 deletions

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

422
app/but/bulletin_but.py Normal file
View File

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

View File

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

View File

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

126
app/but/import_refcomp.py Normal file
View File

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

49
app/comp/df_cache.py Normal file
View File

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

70
app/comp/inscr_mod.py Normal file
View File

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

238
app/comp/moy_mod.py Normal file
View File

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

79
app/comp/moy_sem.py Normal file
View File

@ -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 } 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

230
app/comp/moy_ue.py Normal file
View File

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

View File

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

View File

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

View File

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

19
app/models/but_pn.py Normal file
View File

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

287
app/models/but_refcomp.py Normal file
View File

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

View File

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

View File

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

141
app/models/evaluations.py Normal file
View File

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

View File

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

View File

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

126
app/models/moduleimpls.py Normal file
View File

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

204
app/models/modules.py Normal file
View File

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

View File

@ -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(

View File

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

99
app/models/ues.py Normal file
View File

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

View File

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

View File

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

View File

@ -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()
]
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@
-é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:

View File

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

View File

@ -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:

View File

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

View File

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

173
app/scodoc/sco_edit_apc.py Normal file
View File

@ -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(),
)

View File

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

View File

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

View File

@ -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,
},
)

View File

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

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@ -56,7 +56,7 @@ class ScoValueError(ScoException):
self.dest_url = dest_url
class FormatError(ScoValueError):
class ScoFormatError(ScoValueError):
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

@ -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) ], 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):

View File

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

View File

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

View File

@ -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}" '

View File

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

View File

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

View File

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

View File

@ -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">&nbsp;</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">&nbsp;""" % 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">&nbsp;</td>""")
H.append("""<td>&nbsp;</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 &nbsp;</td><td>""" % name
"""<td colspan="3" class="mievr_grtit">%s &nbsp;</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"]:

View File

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

View File

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

View File

@ -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:

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -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(

View File

@ -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(

View File

@ -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,
}

View File

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

View File

@ -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()),

View File

@ -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.",

View File

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

View File

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

View File

@ -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;
}

View File

@ -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;

View File

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

View File

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