diff --git a/README.md b/README.md index a757f17e..e28a92aa 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,12 @@ base de données (tous les départements, et les utilisateurs) avant de commence On utilise SQLAlchemy avec Alembic et Flask-Migrate. - flask db migrate -m "ScoDoc 9.0.x: ..." # ajuster le message ! + flask db migrate -m "message explicatif....." flask db upgrade -Ne pas oublier de commiter les migrations (`git add migrations` ...). +Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`). -Mémo pour développeurs: séquence re-création d'une base (vérifiez votre `.env` +**Mémo**: séquence re-création d'une base (vérifiez votre `.env` ou variables d'environnement pour interroger la bonne base !). dropdb SCODOC_DEV diff --git a/app/__init__.py b/app/__init__.py index 65707c26..41875f33 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -317,6 +317,8 @@ def set_sco_dept(scodoc_dept: str): g.scodoc_dept_id = dept.id # l'id if not hasattr(g, "db_conn"): ndb.open_db_connection() + if not hasattr(g, "stored_get_formsemestre"): + g.stored_get_formsemestre = {} def user_db_init(): diff --git a/app/api/auth.py b/app/api/auth.py index 0226976c..24348aab 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -33,7 +33,7 @@ token_auth = HTTPTokenAuth() @basic_auth.verify_password def verify_password(username, password): - user = User.query.filter_by(username=username).first() + user = User.query.filter_by(user_name=username).first() if user and user.check_password(password): return user diff --git a/app/api/sco_api.py b/app/api/sco_api.py index 46be85a7..e2619a0b 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -29,7 +29,7 @@ """ # PAS ENCORE IMPLEMENTEE, juste un essai # Pour P. Bouron, il faudrait en priorité l'équivalent de -# Scolarite/Notes/do_moduleimpl_withmodule_list +# Scolarite/Notes/moduleimpl_withmodule_list (alias scodoc7 do_moduleimpl_withmodule_list) # Scolarite/Notes/evaluation_create # Scolarite/Notes/evaluation_delete # Scolarite/Notes/formation_list diff --git a/app/auth/forms.py b/app/auth/forms.py index 143f6554..3d70054d 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -8,7 +8,7 @@ TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentifi from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import ValidationError, DataRequired, Email, EqualTo -from app.auth.models import User +from app.auth.models import User, is_valid_password _ = lambda x: x # sans babel @@ -43,8 +43,11 @@ class UserCreationForm(FlaskForm): class ResetPasswordRequestForm(FlaskForm): - email = StringField(_l("Email"), validators=[DataRequired(), Email()]) - submit = SubmitField(_l("Request Password Reset")) + email = StringField( + _l("Adresse email associée à votre compte ScoDoc:"), + validators=[DataRequired(), Email()], + ) + submit = SubmitField(_l("Envoyer")) class ResetPasswordForm(FlaskForm): @@ -52,7 +55,11 @@ class ResetPasswordForm(FlaskForm): password2 = PasswordField( _l("Répéter"), validators=[DataRequired(), EqualTo("password")] ) - submit = SubmitField(_l("Request Password Reset")) + submit = SubmitField(_l("Valider ce mot de passe")) + + def validate_password(self, password): + if not is_valid_password(password.data): + raise ValidationError(f"Mot de passe trop simple, recommencez") class DeactivateUserForm(FlaskForm): diff --git a/app/auth/models.py b/app/auth/models.py index ed20d5ee..f243f0e7 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -10,6 +10,7 @@ import re from time import time from typing import Optional +import cracklib # pylint: disable=import-error from flask import current_app, url_for, g from flask_login import UserMixin, AnonymousUserMixin @@ -28,6 +29,23 @@ from app.scodoc import sco_etud # a deplacer dans scu VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$") +def is_valid_password(cleartxt): + """Check password. + returns True if OK. + """ + if ( + hasattr(scu.CONFIG, "MIN_PASSWORD_LENGTH") + and scu.CONFIG.MIN_PASSWORD_LENGTH > 0 + and len(cleartxt) < scu.CONFIG.MIN_PASSWORD_LENGTH + ): + return False # invalid: too short + try: + _ = cracklib.FascistCheck(cleartxt) + return True + except ValueError: + return False + + class User(UserMixin, db.Model): """ScoDoc users, handled by Flask / SQLAlchemy""" diff --git a/app/auth/routes.py b/app/auth/routes.py index 8f01a0c1..df340151 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -46,7 +46,10 @@ def login(): if not next_page or url_parse(next_page).netloc != "": next_page = url_for("scodoc.index") return redirect(next_page) - return render_template("auth/login.html", title=_("Sign In"), form=form) + message = request.args.get("message", "") + return render_template( + "auth/login.html", title=_("Sign In"), form=form, message=message + ) @bp.route("/logout") @@ -95,7 +98,9 @@ def reset_password_request(): current_app.logger.info( "reset_password_request: for unkown user '{}'".format(form.email.data) ) - flash(_("Voir les instructions envoyées par mail")) + flash( + _("Voir les instructions envoyées par mail (pensez à regarder vos spams)") + ) return redirect(url_for("auth.login")) return render_template( "auth/reset_password_request.html", title=_("Reset Password"), form=form @@ -113,6 +118,6 @@ def reset_password(token): if form.validate_on_submit(): user.set_password(form.password.data) db.session.commit() - flash(_("Your password has been reset.")) + flash(_("Votre mot de passe a été changé.")) return redirect(url_for("auth.login")) - return render_template("auth/reset_password.html", form=form) + return render_template("auth/reset_password.html", form=form, user=user) diff --git a/app/decorators.py b/app/decorators.py index 65b89905..df67751a 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -10,12 +10,10 @@ import logging import werkzeug from werkzeug.exceptions import BadRequest import flask -from flask import g -from flask import abort, current_app -from flask import request +from flask import g, current_app, request +from flask import abort, url_for, redirect from flask_login import current_user from flask_login import login_required -from flask import current_app import flask_login import app @@ -52,6 +50,15 @@ def scodoc(func): @wraps(func) def scodoc_function(*args, **kwargs): + # interdit les POST si pas loggué + if request.method == "POST" and not current_user.is_authenticated: + current_app.logger.info("POST by non authenticated user") + return redirect( + url_for( + "auth.login", + message="La page a expiré. Identifiez-vous et recommencez l'opération", + ) + ) if "scodoc_dept" in kwargs: dept_acronym = kwargs["scodoc_dept"] # current_app.logger.info("setting dept to " + dept_acronym) @@ -81,7 +88,7 @@ def permission_required(permission): def permission_required_compat_scodoc7(permission): - """Décorateur pour les fonctions utilisée comme API dans ScoDoc 7 + """Décorateur pour les fonctions utilisées comme API dans ScoDoc 7 Comme @permission_required mais autorise de passer directement les informations d'auth en paramètres: __ac_name, __ac_password diff --git a/app/models/formations.py b/app/models/formations.py index 5002ab08..bb822911 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -32,6 +32,7 @@ class NotesFormation(db.Model): ues = db.relationship("NotesUE", backref="formation", lazy="dynamic") formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") + ues = db.relationship("NotesUE", lazy="dynamic", backref="formation") def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>" @@ -65,6 +66,10 @@ class NotesUE(db.Model): # coef UE, utilise seulement si l'option use_ue_coefs est activée: coefficient = db.Column(db.Float) + # relations + matieres = db.relationship("NotesMatiere", lazy="dynamic", backref="ue") + modules = db.relationship("NotesModule", lazy="dynamic", backref="ue") + def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>" @@ -84,6 +89,8 @@ 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""" @@ -110,6 +117,8 @@ class NotesModule(db.Model): # 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): diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 93b781b7..73830b06 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -70,9 +70,14 @@ class FormSemestre(db.Model): # code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...' elt_annee_apo = db.Column(db.Text()) + # Relations: etapes = db.relationship( - "NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre" + "NotesFormsemestreEtape", cascade="all,delete", backref="formsemestre" ) + formsemestres = db.relationship( + "NotesModuleImpl", backref="formsemestre", lazy="dynamic" + ) + # Ancien id ScoDoc7 pour les migrations de bases anciennes # ne pas utiliser après migrate_scodoc7_dept_archive scodoc7_id = db.Column(db.Text(), nullable=True) diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index aef08398..46e706ee 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -44,7 +44,6 @@ import unicodedata import app.scodoc.sco_utils as scu from app import log -import six PE_DEBUG = 0 @@ -145,7 +144,7 @@ def escape_for_latex(s): } exp = re.compile( "|".join( - re.escape(six.text_type(key)) + re.escape(key) for key in sorted(list(conv.keys()), key=lambda item: -len(item)) ) ) diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 51762b4c..4a3f8aba 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -194,7 +194,8 @@ def bonus_tours(notes_sport, coefs, infos=None): def bonus_iutr(notes_sport, coefs, infos=None): - """Calcul du bonus , regle de l'IUT de Roanne (contribuée par Raphael C., nov 2012) + """Calcul du bonus , règle de l'IUT de Roanne + (contribuée par Raphael C., nov 2012) Le bonus est compris entre 0 et 0.35 point. cette procédure modifie la moyenne de chaque UE capitalisable. diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index f6ac6c34..a986494e 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -752,6 +752,8 @@ if __name__ == "__main__": ) document.build(objects) data = doc.getvalue() - open("/tmp/gen_table.pdf", "wb").write(data) + with open("/tmp/gen_table.pdf", "wb") as f: + f.write(data) p = T.make_page(format="pdf") - open("toto.pdf", "wb").write(p) + with open("toto.pdf", "wb") as f: + f.write(p) diff --git a/app/scodoc/htmlutils.py b/app/scodoc/htmlutils.py index cb356875..68e835c3 100644 --- a/app/scodoc/htmlutils.py +++ b/app/scodoc/htmlutils.py @@ -104,6 +104,8 @@ def make_menu(title, items, css_class="", alone=False): item["urlq"] = url_for( item["endpoint"], scodoc_dept=g.scodoc_dept, **args ) + elif "url" in item: + item["urlq"] = item["url"] else: item["urlq"] = "#" item["attr"] = item.get("attr", "") diff --git a/app/scodoc/intervals.py b/app/scodoc/intervals.py index 9c9b59cc..f4159bea 100644 --- a/app/scodoc/intervals.py +++ b/app/scodoc/intervals.py @@ -4,11 +4,8 @@ # Code from http://code.activestate.com/recipes/457411/ -from __future__ import print_function from bisect import bisect_left, bisect_right -from six.moves import zip - class intervalmap(object): """ diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 3ded5868..fb18d050 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -27,10 +27,7 @@ """Calculs sur les notes et cache des resultats """ -import inspect -import os -import pdb -import time + from operator import itemgetter from flask import g, url_for @@ -40,12 +37,8 @@ import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log from app.scodoc.sco_formulas import NoteVector -from app.scodoc.sco_exceptions import ( - AccessDenied, - NoteProcessError, - ScoException, - ScoValueError, -) +from app.scodoc.sco_exceptions import ScoValueError + from app.scodoc.sco_formsemestre import ( formsemestre_uecoef_list, formsemestre_uecoef_create, @@ -109,15 +102,13 @@ def get_sem_ues_modimpls(formsemestre_id, modimpls=None): (utilisé quand on ne peut pas construire nt et faire nt.get_ues()) """ if modimpls is None: - modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) + modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) uedict = {} for modimpl in modimpls: - mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[ - 0 - ] + mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0] modimpl["module"] = mod if not mod["ue_id"] in uedict: - ue = sco_edit_ue.do_ue_list(args={"ue_id": mod["ue_id"]})[0] + ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0] uedict[ue["ue_id"]] = ue ues = list(uedict.values()) ues.sort(key=lambda u: u["numero"]) @@ -219,26 +210,29 @@ class NotesTable(object): valid_evals, mods_att, self.expr_diagnostics, - ) = sco_compute_moy.do_formsemestre_moyennes(self, formsemestre_id) + ) = sco_compute_moy.formsemestre_compute_modimpls_moyennes( + self, formsemestre_id + ) self._mods_att = mods_att # liste des modules avec des notes en attente self._matmoys = {} # moyennes par matieres self._valid_evals = {} # { evaluation_id : eval } for e in valid_evals: self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE uedict = {} # public member: { ue_id : ue } - self.uedict = uedict + self.uedict = uedict # les ues qui ont un modimpl dans ce semestre for modimpl in self._modimpls: - mod = modimpl["module"] # has been added here by do_formsemestre_moyennes + # module has been added by formsemestre_compute_modimpls_moyennes + mod = modimpl["module"] if not mod["ue_id"] in uedict: - ue = sco_edit_ue.do_ue_list(args={"ue_id": mod["ue_id"]})[0] + ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0] uedict[ue["ue_id"]] = ue else: ue = uedict[mod["ue_id"]] modimpl["ue"] = ue # add ue dict to moduleimpl self._matmoys[mod["matiere_id"]] = {} - mat = sco_edit_matiere.do_matiere_list( - args={"matiere_id": mod["matiere_id"]} - )[0] + mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[ + 0 + ] modimpl["mat"] = mat # add matiere dict to moduleimpl # calcul moyennes du module et stocke dans le module # nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif= @@ -1059,7 +1053,7 @@ class NotesTable(object): "Warning: %s capitalized an UE %s which is not part of current sem %s" % (etudid, ue_id, self.formsemestre_id) ) - ue = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0] + ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] self.uedict[ue_id] = ue # record this UE if ue_id not in self._uecoef: cl = formsemestre_uecoef_list( diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 5a53c544..6fa29fb9 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -96,7 +96,7 @@ def DBInsertDict( convert_empty_to_nulls=1, return_id=True, ignore_conflicts=False, -): +) -> int: """insert into table values in dict 'vals' Return: id de l'object créé """ @@ -327,7 +327,7 @@ class EditableTable(object): self.sql_default_values = None self.insert_ignore_conflicts = insert_ignore_conflicts - def create(self, cnx, args): + def create(self, cnx, args) -> int: "create object in table" vals = dictfilter(args, self.dbfields, self.filter_nulls) if self.id_name in vals: diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index 5eb7f7f4..7d5e7976 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -474,7 +474,7 @@ def _get_abs_description(a, cursor=None): desc = a["description"] if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL": # Trouver le nom du module - Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( + Mlist = sco_moduleimpl.moduleimpl_withmodule_list( moduleimpl_id=a["moduleimpl_id"] ) if Mlist: diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index d4632bbc..50984e04 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -115,7 +115,7 @@ def doSignaleAbsence( J = "NON " M = "" if moduleimpl_id and moduleimpl_id != "NULL": - mod = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] + mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] formsemestre_id = mod["formsemestre_id"] nt = sco_cache.NotesTableCache.get(formsemestre_id) ues = nt.get_ues(etudid=etudid) @@ -939,7 +939,7 @@ def _tables_abs_etud( return "" ex = [] for ev in a["evals"]: - mod = sco_moduleimpl.do_moduleimpl_withmodule_list( + mod = sco_moduleimpl.moduleimpl_withmodule_list( moduleimpl_id=ev["moduleimpl_id"] )[0] if format == "html": @@ -957,7 +957,7 @@ def _tables_abs_etud( def descr_abs(a): ex = [] for ev in a.get("absent", []): - mod = sco_moduleimpl.do_moduleimpl_withmodule_list( + mod = sco_moduleimpl.moduleimpl_withmodule_list( moduleimpl_id=ev["moduleimpl_id"] )[0] if format == "html": diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 8ebab00a..f747d912 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -203,7 +203,9 @@ class BaseArchiver(object): def get_archive_description(self, archive_id): """Return description of archive""" self.initialize() - return open(os.path.join(archive_id, "_description.txt")).read() + with open(os.path.join(archive_id, "_description.txt")) as f: + descr = f.read() + return descr def create_obj_archive(self, oid: int, description: str): """Creates a new archive for this object and returns its id.""" @@ -232,9 +234,8 @@ class BaseArchiver(object): try: scu.GSL.acquire() fname = os.path.join(archive_id, filename) - f = open(fname, "wb") - f.write(data) - f.close() + with open(fname, "wb") as f: + f.write(data) finally: scu.GSL.release() return filename @@ -247,7 +248,9 @@ class BaseArchiver(object): raise ValueError("invalid filename") fname = os.path.join(archive_id, filename) log("reading archive file %s" % fname) - return open(fname, "rb").read() + with open(fname, "rb") as f: + data = f.read() + return data def get_archived_file(self, oid, archive_name, filename): """Recupere donnees du fichier indiqué et envoie au client""" diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 160fc30d..4bcf839e 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -360,7 +360,7 @@ def formsemestre_bulletinetud_published_dict( "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.do_ue_list({"ue_id": ue_id})[0] + ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0] d["decision_ue"].append( dict( ue_id=ue["ue_id"], diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index bd20c7a1..efdbe8c0 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -385,7 +385,7 @@ def make_xml_formsemestre_bulletinetud( "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.do_ue_list({"ue_id": ue_id})[0] + ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0] doc.append( Element( "decision_ue", diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 043104ed..53a26d77 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -292,7 +292,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa class DefferedSemCacheManager: - """Experimental: pour effectuer des opérations indépendantes dans la + """Contexte pour effectuer des opérations indépendantes dans la même requete qui invalident le cache. Par exemple, quand on inscrit des étudiants un par un à un semestre, chaque inscription va invalider le cache, et la suivante va le reconstruire... pour l'invalider juste après. diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 251a7ada..ce0ab664 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -28,7 +28,6 @@ """Semestres: Codes gestion parcours (constantes) """ import collections -from six.moves import range 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) diff --git a/app/scodoc/sco_compute_moy.py b/app/scodoc/sco_compute_moy.py index 94d1f1f3..4d46f065 100644 --- a/app/scodoc/sco_compute_moy.py +++ b/app/scodoc/sco_compute_moy.py @@ -27,10 +27,10 @@ """Calcul des moyennes de module """ - -import traceback import pprint +import traceback +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 ( @@ -40,7 +40,7 @@ from app.scodoc.sco_utils import ( EVALUATION_RATTRAPAGE, EVALUATION_SESSION2, ) -from app.scodoc.sco_exceptions import ScoException +from app.scodoc.sco_exceptions import ScoValueError from app import log from app.scodoc import sco_abs from app.scodoc import sco_edit_module @@ -65,7 +65,8 @@ def moduleimpl_has_expression(mod): def formsemestre_expressions_use_abscounts(formsemestre_id): """True si les notes de ce semestre dépendent des compteurs d'absences. - Cela n'est normalement pas le cas, sauf si des formules utilisateur utilisent ces compteurs. + Cela n'est normalement pas le cas, sauf si des formules utilisateur + utilisent ces compteurs. """ # check presence of 'nbabs' in expressions ab = "nb_abs" # chaine recherchée @@ -79,7 +80,7 @@ def formsemestre_expressions_use_abscounts(formsemestre_id): if expr and expr[0] != "#" and ab in expr: return True # 2- moyennes de modules - for mod in sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id): + for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id): if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]: return True return False @@ -128,7 +129,7 @@ def compute_user_formula( coefs, coefs_mask, formula, - diag_info={}, # infos supplementaires a placer ds messages d'erreur + diag_info=None, # infos supplementaires a placer ds messages d'erreur use_abs=True, ): """Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine). @@ -164,9 +165,14 @@ def compute_user_formula( if (user_moy > 20) or (user_moy < 0): etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - raise ScoException( - """valeur moyenne %s hors limite pour %s""" - % (user_moy, sem["formsemestre_id"], etudid, etud["nomprenom"]) + raise ScoValueError( + f""" + Valeur moyenne {user_moy} hors limite pour + {etud["nomprenom"]}""" ) except: log( @@ -183,7 +189,7 @@ def compute_user_formula( return user_moy -def do_moduleimpl_moyennes(nt, mod): +def compute_moduleimpl_moyennes(nt, modimpl): """Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées ou en attente), et att (vrai s'il y a des notes en attente dans ce module). @@ -193,13 +199,13 @@ def do_moduleimpl_moyennes(nt, mod): S'il manque des notes et que le coef n'est pas nul, la moyenne n'est pas calculée: NA Ne prend en compte que les evaluations où toutes les notes sont entrées. - Le résultat est une note sur 20. + Le résultat note_moyenne est une note sur 20. """ diag_info = {} # message d'erreur formule - moduleimpl_id = mod["moduleimpl_id"] - is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS - sem = sco_formsemestre.get_formsemestre(mod["formsemestre_id"]) - etudids = sco_moduleimpl.do_moduleimpl_listeetuds( + moduleimpl_id = modimpl["moduleimpl_id"] + is_malus = modimpl["module"]["module_type"] == scu.MODULE_MALUS + sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"]) + etudids = sco_moduleimpl.moduleimpl_listeetuds( moduleimpl_id ) # tous, y compris demissions # Inscrits au semestre (pour traiter les demissions): @@ -207,7 +213,7 @@ def do_moduleimpl_moyennes(nt, mod): [ x["etudid"] for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( - mod["formsemestre_id"] + modimpl["formsemestre_id"] ) ] ) @@ -218,7 +224,7 @@ def do_moduleimpl_moyennes(nt, mod): key=lambda x: (x["numero"], x["jour"], x["heure_debut"]) ) # la plus ancienne en tête - user_expr = moduleimpl_has_expression(mod) + user_expr = moduleimpl_has_expression(modimpl) attente = False # recupere les notes de toutes les evaluations eval_rattr = None @@ -268,7 +274,7 @@ def do_moduleimpl_moyennes(nt, mod): ] # R = {} - formula = scu.unescape_html(mod["computation_expr"]) + formula = scu.unescape_html(modimpl["computation_expr"]) formula_use_abs = "abs" in formula for etudid in insmod_set: # inscrits au semestre et au module @@ -365,7 +371,7 @@ def do_moduleimpl_moyennes(nt, mod): return R, valid_evals, attente, diag_info -def do_formsemestre_moyennes(nt, formsemestre_id): +def formsemestre_compute_modimpls_moyennes(nt, formsemestre_id): """retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } }, la liste des moduleimpls, la liste des evaluations valides, liste des moduleimpls avec notes en attente. @@ -375,7 +381,7 @@ def do_formsemestre_moyennes(nt, formsemestre_id): # args={"formsemestre_id": formsemestre_id} # ) # etudids = [x["etudid"] for x in inscr] - modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) + modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) # recupere les moyennes des etudiants de tous les modules D = {} valid_evals = [] @@ -383,15 +389,16 @@ def do_formsemestre_moyennes(nt, formsemestre_id): mods_att = [] expr_diags = [] for modimpl in modimpls: - mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[ - 0 - ] + mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0] modimpl["module"] = mod # add module dict to moduleimpl (used by nt) moduleimpl_id = modimpl["moduleimpl_id"] assert moduleimpl_id not in D - D[moduleimpl_id], valid_evals_mod, attente, expr_diag = do_moduleimpl_moyennes( - nt, modimpl - ) + ( + D[moduleimpl_id], + valid_evals_mod, + attente, + expr_diag, + ) = compute_moduleimpl_moyennes(nt, modimpl) valid_evals_per_mod[moduleimpl_id] = valid_evals_mod valid_evals += valid_evals_mod if attente: diff --git a/app/scodoc/sco_cost_formation.py b/app/scodoc/sco_cost_formation.py index 1c28ed4f..d53d3861 100644 --- a/app/scodoc/sco_cost_formation.py +++ b/app/scodoc/sco_cost_formation.py @@ -59,9 +59,7 @@ def formsemestre_table_estim_cost( """ sem = sco_formsemestre.get_formsemestre(formsemestre_id) sco_formsemestre_status.fill_formsemestre(sem) - Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( - formsemestre_id=formsemestre_id - ) + Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) T = [] for M in Mlist: Mod = M["module"] diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py index 126d2783..8fa2e209 100644 --- a/app/scodoc/sco_dump_db.py +++ b/app/scodoc/sco_dump_db.py @@ -167,7 +167,8 @@ def _anonymize_db(ano_db_name): def _get_scodoc_serial(): try: - return int(open(os.path.join(scu.SCODOC_VERSION_DIR, "scodoc.sn")).read()) + with open(os.path.join(scu.SCODOC_VERSION_DIR, "scodoc.sn")) as f: + return int(f.read()) except: return 0 diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index ec97d908..7d2e7e72 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -104,7 +104,7 @@ def do_formation_delete(oid): raise ScoLockedFormError() cnx = ndb.GetDBConnexion() # delete all UE in this formation - ues = sco_edit_ue.do_ue_list({"formation_id": oid}) + ues = sco_edit_ue.ue_list({"formation_id": oid}) for ue in ues: sco_edit_ue.do_ue_delete(ue["ue_id"], force=True) @@ -252,7 +252,7 @@ def formation_edit(formation_id=None, create=False): do_formation_edit(tf[2]) return flask.redirect( url_for( - "notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id ) ) @@ -313,13 +313,13 @@ def invalidate_sems_in_formation(formation_id): def module_move(module_id, after=0, redirect=1): """Move before/after previous one (decrement/increment numero)""" - module = sco_edit_module.do_module_list({"module_id": module_id})[0] + module = sco_edit_module.module_list({"module_id": module_id})[0] redirect = int(redirect) 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.do_module_list({"matiere_id": module["matiere_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) @@ -343,21 +343,21 @@ def module_move(module_id, after=0, redirect=1): if redirect: return flask.redirect( url_for( - "notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id ) ) def ue_move(ue_id, after=0, redirect=1): """Move UE before/after previous one (decrement/increment numero)""" - o = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0] + o = sco_edit_ue.ue_list({"ue_id": ue_id})[0] # log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after)) redirect = int(redirect) after = int(after) # 0: deplace avant, 1 deplace apres if after not in (0, 1): raise ValueError('invalid value for "after"') formation_id = o["formation_id"] - others = sco_edit_ue.do_ue_list({"formation_id": formation_id}) + others = sco_edit_ue.ue_list({"formation_id": formation_id}) if len(others) > 1: idx = [p["ue_id"] for p in others].index(ue_id) neigh = None # object to swap with @@ -378,7 +378,7 @@ def ue_move(ue_id, after=0, redirect=1): if redirect: return flask.redirect( url_for( - "notes.ue_list", + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=o["formation_id"], ) diff --git a/app/scodoc/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py index d4bdc6d8..1cbd199d 100644 --- a/app/scodoc/sco_edit_matiere.py +++ b/app/scodoc/sco_edit_matiere.py @@ -47,7 +47,7 @@ _matiereEditor = ndb.EditableTable( ) -def do_matiere_list(*args, **kw): +def matiere_list(*args, **kw): "list matieres" cnx = ndb.GetDBConnexion() return _matiereEditor.list(cnx, *args, **kw) @@ -60,12 +60,12 @@ def do_matiere_edit(*args, **kw): cnx = ndb.GetDBConnexion() # check - mat = do_matiere_list({"matiere_id": args[0]["matiere_id"]})[0] + mat = matiere_list({"matiere_id": args[0]["matiere_id"]})[0] if matiere_is_locked(mat["matiere_id"]): raise ScoLockedFormError() # edit _matiereEditor.edit(cnx, *args, **kw) - formation_id = sco_edit_ue.do_ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"] + formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"] sco_edit_formation.invalidate_sems_in_formation(formation_id) @@ -77,7 +77,7 @@ def do_matiere_create(args): cnx = ndb.GetDBConnexion() # check - ue = sco_edit_ue.do_ue_list({"ue_id": args["ue_id"]})[0] + ue = sco_edit_ue.ue_list({"ue_id": args["ue_id"]})[0] # create matiere r = _matiereEditor.create(cnx, args) @@ -96,7 +96,7 @@ def matiere_create(ue_id=None): """Creation d'une matiere""" from app.scodoc import sco_edit_ue - UE = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0] + UE = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] H = [ html_sco_header.sco_header(page_title="Création d'une matière"), """
Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.
- reprendre """ raise ScoGenError(err_page) @@ -285,7 +285,7 @@ def module_delete(module_id=None): """Delete a module""" if not module_id: raise ScoValueError("invalid module !") - Mods = do_module_list(args={"module_id": module_id}) + Mods = module_list(args={"module_id": module_id}) if not Mods: raise ScoValueError("Module inexistant !") Mod = Mods[0] @@ -317,7 +317,7 @@ def do_module_edit(val): from app.scodoc import sco_edit_formation # check - mod = do_module_list({"module_id": val["module_id"]})[0] + mod = module_list({"module_id": val["module_id"]})[0] if module_is_locked(mod["module_id"]): # formation verrouillée: empeche de modifier certains champs: protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id") @@ -332,7 +332,7 @@ def do_module_edit(val): def check_module_code_unicity(code, field, formation_id, module_id=None): "true si code module unique dans la formation" - Mods = do_module_list(args={"code": code, "formation_id": formation_id}) + Mods = module_list(args={"code": code, "formation_id": formation_id}) if module_id: # edition: supprime le module en cours Mods = [m for m in Mods if m["module_id"] != module_id] @@ -346,7 +346,7 @@ def module_edit(module_id=None): if not module_id: raise ScoValueError("invalid module !") - Mod = do_module_list(args={"module_id": module_id}) + Mod = module_list(args={"module_id": module_id}) if not Mod: raise ScoValueError("invalid module !") Mod = Mod[0] @@ -521,7 +521,7 @@ def edit_module_set_code_apogee(id=None, value=None): value = value.strip("-_ \t") log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value)) - modules = do_module_list(args={"module_id": module_id}) + modules = module_list(args={"module_id": module_id}) if not modules: return "module invalide" # should not occur @@ -531,7 +531,7 @@ def edit_module_set_code_apogee(id=None, value=None): return value -def module_list(formation_id): +def module_table(formation_id): """Liste des modules de la formation (XXX inutile ou a revoir) """ @@ -548,7 +548,7 @@ def module_list(formation_id): ] editable = current_user.has_permission(Permission.ScoChangeFormation) - for Mod in do_module_list(args={"formation_id": formation_id}): + for Mod in module_list(args={"formation_id": formation_id}): H.append('UE : %(acronyme)s
" % ue) # store min/max values used by JS client-side checks: H.append( @@ -1115,7 +1112,7 @@ def evaluation_create_form( the_eval = do_evaluation_list({"evaluation_id": evaluation_id})[0] moduleimpl_id = the_eval["moduleimpl_id"] # - M = sco_moduleimpl.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] + 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 @@ -1174,7 +1171,7 @@ def evaluation_create_form( else: min_note_max_str = "0" # - Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] + Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] # help = """Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module. diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index f3fc0ba2..77fe375d 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -285,10 +285,25 @@ class ScoExcelSheet: def make_cell(self, value: any = None, style=None, comment=None): """Construit une cellule. - value -- contenu de la cellule (texte ou numérique) + value -- contenu de la cellule (texte, numérique, booléen ou date) style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié """ - cell = WriteOnlyCell(self.ws, value or "") + # adapatation des valeurs si nécessaire + if value is None: + value = "" + elif value is True: + value = 1 + elif value is False: + value = 0 + elif isinstance(value, datetime.datetime): + value = value.replace( + tzinfo=None + ) # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones) + + # création de la cellule + cell = WriteOnlyCell(self.ws, value) + + # recopie des styles if style is None: style = self.default_style if "font" in style: @@ -310,7 +325,8 @@ class ScoExcelSheet: lines = comment.splitlines() cell.comment.width = 7 * max([len(line) for line in lines]) cell.comment.height = 20 * len(lines) - # test datatype at the end so that datetime format may be overwritten + + # test datatype to overwrite datetime format if isinstance(value, datetime.date): cell.data_type = "d" cell.number_format = FORMAT_DATE_DDMMYY @@ -318,6 +334,7 @@ class ScoExcelSheet: cell.data_type = "n" else: cell.data_type = "s" + return cell def make_row(self, values: list, style=None, comments=None): diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 019d7833..a320afe5 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -98,7 +98,7 @@ def formation_export(formation_id, export_ids=False, export_tags=True, format=No in desired format """ F = formation_list(args={"formation_id": formation_id})[0] - ues = sco_edit_ue.do_ue_list({"formation_id": formation_id}) + ues = sco_edit_ue.ue_list({"formation_id": formation_id}) F["ue"] = ues for ue in ues: ue_id = ue["ue_id"] @@ -107,14 +107,14 @@ def formation_export(formation_id, export_ids=False, export_tags=True, format=No del ue["formation_id"] if ue["ects"] is None: del ue["ects"] - mats = sco_edit_matiere.do_matiere_list({"ue_id": ue_id}) + mats = sco_edit_matiere.matiere_list({"ue_id": ue_id}) ue["matiere"] = mats for mat in mats: matiere_id = mat["matiere_id"] if not export_ids: del mat["matiere_id"] del mat["ue_id"] - mods = sco_edit_module.do_module_list({"matiere_id": matiere_id}) + mods = sco_edit_module.module_list({"matiere_id": matiere_id}) mat["module"] = mods for mod in mods: if export_tags: @@ -130,7 +130,9 @@ def formation_export(formation_id, export_ids=False, export_tags=True, format=No if mod["ects"] is None: del mod["ects"] - return scu.sendResult(F, name="formation", format=format, force_outer_xml_tag=False, attached=True) + return scu.sendResult( + F, name="formation", format=format, force_outer_xml_tag=False, attached=True + ) def formation_import_xml(doc: str, import_tags=True): @@ -364,7 +366,7 @@ def formation_create_new_version(formation_id, redirect=True): if redirect: return flask.redirect( url_for( - "notes.ue_list", + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=new_id, msg="Nouvelle version !", diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index e65a9205..6d29b8cf 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -246,7 +246,7 @@ def do_formsemestre_create(args, silent=False): default=True, redirect=0, ) - _group_id = sco_groups.createGroup(partition_id, default=True) + _group_id = sco_groups.create_group(partition_id, default=True) # news if "titre" not in args: diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 2e238fe5..416e531e 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -51,6 +51,7 @@ from app.scodoc import sco_etud from app.scodoc import sco_evaluations from app.scodoc import sco_formations from app.scodoc import sco_formsemestre +from app.scodoc import sco_groups_copy from app.scodoc import sco_modalites from app.scodoc import sco_moduleimpl from app.scodoc import sco_parcours_dut @@ -58,7 +59,6 @@ from app.scodoc import sco_permissions_check from app.scodoc import sco_portal_apogee from app.scodoc import sco_preferences from app.scodoc import sco_users -import six def _default_sem_title(F): @@ -171,7 +171,7 @@ def do_formsemestre_createwithmodules(edit=False): initvalues = sem semestre_id = initvalues["semestre_id"] # add associated modules to tf-checked: - ams = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) + ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) sem_module_ids = set([x["module_id"] for x in ams]) initvalues["tf-checked"] = ["MI" + str(x["module_id"]) for x in ams] for x in ams: @@ -205,11 +205,11 @@ def do_formsemestre_createwithmodules(edit=False): # on pourrait faire un simple module_list( ) # mais si on veut l'ordre du PPN (groupe par UE et matieres) il faut: mods = [] # liste de dicts - uelist = sco_edit_ue.do_ue_list({"formation_id": formation_id}) + uelist = sco_edit_ue.ue_list({"formation_id": formation_id}) for ue in uelist: - matlist = sco_edit_matiere.do_matiere_list({"ue_id": ue["ue_id"]}) + matlist = sco_edit_matiere.matiere_list({"ue_id": ue["ue_id"]}) for mat in matlist: - modsmat = sco_edit_module.do_module_list({"matiere_id": mat["matiere_id"]}) + modsmat = sco_edit_module.module_list({"matiere_id": mat["matiere_id"]}) # XXX debug checks for m in modsmat: if m["ue_id"] != ue["ue_id"]: @@ -751,7 +751,7 @@ def do_formsemestre_createwithmodules(edit=False): # (retire le "MI" du début du nom de champs) checkedmods = [int(x[2:]) for x in tf[2]["tf-checked"]] sco_formsemestre.do_formsemestre_edit(tf[2]) - ams = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) + ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) existingmods = [x["module_id"] for x in ams] mods_tocreate = [x for x in checkedmods if not x in existingmods] # modules a existants a modifier @@ -767,7 +767,7 @@ def do_formsemestre_createwithmodules(edit=False): "responsable_id": tf[2]["MI" + str(module_id)], } moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs) - mod = sco_edit_module.do_module_list({"module_id": module_id})[0] + mod = sco_edit_module.module_list({"module_id": module_id})[0] msg += ["création de %s (%s)" % (mod["code"], mod["titre"])] # INSCRIPTIONS DES ETUDIANTS log( @@ -801,7 +801,7 @@ def do_formsemestre_createwithmodules(edit=False): ok, diag = formsemestre_delete_moduleimpls(formsemestre_id, mods_todelete) msg += diag for module_id in mods_toedit: - moduleimpl_id = sco_moduleimpl.do_moduleimpl_list( + moduleimpl_id = sco_moduleimpl.moduleimpl_list( formsemestre_id=formsemestre_id, module_id=module_id )[0]["moduleimpl_id"] modargs = { @@ -813,7 +813,7 @@ def do_formsemestre_createwithmodules(edit=False): sco_moduleimpl.do_moduleimpl_edit( modargs, formsemestre_id=formsemestre_id ) - mod = sco_edit_module.do_module_list({"module_id": module_id})[0] + mod = sco_edit_module.module_list({"module_id": module_id})[0] if msg: msg_html = ( @@ -846,10 +846,10 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del): msg = [] for module_id in module_ids_to_del: # get id - moduleimpl_id = sco_moduleimpl.do_moduleimpl_list( + moduleimpl_id = sco_moduleimpl.moduleimpl_list( formsemestre_id=formsemestre_id, module_id=module_id )[0]["moduleimpl_id"] - mod = sco_edit_module.do_module_list({"module_id": module_id})[0] + 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}) if evals: @@ -1015,7 +1015,7 @@ def do_formsemestre_clone( formsemestre_id = sco_formsemestre.do_formsemestre_create(args) log("created formsemestre %s" % formsemestre_id) # 2- create moduleimpls - mods_orig = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=orig_formsemestre_id) + mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id) for mod_orig in mods_orig: args = mod_orig.copy() args["formsemestre_id"] = formsemestre_id @@ -1074,32 +1074,11 @@ def do_formsemestre_clone( args["formsemestre_id"] = formsemestre_id _ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args) - # 5- Copy partitions + # 5- Copy partitions and groups if clone_partitions: - listgroups = [] - listnamegroups = [] - # Création des partitions: - for part in sco_groups.get_partitions_list(orig_formsemestre_id): - if part["partition_name"] != None: - partname = part["partition_name"] - new_partition_id = sco_groups.partition_create( - formsemestre_id, - partition_name=partname, - redirect=0, - ) - for g in sco_groups.get_partition_groups(part): - if g["group_name"] != None: - listnamegroups.append(g["group_name"]) - listgroups.append([new_partition_id, listnamegroups]) - listnamegroups = [] - - # Création des groupes dans les nouvelles partitions: - for newpart in sco_groups.get_partitions_list(formsemestre_id): - for g in listgroups: - if newpart["partition_id"] == g[0]: - part_id = g[0] - for group_name in g[1]: - _ = sco_groups.createGroup(part_id, group_name=group_name) + sco_groups_copy.clone_partitions_and_groups( + orig_formsemestre_id, formsemestre_id + ) return formsemestre_id @@ -1212,7 +1191,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new) et met à jour les décisions de jury (validations d'UE). """ # re-associate moduleimpls to new modules: - modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) + modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) for mod in modimpls: mod["module_id"] = modules_old2new[mod["module_id"]] sco_moduleimpl.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id) @@ -1329,7 +1308,7 @@ def do_formsemestre_delete(formsemestre_id): sco_cache.EvaluationCache.invalidate_sem(formsemestre_id) # --- Destruction des modules de ce semestre - mods = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) + mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) for mod in mods: # evaluations evals = sco_evaluations.do_evaluation_list( diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index 23cea7a4..412e06d4 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -439,7 +439,7 @@ def _list_ue_with_coef_and_validations(sem, etudid): """ cnx = ndb.GetDBConnexion() formsemestre_id = sem["formsemestre_id"] - ue_list = sco_edit_ue.do_ue_list({"formation_id": sem["formation_id"]}) + ue_list = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]}) for ue in ue_list: # add coefficient uecoef = sco_formsemestre.formsemestre_uecoef_list( diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index c6bd3fab..bbfb81b9 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -237,7 +237,7 @@ def do_formsemestre_inscription_with_modules( gdone[group_id] = 1 # inscription a tous les modules de ce semestre - modimpls = sco_moduleimpl.do_moduleimpl_withmodule_list( + modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id ) for mod in modimpls: @@ -448,7 +448,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id): ] # Cherche les moduleimpls et les inscriptions - mods = sco_moduleimpl.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) + mods = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) inscr = sco_moduleimpl.do_moduleimpl_inscription_list(etudid=etudid) # Formulaire modimpls_by_ue_ids = scu.DictDefault(defaultvalue=[]) # ue_id : [ moduleimpl_id ] @@ -680,7 +680,7 @@ def do_moduleimpl_incription_options( # inscriptions for moduleimpl_id in a_inscrire: # verifie que ce module existe bien - mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) + mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) if len(mods) != 1: raise ScoValueError( "inscription: invalid moduleimpl_id: %s" % moduleimpl_id @@ -693,7 +693,7 @@ def do_moduleimpl_incription_options( # desinscriptions for moduleimpl_id in a_desinscrire: # verifie que ce module existe bien - mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) + mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) if len(mods) != 1: raise ScoValueError( "desinscription: invalid moduleimpl_id: %s" % moduleimpl_id diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 5aa948e4..26aab160 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -141,7 +141,7 @@ def formsemestre_status_menubar(sem): }, { "title": "Voir la formation %(acronyme)s (v%(version)s)" % F, - "endpoint": "notes.ue_list", + "endpoint": "notes.ue_table", "args": {"formation_id": sem["formation_id"]}, "enabled": True, "helpmsg": "Tableau de bord du semestre", @@ -453,7 +453,7 @@ def retreive_formsemestre_from_request() -> int: if "formsemestre_id" in args: formsemestre_id = args["formsemestre_id"] elif "moduleimpl_id" in args and args["moduleimpl_id"]: - modimpl = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=args["moduleimpl_id"]) + modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"]) if not modimpl: return None # suppressed ? modimpl = modimpl[0] @@ -463,7 +463,7 @@ def retreive_formsemestre_from_request() -> int: if not E: return None # evaluation suppressed ? E = E[0] - modimpl = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] + modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] formsemestre_id = modimpl["formsemestre_id"] elif "group_id" in args: group = sco_groups.get_group(args["group_id"]) @@ -593,9 +593,7 @@ 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.do_moduleimpl_withmodule_list( - formsemestre_id=formsemestre_id - ) + Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) R = [] sum_coef = 0 @@ -885,7 +883,7 @@ def html_expr_diagnostic(diagnostics): last_id, last_msg = None, None for diag in diagnostics: if "moduleimpl_id" in diag: - mod = sco_moduleimpl.do_moduleimpl_withmodule_list( + mod = sco_moduleimpl.moduleimpl_withmodule_list( moduleimpl_id=diag["moduleimpl_id"] )[0] H.append( @@ -898,7 +896,7 @@ def html_expr_diagnostic(diagnostics): ) else: if diag["ue_id"] != last_id or diag["msg"] != last_msg: - ue = sco_edit_ue.do_ue_list({"ue_id": diag["ue_id"]})[0] + ue = sco_edit_ue.ue_list({"ue_id": diag["ue_id"]})[0] H.append( '
Formation: |
- {F['titre']}""",
]
if sem["semestre_id"] >= 0:
@@ -982,9 +980,7 @@ 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.do_moduleimpl_withmodule_list(
- formsemestre_id=formsemestre_id
- )
+ Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
# inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
# args={"formsemestre_id": formsemestre_id}
# )
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index 7c49c792..310173fd 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -27,7 +27,7 @@
"""Semestres: validation semestre et UE dans parcours
"""
-import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error, time, datetime
+import time
import flask
from flask import url_for, g, request
@@ -494,7 +494,7 @@ def formsemestre_recap_parcours_table(
with_links=False,
with_all_columns=True,
a_url="",
- sem_info={},
+ sem_info=None,
show_details=False,
):
"""Tableau HTML recap parcours
@@ -502,6 +502,7 @@ def formsemestre_recap_parcours_table(
sem_info = { formsemestre_id : txt } permet d'ajouter des informations associées à chaque semestre
with_all_columns: si faux, pas de colonne "assiduité".
"""
+ sem_info = sem_info or {}
H = []
linktmpl = '%s'
minuslink = linktmpl % scu.icontag("minus_img", border="0", alt="-")
@@ -1021,7 +1022,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
]
# Toutes les UE de cette formation sont présentées (même celles des autres semestres)
- ues = sco_edit_ue.do_ue_list({"formation_id": Fo["formation_id"]})
+ ues = sco_edit_ue.ue_list({"formation_id": Fo["formation_id"]})
ue_names = ["Choisir..."] + ["%(acronyme)s %(titre)s" % ue for ue in ues]
ue_ids = [""] + [ue["ue_id"] for ue in ues]
tf = TrivialFormulator(
@@ -1233,7 +1234,7 @@ def check_formation_ues(formation_id):
définition du programme: cette fonction retourne un bout de HTML
à afficher pour prévenir l'utilisateur, ou '' si tout est ok.
"""
- ues = sco_edit_ue.do_ue_list({"formation_id": formation_id})
+ ues = sco_edit_ue.ue_list({"formation_id": formation_id})
ue_multiples = {} # { ue_id : [ liste des formsemestre ] }
for ue in ues:
# formsemestres utilisant cette ue ?
diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py
index 0a776813..7a31b995 100644
--- a/app/scodoc/sco_groups.py
+++ b/app/scodoc/sco_groups.py
@@ -47,7 +47,7 @@ from flask import url_for, make_response
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
-from app import log
+from app import log, cache
from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
@@ -59,20 +59,6 @@ from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator
-import six
-
-
-def checkGroupName(
- groupName,
-): # XXX unused: now allow any string as a group or partition name
- "Raises exception if not a valid group name"
- if groupName and (
- not re.match(r"^\w+$", groupName)
- or (scu.simplesqlquote(groupName) != groupName)
- ):
- log("!!! invalid group name: " + groupName)
- raise ValueError("invalid group name: " + groupName)
-
partitionEditor = ndb.EditableTable(
"partition",
@@ -219,7 +205,7 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
partition_id = partition_create(
formsemestre_id, default=True, redirect=False
)
- group_id = createGroup(partition_id, default=True)
+ group_id = create_group(partition_id, default=True)
return group_id
# debug check
if len(r) != 1:
@@ -452,6 +438,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
return etud
+@cache.memoize(timeout=50) # seconds
def get_etud_groups_in_partition(partition_id):
"""Returns { etudid : group }, with all students in this partition"""
infos = ndb.SimpleDictFetch(
@@ -724,7 +711,7 @@ def setGroups(
# Supprime les groupes indiqués comme supprimés:
for group_id in groupsToDelete:
- suppressGroup(group_id, partition_id=partition_id)
+ delete_group(group_id, partition_id=partition_id)
# Crée les nouveaux groupes
for line in groupsToCreate.split("\n"): # for each group_name (one per line)
@@ -732,11 +719,7 @@ def setGroups(
group_name = fs[0].strip()
if not group_name:
continue
- # ajax arguments are encoded in utf-8:
- # group_name = six.text_type(group_name, "utf-8").encode(
- # scu.SCO_ENCODING
- # ) # #py3 #sco8
- group_id = createGroup(partition_id, group_name)
+ group_id = create_group(partition_id, group_name)
# Place dans ce groupe les etudiants indiqués:
for etudid in fs[1:-1]:
change_etud_group_in_partition(etudid, group_id, partition)
@@ -749,7 +732,7 @@ def setGroups(
return response
-def createGroup(partition_id, group_name="", default=False):
+def create_group(partition_id, group_name="", default=False) -> int:
"""Create a new group in this partition"""
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
@@ -769,12 +752,12 @@ def createGroup(partition_id, group_name="", default=False):
group_id = groupEditor.create(
cnx, {"partition_id": partition_id, "group_name": group_name}
)
- log("createGroup: created group_id=%s" % group_id)
+ log("create_group: created group_id=%s" % group_id)
#
return group_id
-def suppressGroup(group_id, partition_id=None):
+def delete_group(group_id, partition_id=None):
"""form suppression d'un groupe.
(ne desinscrit pas les etudiants, change juste leur
affectation aux groupes)
@@ -791,7 +774,7 @@ def suppressGroup(group_id, partition_id=None):
if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
log(
- "suppressGroup: group_id=%s group_name=%s partition_name=%s"
+ "delete_group: group_id=%s group_name=%s partition_name=%s"
% (group_id, group["group_name"], partition["partition_name"])
)
group_delete(group)
@@ -808,7 +791,7 @@ def partition_create(
if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if partition_name:
- partition_name = partition_name.strip()
+ partition_name = str(partition_name).strip()
if default:
partition_name = None
if not partition_name and not default:
@@ -840,7 +823,7 @@ def partition_create(
return partition_id
-def getArrowIconsTags():
+def get_arrow_icons_tags():
"""returns html tags for arrows"""
#
arrow_up = scu.icontag("arrow_up", title="remonter")
@@ -856,7 +839,7 @@ def editPartitionForm(formsemestre_id=None):
if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
partitions = get_partitions_list(formsemestre_id)
- arrow_up, arrow_down, arrow_none = getArrowIconsTags()
+ arrow_up, arrow_down, arrow_none = get_arrow_icons_tags()
suppricon = scu.icontag(
"delete_small_img", border="0", alt="supprimer", title="Supprimer"
)
@@ -1123,7 +1106,7 @@ def partition_rename(partition_id):
def partition_set_name(partition_id, partition_name, redirect=1):
"""Set partition name"""
- partition_name = partition_name.strip()
+ partition_name = str(partition_name).strip()
if not partition_name:
raise ValueError("partition name must be non empty")
partition = get_partition(partition_id)
@@ -1159,7 +1142,7 @@ def partition_set_name(partition_id, partition_name, redirect=1):
)
-def group_set_name(group_id, group_name, redirect=1):
+def group_set_name(group_id, group_name, redirect=True):
"""Set group name"""
if group_name:
group_name = group_name.strip()
@@ -1229,7 +1212,7 @@ def group_rename(group_id):
)
else:
# form submission
- return group_set_name(group_id, tf[2]["group_name"], redirect=1)
+ return group_set_name(group_id, tf[2]["group_name"])
def groups_auto_repartition(partition_id=None):
@@ -1304,7 +1287,7 @@ def groups_auto_repartition(partition_id=None):
# except:
# H.append(' Nom de groupe invalide: %s '%group_name) # return '\n'.join(H) + tf[1] + html_sco_header.sco_footer() - group_ids.append(createGroup(partition_id, group_name)) + group_ids.append(create_group(partition_id, group_name)) # nt = sco_cache.NotesTableCache.get(formsemestre_id) # > identdict identdict = nt.identdict @@ -1364,6 +1347,7 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"): """ from app.scodoc import sco_formsemestre_inscriptions + partition_name = str(partition_name) log("create_etapes_partition(%s)" % formsemestre_id) ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} @@ -1388,7 +1372,7 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"): groups_by_names = {g["group_name"]: g for g in groups} for etape in etapes: if not (etape in groups_by_names): - gid = createGroup(pid, etape) + gid = create_group(pid, etape) g = get_group(gid) groups_by_names[etape] = g # Place les etudiants dans les groupes @@ -1409,6 +1393,7 @@ def do_evaluation_listeetuds_groups( Si include_dems, compte aussi les etudiants démissionnaires (sinon, par défaut, seulement les 'I') """ + # nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et include_dems faux fromtables = [ "notes_moduleimpl_inscription Im", "notes_formsemestre_inscription Isem", @@ -1420,7 +1405,7 @@ def do_evaluation_listeetuds_groups( if not groups: return [] # no groups, so no students rg = ["gm.group_id = '%(group_id)s'" % g for g in groups] - rq = """and Isem.etudid = gm.etudid + rq = """and Isem.etudid = gm.etudid and gd.partition_id = p.id and p.formsemestre_id = Isem.formsemestre_id """ @@ -1433,7 +1418,7 @@ def do_evaluation_listeetuds_groups( req = ( "SELECT distinct Im.etudid FROM " + ", ".join(fromtables) - + """ WHERE Isem.etudid = Im.etudid + + """ WHERE Isem.etudid = Im.etudid and Im.moduleimpl_id = M.id and Isem.formsemestre_id = M.formsemestre_id and E.moduleimpl_id = M.id @@ -1444,10 +1429,9 @@ def do_evaluation_listeetuds_groups( req += " and Isem.etat='I'" req += r cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cursor = cnx.cursor() cursor.execute(req, {"evaluation_id": evaluation_id}) - res = cursor.fetchall() - return [x[0] for x in res] + return [x[0] for x in cursor] def do_evaluation_listegroupes(evaluation_id, include_default=False): @@ -1461,7 +1445,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False): else: c = " AND p.partition_name is not NULL" cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cursor = cnx.cursor() cursor.execute( """SELECT DISTINCT gd.id AS group_id FROM group_descr gd, group_membership gm, partition p, @@ -1475,8 +1459,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False): + c, {"evaluation_id": evaluation_id}, ) - res = cursor.fetchall() - group_ids = [x[0] for x in res] + group_ids = [x[0] for x in cursor] return listgroups(group_ids) diff --git a/app/scodoc/sco_groups_copy.py b/app/scodoc/sco_groups_copy.py new file mode 100644 index 00000000..77d6e1d0 --- /dev/null +++ b/app/scodoc/sco_groups_copy.py @@ -0,0 +1,66 @@ +from app import db + +from app.scodoc import sco_groups +import app.scodoc.notesdb as ndb + + +def clone_partitions_and_groups( + orig_formsemestre_id: int, formsemestre_id: int, inscrit_etuds=False +): + """Crée dans le semestre formsemestre_id les mêmes partitions et groupes que ceux + de orig_formsemestre_id. + Si inscrit_etuds, inscrit les mêmes étudiants (rarement souhaité). + """ + list_groups_per_part = [] + list_groups = [] + groups_old2new = {} # old group_id : new_group_id + # Création des partitions: + for part in sco_groups.get_partitions_list(orig_formsemestre_id): + if part["partition_name"] is not None: + partname = part["partition_name"] + new_partition_id = sco_groups.partition_create( + formsemestre_id, + partition_name=partname, + numero=part["numero"], + redirect=False, + ) + for group in sco_groups.get_partition_groups(part): + if group["group_name"] != None: + list_groups.append(group) + list_groups_per_part.append([new_partition_id, list_groups]) + list_groups = [] + + # Création des groupes dans les nouvelles partitions: + for newpart in sco_groups.get_partitions_list(formsemestre_id): + for (new_partition_id, list_groups) in list_groups_per_part: + if newpart["partition_id"] == new_partition_id: + for group in list_groups: + new_group_id = sco_groups.create_group( + new_partition_id, group_name=group["group_name"] + ) + groups_old2new[group["group_id"]] = new_group_id + # + if inscrit_etuds: + cnx = ndb.GetDBConnexion() + cursor = cnx.cursor() + for old_group_id, new_group_id in groups_old2new.items(): + cursor.execute( + """ + WITH etuds AS ( + SELECT gm.etudid + FROM group_membership gm, notes_formsemestre_inscription ins + WHERE ins.etudid = gm.etudid + AND ins.formsemestre_id = %(orig_formsemestre_id)s + AND gm.group_id=%(old_group_id)s + ) + INSERT INTO group_membership (etudid, group_id) + SELECT *, %(new_group_id)s FROM etuds + ON CONFLICT DO NOTHING + """, + { + "orig_formsemestre_id": orig_formsemestre_id, + "old_group_id": old_group_id, + "new_group_id": new_group_id, + }, + ) + cnx.commit() diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py index bc419690..d5e09b76 100644 --- a/app/scodoc/sco_groups_edit.py +++ b/app/scodoc/sco_groups_edit.py @@ -42,7 +42,7 @@ def affect_groups(partition_id): partition = sco_groups.get_partition(partition_id) formsemestre_id = partition["formsemestre_id"] if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): - raise AccessDenied("vous n'avez pas la permission d'effectuer cette opération") + raise AccessDenied("vous n'avez pas la permission de modifier les groupes") return render_template( "scolar/affect_groups.html", sco_header=html_sco_header.sco_header( diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 91444729..9a28d8d7 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -304,7 +304,7 @@ class DisplayedGroupsInfos(object): else: group_ids = [int(g) for g in group_ids] if not formsemestre_id and moduleimpl_id: - mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) + mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) if len(mods) != 1: raise ValueError("invalid moduleimpl_id") formsemestre_id = mods[0]["formsemestre_id"] @@ -777,13 +777,7 @@ def groups_table( m["parcours"] = Se.get_parcours_descr() m["codeparcours"], _ = sco_report.get_codeparcoursetud(etud) - def dicttakestr(d, keys): - r = [] - for k in keys: - r.append(str(d.get(k, ""))) - return r - - L = [dicttakestr(m, keys) for m in groups_infos.members] + L = [[m.get(k, "") for k in keys] for m in groups_infos.members] title = "etudiants_%s" % groups_infos.groups_filename xls = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) filename = title diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py index f5056b59..d7c360a4 100644 --- a/app/scodoc/sco_import_users.py +++ b/app/scodoc/sco_import_users.py @@ -27,27 +27,23 @@ """Import d'utilisateurs via fichier Excel """ -import random, time -import re +import random +import time from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.header import Header +from flask import g, url_for +from flask_login import current_user -from app import db, Departement +from app import db +from app import email +from app.auth.models import User, UserRole import app.scodoc.sco_utils as scu from app import log -from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoException +from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc import sco_excel from app.scodoc import sco_preferences from app.scodoc import sco_users -from flask import g -from flask_login import current_user -from app.auth.models import User, UserRole - -from app import email - TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept") COMMENTS = ( @@ -86,11 +82,11 @@ def generate_excel_sample(): ) -def import_excel_file(datafile): +def import_excel_file(datafile, force=""): """ Import scodoc users from Excel file. This method: - * checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any deprtment (or all) + * checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any department (or all) * Once the check is done ans successfull, build the list of users (does not check the data) * call :func:`import_users` to actually do the job history: scodoc7 with no SuperAdmin every Admin_XXX could import users. @@ -98,7 +94,6 @@ def import_excel_file(datafile): :return: same as import users """ # Check current user privilege - auth_dept = current_user.dept auth_name = str(current_user) if not current_user.is_administrator(): raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) @@ -127,7 +122,8 @@ def import_excel_file(datafile): del cols[tit] if cols or unknown: raise ScoValueError( - "colonnes incorrectes (on attend %d, et non %d)(colonnes manquantes: %s, colonnes invalides: %s)" + """colonnes incorrectes (on attend %d, et non %d) + (colonnes manquantes: %s, colonnes invalides: %s)""" % (len(TITLES), len(fs), list(cols.keys()), unknown) ) # ok, same titles... : build the list of dictionaries @@ -138,10 +134,10 @@ def import_excel_file(datafile): d[fs[i]] = line[i] users.append(d) - return import_users(users) + return import_users(users=users, force=force) -def import_users(users): +def import_users(users, force=""): """ Import users from a list of users_descriptors. @@ -182,19 +178,17 @@ def import_users(users): line = line + 1 user_ok, msg = sco_users.check_modif_user( 0, - ignore_optionals=False, + enforce_optionals=not force, user_name=u["user_name"], nom=u["nom"], prenom=u["prenom"], email=u["email"], - roles=u["roles"].split(","), + roles=[r for r in u["roles"].split(",") if r], dept=u["dept"], ) if not user_ok: append_msg("identifiant '%s' %s" % (u["user_name"], msg)) - # raise ScoValueError( - # "données invalides pour %s: %s" % (u["user_name"], msg) - # ) + u["passwd"] = generate_password() # # check identifiant @@ -224,7 +218,7 @@ def import_users(users): import_ok = False except ScoValueError as value_error: log("import_users: exception: abort create %s" % str(created.keys())) - raise ScoValueError(msg) # re-raise exception + raise ScoValueError(msg) from value_error if import_ok: for u in created.values(): # Création de l'utilisateur (via SQLAlchemy) @@ -244,7 +238,7 @@ def import_users(users): ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU""" -PASSLEN = 6 +PASSLEN = 8 RNG = random.Random(time.time()) @@ -259,23 +253,18 @@ def generate_password(): return "".join(RNG.sample(l, PASSLEN)) -def mail_password(u, context=None, reset=False): +def mail_password(user: dict, reset=False) -> None: "Send password by email" - if not u["email"]: + if not user["email"]: return - u[ - "url" - ] = ( - scu.ScoURL() - ) # TODO set auth page URL ? (shared by all departments) ../auth/login - + user["url"] = url_for("scodoc.index", _external=True) txt = ( """ Bonjour %(prenom)s %(nom)s, """ - % u + % user ) if reset: txt += ( @@ -285,10 +274,10 @@ votre mot de passe ScoDoc a été ré-initialisé. Le nouveau mot de passe est: %(passwd)s Votre nom d'utilisateur est %(user_name)s -Vous devrez changer ce mot de passe lors de votre première connexion +Vous devrez changer ce mot de passe lors de votre première connexion sur %(url)s """ - % u + % user ) else: txt += ( @@ -300,16 +289,15 @@ Votre mot de passe est: %(passwd)s Le logiciel est accessible sur: %(url)s -Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur -votre nom en haut à gauche de la page d'accueil). +Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil). """ - % u + % user ) txt += ( """ - -ScoDoc est un logiciel libre développé à l'Université Paris 13 par Emmanuel Viennet. +_______ +ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc. Pour plus d'informations sur ce logiciel, voir %s """ @@ -321,4 +309,4 @@ Pour plus d'informations sur ce logiciel, voir %s else: subject = "Votre accès ScoDoc" sender = sco_preferences.get_preference("email_from_addr") - email.send_email(subject, sender, [u["email"]], txt) + email.send_email(subject, sender, [user["email"]], txt) diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index a9ea6ce8..cee1a4c3 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -228,8 +228,8 @@ def _make_table_notes( return " Aucune évaluation ! " E = evals[0] moduleimpl_id = E["moduleimpl_id"] - M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] + 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"]) # (debug) check that all evals are in same module: for e in evals: @@ -872,9 +872,7 @@ def formsemestre_check_absences_html(formsemestre_id): """, ] # Modules, dans l'ordre - Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( - formsemestre_id=formsemestre_id - ) + 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"]} diff --git a/app/scodoc/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py index b1d0ea47..739061b1 100644 --- a/app/scodoc/sco_moduleimpl.py +++ b/app/scodoc/sco_moduleimpl.py @@ -100,7 +100,7 @@ def do_moduleimpl_delete(oid, formsemestre_id=None): ) # > moduleimpl_delete -def do_moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None): +def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None): "list moduleimpls" args = locals() cnx = ndb.GetDBConnexion() @@ -122,10 +122,11 @@ def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None): ) # > modif moduleimpl -def do_moduleimpl_withmodule_list( +def moduleimpl_withmodule_list( moduleimpl_id=None, formsemestre_id=None, module_id=None ): - """Liste les moduleimpls et ajoute dans chacun le module correspondant + """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. Attention: Cette fonction fait partie de l'API ScoDoc 7 et est publiée. @@ -134,22 +135,33 @@ def do_moduleimpl_withmodule_list( from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module - args = locals() - modimpls = do_moduleimpl_list( + modimpls = moduleimpl_list( **{ "moduleimpl_id": moduleimpl_id, "formsemestre_id": formsemestre_id, "module_id": module_id, } ) - for mo in modimpls: - mo["module"] = sco_edit_module.do_module_list( - args={"module_id": mo["module_id"]} - )[0] - mo["ue"] = sco_edit_ue.do_ue_list(args={"ue_id": mo["module"]["ue_id"]})[0] - mo["matiere"] = sco_edit_matiere.do_matiere_list( - args={"matiere_id": mo["module"]["matiere_id"]} - )[0] + ues = {} + matieres = {} + modules = {} + for mi in modimpls: + module_id = mi["module_id"] + if not mi["module_id"] in modules: + modules[module_id] = sco_edit_module.module_list( + args={"module_id": module_id} + )[0] + mi["module"] = modules[module_id] + ue_id = mi["module"]["ue_id"] + if not ue_id in ues: + ues[ue_id] = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] + mi["ue"] = ues[ue_id] + matiere_id = mi["module"]["matiere_id"] + if not matiere_id in matieres: + matieres[matiere_id] = sco_edit_matiere.matiere_list( + args={"matiere_id": matiere_id} + )[0] + mi["matiere"] = matieres[matiere_id] # tri par semestre/UE/numero_matiere/numero_module modimpls.sort( @@ -173,7 +185,7 @@ def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None): return _moduleimpl_inscriptionEditor.list(cnx, args) -def do_moduleimpl_listeetuds(moduleimpl_id): +def moduleimpl_listeetuds(moduleimpl_id): "retourne liste des etudids inscrits a ce module" req = """SELECT DISTINCT Im.etudid FROM notes_moduleimpl_inscription Im, @@ -306,7 +318,7 @@ def can_change_module_resp(moduleimpl_id): """Check if current user can modify module resp. (raise exception if not). = Admin, et dir des etud. (si option l'y autorise) """ - M = do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] + M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] # -- check lock sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) if not sem["etat"]: @@ -322,7 +334,7 @@ def can_change_module_resp(moduleimpl_id): def can_change_ens(moduleimpl_id, raise_exc=True): "check if current user can modify ens list (raise exception if not)" - M = do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] + M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] # -- check lock sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) if not sem["etat"]: diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index 93d420d0..5efe51f0 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -63,9 +63,9 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False): * Si pas les droits: idem en readonly """ - M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] + M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] formsemestre_id = M["formsemestre_id"] - mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] + mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] sem = sco_formsemestre.get_formsemestre(formsemestre_id) # -- check lock if not sem["etat"]: @@ -263,9 +263,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id): can_change = authuser.has_permission(Permission.ScoEtudInscrit) and sem["etat"] # Liste des modules - Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( - formsemestre_id=formsemestre_id - ) + Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) # Decrit les inscriptions aux modules: commons = [] # modules communs a tous les etuds du semestre options = [] # modules ou seuls quelques etudiants sont inscrits @@ -341,7 +339,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id): UECaps = get_etuds_with_capitalized_ue(formsemestre_id) if UECaps: H.append('Etudiants avec UEs capitalisées:
Utilisateur: %s" % user.user_name) - info = user.to_dict() - if info: - H.append(" (%(status_txt)s)" % info) - H.append("") - if not info: - H.append( - "L' utilisateur '%s' n'est pas défini dans ce module. " % user_name - ) - if user.has_permission(Permission.ScoEditAllNotes, dept): - H.append("(il peut modifier toutes les notes de %s) " % dept) - if user.has_permission(Permission.ScoEditAllEvals, dept): - H.append("(il peut modifier toutes les évaluations de %s) " % dept) - if user.has_permission(Permission.ScoImplement, dept): - H.append("(il peut creer des formations en %s) " % dept) - else: - H.append( - """
- Login : %(user_name)s
Se déconnecter: logout ' - % url_for("auth.logout") - ) - # Liste des permissions - H.append( - ' ") - - if current_user.has_permission(Permission.ScoUsersAdmin, dept): - H.append( - 'Liste de tous les utilisateurs ' - % url_for("users.index_html", scodoc_dept=g.scodoc_dept) - ) - return "\n".join(H) + F - - def check_modif_user( edit, - ignore_optionals=False, + enforce_optionals=False, user_name="", nom="", prenom="", @@ -400,9 +287,9 @@ def check_modif_user( returns (ok, msg) - ok : si vrai, peut continuer avec ces parametres (si ok est faux, l'utilisateur peut quand même forcer la creation) - - msg: message warning a presenter l'utilisateur + - msg: message warning à presenter à l'utilisateur """ - MSG_OPT = """Attention: %s (vous pouvez forcer l'opération en cochant "Ignorer les avertissements" en bas de page)""" + MSG_OPT = """Attention: (vous pouvez forcer l'opération en cochant "Ignorer les avertissements" en bas de page)""" # ce login existe ? user = _user_list(user_name) if edit and not user: # safety net, le user_name ne devrait pas changer @@ -417,11 +304,11 @@ def check_modif_user( "identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)" % user_name, ) - if ignore_optionals and len(user_name) > 64: + if enforce_optionals and len(user_name) > 64: return False, "identifiant '%s' trop long (64 caractères)" % user_name - if ignore_optionals and len(nom) > 64: + if enforce_optionals and len(nom) > 64: return False, "nom '%s' trop long (64 caractères)" % nom + MSG_OPT - if ignore_optionals and len(prenom) > 64: + if enforce_optionals and len(prenom) > 64: return False, "prenom '%s' trop long (64 caractères)" % prenom + MSG_OPT # check that tha same user_name has not already been described in this import if not email: @@ -432,13 +319,22 @@ def check_modif_user( return False, "l'adresse mail semble incorrecte" # check département if ( - ignore_optionals + enforce_optionals and dept != "" and Departement.query.filter_by(acronym=dept).first() is None ): - return False, "département '%s' inexistant" % u["dept"] + MSG_OPT - if ignore_optionals and not roles: + return False, "département '%s' inexistant" % dept + MSG_OPT + if enforce_optionals and not roles: return False, "aucun rôle sélectionné, êtes vous sûr ?" + MSG_OPT + # Unicité du mail + users_with_this_mail = User.query.filter_by(email=email).all() + if edit: # modification + if email != user["email"] and len(users_with_this_mail) > 0: + return False, "un autre utilisateur existe déjà avec cette adresse mail" + else: # création utilisateur + if len(users_with_this_mail) > 0: + return False, "un autre utilisateur existe déjà avec cette adresse mail" + # ok # Des noms/prénoms semblables existent ? nom = nom.lower().strip() @@ -450,7 +346,7 @@ def check_modif_user( minmatch = 1 else: minmatch = 0 - if len(similar_users) > minmatch: + if enforce_optionals and len(similar_users) > minmatch: return ( False, "des utilisateurs proches existent: " diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 70527702..e5dec56b 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -290,8 +290,7 @@ SCO_DEV_MAIL = "emmanuel.viennet@gmail.com" # SVP ne pas changer # Adresse pour l'envoi des dumps (pour assistance technnique): # ne pas changer (ou vous perdez le support) -SCO_DUMP_UP_URL = "https://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/upload-dump" -# SCO_DUMP_UP_URL = "http://192.168.56.1:5000/upload_dump" +SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump" CSV_FIELDSEP = ";" CSV_LINESEP = "\n" diff --git a/app/static/js/user_form.js b/app/static/js/user_form.js new file mode 100644 index 00000000..4ad06672 --- /dev/null +++ b/app/static/js/user_form.js @@ -0,0 +1,30 @@ + +function refresh() { + if ($("input[name='welcome:list']").is(":checked")) { + $("input[name='reset_password:list']").closest("tr").css("display", "table-row") + if ($("input[name='reset_password:list']").is(":checked")) { + $("#tf_password").closest('tr').css("display", "none"); + $("#tf_password2").closest('tr').css("display", "none"); + } else { + // Le mot de passe doit être saisi + $("#tf_password").closest('tr').css("display", "table-row"); + $("#tf_password2").closest('tr').css("display", "table-row"); + } + } else { + // Le mot de passe doit être saisi + $("input[name='reset_password:list']").closest("tr").css("display", "none") + $("#tf_password").closest('tr').css("display", "table-row"); + $("#tf_password2").closest('tr').css("display", "table-row"); + } +} + +$(function () { + $("input[name='welcome:list']").click(function () { + refresh(); + }) + $("input[name='reset_password:list']").click(function () { + refresh(); + }) + refresh(); +}) + diff --git a/app/templates/auth/change_password.html b/app/templates/auth/change_password.html new file mode 100644 index 00000000..975e8bc9 --- /dev/null +++ b/app/templates/auth/change_password.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% macro render_field(field) %} + {{ field.label }} |
+ {{ field(**kwargs)|safe }}
+ {% if field.errors %}
+ |
+
Modification du compte ScoDoc {{form.user_name.data}}+
+
+ Identifiez-vous avez votre mot de passe actuel +Vous pouvez changer le mot de passe et/ou l'adresse email. +Les champs vides ne seront pas changés. + |