Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into edit_roles

This commit is contained in:
Emmanuel Viennet 2023-09-28 22:55:55 +02:00
commit db062d5b4a
27 changed files with 384 additions and 106 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(
"""&nbsp; Bac/Specialité: <select name="bacspecialite" onchange="javascript: submit(this);">
<option value="" %s>tous</option>
f"""&nbsp; 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(
"""&nbsp; Genre: <select name="civilite" onchange="javascript: submit(this);">
<option value="" %s>tous</option>
f"""&nbsp; 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(
"""&nbsp; Statut: <select name="statut" onchange="javascript: submit(this);">
<option value="" %s>tous</option>
f"""&nbsp; 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,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

View File

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

View File

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

View File

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

View 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 %}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.35"
SCOVERSION = "9.6.38"
SCONAME = "ScoDoc"

View File

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

View File

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