diff --git a/app/auth/models.py b/app/auth/models.py index 073f687e9a..3115119135 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -353,8 +353,8 @@ class User(UserMixin, db.Model): return mails # Permissions management: - def has_permission(self, perm: int, dept=False): - """Check if user has permission `perm` in given `dept`. + def has_permission(self, perm: int, dept: str = False): + """Check if user has permission `perm` in given `dept` (acronym). Similar to Zope ScoDoc7 `has_permission`` Args: diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 82b6d3dc3e..0435281a36 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -70,16 +70,11 @@ class Assiduite(db.Model): def to_dict(self, format_api=True) -> dict: """Retourne la représentation json de l'assiduité""" etat = self.etat - username = self.user_id + user: User = None if format_api: etat = EtatAssiduite.inverse().get(self.etat).name if self.user_id is not None: - user: User = db.session.get(User, self.user_id) - - if user is None: - username = "Non renseigné" - else: - username = user.get_prenomnom() + user = db.session.get(User, self.user_id) data = { "assiduite_id": self.id, "etudid": self.etudid, @@ -90,7 +85,8 @@ class Assiduite(db.Model): "etat": etat, "desc": self.description, "entry_date": self.entry_date, - "user_id": username, + "user_id": None if user is None else user.id, # l'uid + "user_name": None if user is None else user.user_name, # le login "est_just": self.est_just, "external_data": self.external_data, } diff --git a/app/models/departements.py b/app/models/departements.py index 6f3f775989..d4005d24d8 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -80,8 +80,6 @@ class Departement(db.Model): def create_dept(acronym: str, visible=True) -> Departement: "Create new departement" - from app.models import ScoPreference - if Departement.invalid_dept_acronym(acronym): raise ScoValueError("acronyme departement invalide") existing = Departement.query.filter_by(acronym=acronym).count() diff --git a/app/models/etudiants.py b/app/models/etudiants.py index a451526f7d..6dc036782d 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -86,6 +86,50 @@ class Identite(db.Model): f"" ) + def clone(self, not_copying=(), new_dept_id: int = None): + """Clone, not copying the given attrs + Clone aussi les adresses. + Si new_dept_id est None, le nouvel étudiant n'a pas de département. + Attention: la copie n'a pas d'id avant le prochain flush ou commit. + """ + if new_dept_id == self.dept_id: + raise ScoValueError( + "clonage étudiant: le département destination est identique à celui de départ" + ) + # Vérifie les contraintes d'unicité + # ("dept_id", "code_nip") et ("dept_id", "code_ine") + if ( + self.code_nip is not None + and Identite.query.filter_by( + dept_id=new_dept_id, code_nip=self.code_nip + ).count() + > 0 + ) or ( + self.code_ine is not None + and Identite.query.filter_by( + dept_id=new_dept_id, code_ine=self.code_ine + ).count() + > 0 + ): + raise ScoValueError( + """clonage étudiant: un étudiant de même code existe déjà + dans le département destination""" + ) + d = dict(self.__dict__) + d.pop("id", None) # get rid of id + d.pop("_sa_instance_state", None) # get rid of SQLAlchemy special attr + d.pop("departement", None) # relationship + d["dept_id"] = new_dept_id + for k in not_copying: + d.pop(k, None) + copy = self.__class__(**d) + copy.adresses = [adr.clone() for adr in self.adresses] + db.session.add(copy) + log( + f"cloning etud <{self.id} {self.nom!r} {self.prenom!r}> in dept_id={new_dept_id}" + ) + return copy + def html_link_fiche(self) -> str: "lien vers la fiche" return f""" "FormSemestre": - """ "FormSemestre ou 404, cherche uniquement dans le département courant""" + def get_formsemestre( + cls, formsemestre_id: int, dept_id: int = None + ) -> "FormSemestre": + """ "FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant""" if g.scodoc_dept: + dept_id = dept_id if dept_id is not None else g.scodoc_dept_id + if dept_id is not None: return cls.query.filter_by( - id=formsemestre_id, dept_id=g.scodoc_dept_id + id=formsemestre_id, dept_id=dept_id ).first_or_404() return cls.query.filter_by(id=formsemestre_id).first_or_404() diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py index b673a3eb2d..b0334ae78c 100644 --- a/app/scodoc/TrivialFormulator.py +++ b/app/scodoc/TrivialFormulator.py @@ -237,7 +237,7 @@ class TF(object): def setdefaultvalues(self): "set default values and convert numbers to strings" - for (field, descr) in self.formdescription: + for field, descr in self.formdescription: # special case for boolcheckbox if descr.get("input_type", None) == "boolcheckbox" and self.submitted(): if field not in self.values: @@ -278,7 +278,7 @@ class TF(object): "check values. Store .result and returns msg" ok = 1 msg = [] - for (field, descr) in self.formdescription: + for field, descr in self.formdescription: val = self.values[field] # do not check "unckecked" items if descr.get("withcheckbox", False): @@ -287,7 +287,7 @@ class TF(object): # null values allow_null = descr.get("allow_null", True) if not allow_null: - if val == "" or val == None: + if val is None or (isinstance(val, str) and not val.strip()): msg.append( "Le champ '%s' doit être renseigné" % descr.get("title", field) ) @@ -871,7 +871,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts); def _ReadOnlyVersion(self, formdescription): "Generate HTML for read-only view of the form" R = [''] - for (field, descr) in formdescription: + for field, descr in formdescription: R.append(self._ReadOnlyElement(field, descr)) R.append("
") return R diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index c6dbd4472b..125569d019 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -19,26 +19,35 @@ class Trace: """gestionnaire de la trace des fichiers justificatifs""" def __init__(self, path: str) -> None: - log(f"init Trace {path}") self.path: str = path + "/_trace.csv" self.content: dict[str, list[datetime, datetime, str]] = {} self.import_from_file() def import_from_file(self): """import trace from file""" - if os.path.isfile(self.path): - with open(self.path, "r", encoding="utf-8") as file: + + def import_from_csv(path): + with open(path, "r", encoding="utf-8") as file: for line in file.readlines(): csv = line.split(",") if len(csv) < 4: continue fname: str = csv[0] + if fname not in os.listdir(self.path.replace("/_trace.csv", "")): + continue entry_date: datetime = is_iso_formated(csv[1], True) delete_date: datetime = is_iso_formated(csv[2], True) user_id = csv[3] - self.content[fname] = [entry_date, delete_date, user_id] + if os.path.isfile(self.path): + import_from_csv(self.path) + else: + parent_dir: str = self.path[: self.path.rfind("/", 0, self.path.rfind("/"))] + if os.path.isfile(parent_dir + "/_trace.csv"): + import_from_csv(parent_dir + "/_trace.csv") + self.save_trace() + def set_trace(self, *fnames: str, mode: str = "entry", current_user: str = None): """Ajoute une trace du fichier donné mode : entry / delete @@ -57,9 +66,11 @@ class Trace: ) self.save_trace() - def save_trace(self): + def save_trace(self, new_path: str = None): """Enregistre la trace dans le fichier _trace.csv""" lines: list[str] = [] + if new_path is not None: + self.path = new_path for fname, traced in self.content.items(): date_fin: datetime or None = traced[1].isoformat() if traced[1] else "None" if traced[0] is not None: @@ -126,7 +137,6 @@ class JustificatifArchiver(BaseArchiver): ) fname: str = self.store(archive_id, filename, data, dept_id=etud.dept_id) - log(f"obj_dir {self.get_obj_dir(etud.id, dept_id=etud.dept_id)} | {archive_id}") trace = Trace(archive_id) trace.set_trace(fname, mode="entry") if user_id is not None: diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index a3786c828c..61a44cf006 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -136,7 +136,7 @@ class WrapDict(object): try: value = self.dict[key] except KeyError: - raise + return f"XXX {key} invalide XXX" if value is None: return self.none_value return value diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 38110c6c8d..2d58b57734 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -31,6 +31,7 @@ import flask from flask import url_for, g from flask_login import current_user +import sqlalchemy as sa from app import db, log @@ -72,7 +73,7 @@ _evaluationEditor = ndb.EditableTable( ) -def get_evaluation_dict(args: dict) -> list[dict]: +def get_evaluations_dict(args: dict) -> list[dict]: """Liste evaluations, triées numero (or most recent date first). Fonction de transition pour ancien code ScoDoc7. @@ -83,7 +84,12 @@ def get_evaluation_dict(args: dict) -> list[dict]: 'descrheure' : ' de 15h00 à 16h30' """ # calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi - return [e.to_dict() for e in Evaluation.query.filter_by(**args)] + return [ + e.to_dict() + for e in Evaluation.query.filter_by(**args).order_by( + sa.desc(Evaluation.numero), sa.desc(Evaluation.date_debut) + ) + ] def do_evaluation_list_in_formsemestre(formsemestre_id): @@ -91,7 +97,7 @@ def do_evaluation_list_in_formsemestre(formsemestre_id): mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) evals = [] for modimpl in mods: - evals += get_evaluation_dict(args={"moduleimpl_id": modimpl["moduleimpl_id"]}) + evals += get_evaluations_dict(args={"moduleimpl_id": modimpl["moduleimpl_id"]}) return evals @@ -161,7 +167,6 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1): (published) """ evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) - moduleimpl_id = evaluation.moduleimpl_id redirect = int(redirect) # access: can change eval ? if not evaluation.moduleimpl.can_edit_evaluation(current_user): @@ -171,12 +176,12 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1): Evaluation.moduleimpl_evaluation_renumber( evaluation.moduleimpl, only_if_unumbered=True ) - e = get_evaluation_dict(args={"evaluation_id": evaluation_id})[0] + e = get_evaluations_dict(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 = get_evaluation_dict({"moduleimpl_id": e["moduleimpl_id"]}) + mod_evals = get_evaluations_dict({"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 diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 7c5ffd4743..f6beeceb59 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -133,7 +133,7 @@ def do_evaluation_etat( ) # { etudid : note } # ---- Liste des groupes complets et incomplets - E = sco_evaluation_db.get_evaluation_dict(args={"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.get_evaluations_dict(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"] == ModuleType.MALUS # True si module de malus diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 7b1ceb41a9..bd0f6f42aa 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1445,7 +1445,7 @@ def do_formsemestre_delete(formsemestre_id): mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) for mod in mods: # evaluations - evals = sco_evaluation_db.get_evaluation_dict( + evals = sco_evaluation_db.get_evaluations_dict( args={"moduleimpl_id": mod["moduleimpl_id"]} ) for e in evals: diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 282b51957b..7837c299f2 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -275,14 +275,16 @@ def do_formsemestre_inscription_with_modules( etat=scu.INSCRIT, etape=None, method="inscription_with_modules", + dept_id: int = None, ): """Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS (donc sauf le sport) + Si dept_id est spécifié, utilise ce département au lieu du courant. """ group_ids = group_ids or [] if isinstance(group_ids, int): group_ids = [group_ids] - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id) # inscription au semestre args = {"formsemestre_id": formsemestre_id, "etudid": etudid} if etat is not None: diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 1a0163b4ba..520af7e8f4 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -490,7 +490,7 @@ def retreive_formsemestre_from_request() -> int: modimpl = modimpl[0] formsemestre_id = modimpl["formsemestre_id"] elif "evaluation_id" in args: - E = sco_evaluation_db.get_evaluation_dict( + E = sco_evaluation_db.get_evaluations_dict( {"evaluation_id": args["evaluation_id"]} ) if not E: @@ -884,7 +884,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str: jour = datetime.date.today().isoformat(), group_ids=group.id, )}"> -
+
str: """Affiche suivi cohortes par numero de semestre""" annee_bac = str(annee_bac or "") annee_admission = str(annee_admission or "") @@ -794,14 +794,6 @@ def formsemestre_suivi_cohorte( '

Afficher les résultats en pourcentages

' % burl ) - help = ( - pplink - + """ -

Nombre d'étudiants dans chaque semestre. Les dates indiquées sont les dates approximatives de début des semestres (les semestres commençant à des dates proches sont groupés). Le nombre de diplômés est celui à la fin du semestre correspondant. Lorsqu'il y a moins de %s étudiants dans une case, vous pouvez afficher leurs noms en passant le curseur sur le chiffre.

-

Les menus permettent de n'étudier que certaines catégories d'étudiants (titulaires d'un type de bac, garçons ou filles). La case "restreindre aux primo-entrants" permet de ne considérer que les étudiants qui n'ont jamais été inscrits dans ScoDoc avant le semestre considéré.

- """ - % (MAX_ETUD_IN_DESCR,) - ) H = [ html_sco_header.sco_header(page_title=tab.page_title), @@ -824,7 +816,20 @@ def formsemestre_suivi_cohorte( percent=percent, ), t, - help, + f"""{pplink} +

Nombre d'étudiants dans chaque semestre. + Les dates indiquées sont les dates approximatives de début des semestres + (les semestres commençant à des dates proches sont groupés). Le nombre de diplômés + est celui à la fin du semestre correspondant. + Lorsqu'il y a moins de {MAX_ETUD_IN_DESCR} étudiants dans une case, vous pouvez + afficher leurs noms en passant le curseur sur le chiffre. +

+

Les menus permettent de n'étudier que certaines catégories + d'étudiants (titulaires d'un type de bac, garçons ou filles). + La case "restreindre aux primo-entrants" permet de ne considérer que les étudiants + qui n'ont jamais été inscrits dans ScoDoc avant le semestre considéré. +

+ """, expl, html_sco_header.sco_footer(), ] @@ -870,35 +875,33 @@ def _gen_form_selectetuds( else: selected = 'selected="selected"' F = [ - """
+ f"""

Bac: ") if bacspecialite: selected = "" else: selected = 'selected="selected"' F.append( - """  Bac/Specialité: + """ - % selected ) for b in bacspecialites: if bacspecialite == b: selected = 'selected="selected"' else: selected = "" - F.append('' % (b, selected, b)) + F.append(f'') F.append("") # F.append( @@ -910,46 +913,44 @@ def _gen_form_selectetuds( ) # F.append( - """  Genre: + """ - % selected ) for b in civilites: if civilite == b: selected = 'selected="selected"' else: selected = "" - F.append('' % (b, selected, b)) + F.append(f'') F.append("") F.append( - """  Statut: + """ - % selected ) for b in statuts: if statut == b: selected = 'selected="selected"' else: selected = "" - F.append('' % (b, selected, b)) + F.append(f'') F.append("") - if only_primo: - checked = 'checked="1"' - else: - checked = "" F.append( - '
Restreindre aux primo-entrants' - % checked + f"""
+ Restreindre aux primo-entrants + + + +

+
+ """ ) - F.append( - '' % formsemestre_id - ) - F.append('' % percent) - F.append("

") + return "\n".join(F) @@ -964,7 +965,7 @@ def _gen_select_annee(field, values, value) -> str: return menu_html + "" -def _descr_etud_set(etudids): +def _descr_etud_set(etudids) -> str: "textual html description of a set of etudids" etuds = [] for etudid in etudids: @@ -980,15 +981,22 @@ def _count_dem_reo(formsemestre_id, etudids): "count nb of demissions and reorientation in this etud set" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - + validations_annuelles = nt.get_validations_annee() if nt.is_apc else {} dems = set() reos = set() for etudid in etudids: if nt.get_etud_etat(etudid) == "D": dems.add(etudid) - dec = nt.get_etud_decision_sem(etudid) - if dec and dec["code"] in codes_cursus.CODES_SEM_REO: - reos.add(etudid) + if nt.is_apc: + # BUT: utilise les validations annuelles + validation = validations_annuelles.get(etudid) + if validation and validation.code in codes_cursus.CODES_SEM_REO: + reos.add(etudid) + else: + # Autres formations: validations de semestres + dec = nt.get_etud_decision_sem(etudid) + if dec and dec["code"] in codes_cursus.CODES_SEM_REO: + reos.add(etudid) return dems, reos diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py index 61b84c0c4a..dd18950485 100644 --- a/app/scodoc/sco_undo_notes.py +++ b/app/scodoc/sco_undo_notes.py @@ -149,7 +149,7 @@ def list_operations(evaluation_id): def evaluation_list_operations(evaluation_id): """Page listing operations on evaluation""" - E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id})[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] Ops = list_operations(evaluation_id) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index baf8105039..500b41094c 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -10,12 +10,12 @@ html, body { - margin: 0; - padding: 0; - width: 100%; background-color: var(--sco-color-background); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12pt; + margin: 0; + padding: 0; + width: 100%; } @media print { @@ -24,6 +24,10 @@ body { } } +div.container { + margin-bottom: 24px; +} + h1, h2, h3 { @@ -1672,6 +1676,10 @@ formsemestre_page_title .lock img { font-family: Arial, Helvetica, sans-serif; } +.menu-etudiant>li { + width: 200px !important; +} + span.inscr_addremove_menu { width: 150px; } diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index 28c4b6159a..c4885b3f8e 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -219,20 +219,27 @@ } function dayOnly() { + const { deb, fin } = getDates(); if (document.getElementById('justi_journee').checked) { document.getElementById("justi_date_debut").type = "date" + document.getElementById("justi_date_debut").value = deb.slice(0, deb.indexOf('T')) + document.getElementById("justi_date_fin").type = "date" + document.getElementById("justi_date_fin").value = fin.slice(0, fin.indexOf('T')) } else { document.getElementById("justi_date_debut").type = "datetime-local" + document.getElementById("justi_date_debut").value = `${deb}T${assi_morning}` + document.getElementById("justi_date_fin").type = "datetime-local" + document.getElementById("justi_date_fin").value = `${fin}T${assi_evening}` } } function getDates() { if (document.querySelector('.page #justi_journee').checked) { const date_str_deb = document.querySelector(".page #justi_date_debut").value - const date_str_fin = document.querySelector(".page #justi_date_debut").value + const date_str_fin = document.querySelector(".page #justi_date_fin").value diff --git a/app/templates/scolar/etud_copy_in_other_dept.j2 b/app/templates/scolar/etud_copy_in_other_dept.j2 new file mode 100644 index 0000000000..55b4f77ffd --- /dev/null +++ b/app/templates/scolar/etud_copy_in_other_dept.j2 @@ -0,0 +1,99 @@ +{# -*- mode: jinja-html -*- #} +{% extends 'base.j2' %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block app_content %} + +

Création d'une copie de {{ etud.html_link_fiche() | safe }}

+ +
+ +

Utiliser cette page lorsqu'un étudinat change de département. ScoDoc gère +séparéement les étudiants des départements. Il faut donc dans ce cas +exceptionnel créer une copie de l'étudiant et l'inscrire dans un semestre de son +nouveau département. Seules les donénes sur l'identité de l'étudiant (état +civil, adresse, ...) sont dupliquées. Dans le noveau département, les résultats +obtenus dans le département d'origine ne seront pas visibles. +

+ +

Si des UEs ou compétences de l'ancien département doivent être validées dans +le nouveau, il faudra utiliser ensuite une "validation d'UE antérieure". +

+ +

Attention: seuls les départements dans lesquels vous avez la permission +d'inscrire des étudiants sont présentés ici. Il faudra peut-être solliciter +l'administrateur de ce ScoDoc. +

+ +

Dans chaque département autorisés, seuls les semestres non verrouillés sont +montrés. Choisir le semestre destination et valider le formulaire. +

+ +

Ensuite, ne pas oublier d'inscrire l'étudiant à ses groupes, notamment son +parcours si besoin. +

+ +
+ +
+ {% for dept in departements.values() %} +
+
Département {{ dept.acronym }}
+ {% for sem in formsemestres_by_dept[dept.id]%} +
+ +
+ {% endfor %} +
+ {% endfor %} + + +
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index d8dda71680..f9a579d5a9 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -517,7 +517,7 @@ def ajout_justificatif_etud(): dept_id=g.scodoc_dept_id, ), assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"), - assi_evening=ScoDocSiteConfig.get("assi_evening_time", "18:00"), + assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"), ), ).build() @@ -1129,7 +1129,7 @@ def signal_assiduites_diff(): defdem=_get_etuds_dem_def(formsemestre), timeMorning=ScoDocSiteConfig.get("assi_morning_time", "08:00:00"), timeNoon=ScoDocSiteConfig.get("assi_lunch_time", "13:00:00"), - timeEvening=ScoDocSiteConfig.get("assi_evening_time", "18:00:00"), + timeEvening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00:00"), defaultDates=_get_days_between_dates(date_deb, date_fin), nonworkdays=_non_work_days(), ), diff --git a/app/views/notes.py b/app/views/notes.py index 9f198ba3bc..af1d011aff 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -407,14 +407,13 @@ def moduleimpl_evaluation_renumber(moduleimpl_id): ) Evaluation.moduleimpl_evaluation_renumber(modimpl) # redirect to moduleimpl page: - if redirect: - return flask.redirect( - url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=moduleimpl_id, - ) + return flask.redirect( + url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=moduleimpl_id, ) + ) sco_publish( diff --git a/app/views/scolar.py b/app/views/scolar.py index 7b13e2dd4f..7d1cf2e258 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -31,11 +31,12 @@ issu de ScoDoc7 / ZScolar.py Emmanuel Viennet, 2021 """ import datetime -import requests import time +import requests + import flask -from flask import url_for, flash, render_template, make_response +from flask import abort, flash, make_response, render_template, url_for from flask import g, request from flask_json import as_json from flask_login import current_user @@ -43,6 +44,7 @@ from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed from wtforms import SubmitField +import app from app import db from app import log from app.decorators import ( @@ -52,6 +54,7 @@ from app.decorators import ( permission_required_compat_scodoc7, ) from app.models import ( + Departement, FormSemestre, Identite, Partition, @@ -69,6 +72,7 @@ from app.scodoc.scolog import logdb from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( AccessDenied, + ScoPermissionDenied, ScoValueError, ) @@ -1770,6 +1774,77 @@ def _etudident_create_or_edit_form(edit): ) +@bp.route("/etud_copy_in_other_dept/", methods=["GET", "POST"]) +@scodoc +@permission_required( + Permission.ScoView +) # il faut aussi ScoEtudInscrit dans le nouveau dept +def etud_copy_in_other_dept(etudid: int): + """Crée une copie de l'étudiant (avec ses adresses et codes) dans un autre département + et l'inscrit à un formsemestre + """ + etud = Identite.get_etud(etudid) + if request.method == "POST": + action = request.form.get("action") + if action == "cancel": + return flask.redirect( + url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id) + ) + try: + formsemestre_id = int(request.form.get("formsemestre_id")) + except ValueError: + log("etud_copy_in_other_dept: invalid formsemestre_id") + abort(404, description="formsemestre_id invalide") + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not current_user.has_permission( + Permission.ScoEtudInscrit, formsemestre.departement.acronym + ): + raise ScoPermissionDenied("non autorisé") + new_etud = etud.clone(new_dept_id=formsemestre.dept_id) + db.session.commit() + # Attention: change le département pour opérer dans le nouveau + # avec les anciennes fonctions ScoDoc7 + orig_dept = g.scodoc_dept + try: + app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False) + sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( + formsemestre.id, + new_etud.id, + method="etud_copy_in_other_dept", + dept_id=formsemestre.dept_id, + ) + finally: + app.set_sco_dept(orig_dept, open_cnx=False) + flash(f"Etudiant dupliqué et inscrit en {formsemestre.departement.acronym}") + # Attention, ce redirect change de département ! + return flask.redirect( + url_for( + "scolar.ficheEtud", + scodoc_dept=formsemestre.departement.acronym, + etudid=new_etud.id, + ) + ) + departements = { + dept.id: dept + for dept in Departement.query.order_by(Departement.acronym) + if current_user.has_permission(Permission.ScoEtudInscrit, dept.acronym) + and dept.id != etud.dept_id + } + formsemestres_by_dept = { + dept.id: dept.formsemestres.filter_by(etat=True) + .filter(FormSemestre.modalite != "EXT") + .order_by(FormSemestre.date_debut, FormSemestre.semestre_id) + .all() + for dept in departements.values() + } + return render_template( + "scolar/etud_copy_in_other_dept.j2", + departements=departements, + etud=etud, + formsemestres_by_dept=formsemestres_by_dept, + ) + + @bp.route("/etudident_delete", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoEtudInscrit) diff --git a/sco_version.py b/sco_version.py index 61dc9fa95b..68196cdec1 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.35" +SCOVERSION = "9.6.38" SCONAME = "ScoDoc" diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index 63df403b5a..adcd2e0a36 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -129,7 +129,7 @@ def check_fields(data: dict, fields: dict = None): """ assert set(data.keys()) == set(fields.keys()) for key in data: - if key in ("moduleimpl_id", "desc", "user_id", "external_data"): + if key in ("moduleimpl_id", "desc", "external_data"): assert ( isinstance(data[key], fields[key]) or data[key] is None ), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]" diff --git a/tests/api/test_api_assiduites.py b/tests/api/test_api_assiduites.py index 1333047182..1a7fb653df 100644 --- a/tests/api/test_api_assiduites.py +++ b/tests/api/test_api_assiduites.py @@ -6,6 +6,7 @@ Ecrit par HARTMANN Matthias """ from random import randint +from types import NoneType from tests.api.setup_test_api import ( GET, @@ -34,7 +35,8 @@ ASSIDUITES_FIELDS = { "etat": str, "desc": str, "entry_date": str, - "user_id": str, + "user_id": (int, NoneType), + "user_name": (str, NoneType), "est_just": bool, "external_data": dict, }