forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -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:
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -86,6 +86,50 @@ class Identite(db.Model):
|
||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||
)
|
||||
|
||||
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"""<a class="stdlink" href="{
|
||||
@ -660,6 +704,19 @@ class Adresse(db.Model):
|
||||
)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("id", None) # get rid of id
|
||||
d.pop("_sa_instance_state", None) # get rid of SQLAlchemy special attr
|
||||
for k in not_copying:
|
||||
d.pop(k, None)
|
||||
copy = self.__class__(**d)
|
||||
db.session.add(copy)
|
||||
return copy
|
||||
|
||||
def to_dict(self, convert_nulls_to_str=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
e = dict(self.__dict__)
|
||||
|
@ -177,11 +177,15 @@ class FormSemestre(db.Model):
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_formsemestre(cls, formsemestre_id: int) -> "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()
|
||||
|
||||
|
@ -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 = ['<table class="tf-ro">']
|
||||
for (field, descr) in formdescription:
|
||||
for field, descr in formdescription:
|
||||
R.append(self._ReadOnlyElement(field, descr))
|
||||
R.append("</table>")
|
||||
return R
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
)}">
|
||||
<button>Visualiser l'assiduité</button></a>
|
||||
<button>Visualiser</button></a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn" href="{
|
||||
|
@ -69,10 +69,10 @@ def do_evaluation_listenotes(
|
||||
mode = None
|
||||
if moduleimpl_id:
|
||||
mode = "module"
|
||||
evals = sco_evaluation_db.get_evaluation_dict({"moduleimpl_id": moduleimpl_id})
|
||||
evals = sco_evaluation_db.get_evaluations_dict({"moduleimpl_id": moduleimpl_id})
|
||||
elif evaluation_id:
|
||||
mode = "eval"
|
||||
evals = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})
|
||||
evals = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id})
|
||||
else:
|
||||
raise ValueError("missing argument: evaluation or module")
|
||||
if not evals:
|
||||
|
@ -642,6 +642,12 @@ def menus_etud(etudid):
|
||||
"args": {"etudid": etud["etudid"]},
|
||||
"enabled": authuser.has_permission(Permission.ScoEtudInscrit),
|
||||
},
|
||||
{
|
||||
"title": "Copier dans un autre département...",
|
||||
"endpoint": "scolar.etud_copy_in_other_dept",
|
||||
"args": {"etudid": etud["etudid"]},
|
||||
"enabled": authuser.has_permission(Permission.ScoEtudInscrit),
|
||||
},
|
||||
{
|
||||
"title": "Supprimer cet étudiant...",
|
||||
"endpoint": "scolar.etudident_delete",
|
||||
@ -656,7 +662,9 @@ def menus_etud(etudid):
|
||||
},
|
||||
]
|
||||
|
||||
return htmlutils.make_menu("Étudiant", menuEtud, alone=True)
|
||||
return htmlutils.make_menu(
|
||||
"Étudiant", menuEtud, alone=True, css_class="menu-etudiant"
|
||||
)
|
||||
|
||||
|
||||
def etud_info_html(etudid, with_photo="1", debug=False):
|
||||
|
@ -138,7 +138,7 @@ class PlacementForm(FlaskForm):
|
||||
|
||||
def set_evaluation_infos(self, evaluation_id):
|
||||
"""Initialise les données du formulaire avec les données de l'évaluation."""
|
||||
eval_data = sco_evaluation_db.get_evaluation_dict(
|
||||
eval_data = sco_evaluation_db.get_evaluations_dict(
|
||||
{"evaluation_id": evaluation_id}
|
||||
)
|
||||
if not eval_data:
|
||||
@ -239,7 +239,7 @@ class PlacementRunner:
|
||||
self.groups_ids = [
|
||||
gid if gid != TOUS else form.tous_id for gid in form["groups"].data
|
||||
]
|
||||
self.eval_data = sco_evaluation_db.get_evaluation_dict(
|
||||
self.eval_data = sco_evaluation_db.get_evaluations_dict(
|
||||
{"evaluation_id": self.evaluation_id}
|
||||
)[0]
|
||||
self.groups = sco_groups.listgroups(self.groups_ids)
|
||||
|
@ -524,11 +524,11 @@ def table_suivi_cohorte(
|
||||
# 3-- Regroupe les semestres par date de debut
|
||||
P = [] # liste de periodsem
|
||||
|
||||
class periodsem(object):
|
||||
class PeriodSem:
|
||||
pass
|
||||
|
||||
# semestre de depart:
|
||||
porigin = periodsem()
|
||||
porigin = PeriodSem()
|
||||
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
|
||||
porigin.datedebut = datetime.datetime(y, m, d)
|
||||
porigin.sems = [sem]
|
||||
@ -543,7 +543,7 @@ def table_suivi_cohorte(
|
||||
merged = True
|
||||
break
|
||||
if not merged:
|
||||
p = periodsem()
|
||||
p = PeriodSem()
|
||||
p.datedebut = s["date_debut_dt"]
|
||||
p.sems = [s]
|
||||
P.append(p)
|
||||
@ -743,7 +743,7 @@ def formsemestre_suivi_cohorte(
|
||||
civilite=None,
|
||||
statut="",
|
||||
only_primo=False,
|
||||
):
|
||||
) -> 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(
|
||||
'<p><a href="%s&percent=1">Afficher les résultats en pourcentages</a></p>'
|
||||
% burl
|
||||
)
|
||||
help = (
|
||||
pplink
|
||||
+ """
|
||||
<p class="help">Nombre d'étudiants dans chaque semestre. Les dates indiquées sont les dates approximatives de <b>début</b> des semestres (les semestres commençant à des dates proches sont groupés). Le nombre de diplômés est celui à la <b>fin</b> 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.</p>
|
||||
<p class="help">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é.</p>
|
||||
"""
|
||||
% (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}
|
||||
<p class="help">Nombre d'étudiants dans chaque semestre.
|
||||
Les dates indiquées sont les dates approximatives de <b>début</b> des semestres
|
||||
(les semestres commençant à des dates proches sont groupés). Le nombre de diplômés
|
||||
est celui à la <b>fin</b> 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.
|
||||
</p>
|
||||
<p class="help">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é.
|
||||
</p>
|
||||
""",
|
||||
expl,
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
@ -870,35 +875,33 @@ def _gen_form_selectetuds(
|
||||
else:
|
||||
selected = 'selected="selected"'
|
||||
F = [
|
||||
"""<form id="f" method="get" action="%s">
|
||||
f"""<form id="f" method="get" action="{request.base_url}">
|
||||
<p>Bac: <select name="bac" onchange="javascript: submit(this);">
|
||||
<option value="" %s>tous</option>
|
||||
<option value="" {selected}>tous</option>
|
||||
"""
|
||||
% (request.base_url, selected)
|
||||
]
|
||||
for b in bacs:
|
||||
if bac == b:
|
||||
selected = 'selected="selected"'
|
||||
else:
|
||||
selected = ""
|
||||
F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
|
||||
F.append(f'<option value="{b}" {selected}>{b}</option>')
|
||||
F.append("</select>")
|
||||
if bacspecialite:
|
||||
selected = ""
|
||||
else:
|
||||
selected = 'selected="selected"'
|
||||
F.append(
|
||||
""" Bac/Specialité: <select name="bacspecialite" onchange="javascript: submit(this);">
|
||||
<option value="" %s>tous</option>
|
||||
f""" Bac/Specialité: <select name="bacspecialite" onchange="javascript: submit(this);">
|
||||
<option value="" {selected}>tous</option>
|
||||
"""
|
||||
% selected
|
||||
)
|
||||
for b in bacspecialites:
|
||||
if bacspecialite == b:
|
||||
selected = 'selected="selected"'
|
||||
else:
|
||||
selected = ""
|
||||
F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
|
||||
F.append(f'<option value="{b}" {selected}>{b}</option>')
|
||||
F.append("</select>")
|
||||
#
|
||||
F.append(
|
||||
@ -910,46 +913,44 @@ def _gen_form_selectetuds(
|
||||
)
|
||||
#
|
||||
F.append(
|
||||
""" Genre: <select name="civilite" onchange="javascript: submit(this);">
|
||||
<option value="" %s>tous</option>
|
||||
f""" Genre: <select name="civilite" onchange="javascript: submit(this);">
|
||||
<option value="" {selected}>tous</option>
|
||||
"""
|
||||
% selected
|
||||
)
|
||||
for b in civilites:
|
||||
if civilite == b:
|
||||
selected = 'selected="selected"'
|
||||
else:
|
||||
selected = ""
|
||||
F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
|
||||
F.append(f'<option value="{b}" {selected}>{b}</option>')
|
||||
F.append("</select>")
|
||||
|
||||
F.append(
|
||||
""" Statut: <select name="statut" onchange="javascript: submit(this);">
|
||||
<option value="" %s>tous</option>
|
||||
f""" Statut: <select name="statut" onchange="javascript: submit(this);">
|
||||
<option value="" {selected}>tous</option>
|
||||
"""
|
||||
% selected
|
||||
)
|
||||
for b in statuts:
|
||||
if statut == b:
|
||||
selected = 'selected="selected"'
|
||||
else:
|
||||
selected = ""
|
||||
F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
|
||||
F.append(f'<option value="{b}" {selected}>{b}</option>')
|
||||
F.append("</select>")
|
||||
|
||||
if only_primo:
|
||||
checked = 'checked="1"'
|
||||
else:
|
||||
checked = ""
|
||||
F.append(
|
||||
'<br><input type="checkbox" name="only_primo" onchange="javascript: submit(this);" %s/>Restreindre aux primo-entrants'
|
||||
% checked
|
||||
f"""<br>
|
||||
<input type="checkbox" name="only_primo"
|
||||
onchange="javascript: submit(this);"
|
||||
{'checked="1"' if only_primo else ""}/>Restreindre aux primo-entrants
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
|
||||
<input type="hidden" name="percent" value="{percent}"/>
|
||||
|
||||
</p>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
F.append(
|
||||
'<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
|
||||
)
|
||||
F.append('<input type="hidden" name="percent" value="%s"/>' % percent)
|
||||
F.append("</p></form>")
|
||||
|
||||
return "\n".join(F)
|
||||
|
||||
|
||||
@ -964,7 +965,7 @@ def _gen_select_annee(field, values, value) -> str:
|
||||
return menu_html + "</select>"
|
||||
|
||||
|
||||
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,12 +981,19 @@ 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)
|
||||
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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
99
app/templates/scolar/etud_copy_in_other_dept.j2
Normal file
99
app/templates/scolar/etud_copy_in_other_dept.j2
Normal file
@ -0,0 +1,99 @@
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
{% extends 'base.j2' %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.dept-name {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.dept {
|
||||
background-color: bisque;
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.dept label {
|
||||
font-weight: normal;
|
||||
}
|
||||
button[name="action"] {
|
||||
margin-right: 32px;
|
||||
}
|
||||
#submit-button:disabled {
|
||||
background-color: #CCCCCC;
|
||||
color: #888888;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid #AAAAAA;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<h2>Création d'une copie de {{ etud.html_link_fiche() | safe }}</h2>
|
||||
|
||||
<div class="help">
|
||||
|
||||
<p>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.
|
||||
</p>
|
||||
|
||||
<p>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".
|
||||
</p>
|
||||
|
||||
<p>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.
|
||||
</p>
|
||||
|
||||
<p>Dans chaque département autorisés, seuls les semestres non verrouillés sont
|
||||
montrés. Choisir le semestre destination et valider le formulaire.
|
||||
</p>
|
||||
|
||||
<p>Ensuite, ne pas oublier d'inscrire l'étudiant à ses groupes, notamment son
|
||||
parcours si besoin.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
{% for dept in departements.values() %}
|
||||
<div class="dept">
|
||||
<div class="dept-name">Département {{ dept.acronym }}</div>
|
||||
{% for sem in formsemestres_by_dept[dept.id]%}
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio" class="formsemestre" name="formsemestre_id" value="{{ sem.id }}">
|
||||
{{ sem.html_link_status() | safe }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" name="action" value="submit" disabled id="submit-button">Créer une copie de l'étudiant et l'inscrire au semestre choisi</button>
|
||||
<button type="submit" name="action" value="cancel">Annuler</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const radioButtons = document.querySelectorAll('input.formsemestre');
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
|
||||
radioButtons.forEach(radioButton => {
|
||||
radioButton.addEventListener('change', () => {
|
||||
const isAnyRadioButtonChecked = [...radioButtons].some(radioButton => radioButton.checked);
|
||||
if (isAnyRadioButtonChecked) {
|
||||
submitButton.removeAttribute('disabled');
|
||||
} else {
|
||||
submitButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -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(),
|
||||
),
|
||||
|
@ -407,7 +407,6 @@ 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",
|
||||
|
@ -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/<int:etudid>", 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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.35"
|
||||
SCOVERSION = "9.6.38"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -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]}]"
|
||||
|
@ -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,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user