This commit is contained in:
IDK 2021-10-24 18:35:10 +02:00
commit 1be2ba1498
31 changed files with 879 additions and 419 deletions

View File

@ -40,7 +40,7 @@ from app.scodoc.sco_permissions import Permission
def sidebar_common():
"partie commune à toutes les sidebar"
H = [
f"""<a class="scodoc_title" href="{url_for("scodoc.about", scodoc_dept=g.scodoc_dept)}">ScoDoc 9</a>
f"""<a class="scodoc_title" href="{url_for("scodoc.index", scodoc_dept=g.scodoc_dept)}">ScoDoc 9</a>
<div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)

View File

@ -27,9 +27,12 @@
"""Various HTML generation functions
"""
from html.parser import HTMLParser
from html.entities import name2codepoint
import re
from flask import g, url_for
import app.scodoc.sco_utils as scu
from . import listhistogram
@ -130,3 +133,63 @@ def make_menu(title, items, css_class="", alone=False):
if alone:
H.append("</ul>")
return "".join(H)
"""
HTML <-> text conversions.
http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python
"""
class _HTMLToText(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self._buf = []
self.hide_output = False
def handle_starttag(self, tag, attrs):
if tag in ("p", "br") and not self.hide_output:
self._buf.append("\n")
elif tag in ("script", "style"):
self.hide_output = True
def handle_startendtag(self, tag, attrs):
if tag == "br":
self._buf.append("\n")
def handle_endtag(self, tag):
if tag == "p":
self._buf.append("\n")
elif tag in ("script", "style"):
self.hide_output = False
def handle_data(self, text):
if text and not self.hide_output:
self._buf.append(re.sub(r"\s+", " ", text))
def handle_entityref(self, name):
if name in name2codepoint and not self.hide_output:
c = chr(name2codepoint[name])
self._buf.append(c)
def handle_charref(self, name):
if not self.hide_output:
n = int(name[1:], 16) if name.startswith("x") else int(name)
self._buf.append(chr(n))
def get_text(self):
return re.sub(r" +", " ", "".join(self._buf))
def html_to_text(html):
"""
Given a piece of HTML, return the plain text it contains.
This handles entities and char refs, but not javascript and stylesheets.
"""
parser = _HTMLToText()
try:
parser.feed(html)
parser.close()
except: # HTMLParseError: No good replacement?
pass
return parser.get_text()

View File

@ -630,7 +630,7 @@ class NotesTable(object):
matiere_sum_notes += val * coef
matiere_sum_coefs += coef
matiere_id_last = matiere_id
except: # val == "NI" "NA"
except TypeError: # val == "NI" "NA"
assert val == "NI" or val == "NA"
nb_missing = nb_missing + 1
coefs.append(0)

View File

@ -597,6 +597,22 @@ def float_null_is_null(x):
return float(x)
BOOL_STR = {
"": False,
"false": False,
"0": False,
"1": True,
"true": "true",
}
def bool_or_str(x):
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
if isinstance(x, str):
return BOOL_STR[x.lower()]
return x
# post filtering
#
def UniqListofDicts(L, key):

View File

@ -30,7 +30,8 @@
les dossiers d'admission et autres pièces utiles.
"""
import flask
from flask import url_for, g, request
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
@ -328,9 +329,9 @@ def etudarchive_import_files_form(group_id):
if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F
elif tf[0] == -1:
# retrouve le semestre à partir du groupe:
group = sco_groups.get_group(group_id)
# retrouve le semestre à partir du groupe:
group = sco_groups.get_group(group_id)
if tf[0] == -1:
return flask.redirect(
url_for(
"notes.formsemestre_status",
@ -340,21 +341,41 @@ def etudarchive_import_files_form(group_id):
)
else:
return etudarchive_import_files(
group_id=tf[2]["group_id"],
formsemestre_id=group["formsemestre_id"],
xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"],
description=tf[2]["description"],
)
def etudarchive_import_files(group_id=None, xlsfile=None, zipfile=None, description=""):
def etudarchive_import_files(
formsemestre_id=None, xlsfile=None, zipfile=None, description=""
):
"Importe des fichiers"
def callback(etud, data, filename):
_store_etud_file_to_new_archive(etud["etudid"], data, filename, description)
filename_title = "fichier_a_charger"
page_title = "Téléchargement de fichiers associés aux étudiants"
# Utilise la fontion au depart developpee pour les photos
r = sco_trombino.zip_excel_import_files(
xlsfile, zipfile, callback, filename_title, page_title
# Utilise la fontion developpée au depart pour les photos
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = sco_trombino.zip_excel_import_files(
xlsfile=xlsfile,
zipfile=zipfile,
callback=callback,
filename_title="fichier_a_charger",
)
return render_template(
"scolar/photos_import_files.html",
page_title="Téléchargement de fichiers associés aux étudiants",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_view",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
)
return r + html_sco_header.sco_footer()

View File

@ -190,7 +190,7 @@ def do_matiere_delete(oid):
def matiere_delete(matiere_id=None):
"""Delete an UE"""
"""Delete matière"""
from app.scodoc import sco_edit_ue
M = matiere_list(args={"matiere_id": matiere_id})[0]
@ -200,7 +200,11 @@ def matiere_delete(matiere_id=None):
"<h2>Suppression de la matière %(titre)s" % M,
" dans l'UE (%(acronyme)s))</h2>" % UE,
]
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(UE["formation_id"])
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(UE["formation_id"]),
)
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
@ -227,13 +231,13 @@ def matiere_edit(matiere_id=None):
if not F:
raise ScoValueError("Matière inexistante !")
F = F[0]
U = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
if not F:
ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
if not ues:
raise ScoValueError("UE inexistante !")
U = U[0]
Fo = sco_formations.formation_list(args={"formation_id": U["formation_id"]})[0]
ue = ues[0]
Fo = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
ues = sco_edit_ue.ue_list(args={"formation_id": U["formation_id"]})
ues = sco_edit_ue.ue_list(args={"formation_id": ue["formation_id"]})
ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
ue_ids = [u["ue_id"] for u in ues]
H = [
@ -278,8 +282,11 @@ associé.
submitlabel="Modifier les valeurs",
)
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(U["formation_id"])
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer()
elif tf[0] == -1:

View File

@ -285,21 +285,25 @@ def module_delete(module_id=None):
"""Delete a module"""
if not module_id:
raise ScoValueError("invalid module !")
Mods = module_list(args={"module_id": module_id})
if not Mods:
modules = module_list(args={"module_id": module_id})
if not modules:
raise ScoValueError("Module inexistant !")
Mod = Mods[0]
mod = modules[0]
H = [
html_sco_header.sco_header(page_title="Suppression d'un module"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % Mod,
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,
]
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"])
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(mod["formation_id"]),
)
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(("module_id", {"input_type": "hidden"}),),
initvalues=Mod,
initvalues=mod,
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)
@ -367,9 +371,11 @@ def module_edit(module_id=None):
Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"])
semestres_indices = list(range(1, parcours.NB_SEM + 1))
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"])
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(Mod["formation_id"]),
)
H = [
html_sco_header.sco_header(
page_title="Modification du module %(titre)s" % Mod,
@ -588,9 +594,9 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
"""Création d'un module de "malus" dans chaque UE d'une formation"""
from app.scodoc import sco_edit_ue
ue_list = sco_edit_ue.ue_list(args={"formation_id": formation_id})
ues = sco_edit_ue.ue_list(args={"formation_id": formation_id})
for ue in ue_list:
for ue in ues:
# Un seul module de malus par UE:
nb_mod_malus = len(
[
@ -603,7 +609,11 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
ue_add_malus_module(ue["ue_id"], titre=titre)
if redirect:
return flask.redirect("ue_list?formation_id=" + str(formation_id))
return flask.redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
)
def ue_add_malus_module(ue_id, titre=None, code=None):

View File

@ -75,7 +75,7 @@ _ueEditor = ndb.EditableTable(
sortkey="numero",
input_formators={
"type": ndb.int_null_is_zero,
"is_external": bool,
"is_external": ndb.bool_or_str,
},
output_formators={
"numero": ndb.int_null_is_zero,
@ -139,7 +139,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
% (len(validations), ue["acronyme"], ue["titre"]),
dest_url="",
target_variable="delete_validations",
cancel_url="ue_list?formation_id=%s" % ue["formation_id"],
cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
),
parameters={"ue_id": ue_id, "dialog_confirmed": 1},
)
if delete_validations:
@ -294,6 +298,14 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
},
),
(
"is_external",
{
"input_type": "boolcheckbox",
"title": "UE externe",
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
},
),
]
if parcours.UE_IS_MODULE:
# demande le semestre pour creer le module immediatement:
@ -374,12 +386,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
)
def _add_ue_semestre_id(ue_list):
def _add_ue_semestre_id(ues):
"""ajoute semestre_id dans les ue, en regardant le premier module de chacune.
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
qui les place à la fin de la liste.
"""
for ue in ue_list:
for ue in ues:
Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
if Modlist:
ue["semestre_id"] = Modlist[0]["semestre_id"]
@ -391,34 +403,38 @@ def next_ue_numero(formation_id, semestre_id=None):
"""Numero d'une nouvelle UE dans cette formation.
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
"""
ue_list = ue_list(args={"formation_id": formation_id})
if not ue_list:
ues = ue_list(args={"formation_id": formation_id})
if not ues:
return 0
if semestre_id is None:
return ue_list[-1]["numero"] + 1000
return ues[-1]["numero"] + 1000
else:
# Avec semestre: (prend le semestre du 1er module de l'UE)
_add_ue_semestre_id(ue_list)
ue_list_semestre = [ue for ue in ue_list if ue["semestre_id"] == semestre_id]
_add_ue_semestre_id(ues)
ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
if ue_list_semestre:
return ue_list_semestre[-1]["numero"] + 10
else:
return ue_list[-1]["numero"] + 1000
return ues[-1]["numero"] + 1000
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE"""
ue = ue_list(args={"ue_id": ue_id})
if not ue:
ues = ue_list(args={"ue_id": ue_id})
if not ues:
raise ScoValueError("UE inexistante !")
ue = ue[0]
ue = ues[0]
if not dialog_confirmed:
return scu.confirm_dialog(
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
dest_url="",
parameters={"ue_id": ue_id},
cancel_url="ue_list?formation_id=%s" % ue["formation_id"],
cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
),
)
return do_ue_delete(ue_id, delete_validations=delete_validations)
@ -438,21 +454,24 @@ def ue_table(formation_id=None, msg=""): # was ue_list
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
locked = sco_formations.formation_has_locked_sems(formation_id)
ue_list = ue_list(args={"formation_id": formation_id})
ues = ue_list(args={"formation_id": formation_id, "is_external": False})
ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True})
# tri par semestre et numero:
_add_ue_semestre_id(ue_list)
ue_list.sort(key=lambda u: (u["semestre_id"], u["numero"]))
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ue_list])) != len(ue_list)
_add_ue_semestre_id(ues)
_add_ue_semestre_id(ues_externes)
ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues)
perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and perm_change
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and has_perm_change
# On autorise maintanant la modification des formations qui ont des semestres verrouillés,
# sauf si cela affect les notes passées (verrouillées):
# - pas de modif des modules utilisés dans des semestres verrouillés
# - pas de changement des codes d'UE utilisés dans des semestres verrouillés
editable = perm_change
editable = has_perm_change
tag_editable = (
current_user.has_permission(Permission.ScoEditFormationTags) or perm_change
current_user.has_permission(Permission.ScoEditFormationTags) or has_perm_change
)
if locked:
lockicon = scu.icontag("lock32_img", title="verrouillé")
@ -556,213 +575,20 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
H.append(
'<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>'
)
cur_ue_semestre_id = None
iue = 0
for UE in ue_list:
if UE["ects"]:
UE["ects_str"] = ", %g ECTS" % UE["ects"]
else:
UE["ects_str"] = ""
if editable:
klass = "span_apo_edit"
else:
klass = ""
UE["code_apogee_str"] = (
""", Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
% (klass, UE["ue_id"], scu.APO_MISSING_CODE_STR)
+ (UE["code_apogee"] or "")
+ "</span>"
H.append(
_ue_table_ues(
parcours,
ues,
editable,
tag_editable,
has_perm_change,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
if cur_ue_semestre_id != UE["semestre_id"]:
cur_ue_semestre_id = UE["semestre_id"]
if iue > 0:
H.append("</ul>")
if UE["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:"
else:
lab = "Semestre %s:" % UE["semestre_id"]
H.append('<div class="ue_list_tit_sem">%s</div>' % lab)
H.append('<ul class="notes_ue_list">')
H.append('<li class="notes_ue_list">')
if iue != 0 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
% (UE["ue_id"], arrow_up)
)
else:
H.append(arrow_none)
if iue < len(ue_list) - 1 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
% (UE["ue_id"], arrow_down)
)
else:
H.append(arrow_none)
iue += 1
UE["acro_titre"] = str(UE["acronyme"])
if UE["titre"] != UE["acronyme"]:
UE["acro_titre"] += " " + str(UE["titre"])
H.append(
"""%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span>
<span class="ue_coef"></span>
"""
% UE
)
if UE["type"] != sco_codes_parcours.UE_STANDARD:
H.append(
'<span class="ue_type">%s</span>'
% sco_codes_parcours.UE_TYPE_NAME[UE["type"]]
)
ue_editable = editable and not ue_is_locked(UE["ue_id"])
if ue_editable:
H.append(
'<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % UE
)
else:
H.append('<span class="locked">[verrouillé]</span>')
if not parcours.UE_IS_MODULE:
H.append('<ul class="notes_matiere_list">')
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": UE["ue_id"]})
for Mat in Matlist:
if not parcours.UE_IS_MODULE:
H.append('<li class="notes_matiere_list">')
if editable and not sco_edit_matiere.matiere_is_locked(
Mat["matiere_id"]
):
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_edit",
scodoc_dept=g.scodoc_dept, matiere_id=Mat["matiere_id"])
}">
"""
)
H.append("%(titre)s" % Mat)
if editable and not sco_edit_matiere.matiere_is_locked(
Mat["matiere_id"]
):
H.append("</a>")
H.append('<ul class="notes_module_list">')
Modlist = sco_edit_module.module_list(
args={"matiere_id": Mat["matiere_id"]}
)
im = 0
for Mod in Modlist:
Mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
Mod["module_id"]
)
klass = "notes_module_list"
if Mod["module_type"] == scu.MODULE_MALUS:
klass += " module_malus"
H.append('<li class="%s">' % klass)
H.append('<span class="notes_module_list_buts">')
if im != 0 and editable:
H.append(
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
% (Mod["module_id"], arrow_up)
)
else:
H.append(arrow_none)
if im < len(Modlist) - 1 and editable:
H.append(
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
% (Mod["module_id"], arrow_down)
)
else:
H.append(arrow_none)
im += 1
if Mod["nb_moduleimpls"] == 0 and editable:
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (Mod["module_id"], delete_icon)
)
else:
H.append(delete_disabled_icon)
H.append("</span>")
mod_editable = editable # and not sco_edit_module.module_is_locked( Mod['module_id'])
if mod_editable:
H.append(
'<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
% Mod
)
H.append(
'<span class="formation_module_tit">%s</span>'
% scu.join_words(Mod["code"], Mod["titre"])
)
if mod_editable:
H.append("</a>")
heurescoef = (
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s"
% Mod
)
if mod_editable:
klass = "span_apo_edit"
else:
klass = ""
heurescoef += (
', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
% (klass, Mod["module_id"], scu.APO_MISSING_CODE_STR)
+ (Mod["code_apogee"] or "")
+ "</span>"
)
if tag_editable:
tag_cls = "module_tag_editor"
else:
tag_cls = "module_tag_editor_ro"
tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
tag_edit = tag_mk.format(
Mod["module_id"],
tag_cls,
",".join(sco_tag_module.module_tag_list(Mod["module_id"])),
)
H.append(
" %s %s" % (parcours.SESSION_NAME, Mod["semestre_id"])
+ " (%s)" % heurescoef
+ tag_edit
)
H.append("</li>")
if not Modlist:
H.append("<li>Aucun module dans cette matière !")
if editable:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_delete",
scodoc_dept=g.scodoc_dept, matiere_id=Mat["matiere_id"])}"
>supprimer cette matière</a>
"""
)
H.append("</li>")
if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
H.append(
f"""<li> <a class="stdlink" href="{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, matiere_id=Mat["matiere_id"])}"
>créer un module</a></li>
"""
)
H.append("</ul>")
H.append("</li>")
if not Matlist:
H.append("<li>Aucune matière dans cette UE ! ")
if editable:
H.append(
"""<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
% UE
)
H.append("</li>")
if editable and not parcours.UE_IS_MODULE:
H.append(
'<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
% UE
)
if not parcours.UE_IS_MODULE:
H.append("</ul>")
H.append("</ul>")
)
if editable:
H.append(
'<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>'
@ -774,6 +600,27 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
)
H.append("</div>") # formation_ue_list
if ues_externes:
H.append('<div class="formation_ue_list formation_ue_list_externes">')
H.append(
'<div class="ue_list_tit">UE externes déclarées (pour information):</div>'
)
H.append(
_ue_table_ues(
parcours,
ues_externes,
editable,
tag_editable,
has_perm_change,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
)
H.append("</div>") # formation_ue_list
H.append("<p><ul>")
if editable:
H.append(
@ -795,7 +642,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</p>"""
% F
)
if perm_change:
if has_perm_change:
H.append(
"""
<h3> <a name="sems">Semestres ou sessions de cette formation</a></h3>
@ -836,6 +683,294 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
return "".join(H)
def _ue_table_ues(
parcours,
ues,
editable,
tag_editable,
has_perm_change,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
):
"""Édition de programme: liste des UEs (avec leurs matières et modules)."""
H = []
cur_ue_semestre_id = None
iue = 0
for ue in ues:
if ue["ects"]:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
else:
ue["ects_str"] = ""
if editable:
klass = "span_apo_edit"
else:
klass = ""
ue["code_apogee_str"] = (
""", Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
% (klass, ue["ue_id"], scu.APO_MISSING_CODE_STR)
+ (ue["code_apogee"] or "")
+ "</span>"
)
if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"]
if iue > 0:
H.append("</ul>")
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:"
else:
lab = "Semestre %s:" % ue["semestre_id"]
H.append('<div class="ue_list_tit_sem">%s</div>' % lab)
H.append('<ul class="notes_ue_list">')
H.append('<li class="notes_ue_list">')
if iue != 0 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
% (ue["ue_id"], arrow_up)
)
else:
H.append(arrow_none)
if iue < len(ues) - 1 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
% (ue["ue_id"], arrow_down)
)
else:
H.append(arrow_none)
iue += 1
ue["acro_titre"] = str(ue["acronyme"])
if ue["titre"] != ue["acronyme"]:
ue["acro_titre"] += " " + str(ue["titre"])
H.append(
"""%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span>
<span class="ue_coef"></span>
"""
% ue
)
if ue["type"] != sco_codes_parcours.UE_STANDARD:
H.append(
'<span class="ue_type">%s</span>'
% sco_codes_parcours.UE_TYPE_NAME[ue["type"]]
)
if ue["is_external"]:
# Cas spécial: si l'UE externe a plus d'un module, c'est peut être une UE
# qui a été déclarée externe par erreur (ou suite à un bug d'import/export xml)
# Dans ce cas, propose de changer le type (même si verrouillée)
if len(sco_moduleimpl.moduleimpls_in_external_ue(ue["ue_id"])) > 1:
H.append('<span class="ue_is_external">')
if has_perm_change:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.ue_set_internal", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
}">transformer en UE ordinaire</a>&nbsp;"""
)
H.append("</span>")
ue_editable = editable and not ue_is_locked(ue["ue_id"])
if ue_editable:
H.append(
'<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % ue
)
else:
H.append('<span class="locked">[verrouillé]</span>')
H.append(
_ue_table_matieres(
parcours,
ue,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
)
return "\n".join(H)
def _ue_table_matieres(
parcours,
ue,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
):
"""Édition de programme: liste des matières (et leurs modules) d'une UE."""
H = []
if not parcours.UE_IS_MODULE:
H.append('<ul class="notes_matiere_list">')
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for mat in matieres:
if not parcours.UE_IS_MODULE:
H.append('<li class="notes_matiere_list">')
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_edit",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])
}">
"""
)
H.append("%(titre)s" % mat)
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
H.append("</a>")
modules = sco_edit_module.module_list(args={"matiere_id": mat["matiere_id"]})
H.append(
_ue_table_modules(
parcours,
mat,
modules,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
)
if not matieres:
H.append("<li>Aucune matière dans cette UE ! ")
if editable:
H.append(
"""<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
% ue
)
H.append("</li>")
if editable and not parcours.UE_IS_MODULE:
H.append(
'<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
% ue
)
if not parcours.UE_IS_MODULE:
H.append("</ul>")
return "\n".join(H)
def _ue_table_modules(
parcours,
mat,
modules,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
):
"""Édition de programme: liste des modules d'une matière d'une UE"""
H = ['<ul class="notes_module_list">']
im = 0
for mod in modules:
mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
mod["module_id"]
)
klass = "notes_module_list"
if mod["module_type"] == scu.MODULE_MALUS:
klass += " module_malus"
H.append('<li class="%s">' % klass)
H.append('<span class="notes_module_list_buts">')
if im != 0 and editable:
H.append(
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
% (mod["module_id"], arrow_up)
)
else:
H.append(arrow_none)
if im < len(modules) - 1 and editable:
H.append(
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
% (mod["module_id"], arrow_down)
)
else:
H.append(arrow_none)
im += 1
if mod["nb_moduleimpls"] == 0 and editable:
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], delete_icon)
)
else:
H.append(delete_disabled_icon)
H.append("</span>")
mod_editable = (
editable # and not sco_edit_module.module_is_locked( Mod['module_id'])
)
if mod_editable:
H.append(
'<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
% mod
)
H.append(
'<span class="formation_module_tit">%s</span>'
% scu.join_words(mod["code"], mod["titre"])
)
if mod_editable:
H.append("</a>")
heurescoef = (
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
)
if mod_editable:
klass = "span_apo_edit"
else:
klass = ""
heurescoef += (
', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
% (klass, mod["module_id"], scu.APO_MISSING_CODE_STR)
+ (mod["code_apogee"] or "")
+ "</span>"
)
if tag_editable:
tag_cls = "module_tag_editor"
else:
tag_cls = "module_tag_editor_ro"
tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
tag_edit = tag_mk.format(
mod["module_id"],
tag_cls,
",".join(sco_tag_module.module_tag_list(mod["module_id"])),
)
H.append(
" %s %s" % (parcours.SESSION_NAME, mod["semestre_id"])
+ " (%s)" % heurescoef
+ tag_edit
)
H.append("</li>")
if not modules:
H.append("<li>Aucun module dans cette matière ! ")
if editable:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_delete",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
>la supprimer</a>
"""
)
H.append("</li>")
if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
H.append(
f"""<li> <a class="stdlink" href="{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
>créer un module</a></li>
"""
)
H.append("</ul>")
H.append("</li>")
return "\n".join(H)
def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
"""HTML list of UE sharing this code
Either ue_code or ue_id may be specified.
@ -964,9 +1099,9 @@ def formation_table_recap(formation_id, format="html"):
raise ScoValueError("invalid formation_id")
F = F[0]
T = []
ue_list = ue_list(args={"formation_id": formation_id})
for UE in ue_list:
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": UE["ue_id"]})
ues = ue_list(args={"formation_id": formation_id})
for ue in ues:
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for Mat in Matlist:
Modlist = sco_edit_module.module_list(
args={"matiere_id": Mat["matiere_id"]}
@ -978,7 +1113,7 @@ def formation_table_recap(formation_id, format="html"):
#
T.append(
{
"UE_acro": UE["acronyme"],
"UE_acro": ue["acronyme"],
"Mat_tit": Mat["titre"],
"Mod_tit": Mod["abbrev"] or Mod["titre"],
"Mod_code": Mod["code"],

View File

@ -356,7 +356,7 @@ def apo_semset_maq_status(
H.append(
", ".join(
[
'<a class="stdlink" href="ue_list?formation_id=%(formation_id)s">%(acronyme)s v%(version)s</a>'
'<a class="stdlink" href="ue_table?formation_id=%(formation_id)s">%(acronyme)s v%(version)s</a>'
% f
for f in formations
]

View File

@ -152,7 +152,7 @@ def format_nom(s, uppercase=True):
def input_civilite(s):
"""Converts external representation of civilite to internal:
'M', 'F', or 'X' (and nothing else).
Raises valueError if conversion fails.
Raises ScoValueError if conversion fails.
"""
s = s.upper().strip()
if s in ("M", "M.", "MR", "H"):
@ -161,12 +161,13 @@ def input_civilite(s):
return "F"
elif s == "X" or not s:
return "X"
raise ValueError("valeur invalide pour la civilité: %s" % s)
raise ScoValueError("valeur invalide pour la civilité: %s" % s)
def format_civilite(civilite):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personne ne souhaitant pas d'affichage)
personne ne souhaitant pas d'affichage).
Raises ScoValueError if conversion fails.
"""
try:
return {
@ -175,7 +176,7 @@ def format_civilite(civilite):
"X": "",
}[civilite]
except KeyError:
raise ValueError("valeur invalide pour la civilité: %s" % civilite)
raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)
def format_lycee(nomlycee):

View File

@ -93,12 +93,21 @@ def formation_has_locked_sems(formation_id):
return sems
def formation_export(formation_id, export_ids=False, export_tags=True, format=None):
def formation_export(
formation_id,
export_ids=False,
export_tags=True,
export_external_ues=False,
format=None,
):
"""Get a formation, with UE, matieres, modules
in desired format
"""
F = formation_list(args={"formation_id": formation_id})[0]
ues = sco_edit_ue.ue_list({"formation_id": formation_id})
selector = {"formation_id": formation_id}
if not export_external_ues:
selector["is_external"] = False
ues = sco_edit_ue.ue_list(selector)
F["ue"] = ues
for ue in ues:
ue_id = ue["ue_id"]
@ -254,7 +263,11 @@ def formation_list_table(formation_id=None, args={}):
).NAME
except:
f["parcours_name"] = ""
f["_titre_target"] = "ue_list?formation_id=%(formation_id)s" % f
f["_titre_target"] = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(f["formation_id"]),
)
f["_titre_link_class"] = "stdlink"
f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-")
# Ajoute les semestres associés à chaque formation:

View File

@ -675,7 +675,7 @@ def do_formsemestre_createwithmodules(edit=False):
if tf[0] == 0 or msg:
return (
'<p>Formation <a class="discretelink" href="ue_list?formation_id=%(formation_id)s"><em>%(titre)s</em> (%(acronyme)s), version %(version)s, code %(formation_code)s</a></p>'
'<p>Formation <a class="discretelink" href="ue_table?formation_id=%(formation_id)s"><em>%(titre)s</em> (%(acronyme)s), version %(version)s, code %(formation_code)s</a></p>'
% F
+ msg
+ str(tf[1])

View File

@ -221,12 +221,11 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
ue_list = _list_ue_with_coef_and_validations(sem, etudid)
descr = _ue_form_description(ue_list, scu.get_request_args())
ues = _list_ue_with_coef_and_validations(sem, etudid)
descr = _ue_form_description(ues, scu.get_request_args())
if request.method == "GET":
initvalues = {
"note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "")
for ue in ue_list
"note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") for ue in ues
}
else:
initvalues = {}
@ -247,15 +246,13 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
return "\n".join(H)
else: # soumission
# simule erreur
ok, message = _check_values(ue_list, tf[2])
ok, message = _check_values(ues, tf[2])
if not ok:
H = _make_page(etud, sem, tf, message=message)
return "\n".join(H)
else:
# Submit
_record_ue_validations_and_coefs(
formsemestre_id, etudid, ue_list, tf[2]
)
_record_ue_validations_and_coefs(formsemestre_id, etudid, ues, tf[2])
return flask.redirect(
"formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
% (formsemestre_id, etudid)
@ -303,7 +300,7 @@ _UE_VALID_CODES = {
}
def _ue_form_description(ue_list, values):
def _ue_form_description(ues, values):
"""Description du formulaire de saisie des UE / validations
Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient.
"""
@ -320,7 +317,7 @@ def _ue_form_description(ue_list, values):
("formsemestre_id", {"input_type": "hidden"}),
("etudid", {"input_type": "hidden"}),
]
for ue in ue_list:
for ue in ues:
# Menu pour code validation UE:
# Ne propose que ADM, CMP et "Non inscrit"
select_name = "valid_" + str(ue["ue_id"])
@ -439,8 +436,8 @@ def _list_ue_with_coef_and_validations(sem, etudid):
"""
cnx = ndb.GetDBConnexion()
formsemestre_id = sem["formsemestre_id"]
ue_list = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]})
for ue in ue_list:
ues = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]})
for ue in ues:
# add coefficient
uecoef = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
@ -462,11 +459,11 @@ def _list_ue_with_coef_and_validations(sem, etudid):
ue["validation"] = validation[0]
else:
ue["validation"] = {}
return ue_list
return ues
def _record_ue_validations_and_coefs(formsemestre_id, etudid, ue_list, values):
for ue in ue_list:
def _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, values):
for ue in ues:
code = values.get("valid_" + str(ue["ue_id"]), False)
if code == "None":
code = None

View File

@ -89,8 +89,8 @@ def get_group(group_id):
"""Returns group object, with partition"""
r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p
WHERE gd.id=%(group_id)s
FROM group_descr gd, partition p
WHERE gd.id=%(group_id)s
AND p.id = gd.partition_id
""",
{"group_id": group_id},
@ -112,8 +112,8 @@ def group_delete(group, force=False):
def get_partition(partition_id):
r = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.*
FROM partition p
"""SELECT p.id AS partition_id, p.*
FROM partition p
WHERE p.id = %(partition_id)s
""",
{"partition_id": partition_id},
@ -126,7 +126,7 @@ def get_partition(partition_id):
def get_partitions_list(formsemestre_id, with_default=True):
"""Liste des partitions pour ce semestre (list of dicts)"""
partitions = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.*
"""SELECT p.id AS partition_id, p.*
FROM partition p
WHERE formsemestre_id=%(formsemestre_id)s
ORDER BY numero""",
@ -143,7 +143,7 @@ def get_default_partition(formsemestre_id):
"""Get partition for 'all' students (this one always exists, with NULL name)"""
r = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.* FROM partition p
WHERE formsemestre_id=%(formsemestre_id)s
WHERE formsemestre_id=%(formsemestre_id)s
AND partition_name is NULL
""",
{"formsemestre_id": formsemestre_id},
@ -170,10 +170,10 @@ def get_partition_groups(partition):
"""List of groups in this partition (list of dicts).
Some groups may be empty."""
return ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p
WHERE gd.partition_id=%(partition_id)s
AND gd.partition_id=p.id
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p
WHERE gd.partition_id=%(partition_id)s
AND gd.partition_id=p.id
ORDER BY group_name
""",
partition,
@ -184,9 +184,9 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
"""Returns group_id for default ('tous') group"""
r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.partition_name is NULL
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.partition_name is NULL
AND p.id = gd.partition_id
""",
{"formsemestre_id": formsemestre_id},
@ -218,8 +218,8 @@ def get_sem_groups(formsemestre_id):
"""Returns groups for this sem (in all partitions)."""
return ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.id = gd.partition_id
""",
{"formsemestre_id": formsemestre_id},
@ -340,7 +340,7 @@ def get_etud_groups(etudid, sem, exclude_default=False):
"""Infos sur groupes de l'etudiant dans ce semestre
[ group + partition_name ]
"""
req = """SELECT p.id AS partition_id, p.*, g.id AS group_id, g.*
req = """SELECT p.id AS partition_id, p.*, g.id AS group_id, g.*
FROM group_descr g, partition p, group_membership gm
WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id
@ -377,10 +377,10 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
{ etudid : { partition_id : group_name }} (attr=group_name or group_id)
"""
infos = ndb.SimpleDictFetch(
"""SELECT i.id AS etudid, p.id AS partition_id,
gd.group_name, gd.id AS group_id
FROM notes_formsemestre_inscription i, partition p,
group_descr gd, group_membership gm
"""SELECT i.id AS etudid, p.id AS partition_id,
gd.group_name, gd.id AS group_id
FROM notes_formsemestre_inscription i, partition p,
group_descr gd, group_membership gm
WHERE i.formsemestre_id=%(formsemestre_id)s
and i.formsemestre_id = p.formsemestre_id
and p.id = gd.partition_id
@ -413,7 +413,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id
and g.partition_id = p.id
and p.formsemestre_id = %(formsemestre_id)s
and p.formsemestre_id = %(formsemestre_id)s
ORDER BY p.numero
""",
{"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]},
@ -806,8 +806,21 @@ def partition_create(
)
cnx = ndb.GetDBConnexion()
if numero is None:
numero = (
ndb.SimpleQuery(
"SELECT MAX(id) FROM partition WHERE formsemestre_id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
).fetchone()[0]
or 0
)
partition_id = partitionEditor.create(
cnx, {"formsemestre_id": formsemestre_id, "partition_name": partition_name}
cnx,
{
"formsemestre_id": formsemestre_id,
"partition_name": partition_name,
"numero": numero,
},
)
log("createPartition: created partition_id=%s" % partition_id)
#
@ -1041,7 +1054,7 @@ def partition_move(partition_id, after=0, redirect=1):
others = get_partitions_list(formsemestre_id)
if len(others) > 1:
pidx = [p["partition_id"] for p in others].index(partition_id)
log("partition_move: after=%s pidx=%s" % (after, pidx))
# log("partition_move: after=%s pidx=%s" % (after, pidx))
neigh = None # partition to swap with
if after == 0 and pidx > 0:
neigh = others[pidx - 1]
@ -1049,8 +1062,20 @@ def partition_move(partition_id, after=0, redirect=1):
neigh = others[pidx + 1]
if neigh: #
# swap numero between partition and its neighbor
log("moving partition %s" % partition_id)
# log("moving partition %s" % partition_id)
cnx = ndb.GetDBConnexion()
# Si aucun numéro n'a été affecté, le met au minimum
min_numero = (
ndb.SimpleQuery(
"SELECT MIN(numero) FROM partition WHERE formsemestre_id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
).fetchone()[0]
or 0
)
if neigh["numero"] is None:
neigh["numero"] = min_numero - 1
if partition["numero"] is None:
partition["numero"] = min_numero - 1 - after
partition["numero"], neigh["numero"] = neigh["numero"], partition["numero"]
partitionEditor.edit(cnx, partition)
partitionEditor.edit(cnx, neigh)
@ -1116,13 +1141,13 @@ def partition_set_name(partition_id, partition_name, redirect=1):
# check unicity
r = ndb.SimpleDictFetch(
"""SELECT p.* FROM partition p
WHERE p.partition_name = %(partition_name)s
"""SELECT p.* FROM partition p
WHERE p.partition_name = %(partition_name)s
AND formsemestre_id = %(formsemestre_id)s
""",
{"partition_name": partition_name, "formsemestre_id": formsemestre_id},
)
if len(r) > 1 or (len(r) == 1 and r[0]["partition_id"] != partition_id):
if len(r) > 1 or (len(r) == 1 and r[0]["id"] != partition_id):
raise ScoValueError(
"Partition %s déjà existante dans ce semestre !" % partition_name
)
@ -1469,9 +1494,9 @@ def listgroups(group_ids):
groups = []
for group_id in group_ids:
cursor.execute(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p
WHERE p.id = gd.partition_id
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p
WHERE p.id = gd.partition_id
AND gd.id = %(group_id)s
""",
{"group_id": group_id},

View File

@ -178,6 +178,19 @@ def moduleimpl_withmodule_list(
return modimpls
def moduleimpls_in_external_ue(ue_id):
"""List of modimpls in this ue"""
cursor = ndb.SimpleQuery(
"""SELECT DISTINCT mi.*
FROM notes_ue u, notes_moduleimpl mi, notes_modules m
WHERE u.is_external is true
AND mi.module_id = m.id AND m.ue_id = %(ue_id)s
""",
{"ue_id": ue_id},
)
return cursor.dictfetchall()
def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None):
"list moduleimpl_inscriptions"
args = locals()

View File

@ -348,7 +348,6 @@ def make_formsemestre_recapcomplet(
if not hidemodules:
h.append("")
pass
if not hidemodules and not ue["is_external"]:
for modimpl in modimpls:
if modimpl["module"]["ue_id"] == ue["ue_id"]:

View File

@ -388,7 +388,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
):
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
NotesDB = sco_evaluations.do_evaluation_get_all_notes(
evaluation_id, by_uid=current_user.user_name
evaluation_id, by_uid=current_user.id
)
else:
raise AccessDenied("Modification des notes impossible pour %s" % current_user)
@ -399,7 +399,10 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
nb_changed, nb_suppress, existing_decisions = _notes_add(
current_user, evaluation_id, notes, do_it=False
)
msg = "<p>Confirmer la suppression des %d notes ?</p>" % nb_suppress
msg = (
"<p>Confirmer la suppression des %d notes ? <em>(peut affecter plusieurs groupes)</em></p>"
% nb_suppress
)
if existing_decisions:
msg += """<p class="warning">Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !</p>"""
return scu.confirm_dialog(

View File

@ -30,6 +30,7 @@
import io
from zipfile import ZipFile, BadZipfile
from flask.templating import render_template
import reportlab
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
@ -531,25 +532,33 @@ def photos_import_files_form(group_ids=[]):
elif tf[0] == -1:
return flask.redirect(back_url)
else:
return photos_import_files(
group_ids=tf[2]["group_ids"],
def callback(etud, data, filename):
sco_photos.store_photo(etud, data)
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = zip_excel_import_files(
xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"],
callback=callback,
filename_title="fichier_photo",
)
return render_template(
"scolar/photos_import_files.html",
page_title="Téléchargement des photos des étudiants",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_view",
scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id,
curtab="tab-photos",
),
)
def photos_import_files(group_ids=[], xlsfile=None, zipfile=None):
"""Importation des photos"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
filename_title = "fichier_photo"
page_title = "Téléchargement des photos des étudiants"
def callback(etud, data, filename):
sco_photos.store_photo(etud, data)
zip_excel_import_files(xlsfile, zipfile, callback, filename_title, page_title)
return flask.redirect(back_url + "&head_message=photos%20 importees")
def zip_excel_import_files(
@ -557,19 +566,19 @@ def zip_excel_import_files(
zipfile=None,
callback=None,
filename_title="", # doit obligatoirement etre specifié
page_title="",
):
"""Importation de fichiers à partir d'un excel et d'un zip
La fonction
callback()
est appelé pour chaque fichier trouvé.
est appelée pour chaque fichier trouvé.
Fonction utilisée pour les photos et les fichiers étudiants (archives).
"""
# 1- build mapping etudid -> filename
exceldata = xlsfile.read()
if not exceldata:
raise ScoValueError("Fichier excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: # probably a bug
if not data:
raise ScoValueError("Fichier excel vide !")
# on doit avoir une colonne etudid et une colonne filename_title ('fichier_photo')
titles = data[0]
@ -591,30 +600,30 @@ def zip_excel_import_files(
fn = fn.split("/")[-1] # use only last component, not directories
return fn
Filename2Etud = {} # filename : etudid
filename_to_etud = {} # filename : etudid
for l in data[1:]:
filename = l[filename_idx].strip()
if filename:
Filename2Etud[normfilename(filename)] = l[etudid_idx]
filename_to_etud[normfilename(filename)] = l[etudid_idx]
# 2- Ouvre le zip et
try:
z = ZipFile(zipfile)
except BadZipfile:
raise ScoValueError("Fichier ZIP incorrect !")
raise ScoValueError("Fichier ZIP incorrect !") from BadZipfile
ignored_zipfiles = []
stored = [] # [ (etud, filename) ]
stored_etud_filename = [] # [ (etud, filename) ]
for name in z.namelist():
if len(name) > 4 and name[-1] != "/" and "." in name:
data = z.read(name)
# match zip filename with name given in excel
normname = normfilename(name)
if normname in Filename2Etud:
etudid = Filename2Etud[normname]
if normname in filename_to_etud:
etudid = filename_to_etud[normname]
# ok, store photo
try:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
del Filename2Etud[normname]
del filename_to_etud[normname]
except:
raise ScoValueError("ID étudiant invalide: %s" % etudid)
@ -624,7 +633,7 @@ def zip_excel_import_files(
normfilename(name, lowercase=False),
)
stored.append((etud, name))
stored_etud_filename.append((etud, name))
else:
log("zip: zip name %s not in excel !" % name)
ignored_zipfiles.append(name)
@ -632,35 +641,9 @@ def zip_excel_import_files(
if name[-1] != "/":
ignored_zipfiles.append(name)
log("zip: ignoring %s" % name)
if Filename2Etud:
if filename_to_etud:
# lignes excel non traitées
unmatched_files = list(Filename2Etud.keys())
unmatched_files = list(filename_to_etud.keys())
else:
unmatched_files = []
# 3- Result page
H = [
_trombino_html_header(),
"""<h2 class="formsemestre">%s</h2>
<h3>Opération effectuée</h3>
"""
% page_title,
]
if ignored_zipfiles:
H.append("<h4>Fichiers ignorés dans le zip:</h4><ul>")
for name in ignored_zipfiles:
H.append("<li>%s</li>" % name)
H.append("</ul>")
if unmatched_files:
H.append(
"<h4>Fichiers indiqués dans feuille mais non trouvés dans le zip:</h4><ul>"
)
for name in unmatched_files:
H.append("<li>%s</li>" % name)
H.append("</ul>")
if stored:
H.append("<h4>Fichiers chargés:</h4><ul>")
for (etud, name) in stored:
H.append("<li>%s: <tt>%s</tt></li>" % (etud["nomprenom"], name))
H.append("</ul>")
return "\n".join(H)
return ignored_zipfiles, unmatched_files, stored_etud_filename

View File

@ -572,17 +572,24 @@ def sendJSON(data, attached=False):
)
def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False):
def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True):
if type(data) != list:
data = [data] # always list-of-dicts
if force_outer_xml_tag:
data = [{tagname: data}]
tagname += "_list"
doc = sco_xml.simple_dictlist2xml(data, tagname=tagname)
doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote)
return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached)
def sendResult(data, name=None, format=None, force_outer_xml_tag=True, attached=False):
def sendResult(
data,
name=None,
format=None,
force_outer_xml_tag=True,
attached=False,
quote_xml=True,
):
if (format is None) or (format == "html"):
return data
elif format == "xml": # name is outer tagname
@ -591,6 +598,7 @@ def sendResult(data, name=None, format=None, force_outer_xml_tag=True, attached=
tagname=name,
force_outer_xml_tag=force_outer_xml_tag,
attached=attached,
quote=quote_xml,
)
elif format == "json":
return sendJSON(data, attached=attached)

View File

@ -82,25 +82,35 @@ def simple_dictlist2xml(dictlist, tagname=None, quote=False, pretty=True):
return ans
def _repr_as_xml(v):
if isinstance(v, bool):
return str(int(v)) # booleans as "0" / "1"
return str(v)
def _dictlist2xml(dictlist, root=None, tagname=None, quote=False):
scalar_types = (bytes, str, int, float)
scalar_types = (bytes, str, int, float, bool)
for d in dictlist:
elem = ElementTree.Element(tagname)
root.append(elem)
if isinstance(d, scalar_types) or isinstance(d, ApoEtapeVDI):
elem.set("code", str(d))
elem.set("code", _repr_as_xml(d))
else:
if quote:
d_scalar = dict(
[
(k, quote_xml_attr(v))
(k, quote_xml_attr(_repr_as_xml(v)))
for (k, v) in d.items()
if isinstance(v, scalar_types)
]
)
else:
d_scalar = dict(
[(k, str(v)) for (k, v) in d.items() if isinstance(v, scalar_types)]
[
(k, _repr_as_xml(v))
for (k, v) in d.items()
if isinstance(v, scalar_types)
]
)
for k in d_scalar:
elem.set(k, d_scalar[k])
@ -134,4 +144,4 @@ def xml_to_dicts(element):
for child in element.childNodes:
if child.nodeType == ELEMENT_NODE:
childs.append(xml_to_dicts(child))
return (element.nodeName, d, childs)
return (element.nodeName, d, childs)

View File

@ -1511,6 +1511,19 @@ span.ue_type {
margin-right: 1.5em;
}
div.formation_ue_list_externes {
background-color: #98cc98;
}
div.formation_ue_list_externes ul.notes_ue_list, div.formation_ue_list_externes li.notes_ue_list {
background-color: #98cc98;
}
span.ue_is_external span {
color: orange;
}
span.ue_is_external a {
font-weight: normal;
}
li.notes_matiere_list {
margin-top: 2px;
}

View File

@ -0,0 +1,39 @@
{% extends 'base.html' %}
{% block app_content %}
<h2 class="formsemestre">{{ page_title }}</h2>
<h3>Opération effectuée</h3>
{% if ignored_zipfiles %}
<h4>Fichiers ignorés dans le zip:</h4>
<ul>
{% for name in ignored_zipfiles %}
<li>{{name}}</li>
{% endfor %}
</ul>
{% endif %}
{% if unmatched_files %}
<h4>Fichiers indiqués dans la feuille mais non trouvés dans le zip:</h4>
<ul>
{% for name in unmatched_files %}
<li>{{name}}</li>
{% endfor %}
</ul>
{% endif %}
{% if stored_etud_filename %}
<h4>Fichiers chargés:</h4>
<ul>
{% for (etud, name) in stored_etud_filename %}
<li>{{etud["nomprenom"]}}: <tt>{{name}}</tt></li>
{% endfor %}
</ul>
{% endif %}
<div>
<p><a href="{{ next_page | safe }}">Continuer</a>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
Importation des photo effectuée
{% if ignored_zipfiles %}
# Fichiers ignorés dans le zip:
{% for name in ignored_zipfiles %}
- {{name}}
{% endfor %}
{% endif %}
{% if unmatched_files %}
# Fichiers indiqués dans la feuille mais non trouvés dans le zip:
{% for name in unmatched_files %}
- {{name}}
{% endfor %}
{% endif %}
{% if stored_etud_filename %}
# Fichiers chargés:
{% for (etud, name) in stored_etud_filename %}
- {{etud["nomprenom"]}}: <tt>{{name}}</tt></li>
{% endfor %}
{% endif %}

View File

@ -1,12 +1,12 @@
<h2 class="insidebar">Dépt. {{ prefs["DeptName"] }}</h2>
<a href="{{ url_for('scodoc.index') }}" class="sidebar">Accueil</a> <br/>
{% if prefs["DeptIntranetURL"] %}
<a href="{{ prefs["DeptIntranetURL"] }}" class="sidebar">
{{ prefs["DeptIntranetTitle"] }}</a>
{% endif %}
<br/>
<a href="{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}" class="sidebar">Accueil</a> <br />
{% if prefs["DeptIntranetURL"] %}
<a href="{{ prefs[" DeptIntranetURL"] }}" class="sidebar">
{{ prefs["DeptIntranetTitle"] }}</a>
{% endif %}
<br />
{#
{#
# Entreprises pas encore supporté en ScoDoc8
# <br/><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br/>
# <br /><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br />
#}

View File

@ -1046,9 +1046,9 @@ def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display
# ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail)
@bp.route("/AddBilletAbsence")
@bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat
@scodoc
@permission_required(Permission.ScoAbsAddBillet)
@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet)
@scodoc7func
def AddBilletAbsence(
begin,
@ -1060,7 +1060,7 @@ def AddBilletAbsence(
justified=True,
xml_reply=True,
):
"""Memorise un "billet"
"""Mémorise un "billet"
begin et end sont au format ISO (eg "1999-01-08 04:05:06")
"""
t0 = time.time()
@ -1251,9 +1251,9 @@ def XMLgetBilletsEtud(etudid=False):
return r
@bp.route("/listeBillets", methods=["GET", "POST"]) # pour compat anciens clients PHP
@bp.route("/listeBillets", methods=["GET"])
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@permission_required(Permission.ScoView)
@scodoc7func
def listeBillets():
"""Page liste des billets non traités et formulaire recherche d'un billet"""

View File

@ -41,9 +41,12 @@ import flask
from flask import url_for
from flask import current_app, g, request
from flask_login import current_user
from werkzeug.utils import redirect
from config import Config
from app import db
from app import models
from app.auth.models import User
from app.decorators import (
@ -334,7 +337,36 @@ sco_publish(
Permission.ScoChangeFormation,
methods=["GET", "POST"],
)
sco_publish("/ue_list", sco_edit_ue.ue_table, Permission.ScoView)
@bp.route("/ue_list") # backward compat
@bp.route("/ue_table")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def ue_table(formation_id=None, msg=""):
return sco_edit_ue.ue_table(formation_id=formation_id, msg=msg)
@bp.route("/ue_set_internal", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoChangeFormation)
@scodoc7func
def ue_set_internal(ue_id):
""""""
ue = models.formations.NotesUE.query.get(ue_id)
if not ue:
raise ScoValueError("invalid ue_id")
ue.is_external = False
db.session.add(ue)
db.session.commit()
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation_id
)
)
sco_publish("/ue_sharing_code", sco_edit_ue.ue_sharing_code, Permission.ScoView)
sco_publish(
"/edit_ue_set_code_apogee",

View File

@ -363,6 +363,8 @@ def search_etud_by_name():
@scodoc7func
def etud_info(etudid=None, format="xml"):
"Donne les informations sur un etudiant"
if not format in ("xml", "json"):
raise ScoValueError("format demandé non supporté par cette fonction.")
t0 = time.time()
args = sco_etud.make_etud_args(etudid=etudid)
cnx = ndb.GetDBConnexion()
@ -413,12 +415,10 @@ def etud_info(etudid=None, format="xml"):
"codelycee",
"date_naissance_iso",
):
d[a] = scu.quote_xml_attr(etud[a])
d["civilite"] = scu.quote_xml_attr(
etud["civilite_str"]
) # exception: ne sort pas la civilite brute
d[a] = etud[a] # ne pas quoter car ElementTree.tostring quote déjà
d["civilite"] = etud["civilite_str"] # exception: ne sort pas la civilite brute
d["sexe"] = d["civilite"] # backward compat pour anciens clients
d["photo_url"] = scu.quote_xml_attr(sco_photos.etud_photo_url(etud))
d["photo_url"] = sco_photos.etud_photo_url(etud)
sem = etud["cursem"]
if sem:
@ -429,10 +429,8 @@ def etud_info(etudid=None, format="xml"):
"formsemestre_id": sem["formsemestre_id"],
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
"date_fin": ndb.DateDMYtoISO(sem["date_fin"]),
"etat": scu.quote_xml_attr(sem["ins"]["etat"]),
"groupes": scu.quote_xml_attr(
etud["groupes"]
), # slt pour semestre courant
"etat": sem["ins"]["etat"],
"groupes": etud["groupes"], # slt pour semestre courant
}
]
else:
@ -444,12 +442,14 @@ def etud_info(etudid=None, format="xml"):
"formsemestre_id": sem["formsemestre_id"],
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
"date_fin": ndb.DateDMYtoISO(sem["date_fin"]),
"etat": scu.quote_xml_attr(sem["ins"]["etat"]),
"etat": sem["ins"]["etat"],
}
)
log("etud_info (%gs)" % (time.time() - t0))
return scu.sendResult(d, name="etudiant", format=format, force_outer_xml_tag=False)
return scu.sendResult(
d, name="etudiant", format=format, force_outer_xml_tag=False, quote_xml=False
)
# -------------------------- FICHE ETUDIANT --------------------------

View File

@ -33,7 +33,7 @@ class Config:
# evite confusion avec le log nginx scodoc_error.log:
SCODOC_ERR_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc_exc.log")
#
MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # Flask uploads
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # Flask uploads (16Mo, en ligne avec nginx)
# STATIC_URL_PATH = "/ScoDoc/static"
# static_folder = "stat"

View File

@ -13,6 +13,7 @@ import sys
import click
import flask
from flask.cli import with_appcontext
from flask.templating import render_template
from app import create_app, cli, db
from app import initialize_scodoc_database
@ -323,6 +324,50 @@ def migrate_scodoc7_dept_archive(dept: str): # migrate-scodoc7-dept-archive
tools.migrate_scodoc7_dept_archive(dept)
@app.cli.command()
@click.argument("formsemestre_id", type=click.INT)
@click.argument("xlsfile", type=click.File("rb"))
@click.argument("zipfile", type=click.File("rb"))
def photos_import_files(formsemestre_id: int, xlsfile: str, zipfile: str):
import app as mapp
from app.scodoc import sco_trombino, sco_photos
from app.scodoc import notesdb as ndb
from flask_login import login_user
from app.auth.models import get_super_admin
sem = mapp.models.formsemestre.FormSemestre.query.get(formsemestre_id)
if not sem:
sys.stderr.write("photos-import-files: numéro de semestre invalide\n")
return 2
with app.test_request_context():
mapp.set_sco_dept(sem.departement.acronym)
admin_user = get_super_admin()
login_user(admin_user)
def callback(etud, data, filename):
sco_photos.store_photo(etud, data)
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = sco_trombino.zip_excel_import_files(
xlsfile=xlsfile,
zipfile=zipfile,
callback=callback,
filename_title="fichier_photo",
)
print(
render_template(
"scolar/photos_import_files.txt",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
)
)
@app.cli.command()
@with_appcontext
def clear_cache(): # clear-cache

View File

@ -324,7 +324,7 @@ class ScoFake(object):
formation (dict), liste d'ue (dicts), liste de modules.
"""
f = self.create_formation(acronyme=acronyme, titre=titre)
ue_list = []
ues = []
mod_list = []
for semestre_id in range(1, nb_semestre + 1):
for n in range(1, nb_ue_per_semestre + 1):
@ -333,7 +333,7 @@ class ScoFake(object):
acronyme="TSU%s%s" % (semestre_id, n),
titre="ue test %s%s" % (semestre_id, n),
)
ue_list.append(ue)
ues.append(ue)
mat = self.create_matiere(ue_id=ue["ue_id"], titre="matière test")
for _ in range(nb_module_per_ue):
mod = self.create_module(
@ -346,7 +346,7 @@ class ScoFake(object):
formation_id=f["formation_id"], # faiblesse de l'API
)
mod_list.append(mod)
return f, ue_list, mod_list
return f, ues, mod_list
def setup_formsemestre(
self,

View File

@ -339,6 +339,10 @@ def test_import_formation(test_client):
f = sco_formations.formation_import_xml(doc)
assert len(f) == 3 # 3-uple
formation_id = f[0]
# --- Vérification des UE
ues = sco_edit_ue.ue_list({"formation_id": formation_id})
assert len(ues) == 10
assert all(not ue["is_external"] for ue in ues) # aucune UE externe dans le XML
# --- Mise en place de 4 semestres
sems = [
G.create_formsemestre(