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(): def sidebar_common():
"partie commune à toutes les sidebar" "partie commune à toutes les sidebar"
H = [ 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="{ <div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page", url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name) scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)

View File

@ -27,9 +27,12 @@
"""Various HTML generation functions """Various HTML generation functions
""" """
from html.parser import HTMLParser
from html.entities import name2codepoint
import re
from flask import g, url_for from flask import g, url_for
import app.scodoc.sco_utils as scu
from . import listhistogram from . import listhistogram
@ -130,3 +133,63 @@ def make_menu(title, items, css_class="", alone=False):
if alone: if alone:
H.append("</ul>") H.append("</ul>")
return "".join(H) 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_notes += val * coef
matiere_sum_coefs += coef matiere_sum_coefs += coef
matiere_id_last = matiere_id matiere_id_last = matiere_id
except: # val == "NI" "NA" except TypeError: # val == "NI" "NA"
assert val == "NI" or val == "NA" assert val == "NI" or val == "NA"
nb_missing = nb_missing + 1 nb_missing = nb_missing + 1
coefs.append(0) coefs.append(0)

View File

@ -597,6 +597,22 @@ def float_null_is_null(x):
return float(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 # post filtering
# #
def UniqListofDicts(L, key): def UniqListofDicts(L, key):

View File

@ -30,7 +30,8 @@
les dossiers d'admission et autres pièces utiles. les dossiers d'admission et autres pièces utiles.
""" """
import flask 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 from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -328,9 +329,9 @@ def etudarchive_import_files_form(group_id):
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F return "\n".join(H) + tf[1] + "</li></ol>" + F
elif tf[0] == -1: # retrouve le semestre à partir du groupe:
# retrouve le semestre à partir du groupe: group = sco_groups.get_group(group_id)
group = sco_groups.get_group(group_id) if tf[0] == -1:
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
@ -340,21 +341,41 @@ def etudarchive_import_files_form(group_id):
) )
else: else:
return etudarchive_import_files( return etudarchive_import_files(
group_id=tf[2]["group_id"], formsemestre_id=group["formsemestre_id"],
xlsfile=tf[2]["xlsfile"], xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"], zipfile=tf[2]["zipfile"],
description=tf[2]["description"], 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): def callback(etud, data, filename):
_store_etud_file_to_new_archive(etud["etudid"], data, filename, description) _store_etud_file_to_new_archive(etud["etudid"], data, filename, description)
filename_title = "fichier_a_charger" # Utilise la fontion developpée au depart pour les photos
page_title = "Téléchargement de fichiers associés aux étudiants" (
# Utilise la fontion au depart developpee pour les photos ignored_zipfiles,
r = sco_trombino.zip_excel_import_files( unmatched_files,
xlsfile, zipfile, callback, filename_title, page_title 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): def matiere_delete(matiere_id=None):
"""Delete an UE""" """Delete matière"""
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
M = matiere_list(args={"matiere_id": matiere_id})[0] 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, "<h2>Suppression de la matière %(titre)s" % M,
" dans l'UE (%(acronyme)s))</h2>" % UE, " 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( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
@ -227,13 +231,13 @@ def matiere_edit(matiere_id=None):
if not F: if not F:
raise ScoValueError("Matière inexistante !") raise ScoValueError("Matière inexistante !")
F = F[0] F = F[0]
U = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]}) ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
if not F: if not ues:
raise ScoValueError("UE inexistante !") raise ScoValueError("UE inexistante !")
U = U[0] ue = ues[0]
Fo = sco_formations.formation_list(args={"formation_id": U["formation_id"]})[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_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
ue_ids = [u["ue_id"] for u in ues] ue_ids = [u["ue_id"] for u in ues]
H = [ H = [
@ -278,8 +282,11 @@ associé.
submitlabel="Modifier les valeurs", 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: if tf[0] == 0:
return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:

View File

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

View File

@ -75,7 +75,7 @@ _ueEditor = ndb.EditableTable(
sortkey="numero", sortkey="numero",
input_formators={ input_formators={
"type": ndb.int_null_is_zero, "type": ndb.int_null_is_zero,
"is_external": bool, "is_external": ndb.bool_or_str,
}, },
output_formators={ output_formators={
"numero": ndb.int_null_is_zero, "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"]), % (len(validations), ue["acronyme"], ue["titre"]),
dest_url="", dest_url="",
target_variable="delete_validations", 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}, parameters={"ue_id": ue_id, "dialog_confirmed": 1},
) )
if delete_validations: 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", "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: if parcours.UE_IS_MODULE:
# demande le semestre pour creer le module immediatement: # 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. """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), Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
qui les place à la fin de la liste. 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"]}) Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
if Modlist: if Modlist:
ue["semestre_id"] = Modlist[0]["semestre_id"] 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. """Numero d'une nouvelle UE dans cette formation.
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
""" """
ue_list = ue_list(args={"formation_id": formation_id}) ues = ue_list(args={"formation_id": formation_id})
if not ue_list: if not ues:
return 0 return 0
if semestre_id is None: if semestre_id is None:
return ue_list[-1]["numero"] + 1000 return ues[-1]["numero"] + 1000
else: else:
# Avec semestre: (prend le semestre du 1er module de l'UE) # Avec semestre: (prend le semestre du 1er module de l'UE)
_add_ue_semestre_id(ue_list) _add_ue_semestre_id(ues)
ue_list_semestre = [ue for ue in ue_list if ue["semestre_id"] == semestre_id] ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
if ue_list_semestre: if ue_list_semestre:
return ue_list_semestre[-1]["numero"] + 10 return ue_list_semestre[-1]["numero"] + 10
else: else:
return ue_list[-1]["numero"] + 1000 return ues[-1]["numero"] + 1000
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE""" """Delete an UE"""
ue = ue_list(args={"ue_id": ue_id}) ues = ue_list(args={"ue_id": ue_id})
if not ue: if not ues:
raise ScoValueError("UE inexistante !") raise ScoValueError("UE inexistante !")
ue = ue[0] ue = ues[0]
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue, "<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
dest_url="", dest_url="",
parameters={"ue_id": ue_id}, 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) 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"]) parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
locked = sco_formations.formation_has_locked_sems(formation_id) 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: # tri par semestre et numero:
_add_ue_semestre_id(ue_list) _add_ue_semestre_id(ues)
ue_list.sort(key=lambda u: (u["semestre_id"], u["numero"])) _add_ue_semestre_id(ues_externes)
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ue_list])) != len(ue_list) 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) has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and perm_change # editable = (not locked) and has_perm_change
# On autorise maintanant la modification des formations qui ont des semestres verrouillés, # On autorise maintanant la modification des formations qui ont des semestres verrouillés,
# sauf si cela affect les notes passées (verrouillées): # sauf si cela affect les notes passées (verrouillées):
# - pas de modif des modules utilisés dans des semestres verrouillés # - 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 # - pas de changement des codes d'UE utilisés dans des semestres verrouillés
editable = perm_change editable = has_perm_change
tag_editable = ( tag_editable = (
current_user.has_permission(Permission.ScoEditFormationTags) or perm_change current_user.has_permission(Permission.ScoEditFormationTags) or has_perm_change
) )
if locked: if locked:
lockicon = scu.icontag("lock32_img", title="verrouillé") lockicon = scu.icontag("lock32_img", title="verrouillé")
@ -556,213 +575,20 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
H.append( H.append(
'<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>' '<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>'
) )
H.append(
cur_ue_semestre_id = None _ue_table_ues(
iue = 0 parcours,
for UE in ue_list: ues,
if UE["ects"]: editable,
UE["ects_str"] = ", %g ECTS" % UE["ects"] tag_editable,
else: has_perm_change,
UE["ects_str"] = "" arrow_up,
if editable: arrow_down,
klass = "span_apo_edit" arrow_none,
else: delete_icon,
klass = "" delete_disabled_icon,
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(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: if editable:
H.append( H.append(
'<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>' '<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 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>") H.append("<p><ul>")
if editable: if editable:
H.append( H.append(
@ -795,7 +642,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</p>""" </p>"""
% F % F
) )
if perm_change: if has_perm_change:
H.append( H.append(
""" """
<h3> <a name="sems">Semestres ou sessions de cette formation</a></h3> <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) 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): def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
"""HTML list of UE sharing this code """HTML list of UE sharing this code
Either ue_code or ue_id may be specified. 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") raise ScoValueError("invalid formation_id")
F = F[0] F = F[0]
T = [] T = []
ue_list = ue_list(args={"formation_id": formation_id}) ues = ue_list(args={"formation_id": formation_id})
for UE in ue_list: for ue in ues:
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": UE["ue_id"]}) Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for Mat in Matlist: for Mat in Matlist:
Modlist = sco_edit_module.module_list( Modlist = sco_edit_module.module_list(
args={"matiere_id": Mat["matiere_id"]} args={"matiere_id": Mat["matiere_id"]}
@ -978,7 +1113,7 @@ def formation_table_recap(formation_id, format="html"):
# #
T.append( T.append(
{ {
"UE_acro": UE["acronyme"], "UE_acro": ue["acronyme"],
"Mat_tit": Mat["titre"], "Mat_tit": Mat["titre"],
"Mod_tit": Mod["abbrev"] or Mod["titre"], "Mod_tit": Mod["abbrev"] or Mod["titre"],
"Mod_code": Mod["code"], "Mod_code": Mod["code"],

View File

@ -356,7 +356,7 @@ def apo_semset_maq_status(
H.append( H.append(
", ".join( ", ".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 % f
for f in formations for f in formations
] ]

View File

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

View File

@ -93,12 +93,21 @@ def formation_has_locked_sems(formation_id):
return sems 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 """Get a formation, with UE, matieres, modules
in desired format in desired format
""" """
F = formation_list(args={"formation_id": formation_id})[0] 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 F["ue"] = ues
for ue in ues: for ue in ues:
ue_id = ue["ue_id"] ue_id = ue["ue_id"]
@ -254,7 +263,11 @@ def formation_list_table(formation_id=None, args={}):
).NAME ).NAME
except: except:
f["parcours_name"] = "" 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_link_class"] = "stdlink"
f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-") f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-")
# Ajoute les semestres associés à chaque formation: # 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: if tf[0] == 0 or msg:
return ( 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 % F
+ msg + msg
+ str(tf[1]) + 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) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
ue_list = _list_ue_with_coef_and_validations(sem, etudid) ues = _list_ue_with_coef_and_validations(sem, etudid)
descr = _ue_form_description(ue_list, scu.get_request_args()) descr = _ue_form_description(ues, scu.get_request_args())
if request.method == "GET": if request.method == "GET":
initvalues = { initvalues = {
"note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") "note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") for ue in ues
for ue in ue_list
} }
else: else:
initvalues = {} initvalues = {}
@ -247,15 +246,13 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
return "\n".join(H) return "\n".join(H)
else: # soumission else: # soumission
# simule erreur # simule erreur
ok, message = _check_values(ue_list, tf[2]) ok, message = _check_values(ues, tf[2])
if not ok: if not ok:
H = _make_page(etud, sem, tf, message=message) H = _make_page(etud, sem, tf, message=message)
return "\n".join(H) return "\n".join(H)
else: else:
# Submit # Submit
_record_ue_validations_and_coefs( _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, tf[2])
formsemestre_id, etudid, ue_list, tf[2]
)
return flask.redirect( return flask.redirect(
"formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
% (formsemestre_id, etudid) % (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 """Description du formulaire de saisie des UE / validations
Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient. 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"}), ("formsemestre_id", {"input_type": "hidden"}),
("etudid", {"input_type": "hidden"}), ("etudid", {"input_type": "hidden"}),
] ]
for ue in ue_list: for ue in ues:
# Menu pour code validation UE: # Menu pour code validation UE:
# Ne propose que ADM, CMP et "Non inscrit" # Ne propose que ADM, CMP et "Non inscrit"
select_name = "valid_" + str(ue["ue_id"]) select_name = "valid_" + str(ue["ue_id"])
@ -439,8 +436,8 @@ def _list_ue_with_coef_and_validations(sem, etudid):
""" """
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
ue_list = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]}) ues = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]})
for ue in ue_list: for ue in ues:
# add coefficient # add coefficient
uecoef = sco_formsemestre.formsemestre_uecoef_list( uecoef = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]} 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] ue["validation"] = validation[0]
else: else:
ue["validation"] = {} ue["validation"] = {}
return ue_list return ues
def _record_ue_validations_and_coefs(formsemestre_id, etudid, ue_list, values): def _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, values):
for ue in ue_list: for ue in ues:
code = values.get("valid_" + str(ue["ue_id"]), False) code = values.get("valid_" + str(ue["ue_id"]), False)
if code == "None": if code == "None":
code = None code = None

View File

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

View File

@ -178,6 +178,19 @@ def moduleimpl_withmodule_list(
return modimpls 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): def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None):
"list moduleimpl_inscriptions" "list moduleimpl_inscriptions"
args = locals() args = locals()

View File

@ -348,7 +348,6 @@ def make_formsemestre_recapcomplet(
if not hidemodules: if not hidemodules:
h.append("") h.append("")
pass pass
if not hidemodules and not ue["is_external"]: if not hidemodules and not ue["is_external"]:
for modimpl in modimpls: for modimpl in modimpls:
if modimpl["module"]["ue_id"] == ue["ue_id"]: 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 # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
NotesDB = sco_evaluations.do_evaluation_get_all_notes( NotesDB = sco_evaluations.do_evaluation_get_all_notes(
evaluation_id, by_uid=current_user.user_name evaluation_id, by_uid=current_user.id
) )
else: else:
raise AccessDenied("Modification des notes impossible pour %s" % current_user) 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( nb_changed, nb_suppress, existing_decisions = _notes_add(
current_user, evaluation_id, notes, do_it=False 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: 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>""" 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( return scu.confirm_dialog(

View File

@ -30,6 +30,7 @@
import io import io
from zipfile import ZipFile, BadZipfile from zipfile import ZipFile, BadZipfile
from flask.templating import render_template
import reportlab import reportlab
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY 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: elif tf[0] == -1:
return flask.redirect(back_url) return flask.redirect(back_url)
else: 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"], xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"], 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( def zip_excel_import_files(
@ -557,19 +566,19 @@ def zip_excel_import_files(
zipfile=None, zipfile=None,
callback=None, callback=None,
filename_title="", # doit obligatoirement etre specifié filename_title="", # doit obligatoirement etre specifié
page_title="",
): ):
"""Importation de fichiers à partir d'un excel et d'un zip """Importation de fichiers à partir d'un excel et d'un zip
La fonction La fonction
callback() 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 # 1- build mapping etudid -> filename
exceldata = xlsfile.read() exceldata = xlsfile.read()
if not exceldata: if not exceldata:
raise ScoValueError("Fichier excel vide ou invalide") raise ScoValueError("Fichier excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata) _, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: # probably a bug if not data:
raise ScoValueError("Fichier excel vide !") raise ScoValueError("Fichier excel vide !")
# on doit avoir une colonne etudid et une colonne filename_title ('fichier_photo') # on doit avoir une colonne etudid et une colonne filename_title ('fichier_photo')
titles = data[0] titles = data[0]
@ -591,30 +600,30 @@ def zip_excel_import_files(
fn = fn.split("/")[-1] # use only last component, not directories fn = fn.split("/")[-1] # use only last component, not directories
return fn return fn
Filename2Etud = {} # filename : etudid filename_to_etud = {} # filename : etudid
for l in data[1:]: for l in data[1:]:
filename = l[filename_idx].strip() filename = l[filename_idx].strip()
if filename: if filename:
Filename2Etud[normfilename(filename)] = l[etudid_idx] filename_to_etud[normfilename(filename)] = l[etudid_idx]
# 2- Ouvre le zip et # 2- Ouvre le zip et
try: try:
z = ZipFile(zipfile) z = ZipFile(zipfile)
except BadZipfile: except BadZipfile:
raise ScoValueError("Fichier ZIP incorrect !") raise ScoValueError("Fichier ZIP incorrect !") from BadZipfile
ignored_zipfiles = [] ignored_zipfiles = []
stored = [] # [ (etud, filename) ] stored_etud_filename = [] # [ (etud, filename) ]
for name in z.namelist(): for name in z.namelist():
if len(name) > 4 and name[-1] != "/" and "." in name: if len(name) > 4 and name[-1] != "/" and "." in name:
data = z.read(name) data = z.read(name)
# match zip filename with name given in excel # match zip filename with name given in excel
normname = normfilename(name) normname = normfilename(name)
if normname in Filename2Etud: if normname in filename_to_etud:
etudid = Filename2Etud[normname] etudid = filename_to_etud[normname]
# ok, store photo # ok, store photo
try: try:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
del Filename2Etud[normname] del filename_to_etud[normname]
except: except:
raise ScoValueError("ID étudiant invalide: %s" % etudid) raise ScoValueError("ID étudiant invalide: %s" % etudid)
@ -624,7 +633,7 @@ def zip_excel_import_files(
normfilename(name, lowercase=False), normfilename(name, lowercase=False),
) )
stored.append((etud, name)) stored_etud_filename.append((etud, name))
else: else:
log("zip: zip name %s not in excel !" % name) log("zip: zip name %s not in excel !" % name)
ignored_zipfiles.append(name) ignored_zipfiles.append(name)
@ -632,35 +641,9 @@ def zip_excel_import_files(
if name[-1] != "/": if name[-1] != "/":
ignored_zipfiles.append(name) ignored_zipfiles.append(name)
log("zip: ignoring %s" % name) log("zip: ignoring %s" % name)
if Filename2Etud: if filename_to_etud:
# lignes excel non traitées # lignes excel non traitées
unmatched_files = list(Filename2Etud.keys()) unmatched_files = list(filename_to_etud.keys())
else: else:
unmatched_files = [] unmatched_files = []
# 3- Result page return ignored_zipfiles, unmatched_files, stored_etud_filename
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)

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: if type(data) != list:
data = [data] # always list-of-dicts data = [data] # always list-of-dicts
if force_outer_xml_tag: if force_outer_xml_tag:
data = [{tagname: data}] data = [{tagname: data}]
tagname += "_list" 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) 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"): if (format is None) or (format == "html"):
return data return data
elif format == "xml": # name is outer tagname 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, tagname=name,
force_outer_xml_tag=force_outer_xml_tag, force_outer_xml_tag=force_outer_xml_tag,
attached=attached, attached=attached,
quote=quote_xml,
) )
elif format == "json": elif format == "json":
return sendJSON(data, attached=attached) return sendJSON(data, attached=attached)

View File

@ -82,25 +82,35 @@ def simple_dictlist2xml(dictlist, tagname=None, quote=False, pretty=True):
return ans 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): 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: for d in dictlist:
elem = ElementTree.Element(tagname) elem = ElementTree.Element(tagname)
root.append(elem) root.append(elem)
if isinstance(d, scalar_types) or isinstance(d, ApoEtapeVDI): if isinstance(d, scalar_types) or isinstance(d, ApoEtapeVDI):
elem.set("code", str(d)) elem.set("code", _repr_as_xml(d))
else: else:
if quote: if quote:
d_scalar = dict( d_scalar = dict(
[ [
(k, quote_xml_attr(v)) (k, quote_xml_attr(_repr_as_xml(v)))
for (k, v) in d.items() for (k, v) in d.items()
if isinstance(v, scalar_types) if isinstance(v, scalar_types)
] ]
) )
else: else:
d_scalar = dict( 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: for k in d_scalar:
elem.set(k, d_scalar[k]) elem.set(k, d_scalar[k])
@ -134,4 +144,4 @@ def xml_to_dicts(element):
for child in element.childNodes: for child in element.childNodes:
if child.nodeType == ELEMENT_NODE: if child.nodeType == ELEMENT_NODE:
childs.append(xml_to_dicts(child)) 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; 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 { li.notes_matiere_list {
margin-top: 2px; 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> <h2 class="insidebar">Dépt. {{ prefs["DeptName"] }}</h2>
<a href="{{ url_for('scodoc.index') }}" class="sidebar">Accueil</a> <br/> <a href="{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}" class="sidebar">Accueil</a> <br />
{% if prefs["DeptIntranetURL"] %} {% if prefs["DeptIntranetURL"] %}
<a href="{{ prefs["DeptIntranetURL"] }}" class="sidebar"> <a href="{{ prefs[" DeptIntranetURL"] }}" class="sidebar">
{{ prefs["DeptIntranetTitle"] }}</a> {{ prefs["DeptIntranetTitle"] }}</a>
{% endif %} {% endif %}
<br/> <br />
{# {#
# Entreprises pas encore supporté en ScoDoc8 # 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) # ----- 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 @scodoc
@permission_required(Permission.ScoAbsAddBillet) @permission_required_compat_scodoc7(Permission.ScoAbsAddBillet)
@scodoc7func @scodoc7func
def AddBilletAbsence( def AddBilletAbsence(
begin, begin,
@ -1060,7 +1060,7 @@ def AddBilletAbsence(
justified=True, justified=True,
xml_reply=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") begin et end sont au format ISO (eg "1999-01-08 04:05:06")
""" """
t0 = time.time() t0 = time.time()
@ -1251,9 +1251,9 @@ def XMLgetBilletsEtud(etudid=False):
return r return r
@bp.route("/listeBillets", methods=["GET", "POST"]) # pour compat anciens clients PHP @bp.route("/listeBillets", methods=["GET"])
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def listeBillets(): def listeBillets():
"""Page liste des billets non traités et formulaire recherche d'un billet""" """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 url_for
from flask import current_app, g, request from flask import current_app, g, request
from flask_login import current_user from flask_login import current_user
from werkzeug.utils import redirect
from config import Config from config import Config
from app import db
from app import models
from app.auth.models import User from app.auth.models import User
from app.decorators import ( from app.decorators import (
@ -334,7 +337,36 @@ sco_publish(
Permission.ScoChangeFormation, Permission.ScoChangeFormation,
methods=["GET", "POST"], 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("/ue_sharing_code", sco_edit_ue.ue_sharing_code, Permission.ScoView)
sco_publish( sco_publish(
"/edit_ue_set_code_apogee", "/edit_ue_set_code_apogee",

View File

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

View File

@ -33,7 +33,7 @@ class Config:
# evite confusion avec le log nginx scodoc_error.log: # evite confusion avec le log nginx scodoc_error.log:
SCODOC_ERR_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc_exc.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_URL_PATH = "/ScoDoc/static"
# static_folder = "stat" # static_folder = "stat"

View File

@ -13,6 +13,7 @@ import sys
import click import click
import flask import flask
from flask.cli import with_appcontext from flask.cli import with_appcontext
from flask.templating import render_template
from app import create_app, cli, db from app import create_app, cli, db
from app import initialize_scodoc_database 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) 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() @app.cli.command()
@with_appcontext @with_appcontext
def clear_cache(): # clear-cache def clear_cache(): # clear-cache

View File

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

View File

@ -339,6 +339,10 @@ def test_import_formation(test_client):
f = sco_formations.formation_import_xml(doc) f = sco_formations.formation_import_xml(doc)
assert len(f) == 3 # 3-uple assert len(f) == 3 # 3-uple
formation_id = f[0] 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 # --- Mise en place de 4 semestres
sems = [ sems = [
G.create_formsemestre( G.create_formsemestre(