forked from ScoDoc/ScoDoc
Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api
This commit is contained in:
commit
f1273f7bb2
@ -69,7 +69,12 @@ Puis remplacer `/opt/scodoc` par un clone du git.
|
||||
cd /opt
|
||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
mv ScoDoc scodoc # important !
|
||||
|
||||
# Renommer le répertoire:
|
||||
mv ScoDoc scodoc
|
||||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
|
||||
|
@ -10,6 +10,7 @@ import traceback
|
||||
|
||||
import logging
|
||||
from logging.handlers import SMTPHandler, WatchedFileHandler
|
||||
from threading import Thread
|
||||
|
||||
from flask import current_app, g, request
|
||||
from flask import Flask
|
||||
@ -27,6 +28,7 @@ import sqlalchemy
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoBugCatcher,
|
||||
ScoGenError,
|
||||
ScoValueError,
|
||||
APIInvalidParams,
|
||||
@ -43,11 +45,13 @@ mail = Mail()
|
||||
bootstrap = Bootstrap()
|
||||
moment = Moment()
|
||||
|
||||
cache = Cache( # XXX TODO: configuration file
|
||||
CACHE_TYPE = os.environ.get("CACHE_TYPE")
|
||||
cache = Cache(
|
||||
config={
|
||||
# see https://flask-caching.readthedocs.io/en/latest/index.html#configuring-flask-caching
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 0, # by default, never expire
|
||||
"CACHE_TYPE": CACHE_TYPE or "RedisCache",
|
||||
# by default, never expire:
|
||||
"CACHE_DEFAULT_TIMEOUT": os.environ.get("CACHE_DEFAULT_TIMEOUT") or 0,
|
||||
}
|
||||
)
|
||||
|
||||
@ -60,7 +64,7 @@ def handle_access_denied(exc):
|
||||
return render_template("error_access_denied.html", exc=exc), 403
|
||||
|
||||
|
||||
def internal_server_error(e):
|
||||
def internal_server_error(exc):
|
||||
"""Bugs scodoc, erreurs 500"""
|
||||
# note that we set the 500 status explicitly
|
||||
return (
|
||||
@ -68,11 +72,35 @@ def internal_server_error(e):
|
||||
"error_500.html",
|
||||
SCOVERSION=sco_version.SCOVERSION,
|
||||
date=datetime.datetime.now().isoformat(),
|
||||
exc=exc,
|
||||
request_url=request.url,
|
||||
),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
def handle_sco_bug(exc):
|
||||
"""Un bug, en général rare, sur lequel les dev cherchent des
|
||||
informations pour le corriger.
|
||||
"""
|
||||
Thread(
|
||||
target=_async_dump, args=(current_app._get_current_object(), request.url)
|
||||
).start()
|
||||
|
||||
return internal_server_error(exc)
|
||||
|
||||
|
||||
def _async_dump(app, request_url: str):
|
||||
from app.scodoc.sco_dump_db import sco_dump_and_send_db
|
||||
|
||||
with app.app_context():
|
||||
ndb.open_db_connection()
|
||||
try:
|
||||
sco_dump_and_send_db("ScoBugCatcher", request_url=request_url)
|
||||
except ScoValueError:
|
||||
pass
|
||||
|
||||
|
||||
def handle_invalid_usage(error):
|
||||
response = jsonify(error.to_dict())
|
||||
response.status_code = error.status_code
|
||||
@ -187,10 +215,12 @@ def create_app(config_class=DevConfig):
|
||||
moment.init_app(app)
|
||||
cache.init_app(app)
|
||||
sco_cache.CACHE = cache
|
||||
if CACHE_TYPE: # non default
|
||||
app.logger.info(f"CACHE_TYPE={CACHE_TYPE}")
|
||||
|
||||
app.register_error_handler(ScoGenError, handle_sco_value_error)
|
||||
app.register_error_handler(ScoValueError, handle_sco_value_error)
|
||||
|
||||
app.register_error_handler(ScoBugCatcher, handle_sco_bug)
|
||||
app.register_error_handler(AccessDenied, handle_access_denied)
|
||||
app.register_error_handler(500, internal_server_error)
|
||||
app.register_error_handler(503, postgresql_server_error)
|
||||
@ -336,7 +366,7 @@ def user_db_init():
|
||||
|
||||
current_app.logger.info("Init User's db")
|
||||
# Create roles:
|
||||
Role.insert_roles()
|
||||
Role.reset_standard_roles_permissions()
|
||||
current_app.logger.info("created initial roles")
|
||||
# Ensure that admin exists
|
||||
admin_mail = current_app.config.get("SCODOC_ADMIN_MAIL")
|
||||
|
@ -173,7 +173,7 @@ class User(UserMixin, db.Model):
|
||||
"id": self.id,
|
||||
"active": self.active,
|
||||
"status_txt": "actif" if self.active else "fermé",
|
||||
"last_seen": self.last_seen.isoformat() + "Z",
|
||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else "",
|
||||
"nom": (self.nom or ""), # sco8
|
||||
"prenom": (self.prenom or ""), # sco8
|
||||
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
||||
@ -270,6 +270,8 @@ class User(UserMixin, db.Model):
|
||||
"""Add a role to this user.
|
||||
:param role: Role to add.
|
||||
"""
|
||||
if not isinstance(role, Role):
|
||||
raise ScoValueError("add_role: rôle invalide")
|
||||
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
|
||||
|
||||
def add_roles(self, roles, dept):
|
||||
@ -281,7 +283,9 @@ class User(UserMixin, db.Model):
|
||||
|
||||
def set_roles(self, roles, dept):
|
||||
"set roles in the given dept"
|
||||
self.user_roles = [UserRole(user=self, role=r, dept=dept) for r in roles]
|
||||
self.user_roles = [
|
||||
UserRole(user=self, role=r, dept=dept) for r in roles if isinstance(r, Role)
|
||||
]
|
||||
|
||||
def get_roles(self):
|
||||
"iterator on my roles"
|
||||
@ -292,7 +296,11 @@ class User(UserMixin, db.Model):
|
||||
"""string repr. of user's roles (with depts)
|
||||
e.g. "Ens_RT, Ens_Info, Secr_CJ"
|
||||
"""
|
||||
return ",".join(f"{r.role.name or ''}_{r.dept or ''}" for r in self.user_roles)
|
||||
return ",".join(
|
||||
f"{r.role.name or ''}_{r.dept or ''}"
|
||||
for r in self.user_roles
|
||||
if r is not None
|
||||
)
|
||||
|
||||
def is_administrator(self):
|
||||
"True if i'm an active SuperAdmin"
|
||||
@ -402,20 +410,30 @@ class Role(db.Model):
|
||||
return self.permissions & perm == perm
|
||||
|
||||
@staticmethod
|
||||
def insert_roles():
|
||||
"""Create default roles"""
|
||||
def reset_standard_roles_permissions(reset_permissions=True):
|
||||
"""Create default roles if missing, then, if reset_permissions,
|
||||
reset their permissions to default values.
|
||||
"""
|
||||
default_role = "Observateur"
|
||||
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
|
||||
role = Role.query.filter_by(name=role_name).first()
|
||||
if role is None:
|
||||
role = Role(name=role_name)
|
||||
role.reset_permissions()
|
||||
for perm in permissions:
|
||||
role.add_permission(perm)
|
||||
role.default = role.name == default_role
|
||||
db.session.add(role)
|
||||
role.default = role.name == default_role
|
||||
db.session.add(role)
|
||||
if reset_permissions:
|
||||
role.reset_permissions()
|
||||
for perm in permissions:
|
||||
role.add_permission(perm)
|
||||
db.session.add(role)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def ensure_standard_roles():
|
||||
"""Create default roles if missing"""
|
||||
Role.reset_standard_roles_permissions(reset_permissions=False)
|
||||
|
||||
@staticmethod
|
||||
def get_named_role(name):
|
||||
"""Returns existing role with given name, or None."""
|
||||
|
@ -19,7 +19,7 @@ from app.auth.forms import (
|
||||
ResetPasswordForm,
|
||||
DeactivateUserForm,
|
||||
)
|
||||
from app.auth.models import Permission
|
||||
from app.auth.models import Role
|
||||
from app.auth.models import User
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.decorators import admin_required
|
||||
@ -121,3 +121,11 @@ def reset_password(token):
|
||||
flash(_("Votre mot de passe a été changé."))
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("auth/reset_password.html", form=form, user=user)
|
||||
|
||||
|
||||
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def reset_standard_roles_permissions():
|
||||
Role.reset_standard_roles_permissions()
|
||||
flash("rôles standard réinitialisés !")
|
||||
return redirect(url_for("scodoc.configuration"))
|
||||
|
1
app/but/__init__.py
Normal file
1
app/but/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# empty but required for pylint
|
@ -7,11 +7,14 @@
|
||||
"""Génération bulletin BUT
|
||||
"""
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import numpy as np
|
||||
from flask import url_for, g
|
||||
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
@ -61,18 +64,15 @@ class BulletinBUT:
|
||||
# }
|
||||
return d
|
||||
|
||||
def etud_ue_results(self, etud, ue):
|
||||
def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict:
|
||||
"dict synthèse résultats UE"
|
||||
res = self.res
|
||||
|
||||
d = {
|
||||
"id": ue.id,
|
||||
"titre": ue.titre,
|
||||
"numero": ue.numero,
|
||||
"type": ue.type,
|
||||
"ECTS": {
|
||||
"acquis": 0, # XXX TODO voir jury #sco92
|
||||
"total": ue.ects,
|
||||
},
|
||||
"color": ue.color,
|
||||
"competence": None, # XXX TODO lien avec référentiel
|
||||
"moyenne": None,
|
||||
@ -85,6 +85,11 @@ class BulletinBUT:
|
||||
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
||||
"saes": self.etud_ue_mod_results(etud, ue, res.saes),
|
||||
}
|
||||
if self.prefs["bul_show_ects"]:
|
||||
d["ECTS"] = {
|
||||
"acquis": decision_ue.get("ects", 0.0),
|
||||
"total": ue.ects or 0.0, # float même si non renseigné
|
||||
}
|
||||
if ue.type != UE_SPORT:
|
||||
if self.prefs["bul_show_ue_rangs"]:
|
||||
rangs, effectif = res.ue_rangs[ue.id]
|
||||
@ -171,10 +176,15 @@ class BulletinBUT:
|
||||
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
||||
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
|
||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||
poids = {
|
||||
ue.acronyme: self.res.modimpls_evals_poids[e.moduleimpl_id][ue.id][e.id]
|
||||
for ue in self.res.ues
|
||||
}
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
||||
try:
|
||||
poids = {
|
||||
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
|
||||
for ue in self.res.ues
|
||||
if ue.type != UE_SPORT
|
||||
}
|
||||
except KeyError:
|
||||
poids = collections.defaultdict(lambda: 0.0)
|
||||
d = {
|
||||
"id": e.id,
|
||||
"description": e.description,
|
||||
@ -212,7 +222,8 @@ class BulletinBUT:
|
||||
details = [
|
||||
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
|
||||
for ue in res.ues
|
||||
if res.modimpls_in_ue(ue.id, etudid)
|
||||
if ue.type != UE_SPORT
|
||||
and res.modimpls_in_ue(ue.id, etudid)
|
||||
and ue.id in res.bonus_ues
|
||||
and bonus_vect[ue.id] > 0.0
|
||||
]
|
||||
@ -272,29 +283,39 @@ class BulletinBUT:
|
||||
"numero": formsemestre.semestre_id,
|
||||
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
|
||||
"groupes": [], # XXX TODO
|
||||
"absences": {
|
||||
}
|
||||
if self.prefs["bul_show_abs"]:
|
||||
semestre_infos["absences"] = {
|
||||
"injustifie": nbabs - nbabsjust,
|
||||
"total": nbabs,
|
||||
},
|
||||
}
|
||||
}
|
||||
decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
|
||||
if self.prefs["bul_show_ects"]:
|
||||
ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0
|
||||
ects_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
|
||||
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
|
||||
semestre_infos.update(
|
||||
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
||||
)
|
||||
if etat_inscription == scu.INSCRIT:
|
||||
semestre_infos.update(
|
||||
{
|
||||
"notes": { # moyenne des moyennes générales du semestre
|
||||
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
||||
"min": fmt_note(res.etud_moy_gen.min()),
|
||||
"moy": fmt_note(res.etud_moy_gen.mean()),
|
||||
"max": fmt_note(res.etud_moy_gen.max()),
|
||||
},
|
||||
"rang": { # classement wrt moyenne général, indicatif
|
||||
"value": res.etud_moy_gen_ranks[etud.id],
|
||||
"total": nb_inscrits,
|
||||
},
|
||||
},
|
||||
)
|
||||
# moyenne des moyennes générales du semestre
|
||||
semestre_infos["notes"] = {
|
||||
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
||||
"min": fmt_note(res.etud_moy_gen.min()),
|
||||
"moy": fmt_note(res.etud_moy_gen.mean()),
|
||||
"max": fmt_note(res.etud_moy_gen.max()),
|
||||
}
|
||||
if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]):
|
||||
# classement wrt moyenne général, indicatif
|
||||
semestre_infos["rang"] = {
|
||||
"value": res.etud_moy_gen_ranks[etud.id],
|
||||
"total": nb_inscrits,
|
||||
}
|
||||
else:
|
||||
semestre_infos["rang"] = {
|
||||
"value": "-",
|
||||
"total": nb_inscrits,
|
||||
}
|
||||
d.update(
|
||||
{
|
||||
"ressources": self.etud_mods_results(
|
||||
@ -302,11 +323,15 @@ class BulletinBUT:
|
||||
),
|
||||
"saes": self.etud_mods_results(etud, res.saes, version=version),
|
||||
"ues": {
|
||||
ue.acronyme: self.etud_ue_results(etud, ue)
|
||||
ue.acronyme: self.etud_ue_results(
|
||||
etud, ue, decision_ue=decisions_ues.get(ue.id, {})
|
||||
)
|
||||
for ue in res.ues
|
||||
if self.res.modimpls_in_ue(
|
||||
ue.id, etud.id
|
||||
) # si l'UE comporte des modules auxquels on est inscrit
|
||||
# si l'UE comporte des modules auxquels on est inscrit:
|
||||
if (
|
||||
(ue.type == UE_SPORT)
|
||||
or self.res.modimpls_in_ue(ue.id, etud.id)
|
||||
)
|
||||
},
|
||||
"semestre": semestre_infos,
|
||||
},
|
||||
@ -349,7 +374,7 @@ class BulletinBUT:
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
etud_etat,
|
||||
self.prefs,
|
||||
decision_sem=d["semestre"].get("decision_sem"),
|
||||
decision_sem=d["semestre"].get("decision"),
|
||||
)
|
||||
if etud_etat == scu.DEMISSION:
|
||||
d["demission"] = "(Démission)"
|
||||
|
@ -6,14 +6,13 @@
|
||||
|
||||
"""Génération bulletin BUT au format PDF standard
|
||||
"""
|
||||
from reportlab.lib.colors import blue
|
||||
from reportlab.lib.units import cm, mm
|
||||
from reportlab.platypus import Paragraph, Spacer
|
||||
|
||||
from app.scodoc.sco_pdf import blue, cm, mm
|
||||
|
||||
from app.scodoc import gen_tables
|
||||
from app.scodoc.sco_utils import fmt_note
|
||||
|
||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||
from app.scodoc import gen_tables
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
|
||||
|
||||
class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
@ -22,8 +21,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
|
||||
"""
|
||||
|
||||
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
|
||||
scale_table_in_page = False
|
||||
# spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur:
|
||||
list_in_menu = False
|
||||
scale_table_in_page = False # pas de mise à l'échelle pleine page auto
|
||||
multi_pages = True # plusieurs pages par bulletins
|
||||
small_fontsize = "8"
|
||||
|
||||
def bul_table(self, format="html"):
|
||||
"""Génère la table centrale du bulletin de notes
|
||||
@ -77,16 +79,29 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
"coef": 2 * cm,
|
||||
}
|
||||
title_bg = tuple(x / 255.0 for x in title_bg)
|
||||
nota_bene = """La moyenne des ressources et SAÉs dans une UE
|
||||
dépend des poids donnés aux évaluations."""
|
||||
# elems pour générer table avec gen_table (liste de dicts)
|
||||
rows = [
|
||||
# Ligne de titres
|
||||
{
|
||||
"titre": "Unités d'enseignement",
|
||||
"moyenne": "Note/20",
|
||||
"moyenne": Paragraph("<para align=right><b>Note/20</b></para>"),
|
||||
"coef": "Coef.",
|
||||
"_coef_pdf": Paragraph("<para align=right><b><i>Coef.</i></b></para>"),
|
||||
"_css_row_class": "note_bold",
|
||||
"_pdf_row_markup": ["b"],
|
||||
"_pdf_style": [
|
||||
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
||||
# ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
||||
],
|
||||
},
|
||||
{
|
||||
"titre": nota_bene,
|
||||
"_titre_pdf": Paragraph(
|
||||
f"<para fontSize={self.small_fontsize}><i>{nota_bene}</i></para>"
|
||||
),
|
||||
"_titre_colspan": 3,
|
||||
"_pdf_style": [
|
||||
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
||||
@ -98,81 +113,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
blue,
|
||||
),
|
||||
],
|
||||
}
|
||||
},
|
||||
]
|
||||
col_keys = ["titre", "coef", "moyenne"] # noms des colonnes à afficher
|
||||
for ue_acronym, ue in self.infos["ues"].items():
|
||||
# 1er ligne titre UE
|
||||
moy_ue = ue.get("moyenne")
|
||||
t = {
|
||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||
"moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-",
|
||||
"_css_row_class": "note_bold",
|
||||
"_pdf_row_markup": ["b"],
|
||||
"_pdf_style": [
|
||||
(
|
||||
"LINEABOVE",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
self.PDF_LINECOLOR,
|
||||
),
|
||||
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
||||
],
|
||||
}
|
||||
rows.append(t)
|
||||
# 2eme ligne titre UE (bonus/malus/ects)
|
||||
ects_txt = f'ECTS: {ue["ECTS"]["acquis"]:.3g} / {ue["ECTS"]["total"]:.3g}'
|
||||
t = {
|
||||
"titre": f"""Bonus: {ue['bonus']} - Malus: {
|
||||
ue["malus"]}""",
|
||||
"coef": ects_txt,
|
||||
"_coef_pdf": Paragraph(f"""<para align=right>{ects_txt}</para>"""),
|
||||
"_coef_colspan": 2,
|
||||
# "_css_row_class": "",
|
||||
# "_pdf_row_markup": [""],
|
||||
"_pdf_style": [
|
||||
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
||||
(
|
||||
"LINEBELOW",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
self.PDF_LINECOLOR,
|
||||
),
|
||||
# cadre autour du bonus/malus
|
||||
(
|
||||
"BOX",
|
||||
(0, 0),
|
||||
(0, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
(0.7, 0.7, 0.7), # gris clair
|
||||
),
|
||||
],
|
||||
}
|
||||
rows.append(t)
|
||||
# Liste chaque ressource puis SAE
|
||||
for mod_type in ("ressources", "saes"):
|
||||
for mod_code, mod in ue[mod_type].items():
|
||||
t = {
|
||||
"titre": f"{mod_code} {self.infos[mod_type][mod_code]['titre']}",
|
||||
"moyenne": mod["moyenne"],
|
||||
"coef": mod["coef"],
|
||||
"_coef_pdf": Paragraph(
|
||||
f"<para align=right><i>{mod['coef']}</i></para>"
|
||||
),
|
||||
"_pdf_style": [
|
||||
(
|
||||
"LINEBELOW",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
(0.7, 0.7, 0.7), # gris clair
|
||||
)
|
||||
],
|
||||
}
|
||||
rows.append(t)
|
||||
self.ue_rows(rows, ue_acronym, ue, title_bg)
|
||||
# Global pdf style commands:
|
||||
pdf_style = [
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
@ -180,6 +125,92 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
]
|
||||
return col_keys, rows, pdf_style, col_widths
|
||||
|
||||
def ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple):
|
||||
"Décrit une UE dans la table synthèse: titre, sous-titre et liste modules"
|
||||
# 1er ligne titre UE
|
||||
moy_ue = ue.get("moyenne")
|
||||
t = {
|
||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||
"moyenne": Paragraph(
|
||||
f"""<para align=right><b>{moy_ue.get("value", "-")
|
||||
if moy_ue is not None else "-"
|
||||
}</b></para>"""
|
||||
),
|
||||
"_css_row_class": "note_bold",
|
||||
"_pdf_row_markup": ["b"],
|
||||
"_pdf_style": [
|
||||
(
|
||||
"LINEABOVE",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
self.PDF_LINECOLOR,
|
||||
),
|
||||
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
||||
],
|
||||
}
|
||||
rows.append(t)
|
||||
if ue["type"] == UE_SPORT:
|
||||
self.ue_sport_rows(rows, ue, title_bg)
|
||||
else:
|
||||
self.ue_std_rows(rows, ue, title_bg)
|
||||
|
||||
def ue_std_rows(self, rows: list, ue: dict, title_bg: tuple):
|
||||
"Lignes décrivant une UE standard dans la table de synthèse"
|
||||
# 2eme ligne titre UE (bonus/malus/ects)
|
||||
if "ECTS" in ue:
|
||||
ects_txt = f'ECTS: {ue["ECTS"]["acquis"]:.3g} / {ue["ECTS"]["total"]:.3g}'
|
||||
else:
|
||||
ects_txt = ""
|
||||
t = {
|
||||
"titre": f"""Bonus: {ue['bonus']} - Malus: {
|
||||
ue["malus"]}""",
|
||||
"coef": ects_txt,
|
||||
"_coef_pdf": Paragraph(f"""<para align=left>{ects_txt}</para>"""),
|
||||
"_coef_colspan": 2,
|
||||
"_pdf_style": [
|
||||
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
||||
("LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR),
|
||||
# cadre autour du bonus/malus, gris clair
|
||||
("BOX", (0, 0), (0, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7)),
|
||||
],
|
||||
}
|
||||
rows.append(t)
|
||||
|
||||
# Liste chaque ressource puis chaque SAE
|
||||
for mod_type in ("ressources", "saes"):
|
||||
for mod_code, mod in ue[mod_type].items():
|
||||
t = {
|
||||
"titre": f"{mod_code} {self.infos[mod_type][mod_code]['titre']}",
|
||||
"moyenne": Paragraph(f'<para align=right>{mod["moyenne"]}</para>'),
|
||||
"coef": mod["coef"],
|
||||
"_coef_pdf": Paragraph(
|
||||
f"<para align=right><i>{mod['coef']}</i></para>"
|
||||
),
|
||||
"_pdf_style": [
|
||||
(
|
||||
"LINEBELOW",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
(0.7, 0.7, 0.7), # gris clair
|
||||
)
|
||||
],
|
||||
}
|
||||
rows.append(t)
|
||||
|
||||
def ue_sport_rows(self, rows: list, ue: dict, title_bg: tuple):
|
||||
"Lignes décrivant l'UE bonus dans la table de synthèse"
|
||||
# UE BONUS
|
||||
for mod_code, mod in ue["modules"].items():
|
||||
rows.append(
|
||||
{
|
||||
"titre": f"{mod_code} {mod['titre']}",
|
||||
}
|
||||
)
|
||||
self.evaluations_rows(rows, mod["evaluations"])
|
||||
|
||||
def but_table_ressources(self):
|
||||
"""La table de synthèse; pour chaque ressources, note et liste d'évaluations
|
||||
Renvoie: colkeys, P, pdf_style, colWidths
|
||||
@ -203,9 +234,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
- pdf_style : commandes table Platypus
|
||||
- largeurs de colonnes pour PDF
|
||||
"""
|
||||
poids_fontsize = "8"
|
||||
# UE à utiliser pour les poids (# colonne/UE)
|
||||
ue_acros = list(self.infos["ues"].keys()) # ['RT1.1', 'RT2.1', 'RT3.1']
|
||||
ue_infos = self.infos["ues"]
|
||||
ue_acros = list(
|
||||
[k for k in ue_infos if ue_infos[k]["type"] != UE_SPORT]
|
||||
) # ['RT1.1', 'RT2.1', 'RT3.1']
|
||||
# Colonnes à afficher:
|
||||
col_keys = ["titre"] + ue_acros + ["coef", "moyenne"]
|
||||
# Largeurs des colonnes:
|
||||
@ -243,7 +276,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
}
|
||||
for ue_acro in ue_acros:
|
||||
t[ue_acro] = Paragraph(
|
||||
f"<para align=right fontSize={poids_fontsize}><i>{ue_acro}</i></para>"
|
||||
f"<para align=right fontSize={self.small_fontsize}><i>{ue_acro}</i></para>"
|
||||
)
|
||||
rows = [t]
|
||||
for mod_code, mod in self.infos[mod_type].items():
|
||||
@ -267,43 +300,52 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
}
|
||||
rows.append(t)
|
||||
# Evaluations:
|
||||
for e in mod["evaluations"]:
|
||||
t = {
|
||||
"titre": f"{e['description']}",
|
||||
"moyenne": e["note"]["value"],
|
||||
"coef": e["coef"],
|
||||
"_coef_pdf": Paragraph(
|
||||
f"<para align=right fontSize={poids_fontsize}><i>{e['coef']}</i></para>"
|
||||
),
|
||||
"_pdf_style": [
|
||||
(
|
||||
"LINEBELOW",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
(0.7, 0.7, 0.7), # gris clair
|
||||
)
|
||||
],
|
||||
}
|
||||
col_idx = 1 # 1ere col. poids
|
||||
for ue_acro in ue_acros:
|
||||
t[ue_acro] = Paragraph(
|
||||
f"""<para align=right fontSize={poids_fontsize}><i>{e["poids"].get(ue_acro, "")}</i></para>"""
|
||||
)
|
||||
t["_pdf_style"].append(
|
||||
(
|
||||
"BOX",
|
||||
(col_idx, 0),
|
||||
(col_idx, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
(0.7, 0.7, 0.7), # gris clair
|
||||
),
|
||||
)
|
||||
col_idx += 1
|
||||
rows.append(t)
|
||||
self.evaluations_rows(rows, mod["evaluations"], ue_acros)
|
||||
|
||||
# Global pdf style commands:
|
||||
pdf_style = [
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
|
||||
]
|
||||
return col_keys, rows, pdf_style, col_widths
|
||||
|
||||
def evaluations_rows(self, rows, evaluations, ue_acros=()):
|
||||
"lignes des évaluations"
|
||||
for e in evaluations:
|
||||
t = {
|
||||
"titre": f"{e['description']}",
|
||||
"moyenne": e["note"]["value"],
|
||||
"_moyenne_pdf": Paragraph(
|
||||
f"""<para align=right>{e["note"]["value"]}</para>"""
|
||||
),
|
||||
"coef": e["coef"],
|
||||
"_coef_pdf": Paragraph(
|
||||
f"<para align=right fontSize={self.small_fontsize}><i>{e['coef']}</i></para>"
|
||||
),
|
||||
"_pdf_style": [
|
||||
(
|
||||
"LINEBELOW",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
(0.7, 0.7, 0.7), # gris clair
|
||||
)
|
||||
],
|
||||
}
|
||||
col_idx = 1 # 1ere col. poids
|
||||
for ue_acro in ue_acros:
|
||||
t[ue_acro] = Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
e["poids"].get(ue_acro, "") or ""}</i></para>"""
|
||||
)
|
||||
t["_pdf_style"].append(
|
||||
(
|
||||
"BOX",
|
||||
(col_idx, 0),
|
||||
(col_idx, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
(0.7, 0.7, 0.7), # gris clair
|
||||
),
|
||||
)
|
||||
col_idx += 1
|
||||
rows.append(t)
|
||||
|
1
app/comp/__init__.py
Normal file
1
app/comp/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# empty but required for pylint
|
@ -13,7 +13,6 @@ Les classes de Bonus fournissent deux méthodes:
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import math
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
@ -89,7 +88,7 @@ class BonusSport:
|
||||
for m in formsemestre.modimpls_sorted
|
||||
]
|
||||
)
|
||||
if not len(modimpl_mask):
|
||||
if len(modimpl_mask) == 0:
|
||||
modimpl_mask = np.s_[:] # il n'y a rien, on prend tout donc rien
|
||||
self.modimpls_spo = [
|
||||
modimpl
|
||||
@ -200,10 +199,11 @@ class BonusSportAdditif(BonusSport):
|
||||
"""
|
||||
|
||||
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)
|
||||
)
|
||||
# les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
|
||||
seuil_comptage = None
|
||||
proportion_point = 0.05 # multiplie les points au dessus du seuil
|
||||
bonux_max = 20.0 # le bonus ne peut dépasser 20 points
|
||||
bonus_min = 0.0 # et ne peut pas être négatif
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus
|
||||
@ -220,19 +220,16 @@ class BonusSportAdditif(BonusSport):
|
||||
)
|
||||
bonus_moy_arr = np.sum(
|
||||
np.where(
|
||||
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
|
||||
(sem_modimpl_moys_inscrits >= self.seuil_moy_gen)
|
||||
& (modimpl_coefs_etuds_no_nan > 0),
|
||||
(sem_modimpl_moys_inscrits - seuil_comptage) * self.proportion_point,
|
||||
0.0,
|
||||
),
|
||||
axis=1,
|
||||
)
|
||||
if self.bonus_max is not None:
|
||||
# Seuil: bonus limité à bonus_max points (et >= 0)
|
||||
bonus_moy_arr = np.clip(
|
||||
bonus_moy_arr, 0.0, self.bonus_max, out=bonus_moy_arr
|
||||
)
|
||||
else: # necessaire pour éviter bonus négatifs !
|
||||
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr)
|
||||
# Seuil: bonus dans [min, max] (défaut [0,20])
|
||||
bonus_max = self.bonus_max or 20.0
|
||||
np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)
|
||||
|
||||
self.bonus_additif(bonus_moy_arr)
|
||||
|
||||
@ -510,14 +507,14 @@ class BonusCachan1(BonusSportAdditif):
|
||||
</li>
|
||||
|
||||
<li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
|
||||
les moyennes d'UE à raison de <em>bonus = (option - 10)*5%</em>.</li>
|
||||
les moyennes d'UE à raison de <em>bonus = (option - 10) * 3%</em>.</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
name = "bonus_cachan1"
|
||||
displayed_name = "IUT de Cachan 1"
|
||||
seuil_moy_gen = 10.0 # tous les points sont comptés
|
||||
proportion_point = 0.05
|
||||
proportion_point = 0.03
|
||||
classic_use_bonus_ues = True
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
@ -679,17 +676,40 @@ class BonusLaRochelle(BonusSportAdditif):
|
||||
proportion_point = 0.01 # 1%
|
||||
|
||||
|
||||
class BonusLeHavre(BonusSportMultiplicatif):
|
||||
"""Bonus sport IUT du Havre sur moyenne générale et UE
|
||||
class BonusLeHavre(BonusSportAdditif):
|
||||
"""Bonus sport IUT du Havre sur les moyennes d'UE
|
||||
|
||||
Les points des modules bonus au dessus de 10/20 sont ajoutés,
|
||||
et les moyennes d'UE augmentées de 5% de ces points.
|
||||
<p>
|
||||
Les enseignements optionnels de langue, préprofessionnalisation,
|
||||
PIX (compétences numériques), l'entrepreneuriat étudiant, l'engagement
|
||||
bénévole au sein d’association dès lors qu’une grille d’évaluation des
|
||||
compétences existe ainsi que les activités sportives et culturelles
|
||||
seront traités au niveau semestriel.
|
||||
</p><p>
|
||||
Le maximum de bonification qu’un étudiant peut obtenir sur sa moyenne
|
||||
est plafonné à 0.5 point.
|
||||
</p><p>
|
||||
Lorsqu’un étudiant suit plus de deux matières qui donnent droit à
|
||||
bonification, l’étudiant choisit les deux notes à retenir.
|
||||
</p><p>
|
||||
Les points bonus ne sont acquis que pour une note supérieure à 10/20.
|
||||
</p><p>
|
||||
La bonification est calculée de la manière suivante :<br>
|
||||
|
||||
Pour chaque matière (max. 2) donnant lieu à bonification :<br>
|
||||
|
||||
Bonification = (N-10) x 0,05,
|
||||
N étant la note de l’activité sur 20.
|
||||
</p>
|
||||
"""
|
||||
|
||||
# note: ScoDoc ne vérifie pas que le nombre de modules avec inscription n'excède pas 2
|
||||
name = "bonus_iutlh"
|
||||
displayed_name = "IUT du Havre"
|
||||
classic_use_bonus_ues = True # sur les UE, même en DUT et LP
|
||||
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
|
||||
amplitude = 0.005 # multiplie les points au dessus du seuil
|
||||
proportion_point = 0.05
|
||||
bonus_max = 0.5 #
|
||||
|
||||
|
||||
class BonusLeMans(BonusSportAdditif):
|
||||
@ -754,8 +774,25 @@ class BonusLille(BonusSportAdditif):
|
||||
)
|
||||
|
||||
|
||||
class BonusLimousin(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (sport, culture) à l'IUT du Limousin
|
||||
|
||||
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières optionnelles
|
||||
sont cumulés.
|
||||
|
||||
La moyenne de chacune des UE du semestre pair est augmentée de 5% du
|
||||
cumul des points de bonus.
|
||||
Le maximum de points bonus est de 0,5.
|
||||
"""
|
||||
|
||||
name = "bonus_limousin"
|
||||
displayed_name = "IUT du Limousin"
|
||||
proportion_point = 0.05
|
||||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusLyonProvisoire(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Lyon (provisoire)
|
||||
"""Calcul bonus modules optionnels (sport, culture) à l'IUT de Lyon (provisoire)
|
||||
|
||||
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
|
||||
optionnelles sont cumulés et 1,8% de ces points cumulés
|
||||
@ -769,8 +806,36 @@ class BonusLyonProvisoire(BonusSportAdditif):
|
||||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusMantes(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.
|
||||
|
||||
<p>
|
||||
Soit N la note attribuée, le bonus (ou malus) correspond à :
|
||||
(N-10) x 0,05
|
||||
appliqué sur chaque UE du semestre sélectionné pour le BUT
|
||||
ou appliqué sur la moyenne générale du semestre sélectionné pour le DUT.
|
||||
</p>
|
||||
<p>Exemples :</p>
|
||||
<ul>
|
||||
<li> pour une note de 20 : bonus de + 0,5</li>
|
||||
<li> pour une note de 15 : bonus de + 0,25</li>
|
||||
<li> note de 10 : Ni bonus, ni malus (+0)</li>
|
||||
<li> note de 5, malus : - 0,25</li>
|
||||
<li> note de 0,malus : - 0,5</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
name = "bonus_mantes"
|
||||
displayed_name = "IUT de Mantes en Yvelines"
|
||||
bonus_min = -0.5 # peut être NEGATIF !
|
||||
bonus_max = 0.5
|
||||
seuil_moy_gen = 0.0 # tous les points comptent
|
||||
seuil_comptage = 10.0 # pivot à 10.
|
||||
proportion_point = 0.05
|
||||
|
||||
|
||||
class BonusMulhouse(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Mulhouse
|
||||
"""Calcul bonus modules optionnels (sport, culture) à l'IUT de Mulhouse
|
||||
|
||||
La moyenne de chacune des UE du semestre sera majorée à hauteur de
|
||||
5% du cumul des points supérieurs à 10 obtenus en matières optionnelles,
|
||||
@ -809,6 +874,19 @@ class BonusNantes(BonusSportAdditif):
|
||||
bonus_max = 0.5 # plafonnement à 0.5 points
|
||||
|
||||
|
||||
class BonusPoitiers(BonusSportAdditif):
|
||||
"""Calcul bonus optionnels (sport, culture), règle IUT de Poitiers.
|
||||
|
||||
Les deux notes d'option supérieure à 10, bonifies les moyennes de chaque UE.
|
||||
|
||||
bonus = (option1 - 10)*5% + (option2 - 10)*5%
|
||||
"""
|
||||
|
||||
name = "bonus_poitiers"
|
||||
displayed_name = "IUT de Poitiers"
|
||||
proportion_point = 0.05
|
||||
|
||||
|
||||
class BonusRoanne(BonusSportAdditif):
|
||||
"""IUT de Roanne.
|
||||
|
||||
@ -831,9 +909,9 @@ class BonusStBrieuc(BonusSportAdditif):
|
||||
<ul>
|
||||
<li>Bonus = (S - 10)/20</li>
|
||||
</ul>
|
||||
<div class="warning">(XXX vérifier si S6 est éligible au bonus, et le S2 du DUT XXX)</div>
|
||||
"""
|
||||
|
||||
# Utilisé aussi par St Malo, voir plus bas
|
||||
name = "bonus_iut_stbrieuc"
|
||||
displayed_name = "IUT de Saint-Brieuc"
|
||||
proportion_point = 1 / 20.0
|
||||
@ -845,6 +923,19 @@ class BonusStBrieuc(BonusSportAdditif):
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
|
||||
|
||||
class BonusStEtienne(BonusSportAdditif):
|
||||
"""IUT de Saint-Etienne.
|
||||
|
||||
Le bonus est compris entre 0 et 0.6 points.
|
||||
"""
|
||||
|
||||
name = "bonus_iutse"
|
||||
displayed_name = "IUT de Saint-Etienne"
|
||||
seuil_moy_gen = 0.0
|
||||
bonus_max = 0.6 # plafonnement à 0.6 points
|
||||
proportion_point = 1
|
||||
|
||||
|
||||
class BonusStDenis(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT Saint-Denis
|
||||
|
||||
@ -862,6 +953,19 @@ class BonusStDenis(BonusSportAdditif):
|
||||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusStMalo(BonusStBrieuc):
|
||||
# identique à St Brieux, sauf la doc
|
||||
"""IUT de Saint Malo
|
||||
|
||||
Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE:
|
||||
<ul>
|
||||
<li>Bonus = (S - 10)/20</li>
|
||||
</ul>
|
||||
"""
|
||||
name = "bonus_iut_stmalo"
|
||||
displayed_name = "IUT de Saint-Malo"
|
||||
|
||||
|
||||
class BonusTarbes(BonusSportAdditif):
|
||||
"""Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
|
||||
|
||||
|
@ -3,11 +3,9 @@
|
||||
|
||||
"""Matrices d'inscription aux modules d'un semestre
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
|
||||
#
|
||||
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
|
||||
@ -17,7 +15,7 @@ from app import models
|
||||
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
||||
"""Charge la matrice des inscriptions aux modules du semestre
|
||||
rows: etudid (inscrits au semestre, avec DEM et DEF)
|
||||
columns: moduleimpl_id (en chaîne)
|
||||
columns: moduleimpl_id
|
||||
value: bool (0/1 inscrit ou pas)
|
||||
"""
|
||||
# méthode la moins lente: une requete par module, merge les dataframes
|
||||
|
@ -16,7 +16,7 @@ from app.scodoc import sco_codes_parcours
|
||||
|
||||
|
||||
class ValidationsSemestre(ResultatsCache):
|
||||
""" """
|
||||
"""Les décisions de jury pour un semestre"""
|
||||
|
||||
_cached_attrs = (
|
||||
"decisions_jury",
|
||||
|
@ -30,7 +30,8 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from flask import flash
|
||||
from flask import flash, g, Markup, url_for
|
||||
from app.models.formations import Formation
|
||||
|
||||
|
||||
def compute_sem_moys_apc_using_coefs(
|
||||
@ -51,7 +52,7 @@ def compute_sem_moys_apc_using_coefs(
|
||||
|
||||
|
||||
def compute_sem_moys_apc_using_ects(
|
||||
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None
|
||||
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None, skip_empty_ues=False
|
||||
) -> pd.Series:
|
||||
"""Calcule les moyennes générales indicatives de tous les étudiants
|
||||
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
|
||||
@ -59,13 +60,29 @@ def compute_sem_moys_apc_using_ects(
|
||||
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
|
||||
ects: liste de floats ou None, 1 par UE
|
||||
|
||||
Si skip_empty_ues: ne compte pas les UE non notées.
|
||||
Sinon (par défaut), une UE non notée compte comme zéro.
|
||||
|
||||
Result: panda Series, index etudid, valeur float (moyenne générale)
|
||||
"""
|
||||
try:
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
|
||||
if skip_empty_ues:
|
||||
# annule les coefs des UE sans notes (NaN)
|
||||
ects = np.where(etud_moy_ue_df.isna(), 0.0, np.array(ects, dtype=float))
|
||||
# ects est devenu nb_etuds x nb_ues
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||
else:
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
|
||||
except TypeError:
|
||||
if None in ects:
|
||||
flash("""Calcul moyenne générale impossible: ECTS des UE manquants !""")
|
||||
formation = Formation.query.get(formation_id)
|
||||
flash(
|
||||
Markup(
|
||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||
(formation: <a href="{url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id)}">{formation.get_titre_version()}</a>)"""
|
||||
)
|
||||
)
|
||||
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
|
||||
else:
|
||||
raise
|
||||
@ -76,8 +93,12 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
|
||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||
numérique) en tenant compte des ex-aequos.
|
||||
|
||||
Result: Series { etudid : rang:str } où rang est une chaine decrivant le rang.
|
||||
Result: couple (tuple)
|
||||
Series { etudid : rang:str } où rang est une chaine decrivant le rang,
|
||||
Series { etudid : rang:int } le rang comme un nombre
|
||||
"""
|
||||
if (notes is None) or (len(notes) == 0):
|
||||
return (pd.Series([], dtype=object), pd.Series([], dtype=int))
|
||||
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
|
||||
rangs_str = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
|
||||
rangs_int = pd.Series(index=notes.index, dtype=int) # le rang numérique pour tris
|
||||
|
@ -12,11 +12,13 @@ import pandas as pd
|
||||
|
||||
from app import log
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig, formsemestre
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class ResultatsSemestreBUT(NotesTableCompat):
|
||||
@ -31,6 +33,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
def __init__(self, formsemestre):
|
||||
super().__init__(formsemestre)
|
||||
|
||||
self.sem_cube = None
|
||||
"""ndarray (etuds x modimpl x ue)"""
|
||||
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
self.compute()
|
||||
@ -38,7 +43,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
||||
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id
|
||||
} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
)
|
||||
|
||||
def compute(self):
|
||||
@ -112,6 +118,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
self.etud_moy_ue,
|
||||
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
skip_empty_ues=sco_preferences.get_preference(
|
||||
"but_moy_skip_empty_ues", self.formsemestre.id
|
||||
),
|
||||
)
|
||||
# --- UE capitalisées
|
||||
self.apply_capitalisation()
|
||||
@ -139,3 +148,30 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
(ne dépend pas des modules auxquels est inscrit l'étudiant, ).
|
||||
"""
|
||||
return self.modimpl_coefs_df.loc[ue.id].sum()
|
||||
|
||||
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]:
|
||||
"""Liste des modimpl ayant des coefs non nuls vers cette UE
|
||||
et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant.
|
||||
"""
|
||||
# sert pour l'affichage ou non de l'UE sur le bulletin et la table recap
|
||||
coefs = self.modimpl_coefs_df # row UE, cols modimpl
|
||||
modimpls = [
|
||||
modimpl
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
if (coefs[modimpl.id][ue_id] != 0)
|
||||
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||
]
|
||||
if not with_bonus:
|
||||
return [
|
||||
modimpl for modimpl in modimpls if modimpl.module.ue.type != UE_SPORT
|
||||
]
|
||||
return modimpls
|
||||
|
||||
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
Utile pour stats bottom tableau recap.
|
||||
Résultat: 1d array of float
|
||||
"""
|
||||
i = self.modimpl_coefs_df.columns.get_loc(modimpl_id)
|
||||
j = self.modimpl_coefs_df.index.get_loc(ue_id)
|
||||
return self.sem_cube[:, i, j]
|
||||
|
@ -11,6 +11,14 @@ from app.models import FormSemestre
|
||||
|
||||
|
||||
class ResultatsCache:
|
||||
"""Résultats cachés (via redis)
|
||||
L'attribut _cached_attrs donne la liste des noms des attributs à cacher
|
||||
(doivent être sérialisables facilement, se limiter à des types simples)
|
||||
|
||||
store() enregistre les attributs dans le cache, et
|
||||
load_cached() les recharge.
|
||||
"""
|
||||
|
||||
_cached_attrs = () # virtual
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre, cache_class=None):
|
||||
|
@ -15,8 +15,8 @@ from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.comp import moy_mat, moy_mod, moy_ue, inscr_mod
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp import moy_mat, moy_mod, moy_sem, moy_ue, inscr_mod
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models.etudiants import Identite
|
||||
@ -35,10 +35,13 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
"modimpl_coefs",
|
||||
"modimpl_idx",
|
||||
"sem_matrix",
|
||||
"mod_rangs",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
super().__init__(formsemestre)
|
||||
self.sem_matrix: np.ndarray = None
|
||||
"sem_matrix : 2d-array (etuds x modimpls)"
|
||||
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
@ -47,7 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"ResultatsSemestreClassic: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
||||
f"""ResultatsSemestreClassic: cached formsemestre_id={
|
||||
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
)
|
||||
# recalculé (aussi rapide que de les cacher)
|
||||
self.moy_min = self.etud_moy_gen.min()
|
||||
@ -142,6 +146,22 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
if sco_preferences.get_preference("bul_show_matieres", self.formsemestre.id):
|
||||
self.compute_moyennes_matieres()
|
||||
|
||||
def compute_rangs(self):
|
||||
"""Calcul des rangs (classements) dans le semestre (moy. gen.), les UE
|
||||
et les modules.
|
||||
"""
|
||||
# rangs moy gen et UEs sont calculées par la méthode commune à toutes les formations:
|
||||
super().compute_rangs()
|
||||
# les rangs des modules n'existent que dans les formations classiques:
|
||||
self.mod_rangs = {}
|
||||
for modimpl_result in self.modimpls_results.values():
|
||||
# ne prend que les rangs sous forme de chaines:
|
||||
rangs = moy_sem.comp_ranks_series(modimpl_result.etuds_moy_module)[0]
|
||||
self.mod_rangs[modimpl_result.moduleimpl_id] = (
|
||||
rangs,
|
||||
modimpl_result.nb_inscrits_module,
|
||||
)
|
||||
|
||||
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||
"""La moyenne de l'étudiant dans le moduleimpl
|
||||
Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
|
||||
@ -172,6 +192,19 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
),
|
||||
}
|
||||
|
||||
def modimpl_notes(
|
||||
self,
|
||||
modimpl_id: int,
|
||||
ue_id: int = None,
|
||||
) -> np.ndarray:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
Utile pour stats bottom tableau recap.
|
||||
ue_id n'est pas utilisé ici (formations classiques)
|
||||
Résultat: 1d array of float
|
||||
"""
|
||||
i = self.modimpl_idx[modimpl_id]
|
||||
return self.sem_matrix[:, i]
|
||||
|
||||
def compute_moyennes_matieres(self):
|
||||
"""Calcul les moyennes par matière. Doit être appelée au besoin, en fin de compute."""
|
||||
self.moyennes_matieres = moy_mat.compute_mat_moys_classic(
|
||||
@ -188,36 +221,29 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
moyenne générale.
|
||||
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
|
||||
"""
|
||||
c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
||||
if c is not None: # inscrit à au moins un module de cette UE
|
||||
return c
|
||||
coef = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
||||
if coef is not None: # inscrit à au moins un module de cette UE
|
||||
return coef
|
||||
# arfff: aucun moyen de déterminer le coefficient de façon sûre
|
||||
log(
|
||||
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s"
|
||||
% (self.formsemestre.id, etudid, ue)
|
||||
f"""* oups: calcul coef UE impossible\nformsemestre_id='{self.formsemestre.id
|
||||
}'\netudid='{etudid}'\nue={ue}"""
|
||||
)
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
raise ScoValueError(
|
||||
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
|
||||
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
|
||||
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||
impossible à déterminer pour l'étudiant <a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||
<p>Il faut <a href="{
|
||||
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre.id, err_ue_id=ue["ue_id"],
|
||||
)
|
||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
</div>
|
||||
"""
|
||||
% (
|
||||
ue.acronyme,
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
etud.nom_disp(),
|
||||
url_for(
|
||||
"notes.formsemestre_edit_uecoefs",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre.id,
|
||||
err_ue_id=ue["ue_id"],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return 0.0 # ?
|
||||
|
||||
|
||||
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
|
||||
"""Calcule la matrice des notes du semestre
|
||||
@ -247,7 +273,7 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
|
||||
(Series rendus par compute_module_moy, index: etud)
|
||||
Resultat: ndarray (etud x module)
|
||||
"""
|
||||
if not len(modimpls_notes):
|
||||
if not modimpls_notes:
|
||||
return np.zeros((0, 0), dtype=float)
|
||||
modimpls_notes_arr = [s.values for s in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
|
File diff suppressed because it is too large
Load Diff
462
app/comp/res_compat.py
Normal file
462
app/comp/res_compat.py
Normal file
@ -0,0 +1,462 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Classe résultats pour compatibilité avec le code ScoDoc 7
|
||||
"""
|
||||
from functools import cached_property
|
||||
|
||||
from flask import flash, g, Markup, url_for
|
||||
|
||||
from app import log
|
||||
from app.comp import moy_sem
|
||||
from app.comp.aux_stats import StatsMoyenne
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
from app.comp import res_sem
|
||||
from app.models import FormSemestre
|
||||
from app.models import Identite
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||||
class NotesTableCompat(ResultatsSemestre):
|
||||
"""Implementation partielle de NotesTable
|
||||
|
||||
Les méthodes définies dans cette classe sont là
|
||||
pour conserver la compatibilité abvec les codes anciens et
|
||||
il n'est pas recommandé de les utiliser dans de nouveaux
|
||||
développements (API malcommode et peu efficace).
|
||||
"""
|
||||
|
||||
_cached_attrs = ResultatsSemestre._cached_attrs + (
|
||||
"malus",
|
||||
"etud_moy_gen_ranks",
|
||||
"etud_moy_gen_ranks_int",
|
||||
"ue_rangs",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre):
|
||||
super().__init__(formsemestre)
|
||||
|
||||
nb_etuds = len(self.etuds)
|
||||
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
|
||||
self.mod_rangs = None # sera surchargé en Classic, mais pas en APC
|
||||
"""{ modimpl_id : (rangs, effectif) }"""
|
||||
self.moy_min = "NA"
|
||||
self.moy_max = "NA"
|
||||
self.moy_moy = "NA"
|
||||
self.expr_diagnostics = ""
|
||||
self.parcours = self.formsemestre.formation.get_parcours()
|
||||
|
||||
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
||||
"""Liste des étudiants inscrits
|
||||
order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative)
|
||||
|
||||
Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace
|
||||
d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]`
|
||||
"""
|
||||
etuds = self.formsemestre.get_inscrits(
|
||||
include_demdef=include_demdef, order=(order_by == "nom")
|
||||
)
|
||||
if order_by == "moy":
|
||||
etuds.sort(
|
||||
key=lambda e: (
|
||||
self.etud_moy_gen_ranks_int.get(e.id, 100000),
|
||||
e.sort_key,
|
||||
)
|
||||
)
|
||||
return etuds
|
||||
|
||||
def get_etudids(self) -> list[int]:
|
||||
"""(deprecated)
|
||||
Liste des etudids inscrits, incluant les démissionnaires.
|
||||
triée par ordre alphabetique de NOM
|
||||
(à éviter: renvoie les etudids, mais est moins efficace que get_inscrits)
|
||||
"""
|
||||
# Note: pour avoir les inscrits non triés,
|
||||
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
|
||||
return [x["etudid"] for x in self.inscrlist]
|
||||
|
||||
@cached_property
|
||||
def sem(self) -> dict:
|
||||
"""le formsemestre, comme un gros et gras dict (nt.sem)"""
|
||||
return self.formsemestre.get_infos_dict()
|
||||
|
||||
@cached_property
|
||||
def inscrlist(self) -> list[dict]: # utilisé par PE
|
||||
"""Liste des inscrits au semestre (avec DEM et DEF),
|
||||
sous forme de dict etud,
|
||||
classée dans l'ordre alphabétique de noms.
|
||||
"""
|
||||
etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True)
|
||||
return [e.to_dict_scodoc7() for e in etuds]
|
||||
|
||||
@cached_property
|
||||
def stats_moy_gen(self):
|
||||
"""Stats (moy/min/max) sur la moyenne générale"""
|
||||
return StatsMoyenne(self.etud_moy_gen)
|
||||
|
||||
def get_ues_stat_dict(
|
||||
self, filter_sport=False, check_apc_ects=True
|
||||
) -> list[dict]: # was get_ues()
|
||||
"""Liste des UEs, ordonnée par numero.
|
||||
Si filter_sport, retire les UE de type SPORT.
|
||||
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
||||
"""
|
||||
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
|
||||
ues_dict = []
|
||||
for ue in ues:
|
||||
d = ue.to_dict()
|
||||
if ue.type != UE_SPORT:
|
||||
moys = self.etud_moy_ue[ue.id]
|
||||
else:
|
||||
moys = None
|
||||
d.update(StatsMoyenne(moys).to_dict())
|
||||
ues_dict.append(d)
|
||||
if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
|
||||
g.checked_apc_ects = True
|
||||
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
|
||||
formation = self.formsemestre.formation
|
||||
ue_sans_ects = [
|
||||
ue for ue in ues if ue.type != UE_SPORT and ue.ects is None
|
||||
]
|
||||
flash(
|
||||
Markup(
|
||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||
(dans {' ,'.join([ue.acronyme for ue in ue_sans_ects])}
|
||||
de la formation: <a href="{url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
||||
}">{formation.get_titre_version()}</a>)
|
||||
)
|
||||
"""
|
||||
),
|
||||
category="danger",
|
||||
)
|
||||
return ues_dict
|
||||
|
||||
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
|
||||
"""Liste des modules pour une UE (ou toutes si ue_id==None),
|
||||
triés par numéros (selon le type de formation)
|
||||
"""
|
||||
modimpls_dict = []
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
if (ue_id is None) or (modimpl.module.ue.id == ue_id):
|
||||
d = modimpl.to_dict()
|
||||
# compat ScoDoc < 9.2: ajoute matières
|
||||
d["mat"] = modimpl.module.matiere.to_dict()
|
||||
modimpls_dict.append(d)
|
||||
return modimpls_dict
|
||||
|
||||
def compute_rangs(self):
|
||||
"""Calcule les classements
|
||||
Moyenne générale: etud_moy_gen_ranks
|
||||
Par UE (sauf ue bonus)
|
||||
"""
|
||||
(
|
||||
self.etud_moy_gen_ranks,
|
||||
self.etud_moy_gen_ranks_int,
|
||||
) = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||
for ue in self.formsemestre.query_ues():
|
||||
moy_ue = self.etud_moy_ue[ue.id]
|
||||
self.ue_rangs[ue.id] = (
|
||||
moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine
|
||||
int(moy_ue.count()),
|
||||
)
|
||||
# .count() -> nb of non NaN values
|
||||
|
||||
def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]:
|
||||
"""Le rang de l'étudiant dans cette ue
|
||||
Result: rang:str, effectif:str
|
||||
"""
|
||||
rangs, effectif = self.ue_rangs[ue_id]
|
||||
if rangs is not None:
|
||||
rang = rangs[etudid]
|
||||
else:
|
||||
return "", ""
|
||||
return rang, effectif
|
||||
|
||||
def etud_check_conditions_ues(self, etudid):
|
||||
"""Vrai si les conditions sur les UE sont remplies.
|
||||
Ne considère que les UE ayant des notes (moyenne calculée).
|
||||
(les UE sans notes ne sont pas comptées comme sous la barre)
|
||||
Prend en compte les éventuelles UE capitalisées.
|
||||
|
||||
Pour les parcours habituels, cela revient à vérifier que
|
||||
les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
|
||||
|
||||
Pour les parcours non standards (LP2014), cela peut être plus compliqué.
|
||||
|
||||
Return: True|False, message explicatif
|
||||
"""
|
||||
ue_status_list = []
|
||||
for ue in self.formsemestre.query_ues():
|
||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_status:
|
||||
ue_status_list.append(ue_status)
|
||||
return self.parcours.check_barre_ues(ue_status_list)
|
||||
|
||||
def all_etuds_have_sem_decisions(self):
|
||||
"""True si tous les étudiants du semestre ont une décision de jury.
|
||||
Ne regarde pas les décisions d'UE.
|
||||
"""
|
||||
for ins in self.formsemestre.inscriptions:
|
||||
if ins.etat != scu.INSCRIT:
|
||||
continue # skip démissionnaires
|
||||
if self.get_etud_decision_sem(ins.etudid) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def etud_has_decision(self, etudid):
|
||||
"""True s'il y a une décision de jury pour cet étudiant"""
|
||||
return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
|
||||
|
||||
def get_etud_decision_ues(self, etudid: int) -> dict:
|
||||
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
|
||||
Ne tient pas compte des UE capitalisées.
|
||||
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
|
||||
Ne renvoie aucune decision d'UE pour les défaillants
|
||||
"""
|
||||
if self.get_etud_etat(etudid) == DEF:
|
||||
return {}
|
||||
else:
|
||||
if not self.validations:
|
||||
self.validations = res_sem.load_formsemestre_validations(
|
||||
self.formsemestre
|
||||
)
|
||||
return self.validations.decisions_jury_ues.get(etudid, None)
|
||||
|
||||
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
|
||||
Si état défaillant, force le code a DEF
|
||||
"""
|
||||
if self.get_etud_etat(etudid) == DEF:
|
||||
return {
|
||||
"code": DEF,
|
||||
"assidu": False,
|
||||
"event_date": "",
|
||||
"compense_formsemestre_id": None,
|
||||
}
|
||||
else:
|
||||
if not self.validations:
|
||||
self.validations = res_sem.load_formsemestre_validations(
|
||||
self.formsemestre
|
||||
)
|
||||
return self.validations.decisions_jury.get(etudid, None)
|
||||
|
||||
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
||||
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
|
||||
if not self.moyennes_matieres:
|
||||
return "nd"
|
||||
return (
|
||||
self.moyennes_matieres[matiere_id].get(etudid, "-")
|
||||
if matiere_id in self.moyennes_matieres
|
||||
else "-"
|
||||
)
|
||||
|
||||
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||
"""La moyenne de l'étudiant dans le moduleimpl
|
||||
En APC, il s'agira d'une moyenne indicative sans valeur.
|
||||
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
|
||||
"""
|
||||
raise NotImplementedError() # virtual method
|
||||
|
||||
def get_etud_moy_gen(self, etudid): # -> float | str
|
||||
"""Moyenne générale de cet etudiant dans ce semestre.
|
||||
Prend en compte les UE capitalisées.
|
||||
Si apc, moyenne indicative.
|
||||
Si pas de notes: 'NA'
|
||||
"""
|
||||
return self.etud_moy_gen[etudid]
|
||||
|
||||
def get_etud_ects_pot(self, etudid: int) -> dict:
|
||||
"""
|
||||
Un dict avec les champs
|
||||
ects_pot : (float) nb de crédits ECTS qui seraient validés
|
||||
(sous réserve de validation par le jury)
|
||||
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
|
||||
|
||||
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
|
||||
encore enregistrées).
|
||||
"""
|
||||
# was nt.get_etud_moy_infos
|
||||
# XXX pour compat nt, à remplacer ultérieurement
|
||||
ues = self.get_etud_ue_validables(etudid)
|
||||
ects_pot = 0.0
|
||||
for ue in ues:
|
||||
if (
|
||||
ue.id in self.etud_moy_ue
|
||||
and ue.ects is not None
|
||||
and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
|
||||
):
|
||||
ects_pot += ue.ects
|
||||
return {
|
||||
"ects_pot": ects_pot,
|
||||
"ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé)
|
||||
}
|
||||
|
||||
def get_etud_rang(self, etudid: int) -> str:
|
||||
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||
Result: "13" ou "12 ex"
|
||||
"""
|
||||
return self.etud_moy_gen_ranks.get(etudid, 99999)
|
||||
|
||||
def get_etud_rang_group(self, etudid: int, group_id: int):
|
||||
"Le rang de l'étudiant dans ce groupe (NON IMPLEMENTE)"
|
||||
return (None, 0) # XXX unimplemented TODO
|
||||
|
||||
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
||||
"""Liste d'informations (compat NotesTable) sur évaluations completes
|
||||
de ce module.
|
||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||
if not modimpl_results:
|
||||
return [] # safeguard
|
||||
evals_results = []
|
||||
for e in modimpl.evaluations:
|
||||
if modimpl_results.evaluations_completes_dict.get(e.id, False):
|
||||
d = e.to_dict()
|
||||
d["heure_debut"] = e.heure_debut # datetime.time
|
||||
d["heure_fin"] = e.heure_fin
|
||||
d["jour"] = e.jour # datetime
|
||||
d["notes"] = {
|
||||
etud.id: {
|
||||
"etudid": etud.id,
|
||||
"value": modimpl_results.evals_notes[e.id][etud.id],
|
||||
}
|
||||
for etud in self.etuds
|
||||
}
|
||||
d["etat"] = {
|
||||
"evalattente": modimpl_results.evaluations_etat[e.id].nb_attente,
|
||||
}
|
||||
evals_results.append(d)
|
||||
elif e.id not in modimpl_results.evaluations_completes_dict:
|
||||
# ne devrait pas arriver ? XXX
|
||||
log(
|
||||
f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?"
|
||||
)
|
||||
return evals_results
|
||||
|
||||
def get_evaluations_etats(self):
|
||||
"""[ {...evaluation et son etat...} ]"""
|
||||
# TODO: à moderniser
|
||||
from app.scodoc import sco_evaluations
|
||||
|
||||
if not hasattr(self, "_evaluations_etats"):
|
||||
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
||||
self.formsemestre.id
|
||||
)
|
||||
|
||||
return self._evaluations_etats
|
||||
|
||||
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||
"""Liste des états des évaluations de ce module"""
|
||||
# XXX TODO à moderniser: lent, recharge des données que l'on a déjà...
|
||||
return [
|
||||
e
|
||||
for e in self.get_evaluations_etats()
|
||||
if e["moduleimpl_id"] == moduleimpl_id
|
||||
]
|
||||
|
||||
def get_moduleimpls_attente(self):
|
||||
"""Liste des modimpls du semestre ayant des notes en attente"""
|
||||
return [
|
||||
modimpl
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
if self.modimpls_results[modimpl.id].en_attente
|
||||
]
|
||||
|
||||
def get_mod_stats(self, moduleimpl_id: int) -> dict:
|
||||
"""Stats sur les notes obtenues dans un modimpl
|
||||
Vide en APC
|
||||
"""
|
||||
return {
|
||||
"moy": "-",
|
||||
"max": "-",
|
||||
"min": "-",
|
||||
"nb_notes": "-",
|
||||
"nb_missing": "-",
|
||||
"nb_valid_evals": "-",
|
||||
}
|
||||
|
||||
def get_nom_short(self, etudid):
|
||||
"formatte nom d'un etud (pour table recap)"
|
||||
etud = self.identdict[etudid]
|
||||
return (
|
||||
(etud["nom_usuel"] or etud["nom"]).upper()
|
||||
+ " "
|
||||
+ etud["prenom"].capitalize()[:2]
|
||||
+ "."
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def T(self):
|
||||
return self.get_table_moyennes_triees()
|
||||
|
||||
def get_table_moyennes_triees(self) -> list:
|
||||
"""Result: liste de tuples
|
||||
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
||||
"""
|
||||
table_moyennes = []
|
||||
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
||||
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
||||
for etudid in etuds_inscriptions:
|
||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||
if moy_gen is False:
|
||||
# pas de moyenne: démissionnaire ou def
|
||||
t = (
|
||||
["-"]
|
||||
+ ["0.00"] * len(self.ues)
|
||||
+ ["NI"] * len(self.formsemestre.modimpls_sorted)
|
||||
)
|
||||
else:
|
||||
moy_ues = []
|
||||
ue_is_cap = {}
|
||||
for ue in ues:
|
||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_status:
|
||||
moy_ues.append(ue_status["moy"])
|
||||
ue_is_cap[ue.id] = ue_status["is_capitalized"]
|
||||
else:
|
||||
moy_ues.append("?")
|
||||
t = [moy_gen] + list(moy_ues)
|
||||
# Moyennes modules:
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
if ue_is_cap.get(modimpl.module.ue.id, False):
|
||||
val = "-c-"
|
||||
else:
|
||||
val = self.get_etud_mod_moy(modimpl.id, etudid)
|
||||
t.append(val)
|
||||
t.append(etudid)
|
||||
table_moyennes.append(t)
|
||||
# tri par moyennes décroissantes,
|
||||
# en laissant les démissionnaires à la fin, par ordre alphabetique
|
||||
etuds = [ins.etud for ins in etuds_inscriptions.values()]
|
||||
etuds.sort(key=lambda e: e.sort_key)
|
||||
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
|
||||
table_moyennes.sort(key=self._row_key)
|
||||
return table_moyennes
|
||||
|
||||
def _row_key(self, x):
|
||||
"""clé de tri par moyennes décroissantes,
|
||||
en laissant les demissionnaires à la fin, par ordre alphabetique.
|
||||
(moy_gen, rang_alpha)
|
||||
"""
|
||||
try:
|
||||
moy = -float(x[0])
|
||||
except (ValueError, TypeError):
|
||||
moy = 1000.0
|
||||
return (moy, self._rang_alpha[x[-1]])
|
||||
|
||||
@cached_property
|
||||
def identdict(self) -> dict:
|
||||
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
|
||||
return {
|
||||
ins.etud.id: ins.etud.to_dict_scodoc7()
|
||||
for ins in self.formsemestre.inscriptions
|
||||
}
|
@ -1,14 +1,11 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
"""Decorators for permissions, roles and ScoDoc7 Zope compatibility
|
||||
"""
|
||||
import functools
|
||||
from functools import wraps
|
||||
import inspect
|
||||
import types
|
||||
import logging
|
||||
|
||||
|
||||
import werkzeug
|
||||
from werkzeug.exceptions import BadRequest
|
||||
import flask
|
||||
from flask import g, current_app, request
|
||||
from flask import abort, url_for, redirect
|
||||
|
@ -15,6 +15,7 @@ from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
def send_async_email(app, msg):
|
||||
"Send an email, async"
|
||||
with app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
|
@ -4,10 +4,11 @@
|
||||
from flask import Blueprint
|
||||
from app.scodoc import sco_etud
|
||||
from app.auth.models import User
|
||||
from app.models import Departement
|
||||
|
||||
bp = Blueprint("entreprises", __name__)
|
||||
|
||||
LOGS_LEN = 10
|
||||
LOGS_LEN = 5
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
@ -21,9 +22,21 @@ def format_nom(s):
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
def get_nomcomplet(s):
|
||||
def get_nomcomplet_by_username(s):
|
||||
user = User.query.filter_by(user_name=s).first()
|
||||
return user.get_nomcomplet()
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
def get_nomcomplet_by_id(id):
|
||||
user = User.query.filter_by(id=id).first()
|
||||
return user.get_nomcomplet()
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
def get_dept_acronym(id):
|
||||
dept = Departement.query.filter_by(id=id).first()
|
||||
return dept.acronym
|
||||
|
||||
|
||||
from app.entreprises import routes
|
||||
|
198
app/entreprises/app_relations_entreprises.py
Normal file
198
app/entreprises/app_relations_entreprises.py
Normal file
@ -0,0 +1,198 @@
|
||||
# -*- 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
|
||||
#
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import os
|
||||
from config import Config
|
||||
import re
|
||||
import requests
|
||||
import glob
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from app.entreprises.models import (
|
||||
Entreprise,
|
||||
EntrepriseContact,
|
||||
EntrepriseOffre,
|
||||
EntrepriseOffreDepartement,
|
||||
EntreprisePreferences,
|
||||
)
|
||||
|
||||
from app import email
|
||||
from app.scodoc import sco_preferences
|
||||
from app.models import Departement
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
def get_depts():
|
||||
"""
|
||||
Retourne une liste contenant les l'id des départements des roles de l'utilisateur courant
|
||||
"""
|
||||
depts = []
|
||||
for role in current_user.user_roles:
|
||||
dept_id = get_dept_id_by_acronym(role.dept)
|
||||
if dept_id is not None:
|
||||
depts.append(dept_id)
|
||||
return depts
|
||||
|
||||
|
||||
def get_dept_id_by_acronym(acronym):
|
||||
"""
|
||||
Retourne l'id d'un departement a l'aide de son acronym
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first()
|
||||
if dept is not None:
|
||||
return dept.id
|
||||
return None
|
||||
|
||||
|
||||
def check_offre_depts(depts, offre_depts):
|
||||
"""
|
||||
Retourne vrai si l'utilisateur a le droit de visibilité sur l'offre
|
||||
"""
|
||||
if current_user.has_permission(Permission.RelationsEntreprisesChange, None):
|
||||
return True
|
||||
for offre_dept in offre_depts:
|
||||
if offre_dept.dept_id in depts:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_offre_files_and_depts(offre: EntrepriseOffre, depts: list):
|
||||
"""
|
||||
Retourne l'offre, les fichiers attachés a l'offre et les département liés
|
||||
"""
|
||||
offre_depts = EntrepriseOffreDepartement.query.filter_by(offre_id=offre.id).all()
|
||||
if not offre_depts or check_offre_depts(depts, offre_depts):
|
||||
files = []
|
||||
path = os.path.join(
|
||||
Config.SCODOC_VAR_DIR,
|
||||
"entreprises",
|
||||
f"{offre.entreprise_id}",
|
||||
f"{offre.id}",
|
||||
)
|
||||
if os.path.exists(path):
|
||||
for dir in glob.glob(
|
||||
f"{path}/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
|
||||
):
|
||||
for _file in glob.glob(f"{dir}/*"):
|
||||
file = [os.path.basename(dir), os.path.basename(_file)]
|
||||
files.append(file)
|
||||
return [offre, files, offre_depts]
|
||||
return None
|
||||
|
||||
|
||||
def send_email_notifications_entreprise(
|
||||
subject, entreprise: Entreprise, contact: EntrepriseContact
|
||||
):
|
||||
txt = [
|
||||
"Une entreprise est en attente de validation",
|
||||
"Entreprise:",
|
||||
f"\tnom: {entreprise.nom}",
|
||||
f"\tsiret: {entreprise.siret}",
|
||||
f"\tadresse: {entreprise.adresse}",
|
||||
f"\tcode postal: {entreprise.codepostal}",
|
||||
f"\tville: {entreprise.ville}",
|
||||
f"\tpays: {entreprise.pays}",
|
||||
"",
|
||||
"Contact:",
|
||||
f"nom: {contact.nom}",
|
||||
f"prenom: {contact.prenom}",
|
||||
f"telephone: {contact.telephone}",
|
||||
f"mail: {contact.mail}",
|
||||
f"poste: {contact.poste}",
|
||||
f"service: {contact.service}",
|
||||
]
|
||||
txt = "\n".join(txt)
|
||||
email.send_email(
|
||||
subject,
|
||||
sco_preferences.get_preference("email_from_addr"),
|
||||
[EntreprisePreferences.get_email_notifications],
|
||||
txt,
|
||||
)
|
||||
return txt
|
||||
|
||||
|
||||
def verif_contact_data(contact_data):
|
||||
"""
|
||||
Verifie les données d'une ligne Excel (contact)
|
||||
contact_data[0]: nom
|
||||
contact_data[1]: prenom
|
||||
contact_data[2]: telephone
|
||||
contact_data[3]: mail
|
||||
contact_data[4]: poste
|
||||
contact_data[5]: service
|
||||
contact_data[6]: entreprise_id
|
||||
"""
|
||||
# champs obligatoires
|
||||
if contact_data[0] == "" or contact_data[1] == "" or contact_data[6] == "":
|
||||
return False
|
||||
|
||||
# entreprise_id existant
|
||||
entreprise = Entreprise.query.filter_by(siret=contact_data[6]).first()
|
||||
if entreprise is None:
|
||||
return False
|
||||
|
||||
# contact possède le meme nom et prénom dans la meme entreprise
|
||||
contact = EntrepriseContact.query.filter_by(
|
||||
nom=contact_data[0], prenom=contact_data[1], entreprise_id=entreprise.id
|
||||
).first()
|
||||
if contact is not None:
|
||||
return False
|
||||
|
||||
if contact_data[2] == "" and contact_data[3] == "": # 1 moyen de contact
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def verif_entreprise_data(entreprise_data):
|
||||
"""
|
||||
Verifie les données d'une ligne Excel (entreprise)
|
||||
"""
|
||||
if EntreprisePreferences.get_check_siret():
|
||||
for data in entreprise_data: # champs obligatoires
|
||||
if data == "":
|
||||
return False
|
||||
else:
|
||||
for data in entreprise_data[1:]: # champs obligatoires
|
||||
if data == "":
|
||||
return False
|
||||
if EntreprisePreferences.get_check_siret():
|
||||
siret = entreprise_data[0].strip() # vérification sur le siret
|
||||
if re.match("^\d{14}$", siret) is None:
|
||||
return False
|
||||
try:
|
||||
req = requests.get(
|
||||
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
print("no internet")
|
||||
if req.status_code != 200:
|
||||
return False
|
||||
entreprise = Entreprise.query.filter_by(siret=siret).first()
|
||||
if entreprise is not None:
|
||||
return False
|
||||
return True
|
@ -31,215 +31,281 @@ from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
from markupsafe import Markup
|
||||
from sqlalchemy import text
|
||||
from wtforms import StringField, SubmitField, TextAreaField, SelectField, HiddenField
|
||||
from wtforms.fields import EmailField, DateField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email
|
||||
from wtforms import (
|
||||
StringField,
|
||||
SubmitField,
|
||||
TextAreaField,
|
||||
SelectField,
|
||||
HiddenField,
|
||||
SelectMultipleField,
|
||||
DateField,
|
||||
BooleanField,
|
||||
)
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, Optional
|
||||
from wtforms.widgets import ListWidget, CheckboxInput
|
||||
|
||||
from app.entreprises.models import Entreprise, EntrepriseContact
|
||||
from app.models import Identite
|
||||
from app.entreprises.models import Entreprise, EntrepriseContact, EntreprisePreferences
|
||||
from app.models import Identite, Departement
|
||||
from app.auth.models import User
|
||||
|
||||
CHAMP_REQUIS = "Ce champ est requis"
|
||||
SUBMIT_MARGE = {"style": "margin-bottom: 10px;"}
|
||||
|
||||
|
||||
def _build_string_field(label, required=True, render_kw=None):
|
||||
if required:
|
||||
return StringField(
|
||||
label,
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
render_kw=render_kw,
|
||||
)
|
||||
else:
|
||||
return StringField(label, validators=[Optional()], render_kw=render_kw)
|
||||
|
||||
|
||||
class EntrepriseCreationForm(FlaskForm):
|
||||
siret = StringField(
|
||||
"SIRET",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
siret = _build_string_field(
|
||||
"SIRET (*)",
|
||||
render_kw={"placeholder": "Numéro composé de 14 chiffres", "maxlength": "14"},
|
||||
)
|
||||
nom_entreprise = StringField(
|
||||
"Nom de l'entreprise",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
adresse = StringField(
|
||||
"Adresse de l'entreprise",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
codepostal = StringField(
|
||||
"Code postal de l'entreprise",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
ville = StringField(
|
||||
"Ville de l'entreprise",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
pays = StringField(
|
||||
"Pays de l'entreprise",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
render_kw={"style": "margin-bottom: 50px;"},
|
||||
)
|
||||
nom_entreprise = _build_string_field("Nom de l'entreprise (*)")
|
||||
adresse = _build_string_field("Adresse de l'entreprise (*)")
|
||||
codepostal = _build_string_field("Code postal de l'entreprise (*)")
|
||||
ville = _build_string_field("Ville de l'entreprise (*)")
|
||||
pays = _build_string_field("Pays de l'entreprise", required=False)
|
||||
|
||||
nom_contact = StringField(
|
||||
"Nom du contact", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
nom_contact = _build_string_field("Nom du contact (*)")
|
||||
prenom_contact = _build_string_field("Prénom du contact (*)")
|
||||
telephone = _build_string_field("Téléphone du contact (*)", required=False)
|
||||
mail = StringField(
|
||||
"Mail du contact (*)",
|
||||
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||
)
|
||||
prenom_contact = StringField(
|
||||
"Prénom du contact",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
telephone = StringField(
|
||||
"Téléphone du contact",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
mail = EmailField(
|
||||
"Mail du contact",
|
||||
validators=[
|
||||
DataRequired(message=CHAMP_REQUIS),
|
||||
Email(message="Adresse e-mail invalide"),
|
||||
],
|
||||
)
|
||||
poste = StringField("Poste du contact", validators=[])
|
||||
service = StringField("Service du contact", validators=[])
|
||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
||||
poste = _build_string_field("Poste du contact", required=False)
|
||||
service = _build_string_field("Service du contact", required=False)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
|
||||
if not self.telephone.data and not self.mail.data:
|
||||
self.telephone.errors.append(
|
||||
"Saisir un moyen de contact (mail ou téléphone)"
|
||||
)
|
||||
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
def validate_siret(self, siret):
|
||||
siret = siret.data.strip()
|
||||
if re.match("^\d{14}$", siret) == None:
|
||||
raise ValidationError("Format incorrect")
|
||||
req = requests.get(
|
||||
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
|
||||
)
|
||||
if req.status_code != 200:
|
||||
raise ValidationError("SIRET inexistant")
|
||||
entreprise = Entreprise.query.filter_by(siret=siret).first()
|
||||
if entreprise is not None:
|
||||
lien = f'<a href="/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}">ici</a>'
|
||||
raise ValidationError(
|
||||
Markup(f"Entreprise déjà présent, lien vers la fiche : {lien}")
|
||||
)
|
||||
if EntreprisePreferences.get_check_siret():
|
||||
siret = siret.data.strip()
|
||||
if re.match("^\d{14}$", siret) is None:
|
||||
raise ValidationError("Format incorrect")
|
||||
try:
|
||||
req = requests.get(
|
||||
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
print("no internet")
|
||||
if req.status_code != 200:
|
||||
raise ValidationError("SIRET inexistant")
|
||||
entreprise = Entreprise.query.filter_by(siret=siret).first()
|
||||
if entreprise is not None:
|
||||
lien = f'<a href="/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}">ici</a>'
|
||||
raise ValidationError(
|
||||
Markup(f"Entreprise déjà présent, lien vers la fiche : {lien}")
|
||||
)
|
||||
|
||||
|
||||
class EntrepriseModificationForm(FlaskForm):
|
||||
siret = StringField("SIRET", validators=[], render_kw={"disabled": ""})
|
||||
nom = StringField(
|
||||
"Nom de l'entreprise",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
adresse = StringField("Adresse", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
codepostal = StringField(
|
||||
"Code postal", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
ville = StringField("Ville", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
pays = StringField("Pays", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
|
||||
hidden_entreprise_siret = HiddenField()
|
||||
siret = StringField("SIRET (*)")
|
||||
nom = _build_string_field("Nom de l'entreprise (*)")
|
||||
adresse = _build_string_field("Adresse (*)")
|
||||
codepostal = _build_string_field("Code postal (*)")
|
||||
ville = _build_string_field("Ville (*)")
|
||||
pays = _build_string_field("Pays", required=False)
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.siret.render_kw = {
|
||||
"disabled": "",
|
||||
"value": self.hidden_entreprise_siret.data,
|
||||
}
|
||||
|
||||
|
||||
class MultiCheckboxField(SelectMultipleField):
|
||||
widget = ListWidget(prefix_label=False)
|
||||
option_widget = CheckboxInput()
|
||||
|
||||
|
||||
class OffreCreationForm(FlaskForm):
|
||||
intitule = StringField("Intitulé", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
intitule = _build_string_field("Intitulé (*)")
|
||||
description = TextAreaField(
|
||||
"Description", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
"Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre",
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
missions = TextAreaField(
|
||||
"Missions", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
"Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
duree = StringField("Durée", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
||||
duree = _build_string_field("Durée (*)")
|
||||
depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
|
||||
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.depts.choices = [
|
||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||
]
|
||||
|
||||
|
||||
class OffreModificationForm(FlaskForm):
|
||||
intitule = StringField("Intitulé", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
intitule = _build_string_field("Intitulé (*)")
|
||||
description = TextAreaField(
|
||||
"Description", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
"Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre",
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
missions = TextAreaField(
|
||||
"Missions", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
"Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
duree = StringField("Durée", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
|
||||
duree = _build_string_field("Durée (*)")
|
||||
depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
|
||||
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.depts.choices = [
|
||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||
]
|
||||
|
||||
|
||||
class ContactCreationForm(FlaskForm):
|
||||
hidden_entreprise_id = HiddenField()
|
||||
nom = StringField("Nom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
prenom = StringField("Prénom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
telephone = StringField(
|
||||
"Téléphone", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
nom = _build_string_field("Nom (*)")
|
||||
prenom = _build_string_field("Prénom (*)")
|
||||
telephone = _build_string_field("Téléphone (*)", required=False)
|
||||
mail = StringField(
|
||||
"Mail (*)",
|
||||
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||
)
|
||||
mail = EmailField(
|
||||
"Mail",
|
||||
validators=[
|
||||
DataRequired(message=CHAMP_REQUIS),
|
||||
Email(message="Adresse e-mail invalide"),
|
||||
],
|
||||
)
|
||||
poste = StringField("Poste", validators=[])
|
||||
service = StringField("Service", validators=[])
|
||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
||||
poste = _build_string_field("Poste", required=False)
|
||||
service = _build_string_field("Service", required=False)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
rv = FlaskForm.validate(self)
|
||||
if not rv:
|
||||
return False
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
|
||||
contact = EntrepriseContact.query.filter_by(
|
||||
entreprise_id=self.hidden_entreprise_id.data,
|
||||
nom=self.nom.data,
|
||||
prenom=self.prenom.data,
|
||||
).first()
|
||||
|
||||
if contact is not None:
|
||||
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
|
||||
self.prenom.errors.append("")
|
||||
return False
|
||||
validate = False
|
||||
|
||||
return True
|
||||
if not self.telephone.data and not self.mail.data:
|
||||
self.telephone.errors.append(
|
||||
"Saisir un moyen de contact (mail ou téléphone)"
|
||||
)
|
||||
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
class ContactModificationForm(FlaskForm):
|
||||
nom = StringField("Nom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
prenom = StringField("Prénom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
telephone = StringField(
|
||||
"Téléphone", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
hidden_contact_id = HiddenField()
|
||||
hidden_entreprise_id = HiddenField()
|
||||
nom = _build_string_field("Nom (*)")
|
||||
prenom = _build_string_field("Prénom (*)")
|
||||
telephone = _build_string_field("Téléphone (*)", required=False)
|
||||
mail = StringField(
|
||||
"Mail (*)",
|
||||
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||
)
|
||||
mail = EmailField(
|
||||
"Mail",
|
||||
validators=[
|
||||
DataRequired(message=CHAMP_REQUIS),
|
||||
Email(message="Adresse e-mail invalide"),
|
||||
],
|
||||
)
|
||||
poste = StringField("Poste", validators=[])
|
||||
service = StringField("Service", validators=[])
|
||||
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
|
||||
poste = _build_string_field("Poste", required=False)
|
||||
service = _build_string_field("Service", required=False)
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
|
||||
contact = EntrepriseContact.query.filter(
|
||||
EntrepriseContact.id != self.hidden_contact_id.data,
|
||||
EntrepriseContact.entreprise_id == self.hidden_entreprise_id.data,
|
||||
EntrepriseContact.nom == self.nom.data,
|
||||
EntrepriseContact.prenom == self.prenom.data,
|
||||
).first()
|
||||
if contact is not None:
|
||||
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
|
||||
self.prenom.errors.append("")
|
||||
validate = False
|
||||
|
||||
if not self.telephone.data and not self.mail.data:
|
||||
self.telephone.errors.append(
|
||||
"Saisir un moyen de contact (mail ou téléphone)"
|
||||
)
|
||||
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
class HistoriqueCreationForm(FlaskForm):
|
||||
etudiant = StringField(
|
||||
"Étudiant",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant puis selectionnez"},
|
||||
etudiant = _build_string_field(
|
||||
"Étudiant (*)",
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||
)
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre",
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
date_debut = DateField(
|
||||
"Date début", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
"Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
date_fin = DateField("Date fin", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
||||
date_fin = DateField(
|
||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
rv = FlaskForm.validate(self)
|
||||
if not rv:
|
||||
return False
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
|
||||
if self.date_debut.data > self.date_fin.data:
|
||||
if (
|
||||
self.date_debut.data
|
||||
and self.date_fin.data
|
||||
and self.date_debut.data > self.date_fin.data
|
||||
):
|
||||
self.date_debut.errors.append("Les dates sont incompatibles")
|
||||
self.date_fin.errors.append("Les dates sont incompatibles")
|
||||
return False
|
||||
return True
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
def validate_etudiant(self, etudiant):
|
||||
etudiant_data = etudiant.data.upper().strip()
|
||||
@ -254,11 +320,11 @@ class HistoriqueCreationForm(FlaskForm):
|
||||
|
||||
|
||||
class EnvoiOffreForm(FlaskForm):
|
||||
responsable = StringField(
|
||||
"Responsable de formation",
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
responsable = _build_string_field(
|
||||
"Responsable de formation (*)",
|
||||
render_kw={"placeholder": "Tapez le nom du responsable de formation"},
|
||||
)
|
||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate_responsable(self, responsable):
|
||||
responsable_data = responsable.data.upper().strip()
|
||||
@ -276,14 +342,38 @@ class EnvoiOffreForm(FlaskForm):
|
||||
|
||||
class AjoutFichierForm(FlaskForm):
|
||||
fichier = FileField(
|
||||
"Fichier",
|
||||
"Fichier (*)",
|
||||
validators=[
|
||||
FileRequired(message=CHAMP_REQUIS),
|
||||
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
||||
submit = SubmitField("Ajouter", render_kw=SUBMIT_MARGE)
|
||||
|
||||
|
||||
class SuppressionConfirmationForm(FlaskForm):
|
||||
submit = SubmitField("Supprimer", render_kw={"style": "margin-bottom: 10px;"})
|
||||
submit = SubmitField("Supprimer", render_kw=SUBMIT_MARGE)
|
||||
|
||||
|
||||
class ValidationConfirmationForm(FlaskForm):
|
||||
submit = SubmitField("Valider", render_kw=SUBMIT_MARGE)
|
||||
|
||||
|
||||
class ImportForm(FlaskForm):
|
||||
fichier = FileField(
|
||||
"Fichier (*)",
|
||||
validators=[
|
||||
FileRequired(message=CHAMP_REQUIS),
|
||||
FileAllowed(["xlsx"], "Fichier .xlsx uniquement"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Importer", render_kw=SUBMIT_MARGE)
|
||||
|
||||
|
||||
class PreferencesForm(FlaskForm):
|
||||
mail_entreprise = StringField(
|
||||
"Mail notifications",
|
||||
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||
)
|
||||
check_siret = BooleanField("Vérification SIRET")
|
||||
submit = SubmitField("Valider", render_kw=SUBMIT_MARGE)
|
||||
|
@ -2,14 +2,15 @@ from app import db
|
||||
|
||||
|
||||
class Entreprise(db.Model):
|
||||
__tablename__ = "entreprises"
|
||||
__tablename__ = "are_entreprises"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
siret = db.Column(db.Text)
|
||||
nom = db.Column(db.Text)
|
||||
adresse = db.Column(db.Text)
|
||||
codepostal = db.Column(db.Text)
|
||||
ville = db.Column(db.Text)
|
||||
pays = db.Column(db.Text)
|
||||
pays = db.Column(db.Text, default="FRANCE")
|
||||
visible = db.Column(db.Boolean, default=False)
|
||||
contacts = db.relationship(
|
||||
"EntrepriseContact",
|
||||
backref="entreprise",
|
||||
@ -26,19 +27,19 @@ class Entreprise(db.Model):
|
||||
def to_dict(self):
|
||||
return {
|
||||
"siret": self.siret,
|
||||
"nom": self.nom,
|
||||
"nom_entreprise": self.nom,
|
||||
"adresse": self.adresse,
|
||||
"codepostal": self.codepostal,
|
||||
"code_postal": self.codepostal,
|
||||
"ville": self.ville,
|
||||
"pays": self.pays,
|
||||
}
|
||||
|
||||
|
||||
class EntrepriseContact(db.Model):
|
||||
__tablename__ = "entreprise_contact"
|
||||
__tablename__ = "are_contacts"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
entreprise_id = db.Column(
|
||||
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
nom = db.Column(db.Text)
|
||||
prenom = db.Column(db.Text)
|
||||
@ -48,6 +49,7 @@ class EntrepriseContact(db.Model):
|
||||
service = db.Column(db.Text)
|
||||
|
||||
def to_dict(self):
|
||||
entreprise = Entreprise.query.filter_by(id=self.entreprise_id).first()
|
||||
return {
|
||||
"nom": self.nom,
|
||||
"prenom": self.prenom,
|
||||
@ -55,31 +57,15 @@ class EntrepriseContact(db.Model):
|
||||
"mail": self.mail,
|
||||
"poste": self.poste,
|
||||
"service": self.service,
|
||||
}
|
||||
|
||||
def to_dict_export(self):
|
||||
entreprise = Entreprise.query.get(self.entreprise_id)
|
||||
return {
|
||||
"nom": self.nom,
|
||||
"prenom": self.prenom,
|
||||
"telephone": self.telephone,
|
||||
"mail": self.mail,
|
||||
"poste": self.poste,
|
||||
"service": self.service,
|
||||
"siret": entreprise.siret,
|
||||
"nom_entreprise": entreprise.nom,
|
||||
"adresse_entreprise": entreprise.adresse,
|
||||
"codepostal": entreprise.codepostal,
|
||||
"ville": entreprise.ville,
|
||||
"pays": entreprise.pays,
|
||||
"entreprise_siret": entreprise.siret,
|
||||
}
|
||||
|
||||
|
||||
class EntrepriseOffre(db.Model):
|
||||
__tablename__ = "entreprise_offre"
|
||||
__tablename__ = "are_offres"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
entreprise_id = db.Column(
|
||||
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
date_ajout = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
intitule = db.Column(db.Text)
|
||||
@ -87,6 +73,8 @@ class EntrepriseOffre(db.Model):
|
||||
type_offre = db.Column(db.Text)
|
||||
missions = db.Column(db.Text)
|
||||
duree = db.Column(db.Text)
|
||||
expiration_date = db.Column(db.Date)
|
||||
expired = db.Column(db.Boolean, default=False)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@ -99,7 +87,7 @@ class EntrepriseOffre(db.Model):
|
||||
|
||||
|
||||
class EntrepriseLog(db.Model):
|
||||
__tablename__ = "entreprise_log"
|
||||
__tablename__ = "are_logs"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
authenticated_user = db.Column(db.Text)
|
||||
@ -108,9 +96,11 @@ class EntrepriseLog(db.Model):
|
||||
|
||||
|
||||
class EntrepriseEtudiant(db.Model):
|
||||
__tablename__ = "entreprise_etudiant"
|
||||
__tablename__ = "are_etudiants"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id"))
|
||||
entreprise_id = db.Column(
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
etudid = db.Column(db.Integer)
|
||||
type_offre = db.Column(db.Text)
|
||||
date_debut = db.Column(db.Date)
|
||||
@ -120,18 +110,78 @@ class EntrepriseEtudiant(db.Model):
|
||||
|
||||
|
||||
class EntrepriseEnvoiOffre(db.Model):
|
||||
__tablename__ = "entreprise_envoi_offre"
|
||||
__tablename__ = "are_envoi_offre"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
receiver_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||
receiver_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||
offre_id = db.Column(db.Integer, db.ForeignKey("are_offres.id", ondelete="cascade"))
|
||||
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
|
||||
class EntrepriseEnvoiOffreEtudiant(db.Model):
|
||||
__tablename__ = "entreprise_envoi_offre_etudiant"
|
||||
__tablename__ = "are_envoi_offre_etudiant"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
receiver_id = db.Column(db.Integer, db.ForeignKey("identite.id"))
|
||||
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||
receiver_id = db.Column(
|
||||
db.Integer, db.ForeignKey("identite.id", ondelete="cascade")
|
||||
)
|
||||
offre_id = db.Column(db.Integer, db.ForeignKey("are_offres.id", ondelete="cascade"))
|
||||
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
|
||||
class EntrepriseOffreDepartement(db.Model):
|
||||
__tablename__ = "are_offre_departement"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
offre_id = db.Column(db.Integer, db.ForeignKey("are_offres.id", ondelete="cascade"))
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id", ondelete="cascade"))
|
||||
|
||||
|
||||
class EntreprisePreferences(db.Model):
|
||||
__tablename__ = "are_preferences"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.Text)
|
||||
value = db.Column(db.Text)
|
||||
|
||||
@classmethod
|
||||
def get_email_notifications(cls):
|
||||
mail = EntreprisePreferences.query.filter_by(
|
||||
name="mail_notifications_entreprise"
|
||||
).first()
|
||||
if mail is None:
|
||||
return ""
|
||||
else:
|
||||
return mail.value
|
||||
|
||||
@classmethod
|
||||
def set_email_notifications(cls, mail: str):
|
||||
if mail != cls.get_email_notifications():
|
||||
m = EntreprisePreferences.query.filter_by(
|
||||
name="mail_notifications_entreprise"
|
||||
).first()
|
||||
if m is None:
|
||||
prefs = EntreprisePreferences(
|
||||
name="mail_notifications_entreprise",
|
||||
value=mail,
|
||||
)
|
||||
db.session.add(prefs)
|
||||
else:
|
||||
m.value = mail
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_check_siret(cls):
|
||||
check_siret = EntreprisePreferences.query.filter_by(name="check_siret").first()
|
||||
if check_siret is None:
|
||||
return 1
|
||||
else:
|
||||
return int(check_siret.value)
|
||||
|
||||
@classmethod
|
||||
def set_check_siret(cls, check_siret: int):
|
||||
cs = EntreprisePreferences.query.filter_by(name="check_siret").first()
|
||||
if cs is None:
|
||||
prefs = EntreprisePreferences(name="check_siret", value=check_siret)
|
||||
db.session.add(prefs)
|
||||
else:
|
||||
cs.value = check_siret
|
||||
db.session.commit()
|
||||
|
File diff suppressed because it is too large
Load Diff
1
app/forms/__init__.py
Normal file
1
app/forms/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# empty but required for pylint
|
1
app/forms/main/__init__.py
Normal file
1
app/forms/main/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# empty but required for pylint
|
@ -29,17 +29,13 @@
|
||||
Formulaires configuration Exports Apogée (codes)
|
||||
"""
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField
|
||||
|
||||
from app import models
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def _build_code_field(code):
|
||||
@ -61,6 +57,7 @@ def _build_code_field(code):
|
||||
|
||||
|
||||
class CodesDecisionsForm(FlaskForm):
|
||||
"Formulaire code décisions Apogée"
|
||||
ADC = _build_code_field("ADC")
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADM = _build_code_field("ADM")
|
||||
@ -73,5 +70,16 @@ class CodesDecisionsForm(FlaskForm):
|
||||
DEM = _build_code_field("DEM")
|
||||
NAR = _build_code_field("NAR")
|
||||
RAT = _build_code_field("RAT")
|
||||
NOTES_FMT = StringField(
|
||||
label="Format notes exportées",
|
||||
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
|
||||
validators=[
|
||||
validators.Length(
|
||||
max=SHORT_STR_LEN,
|
||||
message=f"Le format ne doit pas dépasser {SHORT_STR_LEN} caractères",
|
||||
),
|
||||
validators.DataRequired("format requis"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
@ -47,8 +47,6 @@ from app.scodoc.sco_config_actions import (
|
||||
LogoInsert,
|
||||
)
|
||||
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
|
@ -31,13 +31,14 @@ Formulaires configuration Exports Apogée (codes)
|
||||
|
||||
from flask import flash, url_for, redirect, request, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SelectField, SubmitField
|
||||
from wtforms import BooleanField, SelectField, SubmitField
|
||||
|
||||
import app
|
||||
from app.models import ScoDocSiteConfig
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
class BonusConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration des logos"
|
||||
bonus_sport_func_name = SelectField(
|
||||
label="Fonction de calcul des bonus sport&culture",
|
||||
@ -46,31 +47,57 @@ class ScoDocConfigurationForm(FlaskForm):
|
||||
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
submit_bonus = SubmitField("Valider")
|
||||
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration avancée"
|
||||
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||
submit_scodoc = SubmitField("Valider")
|
||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
|
||||
def configuration():
|
||||
"Page de configuration principale"
|
||||
# nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue
|
||||
form = ScoDocConfigurationForm(
|
||||
form_bonus = BonusConfigurationForm(
|
||||
data={
|
||||
"bonus_sport_func_name": ScoDocSiteConfig.get_bonus_sport_class_name(),
|
||||
}
|
||||
)
|
||||
if request.method == "POST" and form.cancel.data: # cancel button
|
||||
form_scodoc = ScoDocConfigurationForm(
|
||||
data={"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled()}
|
||||
)
|
||||
if request.method == "POST" and (
|
||||
form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data
|
||||
): # cancel button
|
||||
return redirect(url_for("scodoc.index"))
|
||||
if form.validate_on_submit():
|
||||
if form_bonus.submit_bonus.data and form_bonus.validate():
|
||||
if (
|
||||
form.data["bonus_sport_func_name"]
|
||||
form_bonus.data["bonus_sport_func_name"]
|
||||
!= ScoDocSiteConfig.get_bonus_sport_class_name()
|
||||
):
|
||||
ScoDocSiteConfig.set_bonus_sport_class(form.data["bonus_sport_func_name"])
|
||||
ScoDocSiteConfig.set_bonus_sport_class(
|
||||
form_bonus.data["bonus_sport_func_name"]
|
||||
)
|
||||
app.clear_scodoc_cache()
|
||||
flash(f"Fonction bonus sport&culture configurée.")
|
||||
return redirect(url_for("scodoc.index"))
|
||||
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
||||
if ScoDocSiteConfig.enable_entreprises(
|
||||
enabled=form_scodoc.data["enable_entreprises"]
|
||||
):
|
||||
flash(
|
||||
"Module entreprise "
|
||||
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||
)
|
||||
return redirect(url_for("scodoc.index"))
|
||||
|
||||
return render_template(
|
||||
"configuration.html",
|
||||
form=form,
|
||||
form_bonus=form_bonus,
|
||||
form_scodoc=form_scodoc,
|
||||
scu=scu,
|
||||
title="Configuration",
|
||||
)
|
||||
|
@ -29,7 +29,6 @@
|
||||
Formulaires création département
|
||||
"""
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField, BooleanField
|
||||
|
@ -36,6 +36,7 @@ CODES_SCODOC_TO_APO = {
|
||||
DEM: "NAR",
|
||||
NAR: "NAR",
|
||||
RAT: "ATT",
|
||||
"NOTES_FMT": "%3.2f",
|
||||
}
|
||||
|
||||
|
||||
@ -69,6 +70,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
"INSTITUTION_ADDRESS": str,
|
||||
"INSTITUTION_CITY": str,
|
||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||
"enable_entreprises": bool,
|
||||
}
|
||||
|
||||
def __init__(self, name, value):
|
||||
@ -156,32 +158,6 @@ class ScoDocSiteConfig(db.Model):
|
||||
class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
|
||||
return [("", "")] + class_list
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func(cls):
|
||||
"""Fonction bonus_sport ScoDoc 7 XXX
|
||||
Transitoire pour les tests durant la transition #sco92
|
||||
"""
|
||||
"""returns bonus func with specified name.
|
||||
If name not specified, return the configured function.
|
||||
None if no bonus function configured.
|
||||
Raises ScoValueError if func_name not found in module bonus_sport.
|
||||
"""
|
||||
from app.scodoc import bonus_sport
|
||||
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
if c is None:
|
||||
return None
|
||||
func_name = c.value
|
||||
if func_name == "": # pas de bonus défini
|
||||
return None
|
||||
try:
|
||||
return getattr(bonus_sport, func_name)
|
||||
except AttributeError:
|
||||
raise ScoValueError(
|
||||
f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}".
|
||||
(contacter votre administrateur local)."""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_code_apo(cls, code: str) -> str:
|
||||
"""La représentation d'un code pour les exports Apogée.
|
||||
@ -207,3 +183,27 @@ class ScoDocSiteConfig(db.Model):
|
||||
cfg.value = code_apo
|
||||
db.session.add(cfg)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def is_entreprises_enabled(cls) -> bool:
|
||||
"""True si on doit activer le module entreprise"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||
if (cfg is None) or not cfg.value:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def enable_entreprises(cls, enabled=True) -> bool:
|
||||
"""Active (ou déactive) le module entreprises. True si changement."""
|
||||
if enabled != ScoDocSiteConfig.is_entreprises_enabled():
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||
if cfg is None:
|
||||
cfg = ScoDocSiteConfig(
|
||||
name="enable_entreprises", value="on" if enabled else ""
|
||||
)
|
||||
else:
|
||||
cfg.value = "on" if enabled else ""
|
||||
db.session.add(cfg)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
@ -16,6 +16,7 @@ from app import models
|
||||
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc.sco_bac import Baccalaureat
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
@ -123,10 +124,18 @@ class Identite(db.Model):
|
||||
r.append("-".join([x.lower().capitalize() for x in fields]))
|
||||
return " ".join(r)
|
||||
|
||||
@property
|
||||
def nom_short(self):
|
||||
"Nom et début du prénom pour table recap: 'DUPONT Pi.'"
|
||||
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
|
||||
|
||||
@cached_property
|
||||
def sort_key(self) -> tuple:
|
||||
"clé pour tris par ordre alphabétique"
|
||||
return (self.nom_usuel or self.nom).lower(), self.prenom.lower()
|
||||
return (
|
||||
scu.suppress_accents(self.nom_usuel or self.nom or "").lower(),
|
||||
scu.suppress_accents(self.prenom or "").lower(),
|
||||
)
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"Le mail associé à la première adrese de l'étudiant, ou None"
|
||||
@ -145,28 +154,37 @@ class Identite(db.Model):
|
||||
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
"""Infos exportées dans les bulletins"""
|
||||
"""Infos exportées dans les bulletins
|
||||
L'étudiant, et sa première adresse.
|
||||
"""
|
||||
from app.scodoc import sco_photos
|
||||
|
||||
d = {
|
||||
"civilite": self.civilite,
|
||||
"code_ine": self.code_ine,
|
||||
"code_nip": self.code_nip,
|
||||
"date_naissance": self.date_naissance.isoformat()
|
||||
"code_ine": self.code_ine or "",
|
||||
"code_nip": self.code_nip or "",
|
||||
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||
if self.date_naissance
|
||||
else None,
|
||||
"email": self.get_first_email(),
|
||||
else "",
|
||||
"email": self.get_first_email() or "",
|
||||
"emailperso": self.get_first_email("emailperso"),
|
||||
"etudid": self.id,
|
||||
"nom": self.nom_disp(),
|
||||
"prenom": self.prenom,
|
||||
"nomprenom": self.nomprenom,
|
||||
"prenom": self.prenom or "",
|
||||
"nomprenom": self.nomprenom or "",
|
||||
"lieu_naissance": self.lieu_naissance or "",
|
||||
"dept_naissance": self.dept_naissance or "",
|
||||
"nationalite": self.nationalite or "",
|
||||
"boursier": self.boursier or "",
|
||||
}
|
||||
if include_urls:
|
||||
d["fiche_url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||
)
|
||||
d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),)
|
||||
d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
|
||||
adresse = self.adresses.first()
|
||||
if adresse:
|
||||
d.update(adresse.to_dict(convert_nulls_to_str=True))
|
||||
return d
|
||||
|
||||
def inscription_courante(self):
|
||||
@ -280,10 +298,10 @@ class Identite(db.Model):
|
||||
log(
|
||||
f"*** situation inconsistante pour {self} (inscrit mais pas d'event)"
|
||||
)
|
||||
date_ins = "???" # ???
|
||||
situation += " (inscription non enregistrée)" # ???
|
||||
else:
|
||||
date_ins = events[0].event_date
|
||||
situation += date_ins.strftime(" le %d/%m/%Y")
|
||||
situation += date_ins.strftime(" le %d/%m/%Y")
|
||||
else:
|
||||
situation = f"démission de {inscr.formsemestre.titre_mois()}"
|
||||
# Cherche la date de demission dans scolar_events:
|
||||
@ -337,7 +355,10 @@ def make_etud_args(
|
||||
"""
|
||||
args = None
|
||||
if etudid:
|
||||
args = {"etudid": etudid}
|
||||
try:
|
||||
args = {"etudid": int(etudid)}
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Adresse invalide") from exc
|
||||
elif code_nip:
|
||||
args = {"code_nip": code_nip}
|
||||
elif use_request: # use form from current request (Flask global)
|
||||
@ -347,12 +368,15 @@ def make_etud_args(
|
||||
vals = request.args
|
||||
else:
|
||||
vals = {}
|
||||
if "etudid" in vals:
|
||||
args = {"etudid": int(vals["etudid"])}
|
||||
elif "code_nip" in vals:
|
||||
args = {"code_nip": str(vals["code_nip"])}
|
||||
elif "code_ine" in vals:
|
||||
args = {"code_ine": str(vals["code_ine"])}
|
||||
try:
|
||||
if "etudid" in vals:
|
||||
args = {"etudid": int(vals["etudid"])}
|
||||
elif "code_nip" in vals:
|
||||
args = {"code_nip": str(vals["code_nip"])}
|
||||
elif "code_ine" in vals:
|
||||
args = {"code_ine": str(vals["code_ine"])}
|
||||
except ValueError:
|
||||
args = {}
|
||||
if not args:
|
||||
if abort_404:
|
||||
abort(404, "pas d'étudiant sélectionné")
|
||||
@ -388,6 +412,14 @@ class Adresse(db.Model):
|
||||
)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
def to_dict(self, convert_nulls_to_str=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
if convert_nulls_to_str:
|
||||
return {k: e[k] or "" for k in e}
|
||||
return e
|
||||
|
||||
|
||||
class Admission(db.Model):
|
||||
"""Informations liées à l'admission d'un étudiant"""
|
||||
|
@ -59,6 +59,10 @@ class Formation(db.Model):
|
||||
"""get l'instance de TypeParcours de cette formation"""
|
||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
||||
|
||||
def get_titre_version(self) -> str:
|
||||
"""Titre avec version"""
|
||||
return f"{self.acronyme} {self.titre} v{self.version}"
|
||||
|
||||
def is_apc(self):
|
||||
"True si formation APC avec SAE (BUT)"
|
||||
return self.get_parcours().APC_SAE
|
||||
|
@ -161,7 +161,6 @@ class FormSemestre(db.Model):
|
||||
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
|
||||
else:
|
||||
d["periode"] = 2 # typiquement, début en février: S2, S4...
|
||||
d["titre_num"] = self.titre_num()
|
||||
d["titreannee"] = self.titre_annee()
|
||||
d["mois_debut"] = self.mois_debut()
|
||||
d["mois_fin"] = self.mois_fin()
|
||||
@ -174,7 +173,6 @@ class FormSemestre(db.Model):
|
||||
d["session_id"] = self.session_id()
|
||||
d["etapes"] = self.etapes_apo_vdi()
|
||||
d["etapes_apo_str"] = self.etapes_apo_str()
|
||||
d["responsables"] = [u.id for u in self.responsables] # liste des ids
|
||||
return d
|
||||
|
||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||
@ -302,6 +300,10 @@ class FormSemestre(db.Model):
|
||||
else:
|
||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||
|
||||
def est_responsable(self, user):
|
||||
"True si l'user est l'un des responsables du semestre"
|
||||
return user.id in [u.id for u in self.responsables]
|
||||
|
||||
def annee_scolaire_str(self):
|
||||
"2021 - 2022"
|
||||
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
|
||||
@ -312,7 +314,7 @@ class FormSemestre(db.Model):
|
||||
|
||||
def mois_fin(self) -> str:
|
||||
"Jul 2022"
|
||||
return f"{MONTH_NAMES_ABBREV[self.date_fin.month - 1]} {self.date_debut.year}"
|
||||
return f"{MONTH_NAMES_ABBREV[self.date_fin.month - 1]} {self.date_fin.year}"
|
||||
|
||||
def session_id(self) -> str:
|
||||
"""identifiant externe de semestre de formation
|
||||
@ -399,7 +401,7 @@ class FormSemestre(db.Model):
|
||||
|
||||
@cached_property
|
||||
def etudids_actifs(self) -> set:
|
||||
"Set des etudids inscrits non démissionnaires"
|
||||
"Set des etudids inscrits non démissionnaires et non défaillants"
|
||||
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
|
||||
|
||||
@cached_property
|
||||
@ -578,6 +580,9 @@ class FormSemestreInscription(db.Model):
|
||||
# etape apogee d'inscription (experimental 2020)
|
||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={self.formsemestre_id} etat={self.etat}>"
|
||||
|
||||
|
||||
class NotesSemSet(db.Model):
|
||||
"""semsets: ensemble de formsemestres pour exports Apogée"""
|
||||
|
@ -134,7 +134,9 @@ class ModuleImplInscription(db.Model):
|
||||
def etud_modimpls_in_ue(
|
||||
cls, formsemestre_id: int, etudid: int, ue_id: int
|
||||
) -> flask_sqlalchemy.BaseQuery:
|
||||
"""moduleimpls de l'UE auxquels l'étudiant est inscrit"""
|
||||
"""moduleimpls de l'UE auxquels l'étudiant est inscrit.
|
||||
(Attention: inutile en APC, il faut considérer les coefficients)
|
||||
"""
|
||||
return ModuleImplInscription.query.filter(
|
||||
ModuleImplInscription.etudid == etudid,
|
||||
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
|
||||
|
@ -33,7 +33,7 @@ class Module(db.Model):
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
|
||||
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
||||
# Relations:
|
||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
||||
@ -76,6 +76,11 @@ class Module(db.Model):
|
||||
def type_name(self):
|
||||
return scu.MODULE_TYPE_NAMES[self.module_type]
|
||||
|
||||
def type_abbrv(self):
|
||||
""" "mod", "malus", "res", "sae"
|
||||
(utilisées pour style css)"""
|
||||
return scu.ModuleType.get_abbrev(self.module_type)
|
||||
|
||||
def set_ue_coef(self, ue, coef: float) -> None:
|
||||
"""Set coef module vers cette UE"""
|
||||
self.update_ue_coef_dict({ue.id: coef})
|
||||
|
@ -72,23 +72,20 @@ class NotesNotesLog(db.Model):
|
||||
|
||||
def etud_has_notes_attente(etudid, formsemestre_id):
|
||||
"""Vrai si cet etudiant a au moins une note en attente dans ce semestre.
|
||||
(ne compte que les notes en attente dans des évaluation avec coef. non nul).
|
||||
(ne compte que les notes en attente dans des évaluations avec coef. non nul).
|
||||
"""
|
||||
# XXX ancienne méthode de notes_table à ré-écrire
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT n.*
|
||||
cursor = db.session.execute(
|
||||
"""SELECT COUNT(*)
|
||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
|
||||
notes_moduleimpl_inscription i
|
||||
WHERE n.etudid = %(etudid)s
|
||||
and n.value = %(code_attente)s
|
||||
WHERE n.etudid = :etudid
|
||||
and n.value = :code_attente
|
||||
and n.evaluation_id = e.id
|
||||
and e.moduleimpl_id = m.id
|
||||
and m.formsemestre_id = %(formsemestre_id)s
|
||||
and m.formsemestre_id = :formsemestre_id
|
||||
and e.coefficient != 0
|
||||
and m.id = i.moduleimpl_id
|
||||
and i.etudid=%(etudid)s
|
||||
and i.etudid = :etudid
|
||||
""",
|
||||
{
|
||||
"formsemestre_id": formsemestre_id,
|
||||
@ -96,4 +93,4 @@ def etud_has_notes_attente(etudid, formsemestre_id):
|
||||
"code_attente": scu.NOTES_ATTENTE,
|
||||
},
|
||||
)
|
||||
return len(cursor.fetchall()) > 0
|
||||
return cursor.fetchone()[0] > 0
|
||||
|
@ -4,7 +4,6 @@
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
|
1
app/pe/__init__.py
Normal file
1
app/pe/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# empty but required for pylint
|
@ -47,7 +47,7 @@ import os
|
||||
from zipfile import ZipFile
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
|
||||
from app.scodoc.gen_tables import GenTable, SeqGenTable
|
||||
|
@ -38,11 +38,10 @@ Created on Fri Sep 9 09:15:05 2016
|
||||
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.pe import pe_tagtable
|
||||
@ -194,12 +193,14 @@ class SemestreTag(pe_tagtable.TableTag):
|
||||
return tagdict
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def comp_MoyennesTag(self, tag, force=False):
|
||||
"""Calcule et renvoie les "moyennes" de tous les étudiants du SemTag (non défaillants)
|
||||
à un tag donné, en prenant en compte
|
||||
def comp_MoyennesTag(self, tag, force=False) -> list:
|
||||
"""Calcule et renvoie les "moyennes" de tous les étudiants du SemTag
|
||||
(non défaillants) à un tag donné, en prenant en compte
|
||||
tous les modimpl_id concerné par le tag, leur coeff et leur pondération.
|
||||
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
|
||||
Renvoie les informations sous la forme d'une liste [ (moy, somme_coeff_normalise, etudid), ...]
|
||||
|
||||
Renvoie les informations sous la forme d'une liste
|
||||
[ (moy, somme_coeff_normalise, etudid), ...]
|
||||
"""
|
||||
lesMoyennes = []
|
||||
for etudid in self.get_etudids():
|
||||
|
@ -38,6 +38,7 @@ Created on Thu Sep 8 09:36:33 2016
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import numpy as np
|
||||
|
||||
from app.scodoc import notes_table
|
||||
|
||||
@ -287,48 +288,53 @@ class TableTag(object):
|
||||
|
||||
|
||||
# *********************************************
|
||||
def moyenne_ponderee_terme_a_terme(notes, coeffs=None, force=False):
|
||||
def moyenne_ponderee_terme_a_terme(notes, coefs=None, force=False):
|
||||
"""
|
||||
Calcule la moyenne pondérée d'une liste de notes avec d'éventuels coeffs de pondération.
|
||||
Renvoie le résultat sous forme d'un tuple (moy, somme_coeff)
|
||||
|
||||
La liste de notes contient soit : 1) des valeurs numériques 2) des strings "-NA-" (pas de notes) ou "-NI-" (pas inscrit)
|
||||
ou "-c-" ue capitalisée, 3) None.
|
||||
La liste de notes contient soit :
|
||||
1) des valeurs numériques
|
||||
2) des strings "-NA-" (pas de notes) ou "-NI-" (pas inscrit) ou "-c-" ue capitalisée,
|
||||
3) None.
|
||||
|
||||
Le paramètre force indique si le calcul de la moyenne doit être forcée ou non, c'est à
|
||||
dire s'il y a ou non omission des notes non numériques (auquel cas la moyenne est calculée sur les
|
||||
notes disponibles) ; sinon renvoie (None, None).
|
||||
dire s'il y a ou non omission des notes non numériques (auquel cas la moyenne est
|
||||
calculée sur les notes disponibles) ; sinon renvoie (None, None).
|
||||
"""
|
||||
# Vérification des paramètres d'entrée
|
||||
if not isinstance(notes, list) or (
|
||||
coeffs != None and not isinstance(coeffs, list) and len(coeffs) != len(notes)
|
||||
coefs != None and not isinstance(coefs, list) and len(coefs) != len(notes)
|
||||
):
|
||||
raise ValueError("Erreur de paramètres dans moyenne_ponderee_terme_a_terme")
|
||||
|
||||
# Récupération des valeurs des paramètres d'entrée
|
||||
coeffs = [1] * len(notes) if coeffs == None else coeffs
|
||||
coefs = [1] * len(notes) if coefs is None else coefs
|
||||
|
||||
# S'il n'y a pas de notes
|
||||
if not notes: # Si notes = []
|
||||
return (None, None)
|
||||
|
||||
notesValides = [
|
||||
(1 if isinstance(note, float) or isinstance(note, int) else 0) for note in notes
|
||||
] # Liste indiquant les notes valides
|
||||
if force == True or (
|
||||
force == False and sum(notesValides) == len(notes)
|
||||
): # Si on force le calcul de la moyenne ou qu'on ne le force pas et qu'on a le bon nombre de notes
|
||||
(moyenne, ponderation) = (0.0, 0.0)
|
||||
# Liste indiquant les notes valides
|
||||
notes_valides = [
|
||||
(isinstance(note, float) and not np.isnan(note)) or isinstance(note, int)
|
||||
for note in notes
|
||||
]
|
||||
# Si on force le calcul de la moyenne ou qu'on ne le force pas
|
||||
# et qu'on a le bon nombre de notes
|
||||
if force or sum(notes_valides) == len(notes):
|
||||
moyenne, ponderation = 0.0, 0.0
|
||||
for i in range(len(notes)):
|
||||
if notesValides[i]:
|
||||
moyenne += coeffs[i] * notes[i]
|
||||
ponderation += coeffs[i]
|
||||
if notes_valides[i]:
|
||||
moyenne += coefs[i] * notes[i]
|
||||
ponderation += coefs[i]
|
||||
return (
|
||||
(moyenne / (ponderation * 1.0), ponderation)
|
||||
if ponderation != 0
|
||||
else (None, 0)
|
||||
)
|
||||
else: # Si on ne force pas le calcul de la moyenne
|
||||
return (None, None)
|
||||
# Si on ne force pas le calcul de la moyenne
|
||||
return (None, None)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
|
@ -51,27 +51,34 @@ from app.pe import pe_avislatex
|
||||
def _pe_view_sem_recap_form(formsemestre_id):
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
|
||||
"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||
<p class="help">
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de poursuites d'études.
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
||||
poursuites d'études.
|
||||
<br/>
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener noreferrer">
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
||||
voir la documentation</a>.
|
||||
</p>
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form" enctype="multipart/form-data">
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
||||
enctype="multipart/form-data">
|
||||
<div class="pe_template_up">
|
||||
Les templates sont généralement installés sur le serveur ou dans le paramétrage de ScoDoc.<br/>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template (<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template: <input type="file" size="30" name="avis_tmpl_file"/></div>
|
||||
<div class="pe_template_upb">Pied de page: <input type="file" size="30" name="footer_tmpl_file"/></div>
|
||||
Les templates sont généralement installés sur le serveur ou dans le
|
||||
paramétrage de ScoDoc.
|
||||
<br/>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
||||
(<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template:
|
||||
<input type="file" size="30" name="avis_tmpl_file"/>
|
||||
</div>
|
||||
<div class="pe_template_upb">Pied de page:
|
||||
<input type="file" size="30" name="footer_tmpl_file"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="Générer les documents"/>
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
|
||||
</form>
|
||||
""".format(
|
||||
formsemestre_id=formsemestre_id
|
||||
),
|
||||
""",
|
||||
]
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
@ -120,7 +127,6 @@ def pe_view_sem_recap(
|
||||
# template fourni via le formulaire Web
|
||||
if footer_tmpl_file:
|
||||
footer_latex = footer_tmpl_file.read().decode("utf-8")
|
||||
footer_latex = footer_latex
|
||||
else:
|
||||
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
||||
formsemestre_id, champ="pe_avis_latex_footer"
|
||||
|
@ -293,6 +293,13 @@ class TF(object):
|
||||
% (val, field, descr["max_value"])
|
||||
)
|
||||
ok = 0
|
||||
if ok and (typ[:3] == "str") and "max_length" in descr:
|
||||
if len(self.values[field]) > descr["max_length"]:
|
||||
msg.append(
|
||||
"Le champ '%s' est trop long (max %d caractères)"
|
||||
% (field, descr["max_length"])
|
||||
)
|
||||
ok = 0
|
||||
|
||||
# allowed values
|
||||
if "allowed_values" in descr:
|
||||
|
@ -761,7 +761,7 @@ if __name__ == "__main__":
|
||||
doc = io.BytesIO()
|
||||
document = sco_pdf.BaseDocTemplate(doc)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScolarsPageTemplate(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
)
|
||||
)
|
||||
|
@ -105,7 +105,6 @@ _HTML_BEGIN = """<!DOCTYPE html>
|
||||
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
|
||||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
||||
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/sorttable.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){enableTooltips("gtrcontent")};
|
||||
@ -250,6 +249,9 @@ def sco_header(
|
||||
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
|
||||
)
|
||||
H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
|
||||
# H.append(
|
||||
# '<link href="/ScoDoc/static/css/tooltip.css" rel="stylesheet" type="text/css" />'
|
||||
# )
|
||||
# JS additionels
|
||||
for js in javascripts:
|
||||
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
|
||||
|
@ -51,8 +51,8 @@ from app.scodoc.sco_formsemestre import (
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
DEF,
|
||||
UE_SPORT,
|
||||
UE_is_fondamentale,
|
||||
UE_is_professionnelle,
|
||||
ue_is_fondamentale,
|
||||
ue_is_professionnelle,
|
||||
)
|
||||
from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation
|
||||
from app.scodoc import sco_codes_parcours
|
||||
@ -826,11 +826,11 @@ class NotesTable:
|
||||
and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
|
||||
):
|
||||
mu["ects_pot"] = ue["ects"] or 0.0
|
||||
if UE_is_fondamentale(ue["type"]):
|
||||
if ue_is_fondamentale(ue["type"]):
|
||||
mu["ects_pot_fond"] = mu["ects_pot"]
|
||||
else:
|
||||
mu["ects_pot_fond"] = 0.0
|
||||
if UE_is_professionnelle(ue["type"]):
|
||||
if ue_is_professionnelle(ue["type"]):
|
||||
mu["ects_pot_pro"] = mu["ects_pot"]
|
||||
else:
|
||||
mu["ects_pot_pro"] = 0.0
|
||||
|
@ -53,7 +53,11 @@ def _isFarFutur(jour):
|
||||
# check si jour est dans le futur "lointain"
|
||||
# pour autoriser les saisies dans le futur mais pas a plus de 6 mois
|
||||
y, m, d = [int(x) for x in jour.split("-")]
|
||||
j = datetime.date(y, m, d)
|
||||
try:
|
||||
j = datetime.date(y, m, d)
|
||||
except ValueError:
|
||||
# les dates erronées, genre année 20022, sont considéres dans le futur
|
||||
return True
|
||||
# 6 mois ~ 182 jours:
|
||||
return j - datetime.date.today() > datetime.timedelta(182)
|
||||
|
||||
@ -225,8 +229,11 @@ def DateRangeISO(date_beg, date_end, workable=1):
|
||||
date_end = date_beg
|
||||
r = []
|
||||
work_saturday = is_work_saturday()
|
||||
cur = ddmmyyyy(date_beg, work_saturday=work_saturday)
|
||||
end = ddmmyyyy(date_end, work_saturday=work_saturday)
|
||||
try:
|
||||
cur = ddmmyyyy(date_beg, work_saturday=work_saturday)
|
||||
end = ddmmyyyy(date_end, work_saturday=work_saturday)
|
||||
except (AttributeError, ValueError) as e:
|
||||
raise ScoValueError("date invalide !") from e
|
||||
while cur <= end:
|
||||
if (not workable) or cur.iswork():
|
||||
r.append(cur)
|
||||
@ -631,7 +638,7 @@ def add_absence(
|
||||
):
|
||||
"Ajoute une absence dans la bd"
|
||||
if _isFarFutur(jour):
|
||||
raise ScoValueError("date absence trop loin dans le futur !")
|
||||
raise ScoValueError("date absence erronée ou trop loin dans le futur !")
|
||||
estjust = _toboolean(estjust)
|
||||
matin = _toboolean(matin)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
@ -254,7 +254,7 @@ def abs_notification_message(
|
||||
values["nbabsjust"] = nbabsjust
|
||||
values["nbabsnonjust"] = nbabs - nbabsjust
|
||||
values["url_ficheetud"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
|
||||
)
|
||||
|
||||
template = prefs["abs_notification_mail_tmpl"]
|
||||
|
@ -34,7 +34,7 @@ from flask import url_for, g, request, abort
|
||||
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Identite, FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import notesdb as ndb
|
||||
@ -724,6 +724,7 @@ def CalAbs(etudid, sco_year=None):
|
||||
anneescolaire = int(scu.AnneeScolaire(sco_year))
|
||||
datedebut = str(anneescolaire) + "-08-01"
|
||||
datefin = str(anneescolaire + 1) + "-07-31"
|
||||
annee_courante = scu.AnneeScolaire()
|
||||
nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin)
|
||||
nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin)
|
||||
events = []
|
||||
@ -776,7 +777,7 @@ def CalAbs(etudid, sco_year=None):
|
||||
"""Année scolaire %s-%s""" % (anneescolaire, anneescolaire + 1),
|
||||
""" Changer année: <select name="sco_year" onchange="document.f.submit()">""",
|
||||
]
|
||||
for y in range(anneescolaire, anneescolaire - 10, -1):
|
||||
for y in range(annee_courante, min(annee_courante - 6, anneescolaire - 6), -1):
|
||||
H.append("""<option value="%s" """ % y)
|
||||
if y == anneescolaire:
|
||||
H.append("selected")
|
||||
|
@ -83,6 +83,7 @@ XXX A vérifier:
|
||||
import collections
|
||||
import datetime
|
||||
from functools import reduce
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
import pprint
|
||||
@ -97,7 +98,7 @@ from chardet import detect as chardet_detect
|
||||
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -125,7 +126,7 @@ APO_SEP = "\t"
|
||||
APO_NEWLINE = "\r\n"
|
||||
|
||||
|
||||
def _apo_fmt_note(note):
|
||||
def _apo_fmt_note(note, fmt="%3.2f"):
|
||||
"Formatte une note pour Apogée (séparateur décimal: ',')"
|
||||
# if not note and isinstance(note, float): changé le 31/1/2022, étrange ?
|
||||
# return ""
|
||||
@ -133,7 +134,7 @@ def _apo_fmt_note(note):
|
||||
val = float(note)
|
||||
except ValueError:
|
||||
return ""
|
||||
return ("%3.2f" % val).replace(".", APO_DECIMAL_SEP)
|
||||
return (fmt % val).replace(".", APO_DECIMAL_SEP)
|
||||
|
||||
|
||||
def guess_data_encoding(text, threshold=0.6):
|
||||
@ -270,6 +271,9 @@ class ApoEtud(dict):
|
||||
self.export_res_modules = export_res_modules
|
||||
self.export_res_sdj = export_res_sdj # export meme si pas de decision de jury
|
||||
self.export_res_rat = export_res_rat
|
||||
self.fmt_note = functools.partial(
|
||||
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"])
|
||||
@ -423,7 +427,7 @@ class ApoEtud(dict):
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
return dict(
|
||||
N=_apo_fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
||||
@ -443,7 +447,7 @@ class ApoEtud(dict):
|
||||
].split(","):
|
||||
n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||
if n != "NI" and self.export_res_modules:
|
||||
return dict(N=_apo_fmt_note(n), B=20, J="", R="")
|
||||
return dict(N=self.fmt_note(n), B=20, J="", R="")
|
||||
else:
|
||||
module_code_found = True
|
||||
if module_code_found:
|
||||
@ -465,7 +469,7 @@ class ApoEtud(dict):
|
||||
if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF:
|
||||
note_str = "0,01" # note non nulle pour les démissionnaires
|
||||
else:
|
||||
note_str = _apo_fmt_note(note)
|
||||
note_str = self.fmt_note(note)
|
||||
return dict(N=note_str, B=20, J="", R=decision_apo, M="")
|
||||
|
||||
def comp_elt_annuel(self, etudid, cur_sem, autre_sem):
|
||||
@ -531,7 +535,7 @@ class ApoEtud(dict):
|
||||
moy_annuelle = (note + autre_note) / 2
|
||||
except TypeError:
|
||||
moy_annuelle = ""
|
||||
note_str = _apo_fmt_note(moy_annuelle)
|
||||
note_str = self.fmt_note(moy_annuelle)
|
||||
|
||||
if code_semestre_validant(autre_decision["code"]):
|
||||
decision_apo_annuelle = decision_apo
|
||||
|
@ -32,14 +32,14 @@ import email
|
||||
import time
|
||||
|
||||
from flask import g, request
|
||||
from flask import render_template, url_for
|
||||
from flask import flash, render_template, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import email
|
||||
from app import log
|
||||
from app.but import bulletin_but
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Identite, ModuleImplInscription
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
@ -791,7 +791,7 @@ def etud_descr_situation_semestre(
|
||||
|
||||
# ------ Page bulletin
|
||||
def formsemestre_bulletinetud(
|
||||
etudid=None,
|
||||
etud: Identite = None,
|
||||
formsemestre_id=None,
|
||||
format=None,
|
||||
version="long",
|
||||
@ -801,14 +801,13 @@ def formsemestre_bulletinetud(
|
||||
):
|
||||
"page bulletin de notes"
|
||||
format = format or "html"
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
if not formsemestre:
|
||||
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
|
||||
|
||||
bulletin = do_formsemestre_bulletinetud(
|
||||
formsemestre,
|
||||
etudid,
|
||||
etud.id,
|
||||
format=format,
|
||||
version=version,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
@ -818,12 +817,15 @@ def formsemestre_bulletinetud(
|
||||
if format not in {"html", "pdfmail"}:
|
||||
filename = scu.bul_filename(formsemestre, etud, format)
|
||||
return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
|
||||
|
||||
elif format == "pdfmail":
|
||||
return ""
|
||||
H = [
|
||||
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
|
||||
bulletin,
|
||||
render_template(
|
||||
"bul_foot.html",
|
||||
appreciations=None, # déjà affichées
|
||||
css_class="bul_classic_foot",
|
||||
etud=etud,
|
||||
formsemestre=formsemestre,
|
||||
inscription_courante=etud.inscription_courante(),
|
||||
@ -850,7 +852,6 @@ def do_formsemestre_bulletinetud(
|
||||
etudid: int,
|
||||
version="long", # short, long, selectedevals
|
||||
format=None,
|
||||
nohtml=False,
|
||||
xml_with_decisions=False, # force décisions dans XML
|
||||
force_publishing=False, # force publication meme si semestre non publié sur "portail"
|
||||
prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide
|
||||
@ -883,12 +884,12 @@ def do_formsemestre_bulletinetud(
|
||||
return bul, ""
|
||||
|
||||
if formsemestre.formation.is_apc():
|
||||
etud = Identite.query.get(etudid)
|
||||
etudiant = Identite.query.get(etudid)
|
||||
r = bulletin_but.BulletinBUT(formsemestre)
|
||||
I = r.bulletin_etud_complet(etud, version=version)
|
||||
I = r.bulletin_etud_complet(etudiant, version=version)
|
||||
else:
|
||||
I = formsemestre_bulletinetud_dict(formsemestre.id, etudid)
|
||||
etud = I["etud"]
|
||||
etud = I["etud"]
|
||||
|
||||
if format == "html":
|
||||
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
||||
@ -917,13 +918,6 @@ def do_formsemestre_bulletinetud(
|
||||
if not can_send_bulletin_by_mail(formsemestre.id):
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
|
||||
if nohtml:
|
||||
htm = "" # speed up if html version not needed
|
||||
else:
|
||||
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
||||
I, version=version, format="html"
|
||||
)
|
||||
|
||||
pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
||||
I, version=version, format="pdf"
|
||||
)
|
||||
@ -931,31 +925,18 @@ def do_formsemestre_bulletinetud(
|
||||
if prefer_mail_perso:
|
||||
recipient_addr = etud.get("emailperso", "") or etud.get("email", "")
|
||||
else:
|
||||
recipient_addr = etud["email_default"]
|
||||
recipient_addr = etud.get("email", "") or etud.get("emailperso", "")
|
||||
|
||||
if not recipient_addr:
|
||||
if nohtml:
|
||||
h = "" # permet de compter les non-envois
|
||||
else:
|
||||
h = (
|
||||
"<div class=\"boldredmsg\">%s n'a pas d'adresse e-mail !</div>"
|
||||
% etud["nomprenom"]
|
||||
) + htm
|
||||
return h, I["filigranne"]
|
||||
#
|
||||
mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr)
|
||||
emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % (
|
||||
recipient_addr,
|
||||
recipient_addr,
|
||||
)
|
||||
return (
|
||||
('<div class="head_message">Message mail envoyé à %s</div>' % (emaillink))
|
||||
+ htm,
|
||||
I["filigranne"],
|
||||
)
|
||||
flash(f"{etud['nomprenom']} n'a pas d'adresse e-mail !")
|
||||
return False, I["filigranne"]
|
||||
else:
|
||||
mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr)
|
||||
flash(f"mail envoyé à {recipient_addr}")
|
||||
|
||||
else:
|
||||
raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format)
|
||||
return True, I["filigranne"]
|
||||
|
||||
raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format)
|
||||
|
||||
|
||||
def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
|
||||
|
@ -49,7 +49,14 @@ import traceback
|
||||
|
||||
|
||||
import reportlab
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate,
|
||||
DocIf,
|
||||
Paragraph,
|
||||
Spacer,
|
||||
Frame,
|
||||
PageBreak,
|
||||
)
|
||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||
|
||||
from flask import request
|
||||
@ -72,6 +79,7 @@ class BulletinGenerator:
|
||||
description = "superclass for bulletins" # description for user interface
|
||||
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
|
||||
scale_table_in_page = True # rescale la table sur 1 page
|
||||
multi_pages = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -154,30 +162,47 @@ class BulletinGenerator:
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
formsemestre_id = self.infos["formsemestre_id"]
|
||||
|
||||
marque_debut_bulletin = sco_pdf.DebutBulletin(
|
||||
self.infos["etud"]["nomprenom"],
|
||||
filigranne=self.infos["filigranne"],
|
||||
footer_content=f"""ScoDoc - Bulletin de {self.infos["etud"]["nomprenom"]} - {time.strftime("%d/%m/%Y %H:%M")}""",
|
||||
)
|
||||
story = []
|
||||
# partie haute du bulletin
|
||||
objects = self.bul_title_pdf() # pylint: disable=no-member
|
||||
# table des notes
|
||||
objects += self.bul_table(format="pdf") # pylint: disable=no-member
|
||||
# infos sous la table
|
||||
objects += self.bul_part_below(format="pdf") # pylint: disable=no-member
|
||||
# signatures
|
||||
objects += self.bul_signatures_pdf() # pylint: disable=no-member
|
||||
story += self.bul_title_pdf() # pylint: disable=no-member
|
||||
index_obj_debut = len(story)
|
||||
|
||||
# table des notes
|
||||
story += self.bul_table(format="pdf") # pylint: disable=no-member
|
||||
# infos sous la table
|
||||
story += self.bul_part_below(format="pdf") # pylint: disable=no-member
|
||||
# signatures
|
||||
story += self.bul_signatures_pdf() # pylint: disable=no-member
|
||||
if self.scale_table_in_page:
|
||||
# Réduit sur une page
|
||||
objects = [KeepInFrame(0, 0, objects, mode="shrink")]
|
||||
story = [marque_debut_bulletin, KeepInFrame(0, 0, story, mode="shrink")]
|
||||
else:
|
||||
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
|
||||
story.insert(index_obj_debut, marque_debut_bulletin)
|
||||
#
|
||||
# objects.append(sco_pdf.FinBulletin())
|
||||
if not stand_alone:
|
||||
objects.append(PageBreak()) # insert page break at end
|
||||
return objects
|
||||
if self.multi_pages:
|
||||
# Bulletins sur plusieurs page, force début suivant sur page impaire
|
||||
story.append(
|
||||
DocIf("doc.page%2 == 1", [PageBreak(), PageBreak()], [PageBreak()])
|
||||
)
|
||||
else:
|
||||
story.append(PageBreak()) # insert page break at end
|
||||
|
||||
return story
|
||||
else:
|
||||
# Generation du document PDF
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
report = io.BytesIO() # in-memory document, no disk file
|
||||
document = sco_pdf.BaseDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScolarsPageTemplate(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
author="%s %s (E. Viennet) [%s]"
|
||||
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
|
||||
@ -190,7 +215,7 @@ class BulletinGenerator:
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
document.build(story)
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
@ -241,10 +266,15 @@ def make_formsemestre_bulletinetud(
|
||||
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
|
||||
|
||||
gen_class = None
|
||||
if infos.get("type") == "BUT" and format.startswith("pdf"):
|
||||
gen_class = bulletin_get_class(bul_class_name + "BUT")
|
||||
if gen_class is None:
|
||||
gen_class = bulletin_get_class(bul_class_name)
|
||||
for bul_class_name in (
|
||||
sco_preferences.get_preference("bul_class_name", formsemestre_id),
|
||||
# si pas trouvé (modifs locales bizarres ,), ré-essaye avec la valeur par défaut
|
||||
bulletin_default_class_name(),
|
||||
):
|
||||
if infos.get("type") == "BUT" and format.startswith("pdf"):
|
||||
gen_class = bulletin_get_class(bul_class_name + "BUT")
|
||||
if gen_class is None:
|
||||
gen_class = bulletin_get_class(bul_class_name)
|
||||
|
||||
if gen_class is None:
|
||||
raise ValueError(
|
||||
|
@ -33,7 +33,7 @@ import json
|
||||
|
||||
from app.but import bulletin_but
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
|
@ -34,17 +34,19 @@
|
||||
CE FORMAT N'EVOLUERA PLUS ET EST CONSIDERE COMME OBSOLETE.
|
||||
|
||||
"""
|
||||
from reportlab.lib.colors import Color, blue
|
||||
from reportlab.lib.units import cm, mm
|
||||
from reportlab.platypus import Paragraph, Spacer, Table
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc.sco_pdf import Color, Paragraph, Spacer, Table
|
||||
from app.scodoc.sco_pdf import blue, cm, mm
|
||||
from app.scodoc.sco_pdf import SU
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_bulletins_generator
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc.sco_pdf import SU
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
|
||||
class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
||||
|
@ -51,12 +51,11 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
|
||||
|
||||
"""
|
||||
import io
|
||||
import pprint
|
||||
import pydoc
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
from pydoc import html
|
||||
|
||||
from reportlab.platypus.doctemplate import BaseDocTemplate
|
||||
|
||||
from flask import g, request
|
||||
|
||||
@ -74,17 +73,17 @@ import app.scodoc.sco_utils as scu
|
||||
import sco_version
|
||||
|
||||
|
||||
def pdfassemblebulletins(
|
||||
formsemestre_id,
|
||||
objects,
|
||||
bul_title,
|
||||
def assemble_bulletins_pdf(
|
||||
formsemestre_id: int,
|
||||
story: list,
|
||||
bul_title: str,
|
||||
infos,
|
||||
pagesbookmarks,
|
||||
pagesbookmarks=None,
|
||||
filigranne=None,
|
||||
server_name="",
|
||||
):
|
||||
"generate PDF document from a list of PLATYPUS objects"
|
||||
if not objects:
|
||||
"Generate PDF document from a story (list of PLATYPUS objects)."
|
||||
if not story:
|
||||
return ""
|
||||
# Paramètres de mise en page
|
||||
margins = (
|
||||
@ -93,11 +92,10 @@ def pdfassemblebulletins(
|
||||
sco_preferences.get_preference("right_margin", formsemestre_id),
|
||||
sco_preferences.get_preference("bottom_margin", formsemestre_id),
|
||||
)
|
||||
|
||||
report = io.BytesIO() # in-memory document, no disk file
|
||||
document = BaseDocTemplate(report)
|
||||
document = sco_pdf.BulletinDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScolarsPageTemplate(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION),
|
||||
title="Bulletin %s" % bul_title,
|
||||
@ -109,7 +107,7 @@ def pdfassemblebulletins(
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
document.multiBuild(story)
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
@ -121,7 +119,8 @@ def replacement_function(match):
|
||||
if logo is not None:
|
||||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||
raise ScoValueError(
|
||||
'balise "%s": logo "%s" introuvable' % (html.escape(balise), html.escape(name))
|
||||
'balise "%s": logo "%s" introuvable'
|
||||
% (pydoc.html.escape(balise), pydoc.html.escape(name))
|
||||
)
|
||||
|
||||
|
||||
@ -142,7 +141,11 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
||||
cdict
|
||||
) # note that None values are mapped to empty strings
|
||||
except:
|
||||
log("process_field: invalid format=%s" % field)
|
||||
log(
|
||||
f"""process_field: invalid format. field={field!r}
|
||||
values={pprint.pformat(cdict)}
|
||||
"""
|
||||
)
|
||||
text = (
|
||||
"<para><i>format invalide !</i></para><para>"
|
||||
+ traceback.format_exc()
|
||||
@ -174,7 +177,7 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
||||
|
||||
|
||||
def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
||||
"document pdf et filename"
|
||||
"Document pdf avec tous les bulletins du semestre, et filename"
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
cached = sco_cache.SemBulletinsPDFCache.get(str(formsemestre_id) + "_" + version)
|
||||
@ -183,20 +186,14 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
||||
fragments = []
|
||||
# Make each bulletin
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
bookmarks = {}
|
||||
filigrannes = {}
|
||||
i = 1
|
||||
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
|
||||
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
|
||||
frag, _ = sco_bulletins.do_formsemestre_bulletinetud(
|
||||
formsemestre,
|
||||
etud.id,
|
||||
format="pdfpart",
|
||||
version=version,
|
||||
)
|
||||
fragments += frag
|
||||
filigrannes[i] = filigranne
|
||||
bookmarks[i] = etud.sex_nom(no_accents=True)
|
||||
i = i + 1
|
||||
#
|
||||
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
|
||||
if request:
|
||||
@ -205,20 +202,18 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
||||
server_name = ""
|
||||
try:
|
||||
sco_pdf.PDFLOCK.acquire()
|
||||
pdfdoc = pdfassemblebulletins(
|
||||
pdfdoc = assemble_bulletins_pdf(
|
||||
formsemestre_id,
|
||||
fragments,
|
||||
formsemestre.titre_mois(),
|
||||
infos,
|
||||
bookmarks,
|
||||
filigranne=filigrannes,
|
||||
server_name=server_name,
|
||||
)
|
||||
finally:
|
||||
sco_pdf.PDFLOCK.release()
|
||||
#
|
||||
dt = time.strftime("%Y-%m-%d")
|
||||
filename = "bul-%s-%s.pdf" % (formsemestre.titre_num(), dt)
|
||||
date_iso = time.strftime("%Y-%m-%d")
|
||||
filename = "bul-%s-%s.pdf" % (formsemestre.titre_num(), date_iso)
|
||||
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
|
||||
# fill cache
|
||||
sco_cache.SemBulletinsPDFCache.set(
|
||||
@ -255,7 +250,7 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
||||
server_name = ""
|
||||
try:
|
||||
sco_pdf.PDFLOCK.acquire()
|
||||
pdfdoc = pdfassemblebulletins(
|
||||
pdfdoc = assemble_bulletins_pdf(
|
||||
None,
|
||||
fragments,
|
||||
etud["nomprenom"],
|
||||
|
@ -49,6 +49,8 @@ Balises img: actuellement interdites.
|
||||
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table
|
||||
from reportlab.lib.units import cm, mm
|
||||
from reportlab.lib.colors import Color, blue
|
||||
from app.models import FormSemestre
|
||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_pdf import SU
|
||||
@ -73,7 +75,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc
|
||||
supported_formats = ["html", "pdf"]
|
||||
|
||||
def bul_title_pdf(self):
|
||||
def bul_title_pdf(self) -> list:
|
||||
"""Génère la partie "titre" du bulletin de notes.
|
||||
Renvoie une liste d'objets platypus
|
||||
"""
|
||||
@ -115,11 +117,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
- en PDF: une liste d'objets platypus
|
||||
"""
|
||||
H = [] # html
|
||||
Op = [] # objets platypus
|
||||
story = [] # objets platypus
|
||||
# ----- ABSENCES
|
||||
if self.preferences["bul_show_abs"]:
|
||||
nbabs = self.infos["nbabs"]
|
||||
Op.append(Spacer(1, 2 * mm))
|
||||
story.append(Spacer(1, 2 * mm))
|
||||
if nbabs:
|
||||
H.append(
|
||||
"""<p class="bul_abs">
|
||||
@ -130,7 +132,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
"""
|
||||
% self.infos
|
||||
)
|
||||
Op.append(
|
||||
story.append(
|
||||
Paragraph(
|
||||
SU(
|
||||
"%(nbabs)s absences (1/2 journées), dont %(nbabsjust)s justifiées."
|
||||
@ -141,7 +143,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
)
|
||||
else:
|
||||
H.append("""<p class="bul_abs">Pas d'absences signalées.</p>""")
|
||||
Op.append(Paragraph(SU("Pas d'absences signalées."), self.CellStyle))
|
||||
story.append(Paragraph(SU("Pas d'absences signalées."), self.CellStyle))
|
||||
|
||||
# ---- APPRECIATIONS
|
||||
# le dir. des etud peut ajouter des appreciations,
|
||||
@ -168,10 +170,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
% self.infos
|
||||
)
|
||||
H.append("</div>")
|
||||
# Appreciations sur PDF:
|
||||
# Appréciations sur PDF:
|
||||
if self.infos.get("appreciations_list", False):
|
||||
Op.append(Spacer(1, 3 * mm))
|
||||
Op.append(
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
story.append(
|
||||
Paragraph(
|
||||
SU("Appréciation : " + "\n".join(self.infos["appreciations_txt"])),
|
||||
self.CellStyle,
|
||||
@ -180,7 +182,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
|
||||
# ----- DECISION JURY
|
||||
if self.preferences["bul_show_decision"]:
|
||||
Op += sco_bulletins_pdf.process_field(
|
||||
story += sco_bulletins_pdf.process_field(
|
||||
self.preferences["bul_pdf_caption"],
|
||||
self.infos,
|
||||
self.FieldStyle,
|
||||
@ -196,7 +198,12 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
|
||||
# -----
|
||||
if format == "pdf":
|
||||
return [KeepTogether(Op)]
|
||||
if self.scale_table_in_page:
|
||||
# le scaling (pour tenir sur une page) semble incompatible avec
|
||||
# le KeepTogether()
|
||||
return story
|
||||
else:
|
||||
return [KeepTogether(story)]
|
||||
elif format == "html":
|
||||
return "\n".join(H)
|
||||
|
||||
@ -265,7 +272,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
)
|
||||
|
||||
def build_bulletin_table(self):
|
||||
"""Génère la table centrale du bulletin de notes
|
||||
"""Génère la table centrale du bulletin de notes classique (pas BUT)
|
||||
Renvoie: col_keys, P, pdf_style, col_widths
|
||||
- col_keys: nom des colonnes de la table (clés)
|
||||
- table: liste de dicts de chaines de caractères
|
||||
@ -375,10 +382,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
t = {
|
||||
"titre": "Moyenne générale:",
|
||||
"rang": I["rang_nt"],
|
||||
"note": I["moy_gen"],
|
||||
"min": I["moy_min"],
|
||||
"max": I["moy_max"],
|
||||
"moy": I["moy_moy"],
|
||||
"note": I.get("moy_gen", "-"),
|
||||
"min": I.get("moy_min", "-"),
|
||||
"max": I.get("moy_max", "-"),
|
||||
"moy": I.get("moy_moy", "-"),
|
||||
"abs": "%s / %s" % (nbabs, nbabsjust),
|
||||
"_css_row_class": "notes_bulletin_row_gen",
|
||||
"_titre_colspan": 2,
|
||||
@ -412,6 +419,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
for ue in I["ues"]:
|
||||
ue_type = None
|
||||
coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else ""
|
||||
|
||||
ue_descr = ue["ue_descr_txt"]
|
||||
rowstyle = ""
|
||||
plusminus = minuslink #
|
||||
|
@ -32,16 +32,12 @@ On redéfini la table centrale du bulletin de note et hérite de tout le reste d
|
||||
|
||||
E. Viennet, juillet 2011
|
||||
"""
|
||||
from reportlab.lib.colors import Color
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_pdf import blue, cm, mm
|
||||
from app.scodoc.sco_pdf import Color, Paragraph, Spacer, Table
|
||||
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
from app.scodoc import sco_bulletins_generator
|
||||
from app.scodoc import sco_bulletins_standard
|
||||
from app.scodoc import gen_tables
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class BulletinGeneratorUCAC(sco_bulletins_standard.BulletinGeneratorStandard):
|
||||
|
@ -45,7 +45,7 @@ from xml.etree import ElementTree
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
|
@ -49,7 +49,6 @@
|
||||
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
|
||||
#
|
||||
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from flask import g
|
||||
@ -198,6 +197,26 @@ class SemInscriptionsCache(ScoDocCache):
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableRecapCache(ScoDocCache):
|
||||
"""Cache table recap (pour get_table_recap)
|
||||
Clé: formsemestre_id
|
||||
Valeur: le html (<div class="table_recap">...</div>)
|
||||
"""
|
||||
|
||||
prefix = "RECAP"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableRecapWithEvalsCache(ScoDocCache):
|
||||
"""Cache table recap (pour get_table_recap)
|
||||
Clé: formsemestre_id
|
||||
Valeur: le html (<div class="table_recap">...</div>)
|
||||
"""
|
||||
|
||||
prefix = "RECAPWITHEVALS"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
|
||||
formsemestre_id=None, pdfonly=False
|
||||
):
|
||||
@ -209,7 +228,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
if getattr(g, "defer_cache_invalidation", False):
|
||||
g.sem_to_invalidate.add(formsemestre_id)
|
||||
return
|
||||
log("inval_cache, formsemestre_id=%s pdfonly=%s" % (formsemestre_id, pdfonly))
|
||||
log("inval_cache, formsemestre_id={formsemestre_id} pdfonly={pdfonly}")
|
||||
if formsemestre_id is None:
|
||||
# clear all caches
|
||||
log("----- invalidate_formsemestre: clearing all caches -----")
|
||||
@ -247,6 +266,9 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
||||
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
||||
TableRecapCache.delete_many(formsemestre_ids)
|
||||
TableRecapWithEvalsCache.delete_many(formsemestre_ids)
|
||||
|
||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||
|
||||
|
||||
|
@ -81,11 +81,11 @@ UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
||||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||
|
||||
|
||||
def UE_is_fondamentale(ue_type):
|
||||
def ue_is_fondamentale(ue_type):
|
||||
return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
|
||||
|
||||
|
||||
def UE_is_professionnelle(ue_type):
|
||||
def ue_is_professionnelle(ue_type):
|
||||
return (
|
||||
ue_type == UE_PROFESSIONNELLE
|
||||
) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
|
||||
@ -211,7 +211,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
|
||||
|
||||
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
||||
|
||||
# Regles gestion parcours
|
||||
# Règles gestion parcours
|
||||
class DUTRule(object):
|
||||
def __init__(self, rule_id, premise, conclusion):
|
||||
self.rule_id = rule_id
|
||||
@ -222,13 +222,13 @@ class DUTRule(object):
|
||||
def match(self, state):
|
||||
"True if state match rule premise"
|
||||
assert len(state) == len(self.premise)
|
||||
for i in range(len(state)):
|
||||
for i, stat in enumerate(state):
|
||||
prem = self.premise[i]
|
||||
if isinstance(prem, (list, tuple)):
|
||||
if not state[i] in prem:
|
||||
if not stat in prem:
|
||||
return False
|
||||
else:
|
||||
if prem != ALL and prem != state[i]:
|
||||
if prem not in (ALL, stat):
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -244,6 +244,7 @@ class TypeParcours(object):
|
||||
COMPENSATION_UE = True # inutilisé
|
||||
BARRE_MOY = 10.0
|
||||
BARRE_UE_DEFAULT = 8.0
|
||||
BARRE_UE_DISPLAY_WARNING = 8.0
|
||||
BARRE_UE = {}
|
||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||
NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE # barre sur UE
|
||||
|
@ -46,7 +46,9 @@ CONFIG.LOGO_HEADER_HEIGHT = 28
|
||||
#
|
||||
# server_url: URL du serveur ScoDoc
|
||||
# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir sco_version.py)
|
||||
CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s"
|
||||
CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = (
|
||||
"Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s"
|
||||
)
|
||||
|
||||
|
||||
# ------------- Capitalisation des UEs -------------
|
||||
|
@ -33,7 +33,7 @@ from flask import url_for, g, request
|
||||
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -56,7 +56,7 @@ def index_html(showcodes=0, showsemtable=0):
|
||||
H.append(sco_news.scolar_news_summary_html())
|
||||
|
||||
# Avertissement de mise à jour:
|
||||
H.append(sco_up_to_date.html_up_to_date_box())
|
||||
H.append("""<div id="update_warning"></div>""")
|
||||
|
||||
# Liste de toutes les sessions:
|
||||
sems = sco_formsemestre.do_formsemestre_list()
|
||||
@ -293,7 +293,6 @@ def delete_dept(dept_id: int):
|
||||
"create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s",
|
||||
"create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
|
||||
"create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s",
|
||||
"create temp table entreprises_temp as select id from entreprises where dept_id = %(dept_id)s",
|
||||
"create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s",
|
||||
]
|
||||
for r in reqs:
|
||||
@ -345,13 +344,9 @@ def delete_dept(dept_id: int):
|
||||
"delete from notes_formsemestre where dept_id = %(dept_id)s",
|
||||
"delete from scolar_news where dept_id = %(dept_id)s",
|
||||
"delete from notes_semset where dept_id = %(dept_id)s",
|
||||
"delete from entreprise_contact where entreprise_id in (select id from entreprises_temp) ",
|
||||
"delete from entreprise_correspondant where entreprise_id in (select id from entreprises_temp) ",
|
||||
"delete from entreprises where dept_id = %(dept_id)s",
|
||||
"delete from notes_formations where dept_id = %(dept_id)s",
|
||||
"delete from departement where id = %(dept_id)s",
|
||||
"drop table tags_temp",
|
||||
"drop table entreprises_temp",
|
||||
"drop table formations_temp",
|
||||
"drop table moduleimpls_temp",
|
||||
"drop table etudids_temp",
|
||||
|
@ -51,14 +51,12 @@ import fcntl
|
||||
import subprocess
|
||||
import requests
|
||||
|
||||
from flask import flash
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
import sco_version
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
@ -66,10 +64,9 @@ from app.scodoc.sco_exceptions import ScoValueError
|
||||
SCO_DUMP_LOCK = "/tmp/scodump.lock"
|
||||
|
||||
|
||||
def sco_dump_and_send_db():
|
||||
def sco_dump_and_send_db(message: str = "", request_url: str = ""):
|
||||
"""Dump base de données et l'envoie anonymisée pour debug"""
|
||||
H = [html_sco_header.sco_header(page_title="Assistance technique")]
|
||||
# get currect (dept) DB name:
|
||||
# get current (dept) DB name:
|
||||
cursor = ndb.SimpleQuery("SELECT current_database()", {})
|
||||
db_name = cursor.fetchone()[0]
|
||||
ano_db_name = "ANO" + db_name
|
||||
@ -95,28 +92,8 @@ def sco_dump_and_send_db():
|
||||
_anonymize_db(ano_db_name)
|
||||
|
||||
# Send
|
||||
r = _send_db(ano_db_name)
|
||||
if (
|
||||
r.status_code
|
||||
== requests.codes.INSUFFICIENT_STORAGE # pylint: disable=no-member
|
||||
):
|
||||
H.append(
|
||||
"""<p class="warning">
|
||||
Erreur: espace serveur trop plein.
|
||||
Merci de contacter <a href="mailto:{0}">{0}</a></p>""".format(
|
||||
scu.SCO_DEV_MAIL
|
||||
)
|
||||
)
|
||||
elif r.status_code == requests.codes.OK: # pylint: disable=no-member
|
||||
H.append("""<p>Opération effectuée.</p>""")
|
||||
else:
|
||||
H.append(
|
||||
"""<p class="warning">
|
||||
Erreur: code <tt>{0} {1}</tt>
|
||||
Merci de contacter <a href="mailto:{2}">{2}</a></p>""".format(
|
||||
r.status_code, r.reason, scu.SCO_DEV_MAIL
|
||||
)
|
||||
)
|
||||
r = _send_db(ano_db_name, message, request_url)
|
||||
code = r.status_code
|
||||
|
||||
finally:
|
||||
# Drop anonymized database
|
||||
@ -125,8 +102,8 @@ def sco_dump_and_send_db():
|
||||
fcntl.flock(x, fcntl.LOCK_UN)
|
||||
|
||||
log("sco_dump_and_send_db: done.")
|
||||
flash("Données envoyées au serveur d'assistance")
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def _duplicate_db(db_name, ano_db_name):
|
||||
@ -175,7 +152,7 @@ def _get_scodoc_serial():
|
||||
return 0
|
||||
|
||||
|
||||
def _send_db(ano_db_name):
|
||||
def _send_db(ano_db_name: str, message: str = "", request_url: str = ""):
|
||||
"""Dump this (anonymized) database and send it to tech support"""
|
||||
log(f"dumping anonymized database {ano_db_name}")
|
||||
try:
|
||||
@ -184,7 +161,9 @@ def _send_db(ano_db_name):
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log(f"sco_dump_and_send_db: exception in anonymisation: {e}")
|
||||
raise ScoValueError(f"erreur lors de l'anonymisation de la base {ano_db_name}")
|
||||
raise ScoValueError(
|
||||
f"erreur lors de l'anonymisation de la base {ano_db_name}"
|
||||
) from e
|
||||
|
||||
log("uploading anonymized dump...")
|
||||
files = {"file": (ano_db_name + ".dump", dump)}
|
||||
@ -193,7 +172,9 @@ def _send_db(ano_db_name):
|
||||
scu.SCO_DUMP_UP_URL,
|
||||
files=files,
|
||||
data={
|
||||
"dept_name": sco_preferences.get_preference("DeptName"),
|
||||
"dept_name": getattr(g, "scodoc_dept", "-"),
|
||||
"message": message or "",
|
||||
"request_url": request_url or request.url,
|
||||
"serial": _get_scodoc_serial(),
|
||||
"sco_user": str(current_user),
|
||||
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
|
||||
|
@ -166,7 +166,7 @@ def html_edit_formation_apc(
|
||||
|
||||
|
||||
def html_ue_infos(ue):
|
||||
"""page d'information sur une UE"""
|
||||
"""Page d'information sur une UE"""
|
||||
from app.views import ScoData
|
||||
|
||||
formsemestres = (
|
||||
@ -189,7 +189,6 @@ def html_ue_infos(ue):
|
||||
)
|
||||
return render_template(
|
||||
"pn/ue_infos.html",
|
||||
# "pn/tmp.html",
|
||||
titre=f"UE {ue.acronyme} {ue.titre}",
|
||||
ue=ue,
|
||||
formsemestres=formsemestres,
|
||||
|
@ -92,7 +92,7 @@ def do_matiere_create(args):
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=ue["formation_id"],
|
||||
text="Modification de la formation {formation.acronyme}",
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
@ -200,7 +200,7 @@ def do_matiere_delete(oid):
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=ue["formation_id"],
|
||||
text="Modification de la formation {formation.acronyme}",
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
@ -520,7 +520,7 @@ def module_edit(module_id=None):
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Modification du module %(titre)s" % module,
|
||||
page_title=f"Modification du module {a_module.code or a_module.titre or ''}",
|
||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||
javascripts=[
|
||||
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
|
||||
@ -528,7 +528,7 @@ def module_edit(module_id=None):
|
||||
"js/module_tag_editor.js",
|
||||
],
|
||||
),
|
||||
"""<h2>Modification du module %(titre)s""" % module,
|
||||
f"""<h2>Modification du module {a_module.code or ''} {a_module.titre or ''}""",
|
||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
|
||||
render_template(
|
||||
"scodoc/help/modules.html",
|
||||
|
@ -35,7 +35,7 @@ from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import Formation, UniteEns, ModuleImpl, Module
|
||||
from app.models.formations import Matiere
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -141,7 +141,7 @@ def do_ue_create(args):
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=args["formation_id"],
|
||||
text="Modification de la formation {formation.acronyme}",
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
@ -347,7 +347,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"title": "ECTS",
|
||||
"explanation": "nombre de crédits ECTS",
|
||||
"explanation": "nombre de crédits ECTS (indiquer 0 si UE bonus)",
|
||||
"allow_null": not is_apc, # ects requis en APC
|
||||
},
|
||||
),
|
||||
@ -372,7 +372,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
{
|
||||
"size": 12,
|
||||
"title": "Code UE",
|
||||
"explanation": "code interne (non vide). Toutes les UE partageant le même code (et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE). Voir liste ci-dessous.",
|
||||
"max_length": SHORT_STR_LEN,
|
||||
"explanation": """code interne (non vide). Toutes les UE partageant le même code
|
||||
(et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE).
|
||||
Voir liste ci-dessous.""",
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -381,7 +384,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
|
||||
"max_length": APO_CODE_STR_LEN,
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -724,13 +727,16 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
|
||||
</a> """
|
||||
msg_refcomp = "changer"
|
||||
H.append(
|
||||
f"""
|
||||
<ul>
|
||||
<li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
|
||||
H.append(f"""<ul><li>{descr_refcomp}""")
|
||||
if current_user.has_permission(Permission.ScoChangeFormation):
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
||||
}">{msg_refcomp}</a>
|
||||
</li>
|
||||
}">{msg_refcomp}</a>"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""</li>
|
||||
<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)
|
||||
}">éditer les coefficients des ressources et SAÉs</a>
|
||||
|
@ -1,324 +0,0 @@
|
||||
# -*- 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
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions sur les entreprises
|
||||
"""
|
||||
# codes anciens déplacés de ZEntreprise
|
||||
import datetime
|
||||
from operator import itemgetter
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.notesdb import ScoDocCursor, EditableTable, DateISOtoDMY, DateDMYtoISO
|
||||
|
||||
|
||||
def _format_nom(nom):
|
||||
"formatte nom (filtre en entree db) d'une entreprise"
|
||||
if not nom:
|
||||
return nom
|
||||
return nom[0].upper() + nom[1:]
|
||||
|
||||
|
||||
class EntreprisesEditor(EditableTable):
|
||||
def delete(self, cnx, oid):
|
||||
"delete correspondants and contacts, then self"
|
||||
# first, delete all correspondants and contacts
|
||||
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
|
||||
cursor.execute(
|
||||
"delete from entreprise_contact where entreprise_id=%(entreprise_id)s",
|
||||
{"entreprise_id": oid},
|
||||
)
|
||||
cursor.execute(
|
||||
"delete from entreprise_correspondant where entreprise_id=%(entreprise_id)s",
|
||||
{"entreprise_id": oid},
|
||||
)
|
||||
cnx.commit()
|
||||
EditableTable.delete(self, cnx, oid)
|
||||
|
||||
def list(
|
||||
self,
|
||||
cnx,
|
||||
args={},
|
||||
operator="and",
|
||||
test="=",
|
||||
sortkey=None,
|
||||
sort_on_contact=False,
|
||||
limit="",
|
||||
offset="",
|
||||
):
|
||||
# list, then sort on date of last contact
|
||||
R = EditableTable.list(
|
||||
self,
|
||||
cnx,
|
||||
args=args,
|
||||
operator=operator,
|
||||
test=test,
|
||||
sortkey=sortkey,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
if sort_on_contact:
|
||||
for r in R:
|
||||
c = do_entreprise_contact_list(
|
||||
args={"entreprise_id": r["entreprise_id"]},
|
||||
disable_formatting=True,
|
||||
)
|
||||
if c:
|
||||
r["date"] = max([x["date"] or datetime.date.min for x in c])
|
||||
else:
|
||||
r["date"] = datetime.date.min
|
||||
# sort
|
||||
R.sort(key=itemgetter("date"))
|
||||
for r in R:
|
||||
r["date"] = DateISOtoDMY(r["date"])
|
||||
return R
|
||||
|
||||
def list_by_etud(
|
||||
self, cnx, args={}, sort_on_contact=False, disable_formatting=False
|
||||
):
|
||||
"cherche rentreprise ayant eu contact avec etudiant"
|
||||
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
|
||||
cursor.execute(
|
||||
"select E.*, I.nom as etud_nom, I.prenom as etud_prenom, C.date from entreprises E, entreprise_contact C, identite I where C.entreprise_id = E.entreprise_id and C.etudid = I.etudid and I.nom ~* %(etud_nom)s ORDER BY E.nom",
|
||||
args,
|
||||
)
|
||||
_, res = [x[0] for x in cursor.description], cursor.dictfetchall()
|
||||
R = []
|
||||
for r in res:
|
||||
r["etud_prenom"] = r["etud_prenom"] or ""
|
||||
d = {}
|
||||
for key in r:
|
||||
v = r[key]
|
||||
# format value
|
||||
if not disable_formatting and key in self.output_formators:
|
||||
v = self.output_formators[key](v)
|
||||
d[key] = v
|
||||
R.append(d)
|
||||
# sort
|
||||
if sort_on_contact:
|
||||
R.sort(key=lambda x: (x["date"] or datetime.date.min))
|
||||
|
||||
for r in R:
|
||||
r["date"] = DateISOtoDMY(r["date"] or datetime.date.min)
|
||||
return R
|
||||
|
||||
|
||||
_entreprisesEditor = EntreprisesEditor(
|
||||
"entreprises",
|
||||
"entreprise_id",
|
||||
(
|
||||
"entreprise_id",
|
||||
"nom",
|
||||
"adresse",
|
||||
"ville",
|
||||
"codepostal",
|
||||
"pays",
|
||||
"contact_origine",
|
||||
"secteur",
|
||||
"privee",
|
||||
"localisation",
|
||||
"qualite_relation",
|
||||
"plus10salaries",
|
||||
"note",
|
||||
"date_creation",
|
||||
),
|
||||
filter_dept=True,
|
||||
sortkey="nom",
|
||||
input_formators={
|
||||
"nom": _format_nom,
|
||||
"plus10salaries": bool,
|
||||
},
|
||||
)
|
||||
|
||||
# ----------- Correspondants
|
||||
_entreprise_correspEditor = EditableTable(
|
||||
"entreprise_correspondant",
|
||||
"entreprise_corresp_id",
|
||||
(
|
||||
"entreprise_corresp_id",
|
||||
"entreprise_id",
|
||||
"civilite",
|
||||
"nom",
|
||||
"prenom",
|
||||
"fonction",
|
||||
"phone1",
|
||||
"phone2",
|
||||
"mobile",
|
||||
"fax",
|
||||
"mail1",
|
||||
"mail2",
|
||||
"note",
|
||||
),
|
||||
sortkey="nom",
|
||||
)
|
||||
|
||||
|
||||
# ----------- Contacts
|
||||
_entreprise_contactEditor = EditableTable(
|
||||
"entreprise_contact",
|
||||
"entreprise_contact_id",
|
||||
(
|
||||
"entreprise_contact_id",
|
||||
"date",
|
||||
"type_contact",
|
||||
"entreprise_id",
|
||||
"entreprise_corresp_id",
|
||||
"etudid",
|
||||
"description",
|
||||
"enseignant",
|
||||
),
|
||||
sortkey="date",
|
||||
output_formators={"date": DateISOtoDMY},
|
||||
input_formators={"date": DateDMYtoISO},
|
||||
)
|
||||
|
||||
|
||||
def do_entreprise_create(args):
|
||||
"entreprise_create"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _entreprisesEditor.create(cnx, args)
|
||||
return r
|
||||
|
||||
|
||||
def do_entreprise_delete(oid):
|
||||
"entreprise_delete"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_entreprisesEditor.delete(cnx, oid)
|
||||
|
||||
|
||||
def do_entreprise_list(**kw):
|
||||
"entreprise_list"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _entreprisesEditor.list(cnx, **kw)
|
||||
|
||||
|
||||
def do_entreprise_list_by_etud(**kw):
|
||||
"entreprise_list_by_etud"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _entreprisesEditor.list_by_etud(cnx, **kw)
|
||||
|
||||
|
||||
def do_entreprise_edit(*args, **kw):
|
||||
"entreprise_edit"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_entreprisesEditor.edit(cnx, *args, **kw)
|
||||
|
||||
|
||||
def do_entreprise_correspondant_create(args):
|
||||
"entreprise_correspondant_create"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _entreprise_correspEditor.create(cnx, args)
|
||||
return r
|
||||
|
||||
|
||||
def do_entreprise_correspondant_delete(oid):
|
||||
"entreprise_correspondant_delete"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_entreprise_correspEditor.delete(cnx, oid)
|
||||
|
||||
|
||||
def do_entreprise_correspondant_list(**kw):
|
||||
"entreprise_correspondant_list"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _entreprise_correspEditor.list(cnx, **kw)
|
||||
|
||||
|
||||
def do_entreprise_correspondant_edit(*args, **kw):
|
||||
"entreprise_correspondant_edit"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_entreprise_correspEditor.edit(cnx, *args, **kw)
|
||||
|
||||
|
||||
def do_entreprise_correspondant_listnames(args={}):
|
||||
"-> liste des noms des correspondants (pour affichage menu)"
|
||||
C = do_entreprise_correspondant_list(args=args)
|
||||
return [(x["prenom"] + " " + x["nom"], str(x["entreprise_corresp_id"])) for x in C]
|
||||
|
||||
|
||||
def do_entreprise_contact_delete(oid):
|
||||
"entreprise_contact_delete"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_entreprise_contactEditor.delete(cnx, oid)
|
||||
|
||||
|
||||
def do_entreprise_contact_list(**kw):
|
||||
"entreprise_contact_list"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _entreprise_contactEditor.list(cnx, **kw)
|
||||
|
||||
|
||||
def do_entreprise_contact_edit(*args, **kw):
|
||||
"entreprise_contact_edit"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_entreprise_contactEditor.edit(cnx, *args, **kw)
|
||||
|
||||
|
||||
def do_entreprise_contact_create(args):
|
||||
"entreprise_contact_create"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _entreprise_contactEditor.create(cnx, args)
|
||||
return r
|
||||
|
||||
|
||||
def do_entreprise_check_etudiant(etudiant):
|
||||
"""Si etudiant est vide, ou un ETUDID valide, ou un nom unique,
|
||||
retourne (1, ETUDID).
|
||||
Sinon, retourne (0, 'message explicatif')
|
||||
"""
|
||||
etudiant = etudiant.strip().translate(
|
||||
str.maketrans("", "", "'()")
|
||||
) # suppress parens and quote from name
|
||||
if not etudiant:
|
||||
return 1, None
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
|
||||
cursor.execute(
|
||||
"select etudid, nom, prenom from identite where upper(nom) ~ upper(%(etudiant)s) or etudid=%(etudiant)s",
|
||||
{"etudiant": etudiant},
|
||||
)
|
||||
r = cursor.fetchall()
|
||||
if len(r) < 1:
|
||||
return 0, 'Aucun etudiant ne correspond à "%s"' % etudiant
|
||||
elif len(r) > 10:
|
||||
return (
|
||||
0,
|
||||
"<b>%d etudiants</b> correspondent à ce nom (utilisez le code)" % len(r),
|
||||
)
|
||||
elif len(r) > 1:
|
||||
e = ['<ul class="entreprise_etud_list">']
|
||||
for x in r:
|
||||
e.append(
|
||||
"<li>%s %s (code %s)</li>" % ((x[1]).upper(), x[2] or "", x[0].strip())
|
||||
)
|
||||
e.append("</ul>")
|
||||
return (
|
||||
0,
|
||||
"Les étudiants suivants correspondent: préciser le nom complet ou le code\n"
|
||||
+ "\n".join(e),
|
||||
)
|
||||
else: # une seule reponse !
|
||||
return 1, r[0][0].strip()
|
@ -449,7 +449,6 @@ _adresseEditor = ndb.EditableTable(
|
||||
"telephonemobile",
|
||||
"fax",
|
||||
"typeadresse",
|
||||
"entreprise_id",
|
||||
"description",
|
||||
),
|
||||
convert_null_outputs_to_empty=True,
|
||||
|
@ -39,7 +39,7 @@ from flask import request
|
||||
from app import log
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -47,9 +47,12 @@ class ScoValueError(ScoException):
|
||||
self.dest_url = dest_url
|
||||
|
||||
|
||||
class ScoBugCatcher(ScoException):
|
||||
"bug avec enquete en cours"
|
||||
|
||||
|
||||
class NoteProcessError(ScoValueError):
|
||||
"Valeurs notes invalides"
|
||||
pass
|
||||
|
||||
|
||||
class InvalidEtudId(NoteProcessError):
|
||||
@ -112,8 +115,9 @@ class ScoNonEmptyFormationObject(ScoValueError):
|
||||
|
||||
|
||||
class ScoInvalidIdType(ScoValueError):
|
||||
"""Pour les clients qui s'obstinnent à utiliser des bookmarks ou
|
||||
historiques anciens avec des ID ScoDoc7"""
|
||||
"""Pour les clients qui s'obstinent à utiliser des bookmarks
|
||||
ou historiques anciens avec des ID ScoDoc7.
|
||||
"""
|
||||
|
||||
def __init__(self, msg=""):
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -30,7 +30,7 @@
|
||||
from flask import url_for, g, request
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -328,11 +328,15 @@ def formation_list_table(formation_id=None, args={}):
|
||||
"session_id)s<a> " % s
|
||||
for s in f["sems"]
|
||||
]
|
||||
+ [
|
||||
'<a class="stdlink" id="add-semestre-%s" '
|
||||
'href="formsemestre_createwithmodules?formation_id=%s&semestre_id=1">ajouter</a> '
|
||||
% (f["acronyme"].lower().replace(" ", "-"), f["formation_id"])
|
||||
]
|
||||
+ (
|
||||
[
|
||||
'<a class="stdlink" id="add-semestre-%s" '
|
||||
'href="formsemestre_createwithmodules?formation_id=%s&semestre_id=1">ajouter</a> '
|
||||
% (f["acronyme"].lower().replace(" ", "-"), f["formation_id"])
|
||||
]
|
||||
if current_user.has_permission(Permission.ScoImplement)
|
||||
else []
|
||||
)
|
||||
)
|
||||
if f["sems"]:
|
||||
f["date_fin_dernier_sem"] = max([s["date_fin_iso"] for s in f["sems"]])
|
||||
|
@ -648,7 +648,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
||||
# 'allowed_values' : ['X'], 'labels' : [ '' ],
|
||||
# 'title' : '' ,
|
||||
# 'explanation' : 'inscrire tous les étudiants du semestre aux modules ajoutés'}) )
|
||||
submitlabel = "Modifier ce semestre de formation"
|
||||
submitlabel = "Modifier ce semestre"
|
||||
else:
|
||||
submitlabel = "Créer ce semestre de formation"
|
||||
#
|
||||
@ -1314,7 +1314,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
|
||||
|
||||
def formsemestre_delete(formsemestre_id):
|
||||
"""Delete a formsemestre (affiche avertissements)"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Suppression du semestre"),
|
||||
|
@ -38,7 +38,7 @@ from flask import url_for, g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -33,7 +33,7 @@ import flask
|
||||
from flask import url_for, g, request
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
|
@ -36,7 +36,7 @@ from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Module
|
||||
from app.models.formsemestre import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -1170,8 +1170,10 @@ def formsemestre_tableau_modules(
|
||||
H.append('<tr class="formsemestre_status%s">' % fontorange)
|
||||
|
||||
H.append(
|
||||
'<td class="formsemestre_status_code"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="stdlink">%s</a></td>'
|
||||
% (modimpl["moduleimpl_id"], mod_descr, mod.code)
|
||||
f"""<td class="formsemestre_status_code""><a
|
||||
href="{url_for('notes.moduleimpl_status',
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl['moduleimpl_id'])}"
|
||||
title="{mod_descr}" class="stdlink">{mod.code}</a></td>"""
|
||||
)
|
||||
H.append(
|
||||
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
|
||||
|
@ -37,7 +37,7 @@ import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models.notes import etud_has_notes_attente
|
||||
|
||||
@ -591,12 +591,14 @@ def formsemestre_recap_parcours_table(
|
||||
etud_ue_status = {
|
||||
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
|
||||
}
|
||||
ues = [
|
||||
ue
|
||||
for ue in ues
|
||||
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
|
||||
or etud_ue_status[ue["ue_id"]]["is_capitalized"]
|
||||
]
|
||||
if not nt.is_apc:
|
||||
# formations classiques: filtre UE sur inscriptions (et garde UE capitalisées)
|
||||
ues = [
|
||||
ue
|
||||
for ue in ues
|
||||
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
|
||||
or etud_ue_status[ue["ue_id"]]["is_capitalized"]
|
||||
]
|
||||
|
||||
for ue in ues:
|
||||
H.append('<td class="ue_acro"><span>%s</span></td>' % ue["acronyme"])
|
||||
|
@ -46,7 +46,7 @@ from flask import url_for, make_response
|
||||
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, formsemestre
|
||||
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.groups import Partition
|
||||
@ -343,7 +343,7 @@ def get_group_other_partitions(group):
|
||||
return other_partitions
|
||||
|
||||
|
||||
def get_etud_groups(etudid, sem, exclude_default=False):
|
||||
def get_etud_groups(etudid: int, formsemestre_id: int, exclude_default=False):
|
||||
"""Infos sur groupes de l'etudiant dans ce semestre
|
||||
[ group + partition_name ]
|
||||
"""
|
||||
@ -358,18 +358,18 @@ def get_etud_groups(etudid, sem, exclude_default=False):
|
||||
req += " and p.partition_name is not NULL"
|
||||
groups = ndb.SimpleDictFetch(
|
||||
req + " ORDER BY p.numero",
|
||||
{"etudid": etudid, "formsemestre_id": sem["formsemestre_id"]},
|
||||
{"etudid": etudid, "formsemestre_id": formsemestre_id},
|
||||
)
|
||||
return _sortgroups(groups)
|
||||
|
||||
|
||||
def get_etud_main_group(etudid, sem):
|
||||
def get_etud_main_group(etudid: int, formsemestre_id: int):
|
||||
"""Return main group (the first one) for etud, or default one if no groups"""
|
||||
groups = get_etud_groups(etudid, sem, exclude_default=True)
|
||||
groups = get_etud_groups(etudid, formsemestre_id, exclude_default=True)
|
||||
if groups:
|
||||
return groups[0]
|
||||
else:
|
||||
return get_group(get_default_group(sem["formsemestre_id"]))
|
||||
return get_group(get_default_group(formsemestre_id))
|
||||
|
||||
|
||||
def formsemestre_get_main_partition(formsemestre_id):
|
||||
|
@ -154,9 +154,9 @@ def sco_import_generate_excel_sample(
|
||||
with_codesemestre=True,
|
||||
only_tables=None,
|
||||
with_groups=True,
|
||||
exclude_cols=[],
|
||||
extra_cols=[],
|
||||
group_ids=[],
|
||||
exclude_cols=(),
|
||||
extra_cols=(),
|
||||
group_ids=(),
|
||||
):
|
||||
"""Generates an excel document based on format fmt
|
||||
(format is the result of sco_import_format())
|
||||
@ -167,7 +167,7 @@ def sco_import_generate_excel_sample(
|
||||
style = sco_excel.excel_make_style(bold=True)
|
||||
style_required = sco_excel.excel_make_style(bold=True, color=COLORS.RED)
|
||||
titles = []
|
||||
titlesStyles = []
|
||||
titles_styles = []
|
||||
for l in fmt:
|
||||
name = l[0].lower()
|
||||
if (not with_codesemestre) and name == "codesemestre":
|
||||
@ -177,15 +177,15 @@ def sco_import_generate_excel_sample(
|
||||
if name in exclude_cols:
|
||||
continue # colonne exclue
|
||||
if int(l[3]):
|
||||
titlesStyles.append(style)
|
||||
titles_styles.append(style)
|
||||
else:
|
||||
titlesStyles.append(style_required)
|
||||
titles_styles.append(style_required)
|
||||
titles.append(name)
|
||||
if with_groups and "groupes" not in titles:
|
||||
titles.append("groupes")
|
||||
titlesStyles.append(style)
|
||||
titles_styles.append(style)
|
||||
titles += extra_cols
|
||||
titlesStyles += [style] * len(extra_cols)
|
||||
titles_styles += [style] * len(extra_cols)
|
||||
if group_ids:
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||
members = groups_infos.members
|
||||
@ -194,7 +194,7 @@ def sco_import_generate_excel_sample(
|
||||
% (group_ids, len(members))
|
||||
)
|
||||
titles = ["etudid"] + titles
|
||||
titlesStyles = [style] + titlesStyles
|
||||
titles_styles = [style] + titles_styles
|
||||
# rempli table avec données actuelles
|
||||
lines = []
|
||||
for i in members:
|
||||
@ -213,7 +213,7 @@ def sco_import_generate_excel_sample(
|
||||
else:
|
||||
lines = [[]] # empty content, titles only
|
||||
return sco_excel.excel_simple_table(
|
||||
titles=titles, titles_styles=titlesStyles, sheet_name="Etudiants", lines=lines
|
||||
titles=titles, titles_styles=titles_styles, sheet_name="Etudiants", lines=lines
|
||||
)
|
||||
|
||||
|
||||
@ -256,7 +256,7 @@ def scolars_import_excel_file(
|
||||
formsemestre_id=None,
|
||||
check_homonyms=True,
|
||||
require_ine=False,
|
||||
exclude_cols=[],
|
||||
exclude_cols=(),
|
||||
):
|
||||
"""Importe etudiants depuis fichier Excel
|
||||
et les inscrit dans le semestre indiqué (et à TOUS ses modules)
|
||||
@ -302,7 +302,8 @@ def scolars_import_excel_file(
|
||||
else:
|
||||
unknown.append(f)
|
||||
raise ScoValueError(
|
||||
"Nombre de colonnes incorrect (devrait être %d, et non %d) <br/> (colonnes manquantes: %s, colonnes invalides: %s)"
|
||||
"""Nombre de colonnes incorrect (devrait être %d, et non %d)<br/>
|
||||
(colonnes manquantes: %s, colonnes invalides: %s)"""
|
||||
% (len(titles), len(fs), list(missing.keys()), unknown)
|
||||
)
|
||||
titleslist = []
|
||||
@ -313,7 +314,7 @@ def scolars_import_excel_file(
|
||||
# ok, same titles
|
||||
# Start inserting data, abort whole transaction in case of error
|
||||
created_etudids = []
|
||||
NbImportedHomonyms = 0
|
||||
np_imported_homonyms = 0
|
||||
GroupIdInferers = {}
|
||||
try: # --- begin DB transaction
|
||||
linenum = 0
|
||||
@ -377,10 +378,10 @@ def scolars_import_excel_file(
|
||||
if val:
|
||||
try:
|
||||
val = sco_excel.xldate_as_datetime(val)
|
||||
except ValueError:
|
||||
except ValueError as exc:
|
||||
raise ScoValueError(
|
||||
f"date invalide ({val}) sur ligne {linenum}, colonne {titleslist[i]}"
|
||||
)
|
||||
) from exc
|
||||
# INE
|
||||
if (
|
||||
titleslist[i].lower() == "code_ine"
|
||||
@ -404,15 +405,17 @@ def scolars_import_excel_file(
|
||||
if values["code_ine"] and not is_new_ine:
|
||||
raise ScoValueError("Code INE dupliqué (%s)" % values["code_ine"])
|
||||
# Check nom/prenom
|
||||
ok, NbHomonyms = sco_etud.check_nom_prenom(
|
||||
cnx, nom=values["nom"], prenom=values["prenom"]
|
||||
)
|
||||
ok = False
|
||||
if "nom" in values and "prenom" in values:
|
||||
ok, nb_homonyms = sco_etud.check_nom_prenom(
|
||||
cnx, nom=values["nom"], prenom=values["prenom"]
|
||||
)
|
||||
if not ok:
|
||||
raise ScoValueError(
|
||||
"nom ou prénom invalide sur la ligne %d" % (linenum)
|
||||
)
|
||||
if NbHomonyms:
|
||||
NbImportedHomonyms += 1
|
||||
if nb_homonyms:
|
||||
np_imported_homonyms += 1
|
||||
# Insert in DB tables
|
||||
formsemestre_id_etud = _import_one_student(
|
||||
cnx,
|
||||
@ -425,11 +428,11 @@ def scolars_import_excel_file(
|
||||
)
|
||||
|
||||
# Verification proportion d'homonymes: si > 10%, abandonne
|
||||
log("scolars_import_excel_file: detected %d homonyms" % NbImportedHomonyms)
|
||||
if check_homonyms and NbImportedHomonyms > len(created_etudids) / 10:
|
||||
log("scolars_import_excel_file: detected %d homonyms" % np_imported_homonyms)
|
||||
if check_homonyms and np_imported_homonyms > len(created_etudids) / 10:
|
||||
log("scolars_import_excel_file: too many homonyms")
|
||||
raise ScoValueError(
|
||||
"Il y a trop d'homonymes (%d étudiants)" % NbImportedHomonyms
|
||||
"Il y a trop d'homonymes (%d étudiants)" % np_imported_homonyms
|
||||
)
|
||||
except:
|
||||
cnx.rollback()
|
||||
|
@ -27,6 +27,8 @@
|
||||
|
||||
"""Liste des notes d'une évaluation
|
||||
"""
|
||||
from collections import defaultdict
|
||||
import numpy as np
|
||||
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
@ -36,22 +38,22 @@ from app import models
|
||||
from app.comp import res_sem
|
||||
from app.comp import moy_mod
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_users
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -318,10 +320,12 @@ def _make_table_notes(
|
||||
for etudid, etat in etudid_etats:
|
||||
css_row_class = None
|
||||
# infos identite etudiant
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.query.get(etudid)
|
||||
if etud is None:
|
||||
continue
|
||||
|
||||
if etat == "I": # si inscrit, indique groupe
|
||||
groups = sco_groups.get_etud_groups(etudid, sem)
|
||||
groups = sco_groups.get_etud_groups(etudid, modimpl_o["formsemestre_id"])
|
||||
grc = sco_groups.listgroups_abbrev(groups)
|
||||
else:
|
||||
if etat == "D":
|
||||
@ -330,7 +334,7 @@ def _make_table_notes(
|
||||
else:
|
||||
grc = etat
|
||||
|
||||
code = etud.get(anonymous_lst_key)
|
||||
code = getattr(etud, anonymous_lst_key)
|
||||
if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid
|
||||
code = etudid
|
||||
|
||||
@ -339,15 +343,20 @@ def _make_table_notes(
|
||||
"code": str(code), # INE, NIP ou etudid
|
||||
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
|
||||
"etudid": etudid,
|
||||
"nom": etud["nom"].upper(),
|
||||
"_nomprenom_target": "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
|
||||
% (modimpl_o["formsemestre_id"], etudid),
|
||||
"_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
|
||||
"prenom": etud["prenom"].lower().capitalize(),
|
||||
"nomprenom": etud["nomprenom"],
|
||||
"nom": etud.nom.upper(),
|
||||
"_nomprenom_target": url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=modimpl_o["formsemestre_id"],
|
||||
etudid=etudid,
|
||||
),
|
||||
"_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.sort_key}" """,
|
||||
"prenom": etud.prenom.lower().capitalize(),
|
||||
"nomprenom": etud.nomprenom,
|
||||
"group": grc,
|
||||
"email": etud["email"],
|
||||
"emailperso": etud["emailperso"],
|
||||
"_group_td_attrs": 'class="group"',
|
||||
"email": etud.get_first_email(),
|
||||
"emailperso": etud.get_first_email("emailperso"),
|
||||
"_css_row_class": css_row_class or "",
|
||||
}
|
||||
)
|
||||
@ -384,7 +393,7 @@ def _make_table_notes(
|
||||
"_css_row_class": "moyenne sortbottom",
|
||||
"_table_part": "foot",
|
||||
#'_nomprenom_td_attrs' : 'colspan="2" ',
|
||||
"nomprenom": "Moyenne (sans les absents) :",
|
||||
"nomprenom": "Moyenne :",
|
||||
"comment": "",
|
||||
}
|
||||
# Ajoute les notes de chaque évaluation:
|
||||
@ -566,15 +575,13 @@ def _make_table_notes(
|
||||
html_sortable=True,
|
||||
base_url=base_url,
|
||||
filename=filename,
|
||||
origin="Généré par %s le " % sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=caption,
|
||||
html_next_section=html_next_section,
|
||||
page_title="Notes de " + sem["titremois"],
|
||||
html_title=html_title,
|
||||
pdf_title=pdf_title,
|
||||
html_class="table_leftalign notes_evaluation",
|
||||
html_class="notes_evaluation",
|
||||
preferences=sco_preferences.SemPreferences(modimpl_o["formsemestre_id"]),
|
||||
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete
|
||||
)
|
||||
@ -590,9 +597,15 @@ def _make_table_notes(
|
||||
if not e["eval_state"]["evalcomplete"]:
|
||||
all_complete = False
|
||||
if all_complete:
|
||||
eval_info = '<span class="eval_info eval_complete">Evaluations prises en compte dans les moyennes</span>'
|
||||
eval_info = '<span class="eval_info"><span class="eval_complete">Evaluations prises en compte dans les moyennes.</span>'
|
||||
else:
|
||||
eval_info = '<span class="eval_info help">Les évaluations en vert et orange sont prises en compte dans les moyennes. Celles en rouge n\'ont pas toutes leurs notes.</span>'
|
||||
eval_info = """<span class="eval_info help">
|
||||
Les évaluations en vert et orange sont prises en compte dans les moyennes.
|
||||
Celles en rouge n'ont pas toutes leurs notes."""
|
||||
if is_apc:
|
||||
eval_info += """ <span>La moyenne indicative est la moyenne des moyennes d'UE, et n'est pas utilisée en BUT.
|
||||
Les moyennes sur le groupe sont estimées sans les absents (sauf pour les moyennes des moyennes d'UE) ni les démissionnaires.</span>"""
|
||||
eval_info += """</span>"""
|
||||
return html_form + eval_info + t + "<p></p>"
|
||||
else:
|
||||
# Une seule evaluation: ajoute histogramme
|
||||
@ -657,7 +670,23 @@ def _add_eval_columns(
|
||||
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
||||
evaluation_id = e["evaluation_id"]
|
||||
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
|
||||
inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
|
||||
if len(e["jour"]) > 0:
|
||||
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
|
||||
else:
|
||||
titles[evaluation_id] = "%(description)s " % e
|
||||
|
||||
if e["eval_state"]["evalcomplete"]:
|
||||
klass = "eval_complete"
|
||||
elif e["eval_state"]["evalattente"]:
|
||||
klass = "eval_attente"
|
||||
else:
|
||||
klass = "eval_incomplete"
|
||||
titles[evaluation_id] += " (non prise en compte)"
|
||||
titles[f"_{evaluation_id}_td_attrs"] = f'class="{klass}"'
|
||||
|
||||
for row in rows:
|
||||
etudid = row["etudid"]
|
||||
if etudid in notes_db:
|
||||
@ -666,8 +695,13 @@ def _add_eval_columns(
|
||||
nb_abs += 1
|
||||
if val == scu.NOTES_ATTENTE:
|
||||
nb_att += 1
|
||||
# calcul moyenne SANS LES ABSENTS
|
||||
if val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE:
|
||||
# calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
|
||||
if (
|
||||
(etudid in inscrits)
|
||||
and val != None
|
||||
and val != scu.NOTES_NEUTRALISE
|
||||
and val != scu.NOTES_ATTENTE
|
||||
):
|
||||
if e["note_max"] > 0:
|
||||
valsur20 = val * 20.0 / e["note_max"] # remet sur 20
|
||||
else:
|
||||
@ -692,9 +726,11 @@ def _add_eval_columns(
|
||||
val = None
|
||||
|
||||
if val is None:
|
||||
row["_" + str(evaluation_id) + "_td_attrs"] = 'class="etudabs" '
|
||||
row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {klass}" '
|
||||
if not row.get("_css_row_class", ""):
|
||||
row["_css_row_class"] = "etudabs"
|
||||
else:
|
||||
row[f"_{evaluation_id}_td_attrs"] = f'class="{klass}" '
|
||||
# regroupe les commentaires
|
||||
if explanation:
|
||||
if explanation in K:
|
||||
@ -747,18 +783,6 @@ def _add_eval_columns(
|
||||
else:
|
||||
row_moys[evaluation_id] = ""
|
||||
|
||||
if len(e["jour"]) > 0:
|
||||
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
|
||||
else:
|
||||
titles[evaluation_id] = "%(description)s " % e
|
||||
|
||||
if e["eval_state"]["evalcomplete"]:
|
||||
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_complete"'
|
||||
elif e["eval_state"]["evalattente"]:
|
||||
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_attente"'
|
||||
else:
|
||||
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_incomplete"'
|
||||
|
||||
return notes, nb_abs, nb_att # pour histogramme
|
||||
|
||||
|
||||
@ -791,6 +815,7 @@ def _add_moymod_column(
|
||||
col_id = "moymod"
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
inscrits = formsemestre.etudids_actifs
|
||||
|
||||
nb_notes = 0
|
||||
sum_notes = 0
|
||||
@ -800,7 +825,7 @@ def _add_moymod_column(
|
||||
val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI'
|
||||
row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
|
||||
row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
|
||||
if not isinstance(val, str):
|
||||
if etudid in inscrits and not isinstance(val, str):
|
||||
notes.append(val)
|
||||
nb_notes = nb_notes + 1
|
||||
sum_notes += val
|
||||
@ -840,18 +865,16 @@ def _add_apc_columns(
|
||||
# => On recharge tout dans les nouveaux modèles
|
||||
# rows est une liste de dict avec une clé "etudid"
|
||||
# on va y ajouter une clé par UE du semestre
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(modimpl.formsemestre)
|
||||
nt: ResultatsSemestreBUT = res_sem.load_formsemestre_results(modimpl.formsemestre)
|
||||
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
|
||||
|
||||
# XXX A ENLEVER TODO
|
||||
# modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
|
||||
# evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
|
||||
# moduleimpl_id
|
||||
# )
|
||||
# etuds_moy_module = moy_mod.compute_module_moy(
|
||||
# evals_notes, evals_poids, evaluations, evaluations_completes
|
||||
# )
|
||||
inscrits = modimpl.formsemestre.etudids_actifs
|
||||
# les UE dans lesquelles ce module a un coef non nul:
|
||||
ues_with_coef = nt.modimpl_coefs_df[modimpl.id][
|
||||
nt.modimpl_coefs_df[modimpl.id] > 0
|
||||
].index
|
||||
ues = [ue for ue in ues if ue.id in ues_with_coef]
|
||||
sum_by_ue = defaultdict(float)
|
||||
nb_notes_by_ue = defaultdict(int)
|
||||
if is_conforme:
|
||||
# valeur des moyennes vers les UEs:
|
||||
for row in rows:
|
||||
@ -859,6 +882,13 @@ def _add_apc_columns(
|
||||
moy_ue = modimpl_results.etuds_moy_module[ue.id].get(row["etudid"], "?")
|
||||
row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
|
||||
row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
|
||||
if (
|
||||
isinstance(moy_ue, float)
|
||||
and not np.isnan(moy_ue)
|
||||
and row["etudid"] in inscrits
|
||||
):
|
||||
sum_by_ue[ue.id] += moy_ue
|
||||
nb_notes_by_ue[ue.id] += 1
|
||||
# Nom et coefs des UE (lignes titres):
|
||||
ue_coefs = modimpl.module.ue_coefs
|
||||
if is_conforme:
|
||||
@ -873,3 +903,8 @@ def _add_apc_columns(
|
||||
if coefs:
|
||||
row_coefs[f"moy_ue_{ue.id}"] = coefs[0].coef
|
||||
row_coefs[f"_moy_ue_{ue.id}_td_attrs"] = f' class="{coef_class}" '
|
||||
if nb_notes_by_ue[ue.id] > 0:
|
||||
row_moys[col_id] = "%.3g" % (sum_by_ue[ue.id] / nb_notes_by_ue[ue.id])
|
||||
row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
|
||||
else:
|
||||
row_moys[col_id] = ""
|
||||
|
@ -34,7 +34,7 @@ from flask import url_for, g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -192,7 +192,7 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
)
|
||||
H.append("""</input></td>""")
|
||||
|
||||
groups = sco_groups.get_etud_groups(etud["etudid"], sem)
|
||||
groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre_id)
|
||||
for partition in partitions:
|
||||
if partition["partition_name"]:
|
||||
gr_name = ""
|
||||
@ -303,12 +303,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
||||
)
|
||||
for mod in options:
|
||||
if can_change:
|
||||
c_link = (
|
||||
'<a class="discretelink" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">%s</a>'
|
||||
% (
|
||||
mod["moduleimpl_id"],
|
||||
mod["descri"] or "<i>(inscrire des étudiants)</i>",
|
||||
)
|
||||
c_link = '<a class="discretelink" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">%s</a>' % (
|
||||
mod["moduleimpl_id"],
|
||||
mod["descri"] or "<i>(inscrire des étudiants)</i>",
|
||||
)
|
||||
else:
|
||||
c_link = mod["descri"]
|
||||
|
@ -34,8 +34,7 @@ from flask_login import current_user
|
||||
|
||||
from app.auth.models import User
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import ModuleImpl
|
||||
from app.models.evaluations import Evaluation
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -235,7 +235,7 @@ def ficheEtud(etudid=None):
|
||||
)
|
||||
grlink = '<span class="fontred">%s</span>' % descr["situation"]
|
||||
else:
|
||||
group = sco_groups.get_etud_main_group(etudid, sem)
|
||||
group = sco_groups.get_etud_main_group(etudid, sem["formsemestre_id"])
|
||||
if group["partition_name"]:
|
||||
gr_name = group["group_name"]
|
||||
else:
|
||||
@ -584,7 +584,7 @@ def etud_info_html(etudid, with_photo="1", debug=False):
|
||||
elif etud["cursem"]: # le semestre "en cours" pour l'étudiant
|
||||
sem = etud["cursem"]
|
||||
if sem:
|
||||
groups = sco_groups.get_etud_groups(etudid, sem)
|
||||
groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
||||
grc = sco_groups.listgroups_abbrev(groups)
|
||||
H += '<div class="eid_info">En <b>S%d</b>: %s</div>' % (sem["semestre_id"], grc)
|
||||
H += "</div>" # fin partie gauche (eid_left)
|
||||
|
@ -29,7 +29,7 @@
|
||||
"""
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, UniteEns
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -1011,7 +1011,9 @@ def formsemestre_has_decisions(formsemestre_id):
|
||||
|
||||
|
||||
def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
|
||||
"""Vrai si l'étudiant est inscrit à au moins un module de cette UE dans ce semestre"""
|
||||
"""Vrai si l'étudiant est inscrit à au moins un module de cette UE dans ce semestre.
|
||||
Ne pas utiliser pour les formations APC !
|
||||
"""
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT mi.*
|
||||
|
@ -44,22 +44,17 @@ import traceback
|
||||
import unicodedata
|
||||
|
||||
import reportlab
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.platypus import Paragraph, Frame
|
||||
from reportlab.platypus.flowables import Flowable
|
||||
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.rl_config import defaultPageSize # pylint: disable=no-name-in-module
|
||||
from reportlab.lib.units import inch, cm, mm
|
||||
from reportlab.lib.colors import pink, black, red, blue, green, magenta, red
|
||||
from reportlab.lib.colors import Color
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
|
||||
from reportlab.lib import styles
|
||||
from reportlab.lib.pagesizes import letter, A4, landscape
|
||||
|
||||
|
||||
from flask import g
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import CONFIG
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
@ -89,6 +84,12 @@ def SU(s):
|
||||
return s
|
||||
|
||||
|
||||
def get_available_font_names() -> list[str]:
|
||||
"""List installed font names"""
|
||||
can = canvas.Canvas(io.StringIO())
|
||||
return can.getAvailableFonts()
|
||||
|
||||
|
||||
def _splitPara(txt):
|
||||
"split a string, returns a list of <para > ... </para>"
|
||||
L = []
|
||||
@ -147,12 +148,26 @@ def makeParas(txt, style, suppress_empty=False):
|
||||
except Exception as e:
|
||||
log(traceback.format_exc())
|
||||
log("Invalid pdf para format: %s" % txt)
|
||||
result = [
|
||||
Paragraph(
|
||||
SU('<font color="red"><i>Erreur: format invalide</i></font>'),
|
||||
style,
|
||||
)
|
||||
]
|
||||
try:
|
||||
result = [
|
||||
Paragraph(
|
||||
SU('<font color="red"><i>Erreur: format invalide</i></font>'),
|
||||
style,
|
||||
)
|
||||
]
|
||||
except ValueError as e: # probleme font ? essaye sans style
|
||||
# recupere font en cause ?
|
||||
m = re.match(r".*family/bold/italic for (.*)", e.args[0], re.DOTALL)
|
||||
if m:
|
||||
message = f"police non disponible: {m[1]}"
|
||||
else:
|
||||
message = "format invalide"
|
||||
return [
|
||||
Paragraph(
|
||||
SU(f'<font color="red"><b>Erreur: {message}</b></font>'),
|
||||
reportlab.lib.styles.ParagraphStyle({}),
|
||||
)
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
@ -166,7 +181,6 @@ def bold_paras(L, tag="b", close=None):
|
||||
if hasattr(L, "keys"):
|
||||
# L is a dict
|
||||
for k in L:
|
||||
x = L[k]
|
||||
if k[0] != "_":
|
||||
L[k] = b + L[k] or "" + close
|
||||
return L
|
||||
@ -175,7 +189,29 @@ def bold_paras(L, tag="b", close=None):
|
||||
return [b + (x or "") + close for x in L]
|
||||
|
||||
|
||||
class ScolarsPageTemplate(PageTemplate):
|
||||
class BulMarker(Flowable):
|
||||
"""Custom Flowables pour nos bulletins PDF: invisibles, juste pour se repérer"""
|
||||
|
||||
def wrap(self, *args):
|
||||
return (0, 0)
|
||||
|
||||
def draw(self):
|
||||
return
|
||||
|
||||
|
||||
class DebutBulletin(BulMarker):
|
||||
"""Début d'un bulletin.
|
||||
Element vide utilisé pour générer les bookmarks
|
||||
"""
|
||||
|
||||
def __init__(self, bookmark=None, filigranne=None, footer_content=None):
|
||||
self.bookmark = bookmark
|
||||
self.filigranne = filigranne
|
||||
self.footer_content = footer_content
|
||||
super().__init__()
|
||||
|
||||
|
||||
class ScoDocPageTemplate(PageTemplate):
|
||||
"""Our own page template."""
|
||||
|
||||
def __init__(
|
||||
@ -192,17 +228,17 @@ class ScolarsPageTemplate(PageTemplate):
|
||||
preferences=None, # dictionnary with preferences, required
|
||||
):
|
||||
"""Initialise our page template."""
|
||||
from app.scodoc.sco_logos import (
|
||||
find_logo,
|
||||
) # defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
|
||||
# defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
self.preferences = preferences
|
||||
self.pagesbookmarks = pagesbookmarks
|
||||
self.pagesbookmarks = pagesbookmarks or {}
|
||||
self.pdfmeta_author = author
|
||||
self.pdfmeta_title = title
|
||||
self.pdfmeta_subject = subject
|
||||
self.server_name = server_name
|
||||
self.filigranne = filigranne
|
||||
self.page_number = 1
|
||||
self.footer_template = footer_template
|
||||
if self.preferences:
|
||||
self.with_page_background = self.preferences["bul_pdf_with_background"]
|
||||
@ -217,7 +253,7 @@ class ScolarsPageTemplate(PageTemplate):
|
||||
document.pagesize[0] - 20.0 * mm - left * mm - right * mm,
|
||||
document.pagesize[1] - 18.0 * mm - top * mm - bottom * mm,
|
||||
)
|
||||
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
|
||||
super().__init__("ScoDocPageTemplate", [content])
|
||||
self.logo = None
|
||||
logo = find_logo(
|
||||
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id
|
||||
@ -234,7 +270,7 @@ class ScolarsPageTemplate(PageTemplate):
|
||||
if logo is not None:
|
||||
self.background_image_filename = logo.filepath
|
||||
|
||||
def beforeDrawPage(self, canvas, doc):
|
||||
def beforeDrawPage(self, canv, doc):
|
||||
"""Draws (optional) background, logo and contribution message on each page.
|
||||
|
||||
day : Day of the month as a decimal number [01,31]
|
||||
@ -249,10 +285,10 @@ class ScolarsPageTemplate(PageTemplate):
|
||||
"""
|
||||
if not self.preferences:
|
||||
return
|
||||
canvas.saveState()
|
||||
canv.saveState()
|
||||
# ---- Background image
|
||||
if self.background_image_filename and self.with_page_background:
|
||||
canvas.drawImage(
|
||||
canv.drawImage(
|
||||
self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1]
|
||||
)
|
||||
|
||||
@ -263,50 +299,93 @@ class ScolarsPageTemplate(PageTemplate):
|
||||
(width, height),
|
||||
image,
|
||||
) = self.logo
|
||||
canvas.drawImage(image, inch, doc.pagesize[1] - inch, width, height)
|
||||
canv.drawImage(image, inch, doc.pagesize[1] - inch, width, height)
|
||||
|
||||
# ---- Add some meta data and bookmarks
|
||||
if self.pdfmeta_author:
|
||||
canv.setAuthor(SU(self.pdfmeta_author))
|
||||
if self.pdfmeta_title:
|
||||
canv.setTitle(SU(self.pdfmeta_title))
|
||||
if self.pdfmeta_subject:
|
||||
canv.setSubject(SU(self.pdfmeta_subject))
|
||||
|
||||
bookmark = self.pagesbookmarks.get(doc.page, None)
|
||||
if bookmark:
|
||||
canv.bookmarkPage(bookmark)
|
||||
canv.addOutlineEntry(SU(bookmark), bookmark)
|
||||
|
||||
def draw_footer(self, canv, content):
|
||||
"""Print the footer"""
|
||||
canv.setFont(
|
||||
self.preferences["SCOLAR_FONT"], self.preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||
)
|
||||
canv.drawString(
|
||||
self.preferences["pdf_footer_x"] * mm,
|
||||
self.preferences["pdf_footer_y"] * mm,
|
||||
content,
|
||||
)
|
||||
canv.restoreState()
|
||||
|
||||
def footer_string(self) -> str:
|
||||
"""String contenu du pied de page"""
|
||||
d = _makeTimeDict()
|
||||
d["scodoc_name"] = sco_version.SCONAME
|
||||
d["server_url"] = self.server_name
|
||||
return SU(self.footer_template % d)
|
||||
|
||||
def afterDrawPage(self, canv, doc):
|
||||
if not self.preferences:
|
||||
return
|
||||
# ---- Footer
|
||||
foot_content = None
|
||||
if hasattr(doc, "current_footer"):
|
||||
foot_content = doc.current_footer
|
||||
self.draw_footer(canv, foot_content or self.footer_string())
|
||||
# ---- Filigranne (texte en diagonal en haut a gauche de chaque page)
|
||||
if self.filigranne:
|
||||
filigranne = None
|
||||
if hasattr(doc, "filigranne"):
|
||||
# filigranne crée par DebutBulletin
|
||||
filigranne = doc.filigranne
|
||||
if not filigranne and self.filigranne:
|
||||
if isinstance(self.filigranne, str):
|
||||
filigranne = self.filigranne # same for all pages
|
||||
else:
|
||||
filigranne = self.filigranne.get(doc.page, None)
|
||||
if filigranne:
|
||||
canvas.saveState()
|
||||
canvas.translate(9 * cm, 27.6 * cm)
|
||||
canvas.rotate(30)
|
||||
canvas.scale(4.5, 4.5)
|
||||
canvas.setFillColorRGB(1.0, 0.65, 0.65)
|
||||
canvas.drawRightString(0, 0, SU(filigranne))
|
||||
canvas.restoreState()
|
||||
if filigranne:
|
||||
canv.saveState()
|
||||
canv.translate(9 * cm, 27.6 * cm)
|
||||
canv.rotate(30)
|
||||
canv.scale(4.5, 4.5)
|
||||
canv.setFillColorRGB(1.0, 0.65, 0.65, alpha=0.6)
|
||||
canv.drawRightString(0, 0, SU(filigranne))
|
||||
canv.restoreState()
|
||||
doc.filigranne = None
|
||||
|
||||
# ---- Add some meta data and bookmarks
|
||||
if self.pdfmeta_author:
|
||||
canvas.setAuthor(SU(self.pdfmeta_author))
|
||||
if self.pdfmeta_title:
|
||||
canvas.setTitle(SU(self.pdfmeta_title))
|
||||
if self.pdfmeta_subject:
|
||||
canvas.setSubject(SU(self.pdfmeta_subject))
|
||||
bm = self.pagesbookmarks.get(doc.page, None)
|
||||
if bm != None:
|
||||
key = bm
|
||||
txt = SU(bm)
|
||||
canvas.bookmarkPage(key)
|
||||
canvas.addOutlineEntry(txt, bm)
|
||||
# ---- Footer
|
||||
canvas.setFont(
|
||||
self.preferences["SCOLAR_FONT"], self.preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||
)
|
||||
d = _makeTimeDict()
|
||||
d["scodoc_name"] = sco_version.SCONAME
|
||||
d["server_url"] = self.server_name
|
||||
footer_str = SU(self.footer_template % d)
|
||||
canvas.drawString(
|
||||
self.preferences["pdf_footer_x"] * mm,
|
||||
self.preferences["pdf_footer_y"] * mm,
|
||||
footer_str,
|
||||
)
|
||||
canvas.restoreState()
|
||||
def afterPage(self):
|
||||
"""Called after all flowables have been drawn on a page.
|
||||
Increment pageNum since the page has been completed.
|
||||
"""
|
||||
self.page_number += 1
|
||||
|
||||
|
||||
class BulletinDocTemplate(BaseDocTemplate):
|
||||
"""Doc template pour les bulletins PDF
|
||||
ajoute la gestion des bookmarks
|
||||
"""
|
||||
|
||||
# inspired by https://www.reportlab.com/snippets/13/
|
||||
def afterFlowable(self, flowable):
|
||||
"""Called by Reportlab after each flowable"""
|
||||
if isinstance(flowable, DebutBulletin):
|
||||
self.current_footer = ""
|
||||
if flowable.bookmark:
|
||||
self.current_footer = flowable.footer_content
|
||||
self.canv.bookmarkPage(flowable.bookmark)
|
||||
self.canv.addOutlineEntry(
|
||||
SU(flowable.bookmark), flowable.bookmark, level=0, closed=None
|
||||
)
|
||||
if flowable.filigranne:
|
||||
self.filigranne = flowable.filigranne
|
||||
|
||||
|
||||
def _makeTimeDict():
|
||||
@ -333,7 +412,7 @@ def pdf_basic_page(
|
||||
report = io.BytesIO() # in-memory document, no disk file
|
||||
document = BaseDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
ScolarsPageTemplate(
|
||||
ScoDocPageTemplate(
|
||||
document,
|
||||
title=title,
|
||||
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION),
|
||||
@ -378,8 +457,8 @@ class PDFLock(object):
|
||||
return # deja lock pour ce thread
|
||||
try:
|
||||
self.Q.put(1, True, self.timeout)
|
||||
except queue.Full:
|
||||
raise ScoGenError(msg="Traitement PDF occupé: ré-essayez")
|
||||
except queue.Full as e:
|
||||
raise ScoGenError(msg="Traitement PDF occupé: ré-essayez") from e
|
||||
self.current_thread = threading.get_ident()
|
||||
self.nref = 1
|
||||
log("PDFLock: granted to %s" % self.current_thread)
|
||||
@ -406,7 +485,6 @@ class WatchLock:
|
||||
def release(self):
|
||||
t = threading.current_thread()
|
||||
assert (self.native_id == t.native_id) and (self.ident == t.ident)
|
||||
pass
|
||||
|
||||
|
||||
class FakeLock:
|
||||
|
@ -34,7 +34,7 @@ import collections
|
||||
from flask import url_for, g, request
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_abs
|
||||
|
@ -121,6 +121,7 @@ from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoException
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_pdf
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
@ -152,19 +153,18 @@ def get_preference(name, formsemestre_id=None):
|
||||
def _convert_pref_type(p, pref_spec):
|
||||
"""p est une ligne de la bd
|
||||
{'id': , 'dept_id': , 'name': '', 'value': '', 'formsemestre_id': }
|
||||
converti la valeur chane en le type désiré spécifié par pref_spec
|
||||
converti la valeur chaine en le type désiré spécifié par pref_spec
|
||||
"""
|
||||
if "type" in pref_spec:
|
||||
typ = pref_spec["type"]
|
||||
if typ == "float":
|
||||
# special case for float values (where NULL means 0)
|
||||
if p["value"]:
|
||||
p["value"] = float(p["value"])
|
||||
else:
|
||||
p["value"] = 0.0
|
||||
p["value"] = float(p["value"] or 0)
|
||||
elif typ == "int":
|
||||
p["value"] = int(p["value"] or 0)
|
||||
else:
|
||||
func = eval(typ)
|
||||
p["value"] = func(p["value"])
|
||||
raise ValueError("invalid preference type")
|
||||
|
||||
if pref_spec.get("input_type", None) == "boolcheckbox":
|
||||
# boolcheckbox: la valeur stockée en base est une chaine "0" ou "1"
|
||||
# que l'on ressort en True|False
|
||||
@ -195,6 +195,8 @@ def _get_pref_default_value_from_config(name, pref_spec):
|
||||
return value
|
||||
|
||||
|
||||
_INSTALLED_FONTS = ", ".join(sco_pdf.get_available_font_names())
|
||||
|
||||
PREF_CATEGORIES = (
|
||||
# sur page "Paramètres"
|
||||
("general", {}),
|
||||
@ -358,8 +360,22 @@ class BasePreferences(object):
|
||||
"use_ue_coefs",
|
||||
{
|
||||
"initvalue": 0,
|
||||
"title": "Utiliser les coefficients d'UE pour calculer la moyenne générale",
|
||||
"explanation": """Calcule les moyennes dans chaque UE, puis pondère ces résultats pour obtenir la moyenne générale. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. <b>Attention: changer ce réglage va modifier toutes les moyennes du semestre !</b>""",
|
||||
"title": "Utiliser les coefficients d'UE pour calculer la moyenne générale (hors BUT)",
|
||||
"explanation": """Calcule les moyennes dans chaque UE, puis pondère ces résultats pour obtenir la moyenne générale. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. <b>Attention: changer ce réglage va modifier toutes les moyennes du semestre !</b>. Aucun effet en BUT.""",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "misc",
|
||||
"labels": ["non", "oui"],
|
||||
"only_global": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
"but_moy_skip_empty_ues",
|
||||
{
|
||||
"initvalue": 0,
|
||||
"title": "BUT: moyenne générale sans les UE sans notes",
|
||||
"explanation": """La moyenne générale indicative BUT est basée sur les moyennes d'UE pondérées par leurs ECTS.
|
||||
Si cette option est cochée, ne prend pas en compte les UEs sans notes. <b>Attention: changer ce réglage va modifier toutes
|
||||
les moyennes du semestre !</b>. Aucun effet dans les formations non BUT.""",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "misc",
|
||||
"labels": ["non", "oui"],
|
||||
@ -764,7 +780,7 @@ class BasePreferences(object):
|
||||
{
|
||||
"initvalue": "Helvetica",
|
||||
"title": "Police de caractère principale",
|
||||
"explanation": "pour les pdf (Helvetica est recommandée)",
|
||||
"explanation": f"pour les pdf (Helvetica est recommandée, parmi {_INSTALLED_FONTS})",
|
||||
"size": 25,
|
||||
"category": "pdf",
|
||||
},
|
||||
@ -1127,7 +1143,7 @@ class BasePreferences(object):
|
||||
{
|
||||
"initvalue": "Times-Roman",
|
||||
"title": "Police de caractère pour les PV",
|
||||
"explanation": "pour les pdf",
|
||||
"explanation": f"pour les pdf ({_INSTALLED_FONTS})",
|
||||
"size": 25,
|
||||
"category": "pvpdf",
|
||||
},
|
||||
@ -1159,7 +1175,7 @@ class BasePreferences(object):
|
||||
"bul_show_abs", # ex "gestion_absence"
|
||||
{
|
||||
"initvalue": 1,
|
||||
"title": "Indiquer les absences sous les bulletins",
|
||||
"title": "Indiquer les absences dans les bulletins",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "bul",
|
||||
"labels": ["non", "oui"],
|
||||
@ -1221,7 +1237,7 @@ class BasePreferences(object):
|
||||
{
|
||||
"initvalue": 0,
|
||||
"title": "Afficher toutes les évaluations sur les bulletins",
|
||||
"explanation": "y compris incomplètes ou futures",
|
||||
"explanation": "y compris incomplètes ou futures (déconseillé, risque de publier des notes non définitives)",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "bul",
|
||||
"labels": ["non", "oui"],
|
||||
@ -1529,7 +1545,7 @@ class BasePreferences(object):
|
||||
{
|
||||
"initvalue": "Times-Roman",
|
||||
"title": "Police titres bulletins",
|
||||
"explanation": "pour les pdf",
|
||||
"explanation": f"pour les pdf ({_INSTALLED_FONTS})",
|
||||
"size": 25,
|
||||
"category": "bul",
|
||||
},
|
||||
|
@ -36,7 +36,7 @@ from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
@ -55,7 +55,7 @@ import flask
|
||||
from flask import url_for, g, request
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, UniteEns
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -28,15 +28,13 @@
|
||||
"""Edition des PV de jury
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
import reportlab
|
||||
from reportlab.lib.units import cm, mm
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||
from reportlab.platypus.flowables import Flowable
|
||||
from reportlab.lib.enums import TA_RIGHT, TA_JUSTIFY
|
||||
from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak
|
||||
from reportlab.platypus import Table, TableStyle, Image
|
||||
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
|
||||
from reportlab.lib.pagesizes import A4, landscape
|
||||
from reportlab.lib import styles
|
||||
@ -53,7 +51,6 @@ from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
import sco_version
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
from app.scodoc.sco_pdf import PDFLOCK
|
||||
from app.scodoc.sco_pdf import SU
|
||||
|
||||
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
|
||||
@ -317,14 +314,14 @@ class PVTemplate(CourrierIndividuelTemplate):
|
||||
self.with_footer = self.preferences["PV_WITH_FOOTER"]
|
||||
self.with_page_background = self.preferences["PV_WITH_BACKGROUND"]
|
||||
|
||||
def afterDrawPage(self, canvas, doc):
|
||||
def afterDrawPage(self, canv, doc):
|
||||
"""Called after all flowables have been drawn on a page"""
|
||||
pass
|
||||
|
||||
def beforeDrawPage(self, canvas, doc):
|
||||
def beforeDrawPage(self, canv, doc):
|
||||
"""Called before any flowables are drawn on a page"""
|
||||
# If the page number is even, force a page break
|
||||
CourrierIndividuelTemplate.beforeDrawPage(self, canvas, doc)
|
||||
CourrierIndividuelTemplate.beforeDrawPage(self, canv, doc)
|
||||
# Note: on cherche un moyen de generer un saut de page double
|
||||
# (redémarrer sur page impaire, nouvelle feuille en recto/verso). Pas trouvé en Platypus.
|
||||
#
|
||||
|
@ -32,23 +32,23 @@ import json
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from flask import request
|
||||
from flask import make_response
|
||||
from flask import g, request
|
||||
from flask import make_response, url_for
|
||||
|
||||
from app import log
|
||||
from app.but import bulletin_but
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_bac
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_bulletins_xml
|
||||
from app.scodoc import sco_bulletins, sco_excel
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
@ -108,55 +108,49 @@ def formsemestre_recapcomplet(
|
||||
page_title="Récapitulatif",
|
||||
no_side_bar=True,
|
||||
init_qtip=True,
|
||||
javascripts=["libjs/sorttable.js", "js/etud_info.js"],
|
||||
javascripts=["js/etud_info.js", "js/table_recap.js"],
|
||||
),
|
||||
sco_formsemestre_status.formsemestre_status_head(
|
||||
formsemestre_id=formsemestre_id
|
||||
),
|
||||
'<form name="f" method="get" action="%s">' % request.base_url,
|
||||
'<input type="hidden" name="formsemestre_id" value="%s"></input>'
|
||||
% formsemestre_id,
|
||||
'<input type="hidden" name="pref_override" value="0"></input>',
|
||||
]
|
||||
if modejury:
|
||||
H.append(
|
||||
'<input type="hidden" name="modejury" value="%s"></input>' % modejury
|
||||
)
|
||||
H.append(
|
||||
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
||||
)
|
||||
for (format, label) in (
|
||||
("html", "HTML"),
|
||||
("xls", "Fichier tableur (Excel)"),
|
||||
("xlsall", "Fichier tableur avec toutes les évals"),
|
||||
("csv", "Fichier tableur (CSV)"),
|
||||
("xml", "Fichier XML"),
|
||||
("json", "JSON"),
|
||||
):
|
||||
if format == tabformat:
|
||||
selected = " selected"
|
||||
else:
|
||||
selected = ""
|
||||
H.append('<option value="%s"%s>%s</option>' % (format, selected, label))
|
||||
H.append("</select>")
|
||||
if len(formsemestre.inscriptions) > 0:
|
||||
H += [
|
||||
'<form name="f" method="get" action="%s">' % request.base_url,
|
||||
'<input type="hidden" name="formsemestre_id" value="%s"></input>'
|
||||
% formsemestre_id,
|
||||
'<input type="hidden" name="pref_override" value="0"></input>',
|
||||
]
|
||||
|
||||
H.append(
|
||||
"""(cliquer sur un nom pour afficher son bulletin ou <a class="stdlink" href="%s/Notes/formsemestre_bulletins_pdf?formsemestre_id=%s">ici avoir le classeur papier</a>)"""
|
||||
% (scu.ScoURL(), formsemestre_id)
|
||||
)
|
||||
if not parcours.UE_IS_MODULE:
|
||||
if modejury:
|
||||
H.append(
|
||||
'<input type="hidden" name="modejury" value="%s"></input>'
|
||||
% modejury
|
||||
)
|
||||
H.append(
|
||||
"""<input type="checkbox" name="hidemodules" value="1" onchange="document.f.submit()" """
|
||||
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
||||
)
|
||||
if hidemodules:
|
||||
H.append("checked")
|
||||
H.append(""" >cacher les modules</input>""")
|
||||
H.append(
|
||||
"""<input type="checkbox" name="hidebac" value="1" onchange="document.f.submit()" """
|
||||
)
|
||||
if hidebac:
|
||||
H.append("checked")
|
||||
H.append(""" >cacher bac</input>""")
|
||||
for (format, label) in (
|
||||
("html", "Tableau"),
|
||||
("evals", "Avec toutes les évaluations"),
|
||||
("xml", "Bulletins XML (obsolète)"),
|
||||
("json", "Bulletins JSON"),
|
||||
):
|
||||
if format == tabformat:
|
||||
selected = " selected"
|
||||
else:
|
||||
selected = ""
|
||||
H.append('<option value="%s"%s>%s</option>' % (format, selected, label))
|
||||
H.append("</select>")
|
||||
|
||||
H.append(
|
||||
f""" (cliquer sur un nom pour afficher son bulletin ou <a class="stdlink"
|
||||
href="{url_for('notes.formsemestre_bulletins_pdf',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)}">
|
||||
ici avoir le classeur papier</a>)
|
||||
"""
|
||||
)
|
||||
|
||||
data = do_formsemestre_recapcomplet(
|
||||
formsemestre_id,
|
||||
format=tabformat,
|
||||
@ -175,30 +169,31 @@ def formsemestre_recapcomplet(
|
||||
H.append(data)
|
||||
|
||||
if not isFile:
|
||||
H.append("</form>")
|
||||
H.append(
|
||||
"""<p><a class="stdlink" href="formsemestre_pvjury?formsemestre_id=%s">Voir les décisions du jury</a></p>"""
|
||||
% formsemestre_id
|
||||
)
|
||||
if sco_permissions_check.can_validate_sem(formsemestre_id):
|
||||
H.append("<p>")
|
||||
if modejury:
|
||||
H.append(
|
||||
"""<a class="stdlink" href="formsemestre_validation_auto?formsemestre_id=%s">Calcul automatique des décisions du jury</a></p>"""
|
||||
% (formsemestre_id,)
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
"""<a class="stdlink" href="formsemestre_recapcomplet?formsemestre_id=%s&modejury=1&hidemodules=1">Saisie des décisions du jury</a>"""
|
||||
% formsemestre_id
|
||||
)
|
||||
H.append("</p>")
|
||||
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
|
||||
if len(formsemestre.inscriptions) > 0:
|
||||
H.append("</form>")
|
||||
H.append(
|
||||
"""
|
||||
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
|
||||
"""
|
||||
"""<p><a class="stdlink" href="formsemestre_pvjury?formsemestre_id=%s">Voir les décisions du jury</a></p>"""
|
||||
% formsemestre_id
|
||||
)
|
||||
if sco_permissions_check.can_validate_sem(formsemestre_id):
|
||||
H.append("<p>")
|
||||
if modejury:
|
||||
H.append(
|
||||
"""<a class="stdlink" href="formsemestre_validation_auto?formsemestre_id=%s">Calcul automatique des décisions du jury</a></p>"""
|
||||
% (formsemestre_id,)
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
"""<a class="stdlink" href="formsemestre_recapcomplet?formsemestre_id=%s&modejury=1&hidemodules=1">Saisie des décisions du jury</a>"""
|
||||
% formsemestre_id
|
||||
)
|
||||
H.append("</p>")
|
||||
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
|
||||
H.append(
|
||||
"""
|
||||
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
|
||||
"""
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
# HTML or binary data ?
|
||||
if len(H) > 1:
|
||||
@ -223,20 +218,28 @@ def do_formsemestre_recapcomplet(
|
||||
force_publishing=True,
|
||||
):
|
||||
"""Calcule et renvoie le tableau récapitulatif."""
|
||||
data, filename, format = make_formsemestre_recapcomplet(
|
||||
formsemestre_id=formsemestre_id,
|
||||
format=format,
|
||||
hidemodules=hidemodules,
|
||||
hidebac=hidebac,
|
||||
xml_nodate=xml_nodate,
|
||||
modejury=modejury,
|
||||
sortcol=sortcol,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
disable_etudlink=disable_etudlink,
|
||||
rank_partition_id=rank_partition_id,
|
||||
force_publishing=force_publishing,
|
||||
)
|
||||
if format == "xml" or format == "html":
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if (format == "html" or format == "evals") and not modejury:
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
data, filename = gen_formsemestre_recapcomplet_html(
|
||||
formsemestre, res, include_evaluations=(format == "evals")
|
||||
)
|
||||
else:
|
||||
data, filename, format = make_formsemestre_recapcomplet(
|
||||
formsemestre_id=formsemestre_id,
|
||||
format=format,
|
||||
hidemodules=hidemodules,
|
||||
hidebac=hidebac,
|
||||
xml_nodate=xml_nodate,
|
||||
modejury=modejury,
|
||||
sortcol=sortcol,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
disable_etudlink=disable_etudlink,
|
||||
rank_partition_id=rank_partition_id,
|
||||
force_publishing=force_publishing,
|
||||
)
|
||||
# ---
|
||||
if format == "xml" or format == "html" or format == "evals":
|
||||
return data
|
||||
elif format == "csv":
|
||||
return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
|
||||
@ -248,12 +251,12 @@ def do_formsemestre_recapcomplet(
|
||||
js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
|
||||
)
|
||||
else:
|
||||
raise ValueError("unknown format %s" % format)
|
||||
raise ValueError(f"unknown format {format}")
|
||||
|
||||
|
||||
def make_formsemestre_recapcomplet(
|
||||
formsemestre_id=None,
|
||||
format="html", # html, xml, xls, xlsall, json
|
||||
format="html", # html, evals, xml, json
|
||||
hidemodules=False, # ne pas montrer les modules (ignoré en XML)
|
||||
hidebac=False, # pas de colonne Bac (ignoré en XML)
|
||||
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
|
||||
@ -404,7 +407,7 @@ def make_formsemestre_recapcomplet(
|
||||
gr_name = "Déf."
|
||||
is_dem[etudid] = False
|
||||
else:
|
||||
group = sco_groups.get_etud_main_group(etudid, sem)
|
||||
group = sco_groups.get_etud_main_group(etudid, formsemestre_id)
|
||||
gr_name = group["group_name"] or ""
|
||||
is_dem[etudid] = False
|
||||
if rank_partition_id:
|
||||
@ -593,66 +596,29 @@ def make_formsemestre_recapcomplet(
|
||||
<script type="text/javascript">
|
||||
function va_saisir(formsemestre_id, etudid) {
|
||||
loc = 'formsemestre_validation_etud_form?formsemestre_id='+formsemestre_id+'&etudid='+etudid;
|
||||
if (SORT_COLUMN_INDEX) {
|
||||
loc += '&sortcol=' + SORT_COLUMN_INDEX;
|
||||
}
|
||||
loc += '#etudid' + etudid;
|
||||
document.location=loc;
|
||||
}
|
||||
</script>
|
||||
<table class="notes_recapcomplet sortable" id="recapcomplet">
|
||||
<table class="notes_recapcomplet gt_table_searchable compact" id="recapcomplet">
|
||||
"""
|
||||
]
|
||||
if sortcol: # sort table using JS sorttable
|
||||
H.append(
|
||||
"""<script type="text/javascript">
|
||||
function resort_recap() {
|
||||
var clid = %d;
|
||||
// element <a place par sorttable (ligne de titre)
|
||||
lnk = document.getElementById("recap_trtit").childNodes[clid].childNodes[0];
|
||||
ts_resortTable(lnk,clid);
|
||||
// Scroll window:
|
||||
eid = document.location.hash;
|
||||
if (eid) {
|
||||
var eid = eid.substring(1); // remove #
|
||||
var e = document.getElementById(eid);
|
||||
if (e) {
|
||||
var y = e.offsetTop + e.offsetParent.offsetTop;
|
||||
window.scrollTo(0,y);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
addEvent(window, "load", resort_recap);
|
||||
</script>
|
||||
"""
|
||||
% (int(sortcol))
|
||||
)
|
||||
cells = '<tr class="recap_row_tit sortbottom" id="recap_trtit">'
|
||||
for i in range(len(F[0]) - 2):
|
||||
if i in ue_index:
|
||||
cls = "recap_tit_ue"
|
||||
else:
|
||||
cls = "recap_tit"
|
||||
if (
|
||||
i == 0 or F[0][i] == "classement"
|
||||
): # Rang: force tri numerique pour sortable
|
||||
cls = cls + " sortnumeric"
|
||||
if F[0][i] in cod2mod: # lien vers etat module
|
||||
modimpl = cod2mod[F[0][i]]
|
||||
cells += '<td class="%s"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s (%s)">%s</a></td>' % (
|
||||
cls,
|
||||
modimpl.id,
|
||||
modimpl.module.titre,
|
||||
sco_users.user_info(modimpl.responsable_id)["nomcomplet"],
|
||||
F[0][i],
|
||||
)
|
||||
else:
|
||||
cells += '<td class="%s">%s</td>' % (cls, F[0][i])
|
||||
if modejury:
|
||||
cells += '<td class="recap_tit">Décision</td>'
|
||||
ligne_titres = cells + "</tr>"
|
||||
H.append(ligne_titres) # titres
|
||||
|
||||
ligne_titres_head = _ligne_titres(
|
||||
ue_index, F, cod2mod, modejury, with_modules_links=False
|
||||
)
|
||||
ligne_titres_foot = _ligne_titres(
|
||||
ue_index, F, cod2mod, modejury, with_modules_links=True
|
||||
)
|
||||
|
||||
H.append("<thead>\n" + ligne_titres_head + "\n</thead>\n<tbody>\n")
|
||||
if disable_etudlink:
|
||||
etudlink = "%(name)s"
|
||||
else:
|
||||
@ -663,6 +629,9 @@ def make_formsemestre_recapcomplet(
|
||||
nblines = len(F) - 1
|
||||
for l in F[1:]:
|
||||
etudid = l[-1]
|
||||
if ir == nblines - 6:
|
||||
H.append("</tbody>")
|
||||
H.append("<tfoot>")
|
||||
if ir >= nblines - 6:
|
||||
# dernieres lignes:
|
||||
el = l[1]
|
||||
@ -674,7 +643,7 @@ def make_formsemestre_recapcomplet(
|
||||
"recap_row_nbeval",
|
||||
"recap_row_ects",
|
||||
)[ir - nblines + 6]
|
||||
cells = '<tr class="%s sortbottom">' % styl
|
||||
cells = f'<tr class="{styl} sortbottom">'
|
||||
else:
|
||||
el = etudlink % {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
@ -682,17 +651,23 @@ def make_formsemestre_recapcomplet(
|
||||
"name": l[1],
|
||||
}
|
||||
if ir % 2 == 0:
|
||||
cells = '<tr class="recap_row_even" id="etudid%s">' % etudid
|
||||
cells = f'<tr class="recap_row_even" id="etudid{etudid}">'
|
||||
else:
|
||||
cells = '<tr class="recap_row_odd" id="etudid%s">' % etudid
|
||||
cells = f'<tr class="recap_row_odd" id="etudid{etudid}">'
|
||||
ir += 1
|
||||
# XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ]
|
||||
# notes sans le NA:
|
||||
nsn = l[:-2] # copy
|
||||
for i in range(len(nsn)):
|
||||
for i, _ in enumerate(nsn):
|
||||
if nsn[i] == "NA":
|
||||
nsn[i] = "-"
|
||||
cells += '<td class="recap_col">%s</td>' % nsn[0] # rang
|
||||
try:
|
||||
order = int(nsn[0].split()[0])
|
||||
except:
|
||||
order = 99999
|
||||
cells += (
|
||||
f'<td class="recap_col" data-order="{order:05d}">{nsn[0]}</td>' # rang
|
||||
)
|
||||
cells += '<td class="recap_col">%s</td>' % el # nom etud (lien)
|
||||
if not hidebac:
|
||||
cells += '<td class="recap_col_bac">%s</td>' % nsn[2] # bac
|
||||
@ -760,7 +735,8 @@ def make_formsemestre_recapcomplet(
|
||||
cells += "</td>"
|
||||
H.append(cells + "</tr>")
|
||||
|
||||
H.append(ligne_titres)
|
||||
H.append(ligne_titres_foot)
|
||||
H.append("</tfoot>")
|
||||
H.append("</table>")
|
||||
|
||||
# Form pour choisir partition de classement:
|
||||
@ -828,6 +804,40 @@ def make_formsemestre_recapcomplet(
|
||||
raise ValueError("unknown format %s" % format)
|
||||
|
||||
|
||||
def _ligne_titres(ue_index, F, cod2mod, modejury, with_modules_links=True):
|
||||
"""Cellules de la ligne de titre (haut ou bas)"""
|
||||
cells = '<tr class="recap_row_tit sortbottom" id="recap_trtit">'
|
||||
for i in range(len(F[0]) - 2):
|
||||
if i in ue_index:
|
||||
cls = "recap_tit_ue"
|
||||
else:
|
||||
cls = "recap_tit"
|
||||
attr = f'class="{cls}"'
|
||||
if i == 0 or F[0][i] == "classement": # Rang: force tri numerique
|
||||
try:
|
||||
order = int(F[0][i].split()[0])
|
||||
except:
|
||||
order = 99999
|
||||
attr += f' data-order="{order:05d}"'
|
||||
if F[0][i] in cod2mod: # lien vers etat module
|
||||
modimpl = cod2mod[F[0][i]]
|
||||
if with_modules_links:
|
||||
href = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
else:
|
||||
href = ""
|
||||
cells += f"""<td {attr}><a href="{href}" title="{modimpl.module.titre} ({
|
||||
sco_users.user_info(modimpl.responsable_id)["nomcomplet"]})">{F[0][i]}</a></td>"""
|
||||
else:
|
||||
cells += f"<td {attr}>{F[0][i]}</td>"
|
||||
if modejury:
|
||||
cells += '<td class="recap_tit">Décision</td>'
|
||||
return cells + "</tr>"
|
||||
|
||||
|
||||
def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]:
|
||||
"""Liste des notes des evaluations completes de ce module
|
||||
(pour table xls avec evals)
|
||||
@ -994,3 +1004,99 @@ def formsemestres_bulletins(annee_scolaire):
|
||||
jslist.append(J)
|
||||
|
||||
return scu.sendJSON(jslist)
|
||||
|
||||
|
||||
def _gen_cell(key: str, row: dict, elt="td"):
|
||||
"html table cell"
|
||||
klass = row.get(f"_{key}_class")
|
||||
attrs = f'class="{klass}"' if klass else ""
|
||||
order = row.get(f"_{key}_order")
|
||||
if order:
|
||||
attrs += f' data-order="{order}"'
|
||||
content = row.get(key, "")
|
||||
target = row.get(f"_{key}_target")
|
||||
target_attrs = row.get(f"_{key}_target_attrs", "")
|
||||
if target or target_attrs: # avec lien
|
||||
href = f'href="{target}"' if target else ""
|
||||
content = f"<a {href} {target_attrs}>{content}</a>"
|
||||
return f"<{elt} {attrs}>{content}</{elt}>"
|
||||
|
||||
|
||||
def _gen_row(keys: list[str], row, elt="td"):
|
||||
klass = row.get("_tr_class")
|
||||
tr_class = f'class="{klass}"' if klass else ""
|
||||
return f'<tr {tr_class}>{"".join([_gen_cell(key, row, elt) for key in keys])}</tr>'
|
||||
|
||||
|
||||
def gen_formsemestre_recapcomplet_html(
|
||||
formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False
|
||||
):
|
||||
"""Construit table recap pour le BUT
|
||||
Cache le résultat pour le semestre.
|
||||
Return: data, filename
|
||||
"""
|
||||
filename = scu.sanitize_filename(
|
||||
f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
|
||||
)
|
||||
if include_evaluations:
|
||||
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
|
||||
else:
|
||||
table_html = sco_cache.TableRecapCache.get(formsemestre.id)
|
||||
if table_html is None:
|
||||
table_html = _gen_formsemestre_recapcomplet_html(
|
||||
formsemestre, res, include_evaluations, filename
|
||||
)
|
||||
if include_evaluations:
|
||||
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
|
||||
else:
|
||||
sco_cache.TableRecapCache.set(formsemestre.id, table_html)
|
||||
|
||||
return table_html, filename
|
||||
|
||||
|
||||
def _gen_formsemestre_recapcomplet_html(
|
||||
formsemestre: FormSemestre,
|
||||
res: NotesTableCompat,
|
||||
include_evaluations=False,
|
||||
filename: str = "",
|
||||
) -> str:
|
||||
"""Génère le html"""
|
||||
rows, footer_rows, titles, column_ids = res.get_table_recap(
|
||||
convert_values=True, include_evaluations=include_evaluations
|
||||
)
|
||||
if not rows:
|
||||
return (
|
||||
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>',
|
||||
"",
|
||||
)
|
||||
H = [
|
||||
f"""<div class="table_recap"><table class="table_recap {
|
||||
'apc' if formsemestre.formation.is_apc() else 'classic'}"
|
||||
data-filename="{filename}">"""
|
||||
]
|
||||
# header
|
||||
H.append(
|
||||
f"""
|
||||
<thead>
|
||||
{_gen_row(column_ids, titles, "th")}
|
||||
</thead>
|
||||
"""
|
||||
)
|
||||
# body
|
||||
H.append("<tbody>")
|
||||
for row in rows:
|
||||
H.append(f"{_gen_row(column_ids, row)}\n")
|
||||
H.append("</tbody>\n")
|
||||
# footer
|
||||
H.append("<tfoot>")
|
||||
idx_last = len(footer_rows) - 1
|
||||
for i, row in enumerate(footer_rows):
|
||||
H.append(f'{_gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
|
||||
H.append(
|
||||
"""
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
return "".join(H)
|
||||
|
@ -40,7 +40,7 @@ from flask import url_for, g, request
|
||||
import pydot
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -37,7 +37,7 @@ from flask import g, url_for, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
@ -272,6 +272,7 @@ def do_evaluation_upload_xls():
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=mod["moduleimpl_id"],
|
||||
_external=True,
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
@ -846,7 +847,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
||||
etuds = _get_sorted_etuds(E, etudids, formsemestre_id)
|
||||
for e in etuds:
|
||||
etudid = e["etudid"]
|
||||
groups = sco_groups.get_etud_groups(etudid, sem)
|
||||
groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
||||
grc = sco_groups.listgroups_abbrev(groups)
|
||||
|
||||
L.append(
|
||||
@ -1019,7 +1020,7 @@ def _get_sorted_etuds(E, etudids, formsemestre_id):
|
||||
{"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
)[0]
|
||||
# Groupes auxquels appartient cet étudiant:
|
||||
e["groups"] = sco_groups.get_etud_groups(etudid, sem)
|
||||
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
||||
|
||||
# Information sur absence (tenant compte de la demi-journée)
|
||||
jour_iso = ndb.DateDMYtoISO(E["jour"])
|
||||
@ -1270,6 +1271,7 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=M["moduleimpl_id"],
|
||||
_external=True,
|
||||
)
|
||||
result = {"nbchanged": 0} # JSON
|
||||
# Check access: admin, respformation, or responsable_id
|
||||
|
@ -43,7 +43,7 @@ import flask
|
||||
from flask import g
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
@ -89,7 +89,7 @@ class SemSet(dict):
|
||||
if semset_id: # read existing set
|
||||
L = semset_list(cnx, args={"semset_id": semset_id})
|
||||
if not L:
|
||||
raise ValueError("invalid semset_id %s" % semset_id)
|
||||
raise ScoValueError(f"Ensemble inexistant ! (semset {semset_id})")
|
||||
self["title"] = L[0]["title"]
|
||||
self["annee_scolaire"] = L[0]["annee_scolaire"]
|
||||
self["sem_id"] = L[0]["sem_id"]
|
||||
|
@ -38,7 +38,7 @@ import http
|
||||
from flask import g, url_for
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -378,7 +378,7 @@ def _trombino_pdf(groups_infos):
|
||||
# Build document
|
||||
document = BaseDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScolarsPageTemplate(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
||||
)
|
||||
@ -458,7 +458,7 @@ def _listeappel_photos_pdf(groups_infos):
|
||||
# Build document
|
||||
document = BaseDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScolarsPageTemplate(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
||||
)
|
||||
|
@ -33,20 +33,23 @@
|
||||
import io
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.colors import black
|
||||
from reportlab.lib.pagesizes import A4, A3
|
||||
from reportlab.lib import styles
|
||||
from reportlab.lib.pagesizes import landscape
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import KeepInFrame, Paragraph, Table, TableStyle
|
||||
from reportlab.platypus.doctemplate import BaseDocTemplate
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_trombino
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
||||
from app.scodoc.sco_pdf import *
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_pdf import SU, ScoDocPageTemplate
|
||||
|
||||
# Paramétrage de l'aspect graphique:
|
||||
PHOTOWIDTH = 2.8 * cm
|
||||
@ -55,7 +58,7 @@ N_PER_ROW = 5
|
||||
|
||||
|
||||
def pdf_trombino_tours(
|
||||
group_ids=[], # liste des groupes à afficher
|
||||
group_ids=(), # liste des groupes à afficher
|
||||
formsemestre_id=None, # utilisé si pas de groupes selectionné
|
||||
):
|
||||
"""Generation du trombinoscope en fichier PDF"""
|
||||
@ -66,7 +69,6 @@ def pdf_trombino_tours(
|
||||
|
||||
DeptName = sco_preferences.get_preference("DeptName")
|
||||
DeptFullName = sco_preferences.get_preference("DeptFullName")
|
||||
UnivName = sco_preferences.get_preference("UnivName")
|
||||
InstituteName = sco_preferences.get_preference("InstituteName")
|
||||
# Generate PDF page
|
||||
StyleSheet = styles.getSampleStyleSheet()
|
||||
@ -74,7 +76,11 @@ def pdf_trombino_tours(
|
||||
T = Table(
|
||||
[
|
||||
[Paragraph(SU(InstituteName), StyleSheet["Heading3"])],
|
||||
[Paragraph(SU("Département " + DeptFullName), StyleSheet["Heading3"])],
|
||||
[
|
||||
Paragraph(
|
||||
SU("Département " + DeptFullName or "(?)"), StyleSheet["Heading3"]
|
||||
)
|
||||
],
|
||||
[
|
||||
Paragraph(
|
||||
SU("Date ............ / ............ / ......................"),
|
||||
@ -139,9 +145,7 @@ def pdf_trombino_tours(
|
||||
|
||||
for group_id in groups_infos.group_ids:
|
||||
if group_id != "None":
|
||||
members, group, group_tit, sem, nbdem = sco_groups.get_group_infos(
|
||||
group_id, "I"
|
||||
)
|
||||
members, _, group_tit, sem, _ = sco_groups.get_group_infos(group_id, "I")
|
||||
groups += " %s" % group_tit
|
||||
L = []
|
||||
currow = []
|
||||
@ -176,7 +180,9 @@ def pdf_trombino_tours(
|
||||
n = 1
|
||||
for m in members:
|
||||
img = sco_trombino._get_etud_platypus_image(m, image_width=PHOTOWIDTH)
|
||||
etud_main_group = sco_groups.get_etud_main_group(m["etudid"], sem)
|
||||
etud_main_group = sco_groups.get_etud_main_group(
|
||||
m["etudid"], sem["formsemestre_id"]
|
||||
)
|
||||
if group_id != etud_main_group["group_id"]:
|
||||
text_group = " (" + etud_main_group["group_name"] + ")"
|
||||
else:
|
||||
@ -264,7 +270,7 @@ def pdf_trombino_tours(
|
||||
filename = "trombino-%s-%s.pdf" % (DeptName, groups_infos.groups_filename)
|
||||
document = BaseDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
ScolarsPageTemplate(
|
||||
ScoDocPageTemplate(
|
||||
document,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
@ -282,14 +288,14 @@ def pdf_trombino_tours(
|
||||
|
||||
|
||||
def pdf_feuille_releve_absences(
|
||||
group_ids=[], # liste des groupes à afficher
|
||||
group_ids=(), # liste des groupes à afficher
|
||||
formsemestre_id=None, # utilisé si pas de groupes selectionné
|
||||
):
|
||||
"""Generation de la feuille d'absence en fichier PDF, avec photos"""
|
||||
|
||||
NB_CELL_AM = sco_preferences.get_preference("feuille_releve_abs_AM")
|
||||
NB_CELL_PM = sco_preferences.get_preference("feuille_releve_abs_PM")
|
||||
COLWIDTH = 0.85 * cm
|
||||
col_width = 0.85 * cm
|
||||
if sco_preferences.get_preference("feuille_releve_abs_samedi"):
|
||||
days = sco_abs.DAYNAMES[:6] # Lundi, ..., Samedi
|
||||
else:
|
||||
@ -303,7 +309,6 @@ def pdf_feuille_releve_absences(
|
||||
|
||||
DeptName = sco_preferences.get_preference("DeptName")
|
||||
DeptFullName = sco_preferences.get_preference("DeptFullName")
|
||||
UnivName = sco_preferences.get_preference("UnivName")
|
||||
InstituteName = sco_preferences.get_preference("InstituteName")
|
||||
# Generate PDF page
|
||||
StyleSheet = styles.getSampleStyleSheet()
|
||||
@ -321,7 +326,8 @@ def pdf_feuille_releve_absences(
|
||||
],
|
||||
[
|
||||
Paragraph(
|
||||
SU("Département " + DeptFullName), StyleSheet["Heading3"]
|
||||
SU("Département " + (DeptFullName or "(?)")),
|
||||
StyleSheet["Heading3"],
|
||||
),
|
||||
"",
|
||||
],
|
||||
@ -335,7 +341,7 @@ def pdf_feuille_releve_absences(
|
||||
currow = [""] * (NB_CELL_AM + 1 + NB_CELL_PM + 1)
|
||||
elem_day = Table(
|
||||
[currow],
|
||||
colWidths=([COLWIDTH] * (NB_CELL_AM + 1 + NB_CELL_PM + 1)),
|
||||
colWidths=([col_width] * (NB_CELL_AM + 1 + NB_CELL_PM + 1)),
|
||||
style=TableStyle(
|
||||
[
|
||||
("GRID", (0, 0), (NB_CELL_AM - 1, 0), 0.25, black),
|
||||
@ -357,7 +363,7 @@ def pdf_feuille_releve_absences(
|
||||
|
||||
elem_week = Table(
|
||||
W,
|
||||
colWidths=([COLWIDTH * (NB_CELL_AM + 1 + NB_CELL_PM + 1)] * nb_days),
|
||||
colWidths=([col_width * (NB_CELL_AM + 1 + NB_CELL_PM + 1)] * nb_days),
|
||||
style=TableStyle(
|
||||
[
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
@ -373,7 +379,7 @@ def pdf_feuille_releve_absences(
|
||||
|
||||
elem_day_name = Table(
|
||||
[currow],
|
||||
colWidths=([COLWIDTH * (NB_CELL_AM + 1 + NB_CELL_PM + 1)] * nb_days),
|
||||
colWidths=([col_width * (NB_CELL_AM + 1 + NB_CELL_PM + 1)] * nb_days),
|
||||
style=TableStyle(
|
||||
[
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
@ -385,9 +391,7 @@ def pdf_feuille_releve_absences(
|
||||
)
|
||||
|
||||
for group_id in groups_infos.group_ids:
|
||||
members, group, group_tit, sem, nbdem = sco_groups.get_group_infos(
|
||||
group_id, "I"
|
||||
)
|
||||
members, _, group_tit, _, _ = sco_groups.get_group_infos(group_id, "I")
|
||||
L = []
|
||||
|
||||
currow = [
|
||||
@ -424,7 +428,10 @@ def pdf_feuille_releve_absences(
|
||||
T = Table(
|
||||
L,
|
||||
colWidths=(
|
||||
[5.0 * cm, (COLWIDTH * (NB_CELL_AM + 1 + NB_CELL_PM + 1) * nb_days)]
|
||||
[
|
||||
5.0 * cm,
|
||||
(col_width * (NB_CELL_AM + 1 + NB_CELL_PM + 1) * nb_days),
|
||||
]
|
||||
),
|
||||
style=TableStyle(
|
||||
[
|
||||
@ -460,7 +467,7 @@ def pdf_feuille_releve_absences(
|
||||
else:
|
||||
document = BaseDocTemplate(report, pagesize=taille)
|
||||
document.addPageTemplates(
|
||||
ScolarsPageTemplate(
|
||||
ScoDocPageTemplate(
|
||||
document,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user