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

This commit is contained in:
Emmanuel Viennet 2022-03-03 09:05:32 +01:00
commit 09111d9455
16 changed files with 196 additions and 47 deletions

View File

@ -198,7 +198,10 @@ class BonusSportAdditif(BonusSport):
à la moyenne générale du semestre déjà obtenue par l'étudiant. à la moyenne générale du semestre déjà obtenue par l'étudiant.
""" """
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte
seuil_comptage = (
None # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen)
)
proportion_point = 0.05 # multiplie les points au dessus du seuil proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@ -211,11 +214,13 @@ class BonusSportAdditif(BonusSport):
if 0 in sem_modimpl_moys_inscrits.shape: if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module... # pas d'étudiants ou pas d'UE ou pas de module...
return return
seuil_comptage = (
self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
)
bonus_moy_arr = np.sum( bonus_moy_arr = np.sum(
np.where( np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen, sem_modimpl_moys_inscrits > self.seuil_moy_gen,
(sem_modimpl_moys_inscrits - self.seuil_moy_gen) (sem_modimpl_moys_inscrits - seuil_comptage) * self.proportion_point,
* self.proportion_point,
0.0, 0.0,
), ),
axis=1, axis=1,
@ -338,9 +343,12 @@ class BonusAisneStQuentin(BonusSportAdditif):
# pas d'étudiants ou pas d'UE ou pas de module... # pas d'étudiants ou pas d'UE ou pas de module...
return return
# Calcule moyenne pondérée des notes de sport: # Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum( bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5 bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4 bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
@ -604,8 +612,9 @@ class BonusLaRochelle(BonusSportAdditif):
name = "bonus_iutlr" name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle" displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # tous les points sont comptés seuil_moy_gen = 10.0 # si bonus > 10,
proportion_point = 0.01 seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1%
class BonusLeHavre(BonusSportMultiplicatif): class BonusLeHavre(BonusSportMultiplicatif):
@ -823,9 +832,11 @@ class BonusVilleAvray(BonusSport):
# pas d'étudiants ou pas d'UE ou pas de module... # pas d'étudiants ou pas d'UE ou pas de module...
return return
# Calcule moyenne pondérée des notes de sport: # Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum( bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3 bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2 bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2

View File

@ -207,7 +207,11 @@ class FormSemestre(db.Model):
modimpls = self.modimpls.all() modimpls = self.modimpls.all()
if self.formation.is_apc(): if self.formation.is_apc():
modimpls.sort( modimpls.sort(
key=lambda m: (m.module.module_type, m.module.numero, m.module.code) key=lambda m: (
m.module.module_type or 0,
m.module.numero or 0,
m.module.code or 0,
)
) )
else: else:
modimpls.sort( modimpls.sort(

View File

@ -36,6 +36,7 @@
""" """
from flask import send_file, request from flask import send_file, request
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -97,8 +98,12 @@ def pe_view_sem_recap(
template_latex = "" template_latex = ""
# template fourni via le formulaire Web # template fourni via le formulaire Web
if avis_tmpl_file: if avis_tmpl_file:
template_latex = avis_tmpl_file.read().decode('utf-8') try:
template_latex = template_latex template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else: else:
# template indiqué dans préférences ScoDoc ? # template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference( template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
@ -114,7 +119,7 @@ def pe_view_sem_recap(
footer_latex = "" footer_latex = ""
# template fourni via le formulaire Web # template fourni via le formulaire Web
if footer_tmpl_file: if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode('utf-8') footer_latex = footer_tmpl_file.read().decode("utf-8")
footer_latex = footer_latex footer_latex = footer_latex
else: else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference( footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(

View File

@ -300,9 +300,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
if ue_status["coef_ue"] != None: if ue_status["coef_ue"] != None:
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"]) u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
else: else:
# C'est un bug: u["coef_ue_txt"] = "-"
log("u=" + pprint.pformat(u))
raise Exception("invalid None coef for ue")
if ( if (
dpv dpv

View File

@ -52,6 +52,7 @@ def html_edit_formation_apc(
""" """
parcours = formation.get_parcours() parcours = formation.get_parcours()
assert parcours.APC_SAE assert parcours.APC_SAE
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by( ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
Module.semestre_id, Module.numero, Module.code Module.semestre_id, Module.numero, Module.code
) )
@ -68,6 +69,19 @@ def html_edit_formation_apc(
).order_by( ).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
) )
ues_by_sem = {}
ects_by_sem = {}
for semestre_idx in semestre_ids:
ues_by_sem[semestre_idx] = formation.ues.filter_by(
semestre_idx=semestre_idx
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
ects = [ue.ects for ue in ues_by_sem[semestre_idx]]
if None in ects:
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
else:
ects_by_sem[semestre_idx] = sum(ects)
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
icons = { icons = {
@ -93,7 +107,8 @@ def html_edit_formation_apc(
editable=editable, editable=editable,
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
UniteEns=UniteEns, ues_by_sem=ues_by_sem,
ects_by_sem=ects_by_sem,
), ),
] ]
for semestre_idx in semestre_ids: for semestre_idx in semestre_ids:

View File

@ -562,7 +562,7 @@ def module_edit(module_id=None):
"code", "code",
{ {
"size": 10, "size": 10,
"explanation": "code du module (doit être unique dans la formation)", "explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)",
"allow_null": False, "allow_null": False,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id, module_id=module_id val, field, formation_id, module_id=module_id
@ -701,7 +701,10 @@ def module_edit(module_id=None):
{ {
"title": "Code Apogée", "title": "Code Apogée",
"size": 25, "size": 25,
"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 (ce code est propre à chaque établissement, se rapprocher
du référent Apogée).
""",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN, "validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
}, },
), ),

View File

@ -305,7 +305,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
( (
"numero", "numero",
{ {
"size": 2, "size": 4,
"explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage", "explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
"type": "int", "type": "int",
}, },
@ -722,16 +722,16 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<a href="{url_for('notes.refcomp_show', <a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"> scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}">
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long} {formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
</a> """ </a>&nbsp;"""
msg_refcomp = "changer" msg_refcomp = "changer"
H.append( H.append(
f""" f"""
<ul> <ul>
<li>{descr_refcomp}&nbsp; <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation', <li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation_id) scodoc_dept=g.scodoc_dept, formation_id=formation_id)
}">{msg_refcomp}</a> }">{msg_refcomp}</a>
</li> </li>
<li><a class="stdlink" href="{ <li> <a class="stdlink" href="{
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx) url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
}">éditer les coefficients des ressources et SAÉs</a> }">éditer les coefficients des ressources et SAÉs</a>
</li> </li>

View File

@ -151,8 +151,14 @@ def formation_export(
if mod["ects"] is None: if mod["ects"] is None:
del mod["ects"] del mod["ects"]
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
return scu.sendResult( return scu.sendResult(
F, name="formation", format=format, force_outer_xml_tag=False, attached=True F,
name="formation",
format=format,
force_outer_xml_tag=False,
attached=True,
filename=filename,
) )

View File

@ -545,7 +545,7 @@ def do_formsemestre_createwithmodules(edit=False):
for semestre_id in semestre_ids: for semestre_id in semestre_ids:
if formation.is_apc(): if formation.is_apc():
# pour restreindre l'édition aux module du semestre sélectionné # pour restreindre l'édition aux module du semestre sélectionné
tr_class = 'class="sem{semestre_id}"' tr_class = f'class="sem{semestre_id}"'
else: else:
tr_class = "" tr_class = ""
if edit: if edit:

View File

@ -0,0 +1,96 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Exports groupes
"""
from flask import request
from app.scodoc import notesdb as ndb
from app.scodoc import sco_excel
from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
import app.scodoc.sco_utils as scu
import sco_version
def groups_list_annotation(group_ids: list[int]) -> list[dict]:
"""Renvoie la liste des annotations pour les groupes d"étudiants indiqués
Arg: liste des id de groupes
Clés: etudid, ine, nip, nom, prenom, date, comment
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
annotations = []
for group_id in group_ids:
cursor.execute(
"""SELECT i.id AS etudid, i.code_nip, i.code_ine, i.nom, i.prenom, ea.date, ea.comment
FROM group_membership gm, identite i, etud_annotations ea
WHERE gm.group_id=%(group_ids)s
AND gm.etudid=i.id
AND i.id=ea.etudid
""",
{"group_ids": group_id},
)
annotations += cursor.dictfetchall()
return annotations
def groups_export_annotations(group_ids, formsemestre_id=None, format="html"):
"""Les annotations"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id
)
annotations = groups_list_annotation(groups_infos.group_ids)
for annotation in annotations:
annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M")
if format == "xls":
columns_ids = ("etudid", "nom", "prenom", "date", "comment")
else:
columns_ids = ("etudid", "nom", "prenom", "date_str", "comment")
table = GenTable(
rows=annotations,
columns_ids=columns_ids,
titles={
"etudid": "etudid",
"nom": "Nom",
"prenom": "Prénom",
"date": "Date",
"date_str": "Date",
"comment": "Annotation",
},
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
page_title=f"Annotations sur les étudiants de {groups_infos.groups_titles}",
caption="Annotations",
base_url=groups_infos.base_url,
html_sortable=True,
html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
return table.make_page(format=format)

View File

@ -826,6 +826,8 @@ def tab_absences_html(groups_infos, etat=None):
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="trombino?%s&format=pdflist">Liste d'appel avec photos</a></li>""" """<li><a class="stdlink" href="trombino?%s&format=pdflist">Liste d'appel avec photos</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>"""
% groups_infos.groups_query_args,
"</ul>", "</ul>",
] ]
) )

View File

@ -645,21 +645,30 @@ class ScoDocJSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o) return json.JSONEncoder.default(self, o)
def sendJSON(data, attached=False): def sendJSON(data, attached=False, filename=None):
js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
return send_file( return send_file(
js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached js, filename=filename or "sco_data.json", mime=JSON_MIMETYPE, attached=attached
) )
def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True): def sendXML(
data,
tagname=None,
force_outer_xml_tag=True,
attached=False,
quote=True,
filename=None,
):
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, quote=quote) 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=filename or "sco_data.xml", mime=XML_MIMETYPE, attached=attached
)
def sendResult( def sendResult(
@ -669,6 +678,7 @@ def sendResult(
force_outer_xml_tag=True, force_outer_xml_tag=True,
attached=False, attached=False,
quote_xml=True, quote_xml=True,
filename=None,
): ):
if (format is None) or (format == "html"): if (format is None) or (format == "html"):
return data return data
@ -679,9 +689,10 @@ def sendResult(
force_outer_xml_tag=force_outer_xml_tag, force_outer_xml_tag=force_outer_xml_tag,
attached=attached, attached=attached,
quote=quote_xml, quote=quote_xml,
filename=filename,
) )
elif format == "json": elif format == "json":
return sendJSON(data, attached=attached) return sendJSON(data, attached=attached, filename=filename)
else: else:
raise ValueError("invalid format: %s" % format) raise ValueError("invalid format: %s" % format)
@ -799,7 +810,7 @@ def abbrev_prenom(prenom):
# #
def timedate_human_repr(): def timedate_human_repr():
"representation du temps courant pour utilisateur: a localiser" "representation du temps courant pour utilisateur"
return time.strftime("%d/%m/%Y à %Hh%M") return time.strftime("%d/%m/%Y à %Hh%M")

View File

@ -3,11 +3,9 @@
<div class="formation_list_ues"> <div class="formation_list_ues">
<div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div> <div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div>
{% for semestre_idx in semestre_ids %} {% for semestre_idx in semestre_ids %}
<div class="formation_list_ues_sem">Semestre S{{semestre_idx}}</div> <div class="formation_list_ues_sem">Semestre S{{semestre_idx}} (ECTS: {{ects_by_sem[semestre_idx] | safe}})</div>
<ul class="apc_ue_list"> <ul class="apc_ue_list">
{% for ue in formation.ues.filter_by(semestre_idx=semestre_idx).order_by( {% for ue in ues_by_sem[semestre_idx] %}
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
) %}
<li class="notes_ue_list"> <li class="notes_ue_list">
{% if editable and not loop.first %} {% if editable and not loop.first %}
<a href="{{ url_for('notes.ue_move', <a href="{{ url_for('notes.ue_move',

View File

@ -611,8 +611,7 @@ def SignaleAbsenceGrSemestre(
"""<option value="%(modimpl_id)s" %(sel)s>%(modname)s</option>\n""" """<option value="%(modimpl_id)s" %(sel)s>%(modname)s</option>\n"""
% { % {
"modimpl_id": modimpl["moduleimpl_id"], "modimpl_id": modimpl["moduleimpl_id"],
"modname": modimpl["module"]["code"] "modname": (modimpl["module"]["code"] or "")
or ""
+ " " + " "
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]), + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]),
"sel": sel, "sel": sel,

View File

@ -68,8 +68,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
import app import app
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import html_sidebar
from app.scodoc import imageresize
from app.scodoc import sco_import_etuds from app.scodoc import sco_import_etuds
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_archives_etud from app.scodoc import sco_archives_etud
@ -87,12 +85,9 @@ from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_edit from app.scodoc import sco_groups_edit
from app.scodoc import sco_groups_exports
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_logos
from app.scodoc import sco_news
from app.scodoc import sco_page_etud from app.scodoc import sco_page_etud
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_permissions
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
@ -364,6 +359,12 @@ sco_publish(
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish(
"/groups_export_annotations",
sco_groups_exports.groups_export_annotations,
Permission.ScoView,
)
@bp.route("/groups_view") @bp.route("/groups_view")
@scodoc @scodoc

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.2a-66" SCOVERSION = "9.2a-70"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"