forked from ScoDoc/ScoDoc
Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into entreprises
This commit is contained in:
commit
b0a95b73c1
@ -10,10 +10,11 @@ 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
|
||||
from flask import abort, has_request_context, jsonify
|
||||
from flask import abort, flash, has_request_context, jsonify
|
||||
from flask import render_template
|
||||
from flask.logging import default_handler
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
@ -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,11 +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(404, 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)
|
||||
@ -337,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")
|
||||
@ -460,15 +489,12 @@ from app.models import Departement
|
||||
from app.scodoc import notesdb as ndb, sco_preferences
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
# admin_role = Role.query.filter_by(name="SuperAdmin").first()
|
||||
# if admin_role:
|
||||
# admin = (
|
||||
# User.query.join(UserRole)
|
||||
# .filter((UserRole.user_id == User.id) & (UserRole.role_id == admin_role.id))
|
||||
# .first()
|
||||
# )
|
||||
# else:
|
||||
# click.echo(
|
||||
# "Warning: user database not initialized !\n (use: flask user-db-init)"
|
||||
# )
|
||||
# admin = None
|
||||
|
||||
def scodoc_flash_status_messages():
|
||||
"""Should be called on each page: flash messages indicating specific ScoDoc status"""
|
||||
email_test_mode_address = sco_preferences.get_preference("email_test_mode_address")
|
||||
if email_test_mode_address:
|
||||
flash(
|
||||
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
||||
category="warning",
|
||||
)
|
||||
|
@ -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"))
|
||||
|
@ -7,16 +7,19 @@
|
||||
"""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, formsemestre
|
||||
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
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
||||
from app.scodoc.sco_utils import fmt_note
|
||||
|
||||
|
||||
@ -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,
|
||||
@ -80,11 +80,16 @@ class BulletinBUT:
|
||||
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
|
||||
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
||||
else fmt_note(0.0),
|
||||
"malus": res.malus[ue.id][etud.id],
|
||||
"malus": fmt_note(res.malus[ue.id][etud.id]),
|
||||
"capitalise": None, # "AAAA-MM-JJ" TODO #sco92
|
||||
"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]
|
||||
@ -111,9 +116,10 @@ class BulletinBUT:
|
||||
d["modules"] = self.etud_mods_results(etud, modimpls_spo)
|
||||
return d
|
||||
|
||||
def etud_mods_results(self, etud, modimpls) -> dict:
|
||||
def etud_mods_results(self, etud, modimpls, version="long") -> dict:
|
||||
"""dict synthèse résultats des modules indiqués,
|
||||
avec évaluations de chacun."""
|
||||
avec évaluations de chacun (sauf si version == "short")
|
||||
"""
|
||||
res = self.res
|
||||
d = {}
|
||||
# etud_idx = self.etud_index[etud.id]
|
||||
@ -154,12 +160,14 @@ class BulletinBUT:
|
||||
"evaluations": [
|
||||
self.etud_eval_results(etud, e)
|
||||
for e in modimpl.evaluations
|
||||
if e.visibulletin
|
||||
if (e.visibulletin or version == "long")
|
||||
and (
|
||||
modimpl_results.evaluations_etat[e.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
)
|
||||
],
|
||||
]
|
||||
if version != "short"
|
||||
else [],
|
||||
}
|
||||
return d
|
||||
|
||||
@ -168,14 +176,23 @@ 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()
|
||||
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,
|
||||
"date": e.jour.isoformat() if e.jour else None,
|
||||
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
|
||||
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
|
||||
"coef": e.coefficient,
|
||||
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
|
||||
"coef": fmt_note(e.coefficient),
|
||||
"poids": poids,
|
||||
"note": {
|
||||
"value": fmt_note(
|
||||
eval_notes[etud.id],
|
||||
@ -205,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
|
||||
]
|
||||
@ -217,14 +235,22 @@ class BulletinBUT:
|
||||
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
|
||||
|
||||
def bulletin_etud(
|
||||
self, etud: Identite, formsemestre: FormSemestre, force_publishing=False
|
||||
self,
|
||||
etud: Identite,
|
||||
formsemestre: FormSemestre,
|
||||
force_publishing=False,
|
||||
version="long",
|
||||
) -> dict:
|
||||
"""Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
|
||||
- version:
|
||||
"long", "selectedevals": toutes les infos (notes des évaluations)
|
||||
"short" : ne descend pas plus bas que les modules.
|
||||
|
||||
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
||||
(bulletins non publiés).
|
||||
"""
|
||||
res = self.res
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||
d = {
|
||||
@ -257,39 +283,55 @@ 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(etud, res.ressources),
|
||||
"saes": self.etud_mods_results(etud, res.saes),
|
||||
"ressources": self.etud_mods_results(
|
||||
etud, res.ressources, version=version
|
||||
),
|
||||
"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,
|
||||
},
|
||||
@ -317,20 +359,45 @@ class BulletinBUT:
|
||||
|
||||
return d
|
||||
|
||||
def bulletin_etud_complet(self, etud: Identite) -> dict:
|
||||
"""Bulletin dict complet avec toutes les infos pour les bulletins pdf"""
|
||||
d = self.bulletin_etud(etud, self.res.formsemestre, force_publishing=True)
|
||||
def bulletin_etud_complet(self, etud: Identite, version="long") -> dict:
|
||||
"""Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
|
||||
Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
|
||||
"""
|
||||
d = self.bulletin_etud(
|
||||
etud, self.res.formsemestre, version=version, force_publishing=True
|
||||
)
|
||||
d["etudid"] = etud.id
|
||||
d["etud"] = d["etudiant"]
|
||||
d["etud"]["nomprenom"] = etud.nomprenom
|
||||
d.update(self.res.sem)
|
||||
etud_etat = self.res.get_etud_etat(etud.id)
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
self.res.get_etud_etat(etud.id),
|
||||
etud_etat,
|
||||
self.prefs,
|
||||
decision_sem=d["semestre"].get("decision_sem"),
|
||||
)
|
||||
if etud_etat == scu.DEMISSION:
|
||||
d["demission"] = "(Démission)"
|
||||
elif etud_etat == DEF:
|
||||
d["demission"] = "(Défaillant)"
|
||||
else:
|
||||
d["demission"] = ""
|
||||
|
||||
# --- Absences
|
||||
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
|
||||
|
||||
# --- Decision Jury
|
||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||
etud.id,
|
||||
self.res.formsemestre.id,
|
||||
format="html",
|
||||
show_date_inscr=self.prefs["bul_show_date_inscr"],
|
||||
show_decisions=self.prefs["bul_show_decision"],
|
||||
show_uevalid=self.prefs["bul_show_uevalid"],
|
||||
show_mention=self.prefs["bul_show_mention"],
|
||||
)
|
||||
|
||||
d.update(infos)
|
||||
# --- Rangs
|
||||
d[
|
||||
"rang_nt"
|
||||
@ -341,5 +408,6 @@ class BulletinBUT:
|
||||
d.update(
|
||||
sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
|
||||
)
|
||||
# XXX TODO A COMPLETER ?
|
||||
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
|
||||
|
||||
return d
|
||||
|
@ -6,20 +6,13 @@
|
||||
|
||||
"""Génération bulletin BUT au format PDF standard
|
||||
"""
|
||||
from reportlab.platypus import Paragraph, Spacer
|
||||
|
||||
import datetime
|
||||
from app.scodoc.sco_pdf import blue, cm, mm
|
||||
|
||||
from flask import url_for, g
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
from app.scodoc import gen_tables
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.sco_utils import fmt_note
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
|
||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||
|
||||
@ -31,6 +24,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
"""
|
||||
|
||||
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
|
||||
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
|
||||
@ -38,31 +34,38 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
- en HTML: une chaine
|
||||
- en PDF: une liste d'objets PLATYPUS (eg instance de Table).
|
||||
"""
|
||||
formsemestre_id = self.infos["formsemestre_id"]
|
||||
(
|
||||
synth_col_keys,
|
||||
synth_P,
|
||||
synth_pdf_style,
|
||||
synth_col_widths,
|
||||
) = self.but_table_synthese()
|
||||
#
|
||||
table_synthese = gen_tables.GenTable(
|
||||
rows=synth_P,
|
||||
columns_ids=synth_col_keys,
|
||||
pdf_table_style=synth_pdf_style,
|
||||
pdf_col_widths=[synth_col_widths[k] for k in synth_col_keys],
|
||||
preferences=self.preferences,
|
||||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
)
|
||||
# Ici on ajoutera table des ressources, tables des UE
|
||||
# TODO
|
||||
tables_infos = [
|
||||
# ---- TABLE SYNTHESE UES
|
||||
self.but_table_synthese_ues(),
|
||||
]
|
||||
if self.version != "short":
|
||||
tables_infos += [
|
||||
# ---- TABLE RESSOURCES
|
||||
self.but_table_ressources(),
|
||||
# ---- TABLE SAE
|
||||
self.but_table_saes(),
|
||||
]
|
||||
objects = []
|
||||
for i, (col_keys, rows, pdf_style, col_widths) in enumerate(tables_infos):
|
||||
table = gen_tables.GenTable(
|
||||
rows=rows,
|
||||
columns_ids=col_keys,
|
||||
pdf_table_style=pdf_style,
|
||||
pdf_col_widths=[col_widths[k] for k in col_keys],
|
||||
preferences=self.preferences,
|
||||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
)
|
||||
table_objects = table.gen(format=format)
|
||||
objects += table_objects
|
||||
# objects += [KeepInFrame(0, 0, table_objects, mode="shrink")]
|
||||
if i != 2:
|
||||
objects.append(Spacer(1, 6 * mm))
|
||||
|
||||
# XXX à modifier pour générer plusieurs tables:
|
||||
return table_synthese.gen(format=format)
|
||||
return objects
|
||||
|
||||
def but_table_synthese(self):
|
||||
def but_table_synthese_ues(self, title_bg=(182, 235, 255)):
|
||||
"""La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
|
||||
et leurs coefs.
|
||||
Renvoie: colkeys, P, pdf_style, colWidths
|
||||
@ -76,41 +79,270 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
"moyenne": 2 * cm,
|
||||
"coef": 2 * cm,
|
||||
}
|
||||
P = [] # elems pour générer table avec gen_table (liste de dicts)
|
||||
col_keys = ["titre", "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": [],
|
||||
}
|
||||
P.append(t)
|
||||
# 2eme ligne titre UE (bonus/malus/ects)
|
||||
t = {
|
||||
"titre": "",
|
||||
"moyenne": f"""Bonus: {ue['bonus']} - Malus: {
|
||||
ue["malus"]} - ECTS: {ue["ECTS"]["acquis"]} / {ue["ECTS"]["total"]}""",
|
||||
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": 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),
|
||||
(
|
||||
"LINEBELOW",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
blue,
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
col_keys = ["titre", "coef", "moyenne"] # noms des colonnes à afficher
|
||||
for ue_acronym, ue in self.infos["ues"].items():
|
||||
self.ue_rows(rows, ue_acronym, ue, title_bg)
|
||||
# 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 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
|
||||
"""
|
||||
return self.bul_table_modules(
|
||||
mod_type="ressources", title="Ressources", title_bg=(248, 200, 68)
|
||||
)
|
||||
|
||||
def but_table_saes(self):
|
||||
"table des SAEs"
|
||||
return self.bul_table_modules(
|
||||
mod_type="saes",
|
||||
title="Situations d'apprentissage et d'évaluation",
|
||||
title_bg=(198, 255, 171),
|
||||
)
|
||||
|
||||
def bul_table_modules(self, mod_type=None, title="", title_bg=(248, 200, 68)):
|
||||
"""Table ressources ou SAEs
|
||||
- colkeys: nom des colonnes de la table (clés)
|
||||
- P : table (liste de dicts de chaines de caracteres)
|
||||
- pdf_style : commandes table Platypus
|
||||
- largeurs de colonnes pour PDF
|
||||
"""
|
||||
# UE à utiliser pour les poids (# colonne/UE)
|
||||
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:
|
||||
col_widths = {
|
||||
"titre": None,
|
||||
# "poids": None,
|
||||
"moyenne": 2 * cm,
|
||||
"coef": 2 * cm,
|
||||
}
|
||||
for ue_acro in ue_acros:
|
||||
col_widths[ue_acro] = 12 * mm # largeur col. poids
|
||||
|
||||
title_bg = tuple(x / 255.0 for x in title_bg)
|
||||
# elems pour générer table avec gen_table (liste de dicts)
|
||||
# Ligne de titres
|
||||
t = {
|
||||
"titre": title,
|
||||
# "_titre_colspan": 1 + len(ue_acros),
|
||||
"moyenne": "Note/20",
|
||||
"coef": "Coef.",
|
||||
"_coef_pdf": Paragraph("<para align=right><i>Coef.</i></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),
|
||||
(
|
||||
"LINEBELOW",
|
||||
(0, 0),
|
||||
(-1, 0),
|
||||
self.PDF_LINEWIDTH,
|
||||
blue,
|
||||
),
|
||||
],
|
||||
}
|
||||
for ue_acro in ue_acros:
|
||||
t[ue_acro] = Paragraph(
|
||||
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():
|
||||
# 1er ligne titre module
|
||||
t = {
|
||||
"titre": f"{mod_code} - {mod['titre']}",
|
||||
"_titre_colspan": 2 + len(ue_acros),
|
||||
"_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),
|
||||
],
|
||||
}
|
||||
P.append(t)
|
||||
rows.append(t)
|
||||
# Evaluations:
|
||||
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, P, pdf_style, col_widths
|
||||
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)
|
||||
|
@ -72,7 +72,7 @@ def bulletin_but_xml_compat(
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||
nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT]
|
||||
# etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
# etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||
etat_inscription = results.formsemestre.etuds_inscriptions[etudid].etat
|
||||
if (not formsemestre.bul_hide_xml) or force_publishing:
|
||||
published = 1
|
||||
|
@ -89,7 +89,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 +200,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 +221,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 0.0
|
||||
np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)
|
||||
|
||||
self.bonus_additif(bonus_moy_arr)
|
||||
|
||||
@ -510,14 +508,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):
|
||||
@ -754,8 +752,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 +784,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 +852,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.
|
||||
|
||||
@ -824,6 +880,27 @@ class BonusRoanne(BonusSportAdditif):
|
||||
proportion_point = 1
|
||||
|
||||
|
||||
class BonusStBrieuc(BonusSportAdditif):
|
||||
"""IUT de Saint Brieuc
|
||||
|
||||
Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE:
|
||||
<ul>
|
||||
<li>Bonus = (S - 10)/20</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
# Utilisé aussi par St Malo, voir plus bas
|
||||
name = "bonus_iut_stbrieuc"
|
||||
displayed_name = "IUT de Saint-Brieuc"
|
||||
proportion_point = 1 / 20.0
|
||||
classic_use_bonus_ues = True
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus"""
|
||||
if self.formsemestre.semestre_id % 2 == 0:
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
|
||||
|
||||
class BonusStDenis(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT Saint-Denis
|
||||
|
||||
@ -841,6 +918,64 @@ 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.
|
||||
|
||||
<ul>
|
||||
<li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
|
||||
La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
|
||||
</li>
|
||||
<li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE.
|
||||
</li>
|
||||
<li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points
|
||||
sur chaque UE.
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
name = "bonus_tarbes"
|
||||
displayed_name = "IUT de Tazrbes"
|
||||
seuil_moy_gen = 10.0
|
||||
proportion_point = 1 / 30.0
|
||||
classic_use_bonus_ues = True
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus"""
|
||||
# Prend la note de chaque modimpl, sans considération d'UE
|
||||
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
|
||||
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
|
||||
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
|
||||
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
|
||||
ues = self.formsemestre.query_ues(with_sport=False).all()
|
||||
ues_idx = [ue.id for ue in ues]
|
||||
|
||||
if self.formsemestre.formation.is_apc(): # --- BUT
|
||||
bonus_moy_arr = np.where(
|
||||
note_bonus_max > self.seuil_moy_gen,
|
||||
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
|
||||
0.0,
|
||||
)
|
||||
self.bonus_ues = pd.DataFrame(
|
||||
np.stack([bonus_moy_arr] * len(ues)).T,
|
||||
index=self.etuds_idx,
|
||||
columns=ues_idx,
|
||||
dtype=float,
|
||||
)
|
||||
|
||||
|
||||
class BonusTours(BonusDirect):
|
||||
"""Calcul bonus sport & culture IUT Tours.
|
||||
|
||||
|
@ -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
|
||||
|
@ -197,6 +197,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
if len(modimpls_notes):
|
||||
cube = notes_sem_assemble_cube(modimpls_notes)
|
||||
|
@ -14,8 +14,10 @@ from app import log
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.comp.res_common 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 import sco_preferences
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
|
||||
|
||||
@ -112,6 +114,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 +144,16 @@ 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) -> list[ModuleImpl]:
|
||||
"""Liste des modimpl ayant des coefs non nuls vers cette UE
|
||||
et auxquels l'étudiant est inscrit.
|
||||
"""
|
||||
# sert pour l'affichage ou non de l'UE sur le bulletin
|
||||
coefs = self.modimpl_coefs_df # row UE, cols modimpl
|
||||
return [
|
||||
modimpl
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
if (coefs[modimpl.id][ue_id] != 0)
|
||||
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||
]
|
||||
|
@ -15,7 +15,7 @@ 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 import moy_mat, moy_mod, moy_sem, moy_ue, inscr_mod
|
||||
from app.comp.res_common import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
@ -35,6 +35,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
"modimpl_coefs",
|
||||
"modimpl_idx",
|
||||
"sem_matrix",
|
||||
"mod_rangs",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
@ -142,11 +143,32 @@ 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)
|
||||
"""
|
||||
return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI")
|
||||
try:
|
||||
if self.modimpl_inscr_df[moduleimpl_id][etudid]:
|
||||
return self.modimpls_results[moduleimpl_id].etuds_moy_module[etudid]
|
||||
except KeyError:
|
||||
pass
|
||||
return "NI"
|
||||
|
||||
def get_mod_stats(self, moduleimpl_id: int) -> dict:
|
||||
"""Stats sur les notes obtenues dans un modimpl"""
|
||||
|
@ -65,6 +65,9 @@ class ResultatsSemestre(ResultatsCache):
|
||||
self.moyennes_matieres = {}
|
||||
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(formsemestre='{self.formsemestre}')>"
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule toutes les moyennes"
|
||||
# voir ce qui est chargé / calculé ici et dans les sous-classes
|
||||
@ -130,7 +133,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
- En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules
|
||||
du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre.
|
||||
|
||||
- En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont
|
||||
- En classique: toutes les UEs des modimpls auxquels l'étudiant est inscrit sont
|
||||
susceptibles d'être validées.
|
||||
|
||||
Les UE "bonus" (sport) ne sont jamais "validables".
|
||||
@ -149,7 +152,9 @@ class ResultatsSemestre(ResultatsCache):
|
||||
return ues
|
||||
|
||||
def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]:
|
||||
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit"""
|
||||
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit.
|
||||
Utile en formations classiques, surchargée pour le BUT.
|
||||
"""
|
||||
# sert pour l'affichage ou non de l'UE sur le bulletin
|
||||
return [
|
||||
modimpl
|
||||
@ -175,7 +180,6 @@ class ResultatsSemestre(ResultatsCache):
|
||||
if not self.validations:
|
||||
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
||||
ue_capitalisees = self.validations.ue_capitalisees
|
||||
ue_by_code = {}
|
||||
for etudid in ue_capitalisees.index:
|
||||
recompute_mg = False
|
||||
# ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"])
|
||||
@ -365,6 +369,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
self.bonus_ues = None # virtuel
|
||||
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"
|
||||
|
65
app/email.py
65
app/email.py
@ -1,8 +1,17 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
from threading import Thread
|
||||
from flask import current_app
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_mail import Message
|
||||
|
||||
from app import mail
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
def send_async_email(app, msg):
|
||||
@ -11,20 +20,66 @@ def send_async_email(app, msg):
|
||||
|
||||
|
||||
def send_email(
|
||||
subject: str, sender: str, recipients: list, text_body: str, html_body=""
|
||||
subject: str,
|
||||
sender: str,
|
||||
recipients: list,
|
||||
text_body: str,
|
||||
html_body="",
|
||||
bcc=(),
|
||||
attachments=(),
|
||||
):
|
||||
"""
|
||||
Send an email
|
||||
Send an email. _All_ ScoDoc mails SHOULD be sent using this function.
|
||||
|
||||
If html_body is specified, build a multipart message with HTML content,
|
||||
else send a plain text email.
|
||||
|
||||
attachements: list of dict { 'filename', 'mimetype', 'data' }
|
||||
"""
|
||||
msg = Message(subject, sender=sender, recipients=recipients)
|
||||
msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc)
|
||||
msg.body = text_body
|
||||
msg.html = html_body
|
||||
if attachments:
|
||||
for attachment in attachments:
|
||||
msg.attach(
|
||||
attachment["filename"], attachment["mimetype"], attachment["data"]
|
||||
)
|
||||
|
||||
send_message(msg)
|
||||
|
||||
|
||||
def send_message(msg):
|
||||
def send_message(msg: Message):
|
||||
"""Send a message.
|
||||
All ScoDoc emails MUST be sent by this function.
|
||||
|
||||
In mail debug mode, addresses are discarded and all mails are sent to the
|
||||
specified debugging address.
|
||||
"""
|
||||
if hasattr(g, "scodoc_dept"):
|
||||
# on est dans un département, on peut accéder aux préférences
|
||||
email_test_mode_address = sco_preferences.get_preference(
|
||||
"email_test_mode_address"
|
||||
)
|
||||
if email_test_mode_address:
|
||||
# Mode spécial test: remplace les adresses de destination
|
||||
orig_to = msg.recipients
|
||||
orig_cc = msg.cc
|
||||
orig_bcc = msg.bcc
|
||||
msg.recipients = [email_test_mode_address]
|
||||
msg.cc = None
|
||||
msg.bcc = None
|
||||
msg.subject = "[TEST SCODOC] " + msg.subject
|
||||
msg.body = (
|
||||
f"""--- Message ScoDoc dérouté pour tests ---
|
||||
Adresses d'origine:
|
||||
to : {orig_to}
|
||||
cc : {orig_cc}
|
||||
bcc: {orig_bcc}
|
||||
---
|
||||
\n\n"""
|
||||
+ msg.body
|
||||
)
|
||||
|
||||
Thread(
|
||||
target=send_async_email, args=(current_app._get_current_object(), msg)
|
||||
).start()
|
||||
|
@ -4,12 +4,14 @@
|
||||
et données rattachées (adresses, annotations, ...)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from flask import abort, url_for
|
||||
from flask import g, request
|
||||
import sqlalchemy
|
||||
from sqlalchemy import desc, text
|
||||
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app import models
|
||||
|
||||
from app.scodoc import notesdb as ndb
|
||||
@ -82,6 +84,11 @@ class Identite(db.Model):
|
||||
return scu.suppress_accents(s)
|
||||
return s
|
||||
|
||||
@property
|
||||
def e(self):
|
||||
"terminaison en français: 'ne', '', 'ou '(e)'"
|
||||
return {"M": "", "F": "e"}.get(self.civilite, "(e)")
|
||||
|
||||
def nom_disp(self) -> str:
|
||||
"Nom à afficher"
|
||||
if self.nom_usuel:
|
||||
@ -123,7 +130,7 @@ class Identite(db.Model):
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"Le mail associé à la première adrese de l'étudiant, ou None"
|
||||
return self.adresses[0].email or None if self.adresses.count() > 0 else None
|
||||
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
|
||||
|
||||
def to_dict_scodoc7(self):
|
||||
"""Représentation dictionnaire,
|
||||
@ -134,31 +141,41 @@ class Identite(db.Model):
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["etudid"] = self.id
|
||||
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
||||
e["ne"] = {"M": "", "F": "ne"}.get(self.civilite, "(e)")
|
||||
e["ne"] = self.e
|
||||
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,
|
||||
"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):
|
||||
@ -172,6 +189,23 @@ class Identite(db.Model):
|
||||
]
|
||||
return r[0] if r else None
|
||||
|
||||
def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]:
|
||||
"""Liste des inscriptions à des semestres _courants_
|
||||
(il est rare qu'il y en ai plus d'une, mais c'est possible).
|
||||
Triées par date de début de semestre décroissante (le plus récent en premier).
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
return (
|
||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||
.filter(
|
||||
FormSemestreInscription.etudid == self.id,
|
||||
text("date_debut < now() and date_fin > now()"),
|
||||
)
|
||||
.order_by(desc(FormSemestre.date_debut))
|
||||
.all()
|
||||
)
|
||||
|
||||
def inscription_courante_date(self, date_debut, date_fin):
|
||||
"""La première inscription à un formsemestre incluant la
|
||||
période [date_debut, date_fin]
|
||||
@ -183,8 +217,8 @@ class Identite(db.Model):
|
||||
]
|
||||
return r[0] if r else None
|
||||
|
||||
def etat_inscription(self, formsemestre_id):
|
||||
"""etat de l'inscription de cet étudiant au semestre:
|
||||
def inscription_etat(self, formsemestre_id):
|
||||
"""État de l'inscription de cet étudiant au semestre:
|
||||
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
||||
"""
|
||||
# voir si ce n'est pas trop lent:
|
||||
@ -195,6 +229,110 @@ class Identite(db.Model):
|
||||
return ins.etat
|
||||
return False
|
||||
|
||||
def inscription_descr(self) -> dict:
|
||||
"""Description de l'état d'inscription"""
|
||||
inscription_courante = self.inscription_courante()
|
||||
if inscription_courante:
|
||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||
return {
|
||||
"etat_in_cursem": inscription_courante.etat,
|
||||
"inscription_courante": inscription_courante,
|
||||
"inscription": titre_sem,
|
||||
"inscription_str": "Inscrit en " + titre_sem,
|
||||
"situation": self.descr_situation_etud(),
|
||||
}
|
||||
else:
|
||||
if self.formsemestre_inscriptions:
|
||||
# cherche l'inscription la plus récente:
|
||||
fin_dernier_sem = max(
|
||||
[
|
||||
inscr.formsemestre.date_debut
|
||||
for inscr in self.formsemestre_inscriptions
|
||||
]
|
||||
)
|
||||
if fin_dernier_sem > datetime.date.today():
|
||||
inscription = "futur"
|
||||
situation = "futur élève"
|
||||
else:
|
||||
inscription = "ancien"
|
||||
situation = "ancien élève"
|
||||
else:
|
||||
inscription = ("non inscrit",)
|
||||
situation = inscription
|
||||
return {
|
||||
"etat_in_cursem": "?",
|
||||
"inscription_courante": None,
|
||||
"inscription": inscription,
|
||||
"inscription_str": inscription,
|
||||
"situation": situation,
|
||||
}
|
||||
|
||||
def descr_situation_etud(self) -> str:
|
||||
"""Chaîne décrivant la situation _actuelle_ de l'étudiant.
|
||||
Exemple:
|
||||
"inscrit en BUT R&T semestre 2 FI (Jan 2022 - Jul 2022) le 16/01/2022"
|
||||
ou
|
||||
"non inscrit"
|
||||
"""
|
||||
inscriptions_courantes = self.inscriptions_courantes()
|
||||
if inscriptions_courantes:
|
||||
inscr = inscriptions_courantes[0]
|
||||
if inscr.etat == scu.INSCRIT:
|
||||
situation = f"inscrit{self.e} en {inscr.formsemestre.titre_mois()}"
|
||||
# Cherche la date d'inscription dans scolar_events:
|
||||
events = models.ScolarEvent.query.filter_by(
|
||||
etudid=self.id,
|
||||
formsemestre_id=inscr.formsemestre.id,
|
||||
event_type="INSCRIPTION",
|
||||
).all()
|
||||
if not events:
|
||||
log(
|
||||
f"*** situation inconsistante pour {self} (inscrit mais pas d'event)"
|
||||
)
|
||||
situation += " (inscription non enregistrée)" # ???
|
||||
else:
|
||||
date_ins = events[0].event_date
|
||||
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:
|
||||
events = models.ScolarEvent.query.filter_by(
|
||||
etudid=self.id,
|
||||
formsemestre_id=inscr.formsemestre.id,
|
||||
event_type="DEMISSION",
|
||||
).all()
|
||||
if not events:
|
||||
log(
|
||||
f"*** situation inconsistante pour {self} (demission mais pas d'event)"
|
||||
)
|
||||
date_dem = "???" # ???
|
||||
else:
|
||||
date_dem = events[0].event_date
|
||||
situation += date_dem.strftime(" le %d/%m/%Y")
|
||||
else:
|
||||
situation = "non inscrit" + self.e
|
||||
|
||||
return situation
|
||||
|
||||
def photo_html(self, title=None, size="small") -> str:
|
||||
"""HTML img tag for the photo, either in small size (h90)
|
||||
or original size (size=="orig")
|
||||
"""
|
||||
from app.scodoc import sco_photos
|
||||
|
||||
# sco_photo traite des dicts:
|
||||
return sco_photos.etud_photo_html(
|
||||
etud=dict(
|
||||
etudid=self.id,
|
||||
code_nip=self.code_nip,
|
||||
nomprenom=self.nomprenom,
|
||||
nom_disp=self.nom_disp(),
|
||||
photo_filename=self.photo_filename,
|
||||
),
|
||||
title=title,
|
||||
size=size,
|
||||
)
|
||||
|
||||
|
||||
def make_etud_args(
|
||||
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
||||
@ -259,6 +397,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
|
||||
|
@ -12,7 +12,6 @@ from app import log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models import UniteEns
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models.ues import UniteEns
|
||||
@ -23,6 +22,7 @@ from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
||||
|
||||
|
||||
class FormSemestre(db.Model):
|
||||
@ -161,10 +161,9 @@ 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"] = f"{self.date_debut.month} {self.date_debut.year}"
|
||||
d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}"
|
||||
d["mois_debut"] = self.mois_debut()
|
||||
d["mois_fin"] = self.mois_fin()
|
||||
d["titremois"] = "%s %s (%s - %s)" % (
|
||||
d["titre_num"],
|
||||
self.modalite or "",
|
||||
@ -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:
|
||||
@ -294,6 +292,7 @@ class FormSemestre(db.Model):
|
||||
"""chaîne "J. Dupond, X. Martin"
|
||||
ou "Jacques Dupond, Xavier Martin"
|
||||
"""
|
||||
# was "nomcomplet"
|
||||
if not self.responsables:
|
||||
return ""
|
||||
if abbrev_prenom:
|
||||
@ -301,10 +300,22 @@ 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)
|
||||
|
||||
def mois_debut(self) -> str:
|
||||
"Oct 2021"
|
||||
return f"{MONTH_NAMES_ABBREV[self.date_debut.month - 1]} {self.date_debut.year}"
|
||||
|
||||
def mois_fin(self) -> str:
|
||||
"Jul 2022"
|
||||
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
|
||||
Exemple: RT-DUT-FI-S1-ANNEE
|
||||
|
@ -63,6 +63,12 @@ class GroupDescr(db.Model):
|
||||
# "A", "C2", ... (NULL for 'all'):
|
||||
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
|
||||
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="group_membership",
|
||||
lazy="dynamic",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
|
||||
|
@ -6,7 +6,8 @@ import flask_sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.comp import df_cache
|
||||
from app.models import Identite, Module
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.modules import Module
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
@ -133,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,
|
||||
|
@ -194,12 +194,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)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
|
@ -120,7 +120,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:
|
||||
|
@ -63,12 +63,15 @@ from app.scodoc.sco_pdf import SU
|
||||
from app import log
|
||||
|
||||
|
||||
def mark_paras(L, tags):
|
||||
"""Put each (string) element of L between <b>"""
|
||||
def mark_paras(L, tags) -> list[str]:
|
||||
"""Put each (string) element of L between <tag>...</tag>,
|
||||
for each supplied tag.
|
||||
Leave non string elements untouched.
|
||||
"""
|
||||
for tag in tags:
|
||||
b = "<" + tag + ">"
|
||||
c = "</" + tag.split()[0] + ">"
|
||||
L = [b + (x or "") + c for x in L]
|
||||
start = "<" + tag + ">"
|
||||
end = "</" + tag.split()[0] + ">"
|
||||
L = [(start + (x or "") + end) if isinstance(x, str) else x for x in L]
|
||||
return L
|
||||
|
||||
|
||||
@ -233,7 +236,10 @@ class GenTable(object):
|
||||
colspan_count -= 1
|
||||
# if colspan_count > 0:
|
||||
# continue # skip cells after a span
|
||||
content = row.get(cid, "") or "" # nota: None converted to ''
|
||||
if pdf_mode:
|
||||
content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or ""
|
||||
else:
|
||||
content = row.get(cid, "") or "" # nota: None converted to ''
|
||||
colspan = row.get("_%s_colspan" % cid, 0)
|
||||
if colspan > 1:
|
||||
pdf_style_list.append(
|
||||
@ -547,9 +553,16 @@ class GenTable(object):
|
||||
omit_hidden_lines=True,
|
||||
)
|
||||
try:
|
||||
Pt = [
|
||||
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list
|
||||
]
|
||||
Pt = []
|
||||
for line in data_list:
|
||||
Pt.append(
|
||||
[
|
||||
Paragraph(SU(str(x)), CellStyle)
|
||||
if (not isinstance(x, Paragraph))
|
||||
else x
|
||||
for x in line
|
||||
]
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
pdf_style_list += self.pdf_table_style
|
||||
@ -748,7 +761,7 @@ if __name__ == "__main__":
|
||||
doc = io.BytesIO()
|
||||
document = sco_pdf.BaseDocTemplate(doc)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScolarsPageTemplate(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
)
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app import scodoc_flash_status_messages
|
||||
from app.scodoc import html_sidebar
|
||||
import sco_version
|
||||
|
||||
@ -153,13 +153,14 @@ def sco_header(
|
||||
"Main HTML page header for ScoDoc"
|
||||
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
||||
|
||||
scodoc_flash_status_messages()
|
||||
|
||||
# Get head message from http request:
|
||||
if not head_message:
|
||||
if request.method == "POST":
|
||||
head_message = request.form.get("head_message", "")
|
||||
elif request.method == "GET":
|
||||
head_message = request.args.get("head_message", "")
|
||||
|
||||
params = {
|
||||
"page_title": page_title or sco_version.SCONAME,
|
||||
"no_side_bar": no_side_bar,
|
||||
@ -249,6 +250,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)
|
||||
|
@ -35,6 +35,7 @@ import datetime
|
||||
|
||||
from flask import g, url_for
|
||||
from flask_mail import Message
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -55,27 +56,30 @@ def abs_notify(etudid, date):
|
||||
"""
|
||||
from app.scodoc import sco_abs
|
||||
|
||||
sem = retreive_current_formsemestre(etudid, date)
|
||||
if not sem:
|
||||
formsemestre = retreive_current_formsemestre(etudid, date)
|
||||
if not formsemestre:
|
||||
return # non inscrit a la date, pas de notification
|
||||
|
||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
||||
do_abs_notify(sem, etudid, date, nbabs, nbabsjust)
|
||||
nbabs, nbabsjust = sco_abs.get_abs_count_in_interval(
|
||||
etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat()
|
||||
)
|
||||
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
|
||||
|
||||
|
||||
def do_abs_notify(sem, etudid, date, nbabs, nbabsjust):
|
||||
def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust):
|
||||
"""Given new counts of absences, check if notifications are requested and send them."""
|
||||
# prefs fallback to global pref if sem is None:
|
||||
if sem:
|
||||
formsemestre_id = sem["formsemestre_id"]
|
||||
if formsemestre:
|
||||
formsemestre_id = formsemestre.id
|
||||
else:
|
||||
formsemestre_id = None
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id=sem["formsemestre_id"])
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
|
||||
|
||||
destinations = abs_notify_get_destinations(
|
||||
sem, prefs, etudid, date, nbabs, nbabsjust
|
||||
formsemestre, prefs, etudid, date, nbabs, nbabsjust
|
||||
)
|
||||
msg = abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust)
|
||||
|
||||
msg = abs_notification_message(formsemestre, prefs, etudid, nbabs, nbabsjust)
|
||||
if not msg:
|
||||
return # abort
|
||||
|
||||
@ -131,19 +135,19 @@ def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id
|
||||
)
|
||||
|
||||
|
||||
def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust):
|
||||
def abs_notify_get_destinations(
|
||||
formsemestre: FormSemestre, prefs, etudid, date, nbabs, nbabsjust
|
||||
) -> set:
|
||||
"""Returns set of destination emails to be notified"""
|
||||
formsemestre_id = sem["formsemestre_id"]
|
||||
|
||||
destinations = [] # list of email address to notify
|
||||
|
||||
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id):
|
||||
if sem and prefs["abs_notify_respsem"]:
|
||||
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre.id):
|
||||
if prefs["abs_notify_respsem"]:
|
||||
# notifie chaque responsable du semestre
|
||||
for responsable_id in sem["responsables"]:
|
||||
u = sco_users.user_info(responsable_id)
|
||||
if u["email"]:
|
||||
destinations.append(u["email"])
|
||||
for responsable in formsemestre.responsables:
|
||||
if responsable.email:
|
||||
destinations.append(responsable.email)
|
||||
if prefs["abs_notify_chief"] and prefs["email_chefdpt"]:
|
||||
destinations.append(prefs["email_chefdpt"])
|
||||
if prefs["abs_notify_email"]:
|
||||
@ -156,7 +160,7 @@ def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust):
|
||||
# Notification (à chaque fois) des resp. de modules ayant des évaluations
|
||||
# à cette date
|
||||
# nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas
|
||||
if sem and prefs["abs_notify_respeval"]:
|
||||
if prefs["abs_notify_respeval"]:
|
||||
mods = mod_with_evals_at_date(date, etudid)
|
||||
for mod in mods:
|
||||
u = sco_users.user_info(mod["responsable_id"])
|
||||
@ -232,7 +236,9 @@ def user_nbdays_since_last_notif(email_addr, etudid):
|
||||
return None
|
||||
|
||||
|
||||
def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
|
||||
def abs_notification_message(
|
||||
formsemestre: FormSemestre, prefs, etudid, nbabs, nbabsjust
|
||||
):
|
||||
"""Mime notification message based on template.
|
||||
returns a Message instance
|
||||
or None if sending should be canceled (empty template).
|
||||
@ -242,13 +248,13 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
|
||||
# Variables accessibles dans les balises du template: %(nom_variable)s :
|
||||
values = sco_bulletins.make_context_dict(sem, etud)
|
||||
values = sco_bulletins.make_context_dict(formsemestre, etud)
|
||||
|
||||
values["nbabs"] = nbabs
|
||||
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"]
|
||||
@ -264,9 +270,11 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
|
||||
return msg
|
||||
|
||||
|
||||
def retreive_current_formsemestre(etudid, cur_date):
|
||||
def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre:
|
||||
"""Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée
|
||||
date est une chaine au format ISO (yyyy-mm-dd)
|
||||
|
||||
Result: FormSemestre ou None si pas inscrit à la date indiquée
|
||||
"""
|
||||
req = """SELECT i.formsemestre_id
|
||||
FROM notes_formsemestre_inscription i, notes_formsemestre sem
|
||||
@ -278,8 +286,8 @@ def retreive_current_formsemestre(etudid, cur_date):
|
||||
if not r:
|
||||
return None
|
||||
# s'il y a plusieurs semestres, prend le premier (rarissime et non significatif):
|
||||
sem = sco_formsemestre.get_formsemestre(r[0]["formsemestre_id"])
|
||||
return sem
|
||||
formsemestre = FormSemestre.query.get(r[0]["formsemestre_id"])
|
||||
return formsemestre
|
||||
|
||||
|
||||
def mod_with_evals_at_date(date_abs, etudid):
|
||||
|
@ -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")
|
||||
|
@ -70,13 +70,13 @@ from app.scodoc.sco_exceptions import (
|
||||
)
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_pvjury
|
||||
from app.scodoc import sco_pvpdf
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class BaseArchiver(object):
|
||||
@ -254,7 +254,7 @@ class BaseArchiver(object):
|
||||
self.initialize()
|
||||
if not scu.is_valid_filename(filename):
|
||||
log('Archiver.get: invalid filename "%s"' % filename)
|
||||
raise ValueError("invalid filename")
|
||||
raise ScoValueError("archive introuvable (déjà supprimée ?)")
|
||||
fname = os.path.join(archive_id, filename)
|
||||
log("reading archive file %s" % fname)
|
||||
with open(fname, "rb") as f:
|
||||
|
@ -29,13 +29,11 @@
|
||||
|
||||
"""
|
||||
import email
|
||||
import pprint
|
||||
import time
|
||||
|
||||
from flask import g, request
|
||||
from flask import url_for
|
||||
from flask import render_template, url_for
|
||||
from flask_login import current_user
|
||||
from flask_mail import Message
|
||||
|
||||
from app import email
|
||||
from app import log
|
||||
@ -78,33 +76,20 @@ from app.scodoc import sco_bulletins_legacy
|
||||
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
|
||||
|
||||
|
||||
def make_context_dict(sem, etud):
|
||||
def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict:
|
||||
"""Construit dictionnaire avec valeurs pour substitution des textes
|
||||
(preferences bul_pdf_*)
|
||||
"""
|
||||
C = sem.copy()
|
||||
C["responsable"] = " ,".join(
|
||||
[
|
||||
sco_users.user_info(responsable_id)["prenomnom"]
|
||||
for responsable_id in sem["responsables"]
|
||||
]
|
||||
)
|
||||
|
||||
annee_debut = sem["date_debut"].split("/")[2]
|
||||
annee_fin = sem["date_fin"].split("/")[2]
|
||||
if annee_debut != annee_fin:
|
||||
annee = "%s - %s" % (annee_debut, annee_fin)
|
||||
else:
|
||||
annee = annee_debut
|
||||
C["anneesem"] = annee
|
||||
C = formsemestre.get_infos_dict()
|
||||
C["responsable"] = formsemestre.responsables_str()
|
||||
C["anneesem"] = C["annee"] # backward compat
|
||||
C.update(etud)
|
||||
# copie preferences
|
||||
# XXX devrait acceder directement à un dict de preferences, à revoir
|
||||
for name in sco_preferences.get_base_preferences().prefs_name:
|
||||
C[name] = sco_preferences.get_preference(name, sem["formsemestre_id"])
|
||||
C[name] = sco_preferences.get_preference(name, formsemestre.id)
|
||||
|
||||
# ajoute groupes et group_0, group_1, ...
|
||||
sco_groups.etud_add_group_infos(etud, sem)
|
||||
sco_groups.etud_add_group_infos(etud, formsemestre.id)
|
||||
C["groupes"] = etud["groupes"]
|
||||
n = 0
|
||||
for partition_id in etud["partitions"]:
|
||||
@ -125,7 +110,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
Le contenu du dictionnaire dépend des options (rangs, ...)
|
||||
et de la version choisie (short, long, selectedevals).
|
||||
|
||||
Cette fonction est utilisée pour les bulletins HTML et PDF, mais pas ceux en XML.
|
||||
Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
|
||||
en HTML et PDF, mais pas ceux en XML.
|
||||
"""
|
||||
from app.scodoc import sco_abs
|
||||
|
||||
@ -190,7 +176,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
)
|
||||
I["etud_etat"] = nt.get_etud_etat(etudid)
|
||||
I["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
I["etud_etat"], prefs, decision_dem=I["decision_sem"]
|
||||
I["etud_etat"], prefs, decision_sem=I["decision_sem"]
|
||||
)
|
||||
I["demission"] = ""
|
||||
if I["etud_etat"] == scu.DEMISSION:
|
||||
@ -384,7 +370,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
|
||||
|
||||
#
|
||||
C = make_context_dict(I["sem"], I["etud"])
|
||||
C = make_context_dict(formsemestre, I["etud"])
|
||||
C.update(I)
|
||||
#
|
||||
# log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo
|
||||
@ -433,7 +419,9 @@ def _sort_mod_by_matiere(modlist, nt, etudid):
|
||||
return matmod
|
||||
|
||||
|
||||
def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
|
||||
def _ue_mod_bulletin(
|
||||
etudid, formsemestre_id, ue_id, modimpls, nt: NotesTableCompat, version
|
||||
):
|
||||
"""Infos sur les modules (et évaluations) dans une UE
|
||||
(ajoute les informations aux modimpls)
|
||||
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
|
||||
@ -803,31 +791,23 @@ def etud_descr_situation_semestre(
|
||||
|
||||
# ------ Page bulletin
|
||||
def formsemestre_bulletinetud(
|
||||
etudid=None,
|
||||
etud: Identite = None,
|
||||
formsemestre_id=None,
|
||||
format="html",
|
||||
format=None,
|
||||
version="long",
|
||||
xml_with_decisions=False,
|
||||
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
||||
prefer_mail_perso=False,
|
||||
):
|
||||
"page bulletin de notes"
|
||||
try:
|
||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
||||
etudid = etud["etudid"]
|
||||
except:
|
||||
sco_etud.log_unknown_etud()
|
||||
raise ScoValueError("étudiant inconnu")
|
||||
|
||||
format = format or "html"
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
if not formsemestre:
|
||||
# API, donc erreurs admises
|
||||
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
|
||||
sem = formsemestre.to_dict()
|
||||
|
||||
bulletin = do_formsemestre_bulletinetud(
|
||||
formsemestre,
|
||||
etudid,
|
||||
etud.id,
|
||||
format=format,
|
||||
version=version,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
@ -835,51 +815,24 @@ def formsemestre_bulletinetud(
|
||||
prefer_mail_perso=prefer_mail_perso,
|
||||
)[0]
|
||||
if format not in {"html", "pdfmail"}:
|
||||
filename = scu.bul_filename(sem, etud, format)
|
||||
filename = scu.bul_filename(formsemestre, etud, format)
|
||||
return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
|
||||
|
||||
H = [
|
||||
_formsemestre_bulletinetud_header_html(
|
||||
etud, etudid, sem, formsemestre_id, format, version
|
||||
),
|
||||
_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(),
|
||||
inscription_str=etud.inscription_descr()["inscription_str"],
|
||||
),
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
|
||||
H.append("""<p>Situation actuelle: """)
|
||||
if etud["inscription_formsemestre_id"]:
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=etud["inscription_formsemestre_id"])
|
||||
}">"""
|
||||
)
|
||||
H.append(etud["inscriptionstr"])
|
||||
if etud["inscription_formsemestre_id"]:
|
||||
H.append("""</a>""")
|
||||
H.append("""</p>""")
|
||||
if sem["modalite"] == "EXT":
|
||||
H.append(
|
||||
"""<p><a
|
||||
href="formsemestre_ext_edit_ue_validations?formsemestre_id=%s&etudid=%s"
|
||||
class="stdlink">
|
||||
Editer les validations d'UE dans ce semestre extérieur
|
||||
</a></p>"""
|
||||
% (formsemestre_id, etudid)
|
||||
)
|
||||
# Place du diagramme radar
|
||||
H.append(
|
||||
"""<form id="params">
|
||||
<input type="hidden" name="etudid" id="etudid" value="%s"/>
|
||||
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
|
||||
</form>"""
|
||||
% (etudid, formsemestre_id)
|
||||
)
|
||||
H.append('<div id="radar_bulletin"></div>')
|
||||
|
||||
# --- Pied de page
|
||||
H.append(html_sco_header.sco_footer())
|
||||
|
||||
return "".join(H)
|
||||
|
||||
|
||||
@ -897,7 +850,7 @@ def do_formsemestre_bulletinetud(
|
||||
formsemestre: FormSemestre,
|
||||
etudid: int,
|
||||
version="long", # short, long, selectedevals
|
||||
format="html",
|
||||
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"
|
||||
@ -908,6 +861,7 @@ def do_formsemestre_bulletinetud(
|
||||
où bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
|
||||
et filigranne est un message à placer en "filigranne" (eg "Provisoire").
|
||||
"""
|
||||
format = format or "html"
|
||||
if format == "xml":
|
||||
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
|
||||
formsemestre.id,
|
||||
@ -930,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)
|
||||
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(
|
||||
@ -978,7 +932,7 @@ 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:
|
||||
@ -1027,7 +981,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
|
||||
except KeyError as e:
|
||||
raise ScoValueError(
|
||||
"format 'Message d'accompagnement' (bul_intro_mail) invalide, revoir les réglages dans les préférences"
|
||||
)
|
||||
) from e
|
||||
else:
|
||||
hea = ""
|
||||
|
||||
@ -1043,81 +997,32 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
|
||||
bcc = copy_addr.strip()
|
||||
else:
|
||||
bcc = ""
|
||||
msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc])
|
||||
msg.body = hea
|
||||
|
||||
# Attach pdf
|
||||
msg.attach(filename, scu.PDF_MIMETYPE, pdfdata)
|
||||
log("mail bulletin a %s" % recipient_addr)
|
||||
email.send_message(msg)
|
||||
email.send_email(
|
||||
subject,
|
||||
sender,
|
||||
recipients,
|
||||
bcc=[bcc],
|
||||
text_body=hea,
|
||||
attachments=[
|
||||
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _formsemestre_bulletinetud_header_html(
|
||||
etud,
|
||||
etudid,
|
||||
sem,
|
||||
formsemestre_id=None,
|
||||
format=None,
|
||||
version=None,
|
||||
):
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Bulletin de %(nomprenom)s" % etud,
|
||||
javascripts=[
|
||||
"js/bulletin.js",
|
||||
"libjs/d3.v3.min.js",
|
||||
"js/radar_bulletin.js",
|
||||
],
|
||||
cssstyles=["css/radar_bulletin.css"],
|
||||
),
|
||||
"""<table class="bull_head"><tr><td>
|
||||
<h2><a class="discretelink" href="%s">%s</a></h2>
|
||||
"""
|
||||
% (
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
),
|
||||
etud["nomprenom"],
|
||||
),
|
||||
"""
|
||||
<form name="f" method="GET" action="%s">"""
|
||||
% request.base_url,
|
||||
f"""Bulletin <span class="bull_liensemestre"><a href="{
|
||||
url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem["formsemestre_id"])}
|
||||
">{sem["titremois"]}</a></span>
|
||||
<br/>"""
|
||||
% sem,
|
||||
"""<table><tr>""",
|
||||
"""<td>établi le %s (notes sur 20)</td>""" % time.strftime("%d/%m/%Y à %Hh%M"),
|
||||
"""<td><span class="rightjust">
|
||||
<input type="hidden" name="formsemestre_id" value="%s"></input>"""
|
||||
% formsemestre_id,
|
||||
"""<input type="hidden" name="etudid" value="%s"></input>""" % etudid,
|
||||
"""<input type="hidden" name="format" value="%s"></input>""" % format,
|
||||
"""<select name="version" onchange="document.f.submit()" class="noprint">""",
|
||||
]
|
||||
for (v, e) in (
|
||||
("short", "Version courte"),
|
||||
("selectedevals", "Version intermédiaire"),
|
||||
("long", "Version complète"),
|
||||
):
|
||||
if v == version:
|
||||
selected = " selected"
|
||||
else:
|
||||
selected = ""
|
||||
H.append('<option value="%s"%s>%s</option>' % (v, selected, e))
|
||||
H.append("""</select></td>""")
|
||||
# Menu
|
||||
endpoint = "notes.formsemestre_bulletinetud"
|
||||
|
||||
menuBul = [
|
||||
def make_menu_autres_operations(
|
||||
formsemestre: FormSemestre, etud: Identite, endpoint: str, version: str
|
||||
) -> str:
|
||||
etud_email = etud.get_first_email() or ""
|
||||
etud_perso = etud.get_first_email("emailperso") or ""
|
||||
menu_items = [
|
||||
{
|
||||
"title": "Réglages bulletins",
|
||||
"endpoint": "notes.formsemestre_edit_options",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
# "target_url": url_for(
|
||||
# "notes.formsemestre_bulletinetud",
|
||||
# scodoc_dept=g.scodoc_dept,
|
||||
@ -1125,54 +1030,52 @@ def _formsemestre_bulletinetud_header_html(
|
||||
# etudid=etudid,
|
||||
# ),
|
||||
},
|
||||
"enabled": (current_user.id in sem["responsables"])
|
||||
or current_user.has_permission(Permission.ScoImplement),
|
||||
"enabled": formsemestre.can_be_edited_by(current_user),
|
||||
},
|
||||
{
|
||||
"title": 'Version papier (pdf, format "%s")'
|
||||
% sco_bulletins_generator.bulletin_get_class_name_displayed(
|
||||
formsemestre_id
|
||||
formsemestre.id
|
||||
),
|
||||
"endpoint": endpoint,
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
"version": version,
|
||||
"format": "pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Envoi par mail à %s" % etud["email"],
|
||||
"title": f"Envoi par mail à {etud_email}",
|
||||
"endpoint": endpoint,
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
"version": version,
|
||||
"format": "pdfmail",
|
||||
},
|
||||
# possible slt si on a un mail...
|
||||
"enabled": etud["email"] and can_send_bulletin_by_mail(formsemestre_id),
|
||||
"enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id),
|
||||
},
|
||||
{
|
||||
"title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
|
||||
"title": f"Envoi par mail à {etud_perso} (adr. personnelle)",
|
||||
"endpoint": endpoint,
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
"version": version,
|
||||
"format": "pdfmail",
|
||||
"prefer_mail_perso": 1,
|
||||
},
|
||||
# possible slt si on a un mail...
|
||||
"enabled": etud["emailperso"]
|
||||
and can_send_bulletin_by_mail(formsemestre_id),
|
||||
"enabled": etud_perso and can_send_bulletin_by_mail(formsemestre.id),
|
||||
},
|
||||
{
|
||||
"title": "Version json",
|
||||
"endpoint": endpoint,
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
"version": version,
|
||||
"format": "json",
|
||||
},
|
||||
@ -1181,8 +1084,8 @@ def _formsemestre_bulletinetud_header_html(
|
||||
"title": "Version XML",
|
||||
"endpoint": endpoint,
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
"version": version,
|
||||
"format": "xml",
|
||||
},
|
||||
@ -1191,20 +1094,20 @@ def _formsemestre_bulletinetud_header_html(
|
||||
"title": "Ajouter une appréciation",
|
||||
"endpoint": "notes.appreciation_add_form",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
},
|
||||
"enabled": (
|
||||
(current_user.id in sem["responsables"])
|
||||
or (current_user.has_permission(Permission.ScoEtudInscrit))
|
||||
formsemestre.can_be_edited_by(current_user)
|
||||
or current_user.has_permission(Permission.ScoEtudInscrit)
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "Enregistrer un semestre effectué ailleurs",
|
||||
"endpoint": "notes.formsemestre_ext_create_form",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
},
|
||||
"enabled": current_user.has_permission(Permission.ScoImplement),
|
||||
},
|
||||
@ -1212,71 +1115,72 @@ def _formsemestre_bulletinetud_header_html(
|
||||
"title": "Enregistrer une validation d'UE antérieure",
|
||||
"endpoint": "notes.formsemestre_validate_previous_ue",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
},
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
|
||||
},
|
||||
{
|
||||
"title": "Enregistrer note d'une UE externe",
|
||||
"endpoint": "notes.external_ue_create_form",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
},
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
|
||||
},
|
||||
{
|
||||
"title": "Entrer décisions jury",
|
||||
"endpoint": "notes.formsemestre_validation_etud_form",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
},
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
|
||||
},
|
||||
{
|
||||
"title": "Editer PV jury",
|
||||
"title": "Éditer PV jury",
|
||||
"endpoint": "notes.formsemestre_pvjury_pdf",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
]
|
||||
return htmlutils.make_menu("Autres opérations", menu_items, alone=True)
|
||||
|
||||
H.append("""<td class="bulletin_menubar"><div class="bulletin_menubar">""")
|
||||
H.append(htmlutils.make_menu("Autres opérations", menuBul, alone=True))
|
||||
H.append("""</div></td>""")
|
||||
H.append(
|
||||
'<td> <a href="%s">%s</a></td>'
|
||||
% (
|
||||
url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
etudid=etudid,
|
||||
format="pdf",
|
||||
|
||||
def _formsemestre_bulletinetud_header_html(
|
||||
etud,
|
||||
formsemestre: FormSemestre,
|
||||
format=None,
|
||||
version=None,
|
||||
):
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"Bulletin de {etud.nomprenom}",
|
||||
javascripts=[
|
||||
"js/bulletin.js",
|
||||
"libjs/d3.v3.min.js",
|
||||
"js/radar_bulletin.js",
|
||||
],
|
||||
cssstyles=["css/radar_bulletin.css"],
|
||||
),
|
||||
render_template(
|
||||
"bul_head.html",
|
||||
etud=etud,
|
||||
format=format,
|
||||
formsemestre=formsemestre,
|
||||
menu_autres_operations=make_menu_autres_operations(
|
||||
etud=etud,
|
||||
formsemestre=formsemestre,
|
||||
endpoint="notes.formsemestre_bulletinetud",
|
||||
version=version,
|
||||
),
|
||||
scu.ICON_PDF,
|
||||
)
|
||||
)
|
||||
H.append("""</tr></table>""")
|
||||
#
|
||||
H.append(
|
||||
"""</form></span></td><td class="bull_photo"><a href="%s">%s</a>
|
||||
"""
|
||||
% (
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]),
|
||||
)
|
||||
)
|
||||
H.append(
|
||||
"""</td></tr>
|
||||
</table>
|
||||
"""
|
||||
)
|
||||
|
||||
return "".join(H)
|
||||
scu=scu,
|
||||
time=time,
|
||||
version=version,
|
||||
),
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
@ -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
|
||||
@ -71,6 +78,8 @@ class BulletinGenerator:
|
||||
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
|
||||
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,
|
||||
@ -117,7 +126,7 @@ class BulletinGenerator:
|
||||
def get_filename(self):
|
||||
"""Build a filename to be proposed to the web client"""
|
||||
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
|
||||
return scu.bul_filename(sem, self.infos["etud"], "pdf")
|
||||
return scu.bul_filename_old(sem, self.infos["etud"], "pdf")
|
||||
|
||||
def generate(self, format="", stand_alone=True):
|
||||
"""Return bulletin in specified format"""
|
||||
@ -153,29 +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)
|
||||
|
||||
# Réduit sur une page
|
||||
objects = [KeepInFrame(0, 0, objects, mode="shrink")]
|
||||
# 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
|
||||
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),
|
||||
@ -188,7 +215,7 @@ class BulletinGenerator:
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
document.build(story)
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
@ -219,7 +246,7 @@ class BulletinGenerator:
|
||||
# ---------------------------------------------------------------------------
|
||||
def make_formsemestre_bulletinetud(
|
||||
infos,
|
||||
version="long", # short, long, selectedevals
|
||||
version=None, # short, long, selectedevals
|
||||
format="pdf", # html, pdf
|
||||
stand_alone=True,
|
||||
):
|
||||
@ -231,6 +258,7 @@ def make_formsemestre_bulletinetud(
|
||||
"""
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
version = version or "long"
|
||||
if not version in scu.BULLETINS_VERSIONS:
|
||||
raise ValueError("invalid version code !")
|
||||
|
||||
@ -238,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(
|
||||
@ -297,7 +330,11 @@ def register_bulletin_class(klass):
|
||||
|
||||
|
||||
def bulletin_class_descriptions():
|
||||
return [x.description for x in BULLETIN_CLASSES.values()]
|
||||
return [
|
||||
BULLETIN_CLASSES[class_name].description
|
||||
for class_name in BULLETIN_CLASSES
|
||||
if BULLETIN_CLASSES[class_name].list_in_menu
|
||||
]
|
||||
|
||||
|
||||
def bulletin_class_names() -> list[str]:
|
||||
|
@ -138,7 +138,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||
if not published:
|
||||
return d # stop !
|
||||
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||
if etat_inscription != scu.INSCRIT:
|
||||
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
|
||||
return d
|
||||
|
@ -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"],
|
||||
|
@ -46,10 +46,12 @@ de la forme %(XXX)s sont remplacées par la valeur de XXX, pour XXX dans:
|
||||
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.scodoc.sco_exceptions import ScoBugCatcher
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
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.sco_permissions import Permission
|
||||
@ -72,7 +74,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
|
||||
"""
|
||||
@ -114,11 +116,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">
|
||||
@ -129,7 +131,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."
|
||||
@ -140,7 +142,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,
|
||||
@ -167,10 +169,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,
|
||||
@ -179,7 +181,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,
|
||||
@ -195,7 +197,12 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
|
||||
# -----
|
||||
if format == "pdf":
|
||||
return 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)
|
||||
|
||||
@ -374,10 +381,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,
|
||||
@ -410,7 +417,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
# Chaque UE:
|
||||
for ue in I["ues"]:
|
||||
ue_type = None
|
||||
coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else ""
|
||||
try:
|
||||
coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else ""
|
||||
except TypeError as exc:
|
||||
raise ScoBugCatcher(f"ue={ue!r}") from exc
|
||||
|
||||
ue_descr = ue["ue_descr_txt"]
|
||||
rowstyle = ""
|
||||
plusminus = minuslink #
|
||||
|
@ -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 -------------
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -33,8 +33,7 @@ import os
|
||||
import time
|
||||
from operator import itemgetter
|
||||
|
||||
from flask import url_for, g, request
|
||||
from flask_mail import Message
|
||||
from flask import url_for, g
|
||||
|
||||
from app import email
|
||||
from app import log
|
||||
@ -46,7 +45,6 @@ from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
from app.scodoc import safehtml
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
|
||||
|
||||
def format_etud_ident(etud):
|
||||
@ -860,7 +858,7 @@ def list_scolog(etudid):
|
||||
return cursor.dictfetchall()
|
||||
|
||||
|
||||
def fill_etuds_info(etuds, add_admission=True):
|
||||
def fill_etuds_info(etuds: list[dict], add_admission=True):
|
||||
"""etuds est une liste d'etudiants (mappings)
|
||||
Pour chaque etudiant, ajoute ou formatte les champs
|
||||
-> informations pour fiche etudiant ou listes diverses
|
||||
@ -977,7 +975,10 @@ def etud_inscriptions_infos(etudid: int, ne="") -> dict:
|
||||
|
||||
|
||||
def descr_situation_etud(etudid: int, ne="") -> str:
|
||||
"""chaîne décrivant la situation actuelle de l'étudiant"""
|
||||
"""Chaîne décrivant la situation actuelle de l'étudiant
|
||||
XXX Obsolete, utiliser Identite.descr_situation_etud() dans
|
||||
les nouveaux codes
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
@ -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
|
||||
|
@ -180,7 +180,9 @@ def search_etud_in_dept(expnom=""):
|
||||
e["_nomprenom_target"] = target
|
||||
e["inscription_target"] = target
|
||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
||||
sco_groups.etud_add_group_infos(e, e["cursem"])
|
||||
sco_groups.etud_add_group_infos(
|
||||
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
|
||||
)
|
||||
|
||||
tab = GenTable(
|
||||
columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
|
||||
|
@ -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"]])
|
||||
|
@ -31,7 +31,7 @@
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask import render_template, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
@ -411,7 +411,7 @@ def formsemestre_status_menubar(sem):
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
||||
},
|
||||
{
|
||||
"title": "Editer les PV et archiver les résultats",
|
||||
"title": "Éditer les PV et archiver les résultats",
|
||||
"endpoint": "notes.formsemestre_archive",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
"enabled": sco_permissions_check.can_edit_pv(formsemestre_id),
|
||||
@ -445,6 +445,7 @@ def retreive_formsemestre_from_request() -> int:
|
||||
"""Cherche si on a de quoi déduire le semestre affiché à partir des
|
||||
arguments de la requête:
|
||||
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
|
||||
Returns None si pas défini.
|
||||
"""
|
||||
if request.method == "GET":
|
||||
args = request.args
|
||||
@ -505,34 +506,17 @@ def formsemestre_page_title():
|
||||
return ""
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy()
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
except:
|
||||
log("can't find formsemestre_id %s" % formsemestre_id)
|
||||
return ""
|
||||
|
||||
fill_formsemestre(sem)
|
||||
|
||||
h = f"""<div class="formsemestre_page_title">
|
||||
<div class="infos">
|
||||
<span class="semtitle"><a class="stdlink" title="{sem['session_id']}"
|
||||
href="{url_for('notes.formsemestre_status',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
|
||||
>{sem['titre']}</a><a
|
||||
title="{sem['etape_apo_str']}">{sem['num_sem']}</a>{sem['modalitestr']}</span><span
|
||||
class="dates"><a
|
||||
title="du {sem['date_debut']} au {sem['date_fin']} "
|
||||
>{sem['mois_debut']} - {sem['mois_fin']}</a></span><span
|
||||
class="resp"><a title="{sem['nomcomplet']}">{sem['resp']}</a></span><span
|
||||
class="nbinscrits"><a class="discretelink"
|
||||
href="{url_for("scolar.groups_view",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
|
||||
>{sem['nbinscrits']} inscrits</a></span><span
|
||||
class="lock">{sem['locklink']}</span><span
|
||||
class="eye">{sem['eyelink']}</span>
|
||||
</div>
|
||||
{formsemestre_status_menubar(sem)}
|
||||
</div>
|
||||
"""
|
||||
h = render_template(
|
||||
"formsemestre_page_title.html",
|
||||
formsemestre=formsemestre,
|
||||
scu=scu,
|
||||
sem_menu_bar=formsemestre_status_menubar(formsemestre.to_dict()),
|
||||
)
|
||||
|
||||
return h
|
||||
|
||||
@ -1186,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>'
|
||||
|
@ -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"])
|
||||
|
@ -124,7 +124,7 @@ def get_partition(partition_id):
|
||||
{"partition_id": partition_id},
|
||||
)
|
||||
if not r:
|
||||
raise ValueError("invalid partition_id (%s)" % partition_id)
|
||||
raise ScoValueError(f"Partition inconnue (déjà supprimée ?) ({partition_id})")
|
||||
return r[0]
|
||||
|
||||
|
||||
@ -321,7 +321,7 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud
|
||||
t["etath"] = t["etat"]
|
||||
# Add membership for all partitions, 'partition_id' : group
|
||||
for etud in members: # long: comment eviter ces boucles ?
|
||||
etud_add_group_infos(etud, sem)
|
||||
etud_add_group_infos(etud, sem["formsemestre_id"])
|
||||
|
||||
if group["group_name"] != None:
|
||||
group_tit = "%s %s" % (group["partition_name"], group["group_name"])
|
||||
@ -413,12 +413,12 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
|
||||
return R
|
||||
|
||||
|
||||
def etud_add_group_infos(etud, sem, sep=" "):
|
||||
def etud_add_group_infos(etud, formsemestre_id, sep=" "):
|
||||
"""Add informations on partitions and group memberships to etud (a dict with an etudid)"""
|
||||
etud[
|
||||
"partitions"
|
||||
] = collections.OrderedDict() # partition_id : group + partition_name
|
||||
if not sem:
|
||||
if not formsemestre_id:
|
||||
etud["groupes"] = ""
|
||||
return etud
|
||||
|
||||
@ -430,7 +430,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
|
||||
and p.formsemestre_id = %(formsemestre_id)s
|
||||
ORDER BY p.numero
|
||||
""",
|
||||
{"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]},
|
||||
{"etudid": etud["etudid"], "formsemestre_id": formsemestre_id},
|
||||
)
|
||||
|
||||
for info in infos:
|
||||
@ -439,13 +439,13 @@ def etud_add_group_infos(etud, sem, sep=" "):
|
||||
|
||||
# resume textuel des groupes:
|
||||
etud["groupes"] = sep.join(
|
||||
[g["group_name"] for g in infos if g["group_name"] != None]
|
||||
[gr["group_name"] for gr in infos if gr["group_name"] is not None]
|
||||
)
|
||||
etud["partitionsgroupes"] = sep.join(
|
||||
[
|
||||
g["partition_name"] + ":" + g["group_name"]
|
||||
for g in infos
|
||||
if g["group_name"] != None
|
||||
gr["partition_name"] + ":" + gr["group_name"]
|
||||
for gr in infos
|
||||
if gr["group_name"] is not None
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -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:
|
||||
@ -203,7 +203,7 @@ def sco_import_generate_excel_sample(
|
||||
for field in titles:
|
||||
if field == "groupes":
|
||||
sco_groups.etud_add_group_infos(
|
||||
etud, groups_infos.formsemestre, sep=";"
|
||||
etud, groups_infos.formsemestre_id, sep=";"
|
||||
)
|
||||
l.append(etud["partitionsgroupes"])
|
||||
else:
|
||||
@ -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()
|
||||
|
@ -196,7 +196,10 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
if len(etud["sems"]) < 2:
|
||||
continue
|
||||
prev_formsemestre = etud["sems"][1]
|
||||
sco_groups.etud_add_group_infos(etud, prev_formsemestre)
|
||||
sco_groups.etud_add_group_infos(
|
||||
etud,
|
||||
prev_formsemestre["formsemestre_id"] if prev_formsemestre else None,
|
||||
)
|
||||
|
||||
cursem_groups_by_name = dict(
|
||||
[
|
||||
|
@ -215,7 +215,9 @@ def ficheEtud(etudid=None):
|
||||
info["modifadresse"] = ""
|
||||
|
||||
# Groupes:
|
||||
sco_groups.etud_add_group_infos(info, info["cursem"])
|
||||
sco_groups.etud_add_group_infos(
|
||||
info, info["cursem"]["formsemestre_id"] if info["cursem"] else None
|
||||
)
|
||||
|
||||
# Parcours de l'étudiant
|
||||
if info["sems"]:
|
||||
|
@ -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.*
|
||||
|
@ -175,7 +175,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 +214,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 +239,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
|
||||
@ -265,21 +287,6 @@ class ScolarsPageTemplate(PageTemplate):
|
||||
) = self.logo
|
||||
canvas.drawImage(image, inch, doc.pagesize[1] - inch, width, height)
|
||||
|
||||
# ---- Filigranne (texte en diagonal en haut a gauche de chaque page)
|
||||
if 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()
|
||||
|
||||
# ---- Add some meta data and bookmarks
|
||||
if self.pdfmeta_author:
|
||||
canvas.setAuthor(SU(self.pdfmeta_author))
|
||||
@ -287,27 +294,85 @@ class ScolarsPageTemplate(PageTemplate):
|
||||
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
|
||||
|
||||
bookmark = self.pagesbookmarks.get(doc.page, None)
|
||||
if bookmark:
|
||||
canvas.bookmarkPage(bookmark)
|
||||
canvas.addOutlineEntry(SU(bookmark), bookmark)
|
||||
|
||||
def draw_footer(self, canvas, content):
|
||||
"""Print the 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,
|
||||
content,
|
||||
)
|
||||
canvas.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, canvas, doc):
|
||||
if not self.preferences:
|
||||
return
|
||||
# ---- Footer
|
||||
foot_content = None
|
||||
if hasattr(doc, "current_footer"):
|
||||
foot_content = doc.current_footer
|
||||
self.draw_footer(canvas, foot_content or self.footer_string())
|
||||
# ---- Filigranne (texte en diagonal en haut a gauche de chaque page)
|
||||
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, alpha=0.6)
|
||||
canvas.drawRightString(0, 0, SU(filigranne))
|
||||
canvas.restoreState()
|
||||
doc.filigranne = None
|
||||
|
||||
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():
|
||||
# ... suboptimal but we don't care
|
||||
@ -333,7 +398,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),
|
||||
|
@ -175,7 +175,7 @@ def etud_photo_is_local(etud: dict, size="small"):
|
||||
return photo_pathname(etud["photo_filename"], size=size)
|
||||
|
||||
|
||||
def etud_photo_html(etud=None, etudid=None, title=None, size="small"):
|
||||
def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"):
|
||||
"""HTML img tag for the photo, either in small size (h90)
|
||||
or original size (size=="orig")
|
||||
"""
|
||||
@ -351,7 +351,8 @@ def copy_portal_photo_to_fs(etud):
|
||||
"""Copy the photo from portal (distant website) to local fs.
|
||||
Returns rel. path or None if copy failed, with a diagnostic message
|
||||
"""
|
||||
sco_etud.format_etud_ident(etud)
|
||||
if "nomprenom" not in etud:
|
||||
sco_etud.format_etud_ident(etud)
|
||||
url = photo_portal_url(etud)
|
||||
if not url:
|
||||
return None, "%(nomprenom)s: pas de code NIP" % etud
|
||||
|
@ -152,19 +152,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
|
||||
@ -245,6 +244,7 @@ PREF_CATEGORIES = (
|
||||
),
|
||||
("pe", {"title": "Avis de poursuites d'études"}),
|
||||
("edt", {"title": "Connexion avec le logiciel d'emplois du temps"}),
|
||||
("debug", {"title": "Tests / mise au point"}),
|
||||
)
|
||||
|
||||
|
||||
@ -357,8 +357,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"],
|
||||
@ -1158,7 +1172,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"],
|
||||
@ -1220,7 +1234,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"],
|
||||
@ -1859,6 +1873,19 @@ class BasePreferences(object):
|
||||
"category": "edt",
|
||||
},
|
||||
),
|
||||
(
|
||||
"email_test_mode_address",
|
||||
{
|
||||
"title": "Adresse de test",
|
||||
"initvalue": "",
|
||||
"explanation": """si cette adresse est indiquée, TOUS les mails
|
||||
envoyés par ScoDoc de ce département vont aller vers elle
|
||||
AU LIEU DE LEUR DESTINATION NORMALE !""",
|
||||
"size": 30,
|
||||
"category": "debug",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
self.prefs_name = set([x[0] for x in self.prefs_definition])
|
||||
|
@ -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,
|
||||
@ -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
|
||||
|
@ -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"]
|
||||
|
@ -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"]),
|
||||
)
|
||||
|
@ -74,7 +74,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 ............ / ............ / ......................"),
|
||||
@ -264,7 +268,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(),
|
||||
)
|
||||
@ -321,7 +325,8 @@ def pdf_feuille_releve_absences(
|
||||
],
|
||||
[
|
||||
Paragraph(
|
||||
SU("Département " + DeptFullName), StyleSheet["Heading3"]
|
||||
SU("Département " + (DeptFullName or "(?)")),
|
||||
StyleSheet["Heading3"],
|
||||
),
|
||||
"",
|
||||
],
|
||||
@ -460,7 +465,7 @@ def pdf_feuille_releve_absences(
|
||||
else:
|
||||
document = BaseDocTemplate(report, pagesize=taille)
|
||||
document.addPageTemplates(
|
||||
ScolarsPageTemplate(
|
||||
ScoDocPageTemplate(
|
||||
document,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
@ -28,30 +28,66 @@
|
||||
|
||||
""" Verification version logiciel vs version "stable" sur serveur
|
||||
N'effectue pas la mise à jour automatiquement, mais permet un affichage d'avertissement.
|
||||
|
||||
Désactivé temporairement pour ScoDoc 9.
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
from flask import current_app
|
||||
|
||||
from app import log
|
||||
import app.scodoc.sco_utils as scu
|
||||
from sco_version import SCOVERSION, SCONAME
|
||||
|
||||
def is_up_to_date():
|
||||
"""True if up_to_date
|
||||
Returns status, message
|
||||
|
||||
def is_up_to_date() -> str:
|
||||
"""Check installed version vs last release.
|
||||
Returns html message, empty of ok.
|
||||
"""
|
||||
current_app.logger.debug("Warning: is_up_to_date not implemented for ScoDoc9")
|
||||
return True, "unimplemented"
|
||||
diag = ""
|
||||
try:
|
||||
response = requests.get(scu.SCO_UP2DATE + "/" + SCOVERSION)
|
||||
except requests.exceptions.ConnectionError:
|
||||
current_app.logger.debug("is_up_to_date: %s", diag)
|
||||
return f"""<div>Attention: installation de {SCONAME} non fonctionnelle.</div>
|
||||
<div>Détails: pas de connexion à {scu.SCO_WEBSITE}.
|
||||
Vérifier paramètrages réseau,
|
||||
<a href="https://scodoc.org/GuideInstallDebian11/#informations-sur-les-flux-reseau">voir la documentation</a>.
|
||||
</div>
|
||||
"""
|
||||
except:
|
||||
current_app.logger.debug("is_up_to_date: %s", diag)
|
||||
return f"""<div>Attention: installation de {SCONAME} non fonctionnelle.</div>
|
||||
<div>Détails: erreur inconnue lors de la connexion à {scu.SCO_WEBSITE}.
|
||||
Vérifier paramètrages réseau,
|
||||
<a href="https://scodoc.org/GuideInstallDebian11/#informations-sur-les-flux-reseau">voir la documentation</a>.
|
||||
</div>
|
||||
"""
|
||||
if response.status_code != 200:
|
||||
current_app.logger.debug(
|
||||
f"is_up_to_date: invalid response code ({response.status_code})"
|
||||
)
|
||||
return f"""<div>Attention: réponse invalide de {scu.SCO_WEBSITE}</div>
|
||||
<div>(erreur http {response.status_code}).</div>"""
|
||||
|
||||
try:
|
||||
infos = json.loads(response.text)
|
||||
except json.decoder.JSONDecodeError:
|
||||
current_app.logger.debug(f"is_up_to_date: invalid response (json)")
|
||||
return f"""<div>Attention: réponse invalide de {scu.SCO_WEBSITE}</div>
|
||||
<div>(erreur json).</div>"""
|
||||
|
||||
def html_up_to_date_box():
|
||||
""""""
|
||||
status, msg = is_up_to_date()
|
||||
if status:
|
||||
if infos["status"] != "ok":
|
||||
# problème coté serveur, ignore discrètement
|
||||
log(f"is_up_to_date: server {infos['status']}")
|
||||
return ""
|
||||
return (
|
||||
"""<div class="update_warning">
|
||||
<span>Attention: cette installation de ScoDoc n'est pas à jour.</span>
|
||||
<div class="update_warning_sub">Contactez votre administrateur. %s</div>
|
||||
</div>"""
|
||||
% msg
|
||||
)
|
||||
if (SCOVERSION != infos["last_version"]) and (
|
||||
(time.time() - infos["last_version_date"]) > (24 * 60 * 60)
|
||||
):
|
||||
# nouvelle version publiée depuis plus de 24h !
|
||||
return f"""<div>Attention: {SCONAME} version ({SCOVERSION}) non à jour
|
||||
({infos["last_version"]} disponible).</div>
|
||||
<div>Contacter votre administrateur système
|
||||
(<a href="https://scodoc.org/MisesAJour/">documentation</a>).
|
||||
</div>
|
||||
"""
|
||||
return "" # ok
|
||||
|
@ -361,7 +361,7 @@ SCO_DEV_MAIL = "emmanuel.viennet@gmail.com" # SVP ne pas changer
|
||||
# Adresse pour l'envoi des dumps (pour assistance technnique):
|
||||
# ne pas changer (ou vous perdez le support)
|
||||
SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump"
|
||||
|
||||
SCO_UP2DATE = "https://scodoc.org/scodoc-installmgr/check_version"
|
||||
CSV_FIELDSEP = ";"
|
||||
CSV_LINESEP = "\n"
|
||||
CSV_MIMETYPE = "text/comma-separated-values"
|
||||
@ -608,7 +608,7 @@ def is_valid_filename(filename):
|
||||
return VALID_EXP.match(filename)
|
||||
|
||||
|
||||
def bul_filename(sem, etud, format):
|
||||
def bul_filename_old(sem: dict, etud: dict, format):
|
||||
"""Build a filename for this bulletin"""
|
||||
dt = time.strftime("%Y-%m-%d")
|
||||
filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}"
|
||||
@ -616,6 +616,14 @@ def bul_filename(sem, etud, format):
|
||||
return filename
|
||||
|
||||
|
||||
def bul_filename(formsemestre, etud, format):
|
||||
"""Build a filename for this bulletin"""
|
||||
dt = time.strftime("%Y-%m-%d")
|
||||
filename = f"bul-{formsemestre.titre_num()}-{dt}-{etud.nom}.{format}"
|
||||
filename = make_filename(filename)
|
||||
return filename
|
||||
|
||||
|
||||
def flash_errors(form):
|
||||
"""Flashes form errors (version sommaire)"""
|
||||
for field, errors in form.errors.items():
|
||||
@ -946,7 +954,7 @@ def query_portal(req, msg="Portail Apogee", timeout=3):
|
||||
return r.text
|
||||
|
||||
|
||||
def AnneeScolaire(sco_year=None):
|
||||
def AnneeScolaire(sco_year=None) -> int:
|
||||
"annee de debut de l'annee scolaire courante"
|
||||
if sco_year:
|
||||
year = sco_year
|
||||
|
@ -14,16 +14,25 @@
|
||||
}
|
||||
main{
|
||||
--couleurPrincipale: rgb(240,250,255);
|
||||
--couleurFondTitresUE: rgb(206,255,235);
|
||||
--couleurFondTitresRes: rgb(125, 170, 255);
|
||||
--couleurFondTitresSAE: rgb(211, 255, 255);
|
||||
--couleurFondTitresUE: #b6ebff;
|
||||
--couleurFondTitresRes: #f8c844;
|
||||
--couleurFondTitresSAE: #c6ffab;
|
||||
--couleurSecondaire: #fec;
|
||||
--couleurIntense: #c09;
|
||||
--couleurSurlignage: rgba(232, 255, 132, 0.47);
|
||||
--couleurIntense: rgb(4, 16, 159);;
|
||||
--couleurSurlignage: rgba(255, 253, 110, 0.49);
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
display: none;
|
||||
}
|
||||
.releve a, .releve a:visited {
|
||||
color: navy;
|
||||
text-decoration: none;
|
||||
}
|
||||
.releve a:hover {
|
||||
color: red;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ready .wait{display: none;}
|
||||
.ready main{display: block;}
|
||||
h2{
|
||||
@ -152,23 +161,38 @@ section>div:nth-child(1){
|
||||
column-gap: 4px;
|
||||
flex: none;
|
||||
}
|
||||
.infoSemestre>div:nth-child(1){
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.infoSemestre>div>div:nth-child(even){
|
||||
text-align: right;
|
||||
}
|
||||
.photo {
|
||||
border: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
.rang{
|
||||
font-weight: bold;
|
||||
}
|
||||
.ue .rang{
|
||||
font-weight: 400;
|
||||
}
|
||||
.absencesRecap {
|
||||
align-items: baseline;
|
||||
}
|
||||
.absencesRecap > div:nth-child(2n) {
|
||||
font-weight: normal;
|
||||
}
|
||||
.abs {
|
||||
font-weight: bold;
|
||||
}
|
||||
.decision{
|
||||
margin: 5px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-decoration: underline var(--couleurIntense);
|
||||
}
|
||||
#ects_tot {
|
||||
margin-left: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
.enteteSemestre{
|
||||
color: black;
|
||||
@ -213,7 +237,6 @@ section>div:nth-child(1){
|
||||
scroll-margin-top: 60px;
|
||||
}
|
||||
.module, .ue {
|
||||
background: var(--couleurSecondaire);
|
||||
color: #000;
|
||||
padding: 4px 32px;
|
||||
border-radius: 4px;
|
||||
@ -225,6 +248,15 @@ section>div:nth-child(1){
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.ue {
|
||||
background: var(--couleurFondTitresRes);
|
||||
}
|
||||
.module {
|
||||
background: var(--couleurFondTitresRes);
|
||||
}
|
||||
.module h3 {
|
||||
background: var(--couleurFondTitresRes);
|
||||
}
|
||||
.module::before, .ue::before {
|
||||
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='white'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
|
||||
width: 26px;
|
||||
|
@ -288,13 +288,13 @@ div.logo-insidebar {
|
||||
}
|
||||
div.logo-logo {
|
||||
margin-left: -5px;
|
||||
text-align: center ;
|
||||
text-align: center ;
|
||||
}
|
||||
|
||||
div.logo-logo img {
|
||||
box-sizing: content-box;
|
||||
margin-top: 10px; /* -10px */
|
||||
width: 135px; /* 128px */
|
||||
width: 80px; /* adapter suivant image */
|
||||
padding-right: 5px;
|
||||
}
|
||||
div.sidebar-bottom {
|
||||
@ -1963,7 +1963,20 @@ table.notes_recapcomplet a:hover {
|
||||
div.notes_bulletin {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
div.bull_head {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
div.bull_photo {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
span.bulletin_menubar_but {
|
||||
display: inline-block;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
table.notes_bulletin {
|
||||
border-collapse: collapse;
|
||||
border: 2px solid rgb(100,100,240);
|
||||
@ -2103,12 +2116,6 @@ a.bull_link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
table.bull_head {
|
||||
width: 100%;
|
||||
}
|
||||
td.bull_photo {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.bulletin_menubar {
|
||||
padding-left: 25px;
|
||||
@ -2145,6 +2152,18 @@ div.eval_description {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
div.bul_foot {
|
||||
max-width: 1000px;
|
||||
background: #FFE7D5;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #AAA;
|
||||
padding: 16px 32px;
|
||||
margin: auto;
|
||||
}
|
||||
div.bull_appreciations {
|
||||
border-left: 1px solid black;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
/* Saisie des notes */
|
||||
div.saisienote_etape1 {
|
||||
@ -2864,7 +2883,8 @@ div.othersemlist input {
|
||||
}
|
||||
|
||||
|
||||
div.update_warning {
|
||||
div#update_warning {
|
||||
display: none;
|
||||
border: 1px solid red;
|
||||
background-color: rgb(250,220,220);
|
||||
margin: 3ex;
|
||||
@ -2872,11 +2892,11 @@ div.update_warning {
|
||||
padding-right: 1ex;
|
||||
padding-bottom: 1ex;
|
||||
}
|
||||
div.update_warning span:before {
|
||||
div#update_warning > div:first-child:before {
|
||||
content: url(/ScoDoc/static/icons/warning_img.png);
|
||||
vertical-align: -80%;
|
||||
}
|
||||
div.update_warning_sub {
|
||||
div#update_warning > div:nth-child(2) {
|
||||
font-size: 80%;
|
||||
padding-left: 8ex;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ form.semestre_selector {
|
||||
/* Le tableau */
|
||||
/***************************/
|
||||
.tableau{
|
||||
display: grid;
|
||||
display: inline-grid;
|
||||
grid-auto-rows: minmax(24px, auto);
|
||||
grid-template-columns: fit-content(50px);
|
||||
gap: 2px;
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 34 KiB |
@ -41,7 +41,7 @@ class releveBUT extends HTMLElement {
|
||||
}
|
||||
|
||||
set showData(data) {
|
||||
this.showInformations(data);
|
||||
// this.showInformations(data);
|
||||
this.showSemestre(data);
|
||||
this.showSynthese(data);
|
||||
this.showEvaluations(data);
|
||||
@ -68,13 +68,7 @@ class releveBUT extends HTMLElement {
|
||||
<div>
|
||||
<div class="wait"></div>
|
||||
<main class="releve">
|
||||
<!--------------------------->
|
||||
<!-- Info. étudiant -->
|
||||
<!--------------------------->
|
||||
<section class=etudiant>
|
||||
<img class=studentPic src="" alt="Photo de l'étudiant" width=100 height=120>
|
||||
<div class=infoEtudiant></div>
|
||||
</section>
|
||||
|
||||
|
||||
<!--------------------------------------------------------------------------------------->
|
||||
<!-- Zone spéciale pour que les IUT puisse ajouter des infos locales sur la passerelle -->
|
||||
@ -85,11 +79,11 @@ class releveBUT extends HTMLElement {
|
||||
<!-- Semestre -->
|
||||
<!--------------------------->
|
||||
<section>
|
||||
<h2>Semestre </h2>
|
||||
<div class=flex>
|
||||
<h2 id="identite_etudiant"></h2>
|
||||
<div>
|
||||
<div class=infoSemestre></div>
|
||||
<div>
|
||||
<div class=decision></div>
|
||||
<div><span class=decision></span><span class="ects" id="ects_tot"></span></div>
|
||||
<div class=dateInscription>Inscrit le </div>
|
||||
<em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
|
||||
</div>
|
||||
@ -103,7 +97,7 @@ class releveBUT extends HTMLElement {
|
||||
<section>
|
||||
<div>
|
||||
<div>
|
||||
<h2>Synthèse</h2>
|
||||
<h2>Unités d'enseignement</h2>
|
||||
<em>La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.</em>
|
||||
</div>
|
||||
<div class=CTA_Liste>
|
||||
@ -132,7 +126,7 @@ class releveBUT extends HTMLElement {
|
||||
|
||||
<section>
|
||||
<div>
|
||||
<h2>SAÉ</h2>
|
||||
<h2>Situations d'apprentissage et d'évaluation (SAÉ)</h2>
|
||||
<div class=CTA_Liste>
|
||||
Liste <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 15l-6-6-6 6" />
|
||||
@ -198,7 +192,8 @@ class releveBUT extends HTMLElement {
|
||||
/* Information sur le semestre */
|
||||
/*******************************/
|
||||
showSemestre(data) {
|
||||
this.shadow.querySelector("h2").innerHTML += data.semestre.numero;
|
||||
|
||||
this.shadow.querySelector("#identite_etudiant").innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `;
|
||||
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
|
||||
let output = `
|
||||
<div>
|
||||
@ -209,10 +204,13 @@ class releveBUT extends HTMLElement {
|
||||
<div>Min. promo. :</div><div>${data.semestre.notes.min}</div>
|
||||
</div>
|
||||
<div class=absencesRecap>
|
||||
<div class=enteteSemestre>Absences</div>
|
||||
<div class=enteteSemestre>N.J. ${data.semestre.absences?.injustifie ?? "-"}</div>
|
||||
<div style="grid-column: 2">Total ${data.semestre.absences?.total ?? "-"}</div>
|
||||
</div>`;
|
||||
<div class=enteteSemestre>Absences</div><div class=enteteSemestre>1/2 jour.</div>
|
||||
<div class=abs>Non justifiées</div>
|
||||
<div>${data.semestre.absences?.injustifie ?? "-"}</div>
|
||||
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div>
|
||||
</div>
|
||||
<a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a>
|
||||
`;
|
||||
/*${data.semestre.groupes.map(groupe => {
|
||||
return `
|
||||
<div>
|
||||
@ -226,7 +224,10 @@ class releveBUT extends HTMLElement {
|
||||
}).join("")
|
||||
}*/
|
||||
this.shadow.querySelector(".infoSemestre").innerHTML = output;
|
||||
this.shadow.querySelector(".decision").innerHTML = data.semestre.decision?.code || "";
|
||||
if (data.semestre.decision?.code) {
|
||||
this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || "");
|
||||
}
|
||||
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS : " + (data.semestre.ECTS?.acquis || "-") + " / " + (data.semestre.ECTS?.total || "-");
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
@ -259,7 +260,7 @@ class releveBUT extends HTMLElement {
|
||||
Bonus : ${dataUE.bonus || 0} -
|
||||
Malus : ${dataUE.malus || 0}
|
||||
<span class=ects> -
|
||||
ECTS : ${dataUE.ECTS.acquis} / ${dataUE.ECTS.total}
|
||||
ECTS : ${dataUE.ECTS?.acquis || "-"} / ${dataUE.ECTS?.total || "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
@ -380,9 +381,9 @@ class releveBUT extends HTMLElement {
|
||||
setOptions(options) {
|
||||
Object.entries(options).forEach(([option, value]) => {
|
||||
if (value === false) {
|
||||
document.body.classList.add(option.replace("show", "hide"))
|
||||
this.shadow.children[0].classList.add(option.replace("show", "hide"));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -53,6 +53,18 @@ $(function () {
|
||||
);
|
||||
$(".sco_dropdown_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s");
|
||||
|
||||
/* up-to-date status */
|
||||
var update_div = document.getElementById("update_warning");
|
||||
if (update_div) {
|
||||
fetch('install_info').then(
|
||||
response => response.text()
|
||||
).then(text => {
|
||||
update_div.innerHTML = text;
|
||||
if (text) {
|
||||
update_div.style.display = "block";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
@ -20,6 +20,7 @@ function build_table(data) {
|
||||
data-nbY="${cellule.nbY || 1}"
|
||||
data-data="${cellule.data}"
|
||||
data-orig="${cellule.data}"
|
||||
title="${cellule.title || ""}"
|
||||
style="
|
||||
--x:${cellule.x};
|
||||
--y:${cellule.y};
|
||||
@ -36,10 +37,12 @@ function build_table(data) {
|
||||
/*****************************/
|
||||
/* Gestion des évènements */
|
||||
/*****************************/
|
||||
$(function () {
|
||||
document.body.addEventListener("keydown", key);
|
||||
});
|
||||
|
||||
function installListeners() {
|
||||
if (read_only) {
|
||||
return;
|
||||
}
|
||||
document.body.addEventListener("keydown", key);
|
||||
document.querySelectorAll("[data-editable=true]").forEach(cellule => {
|
||||
cellule.addEventListener("click", function () { selectCell(this) });
|
||||
cellule.addEventListener("dblclick", function () { modifCell(this) });
|
||||
|
61
app/templates/bul_foot.html
Normal file
61
app/templates/bul_foot.html
Normal file
@ -0,0 +1,61 @@
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
{# Pied des bulletins HTML #}
|
||||
|
||||
<div class="{{css_class or "bul_foot"}}">
|
||||
<div>
|
||||
<p>Situation actuelle:
|
||||
{% if inscription_courante %}
|
||||
<a class="stdlink" href="{{url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=inscription_courante.formsemestre_id)
|
||||
}}">{{inscription_str}}</a>
|
||||
{% else %}
|
||||
{{inscription_str}}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if appreciations is not none %}
|
||||
<div class="bull_appreciations">
|
||||
<h3>Appréciations</h3>
|
||||
{% for app in appreciations %}
|
||||
<p><span class="bull_appreciations_date">{{app.date}}</span>{{
|
||||
app.comment}}<span
|
||||
class="bull_appreciations_link">{% if can_edit_appreciations %}<a
|
||||
class="stdlink" href="{{url_for('notes.appreciation_add_form',
|
||||
scodoc_dept=g.scodoc_dept, id=app.id)}}">modifier</a>
|
||||
<a class="stdlink" href="{{url_for('notes.appreciation_add_form',
|
||||
scodoc_dept=g.scodoc_dept, id=app.id, suppress=1)}}">supprimer</a>{% endif %}
|
||||
</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% if can_edit_appreciations %}
|
||||
<p><a class="stdlink" href="{{url_for(
|
||||
'notes.appreciation_add_form', scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id, formsemestre_id=formsemestre_id)
|
||||
}}">Ajouter une appréciation</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if formsemestre.modalite == "EXT" %}
|
||||
<p><a href="{{
|
||||
url_for('notes.formsemestre_ext_edit_ue_validations',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id)}}"
|
||||
class="stdlink">
|
||||
Éditer les validations d'UE dans ce semestre extérieur
|
||||
</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Place du diagramme radar #}
|
||||
<form id="params">
|
||||
<input type="hidden" name="etudid" id="etudid" value="{{etud.id}}"/>
|
||||
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="{{formsemestre.id}}"/>
|
||||
</form>
|
||||
<div id="radar_bulletin"></div>
|
||||
|
||||
|
57
app/templates/bul_head.html
Normal file
57
app/templates/bul_head.html
Normal file
@ -0,0 +1,57 @@
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
{# L'en-tête des bulletins HTML #}
|
||||
{# was _formsemestre_bulletinetud_header_html #}
|
||||
|
||||
<div class="bull_head">
|
||||
<div class="bull_head_text">
|
||||
{% if not is_apc %}
|
||||
<h2><a class="discretelink" href="{{
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id,
|
||||
)}}">{{etud.nomprenom}}</a></h2>
|
||||
{% endif %}
|
||||
<form name="f" method="GET" action="{{request.base_url}}">
|
||||
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}"></input>
|
||||
<input type="hidden" name="etudid" value="{{etud.id}}"></input>
|
||||
<input type="hidden" name="format" value="{{format}}"></input>
|
||||
Bulletin
|
||||
<span class="bull_liensemestre"><a href="{{
|
||||
url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)}}">{{formsemestre.titre_mois()
|
||||
}}</a></span>
|
||||
|
||||
<div>
|
||||
<em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em>
|
||||
<span class="rightjust">
|
||||
<select name="version" onchange="document.f.submit()" class="noprint">
|
||||
{% for (v, e) in (
|
||||
("short", "Version courte"),
|
||||
("selectedevals", "Version intermédiaire"),
|
||||
("long", "Version complète"),
|
||||
) %}
|
||||
<option value="{{v}}" {% if (v == version) %}selected{% endif %}>{{e}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
<span class="bulletin_menubar">
|
||||
<span class="bulletin_menubar_but">{{menu_autres_operations|safe}}</span>
|
||||
<a href="{{url_for(
|
||||
'notes.formsemestre_bulletinetud',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
format='pdf',
|
||||
version=version,
|
||||
)}}">{{scu.ICON_PDF|safe}}</a>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if not is_apc %}
|
||||
<div class="bull_photo"><a href="{{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}}">{{etud.photo_html(title="fiche de " + etud["nom"])|safe}}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -7,8 +7,13 @@
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
{% include 'bul_head.html' %}
|
||||
|
||||
<releve-but></releve-but>
|
||||
<script src="/ScoDoc/static/js/releve-but.js"></script>
|
||||
|
||||
{% include 'bul_foot.html' %}
|
||||
|
||||
<script>
|
||||
let dataSrc = "{{bul_url|safe}}";
|
||||
fetch(dataSrc)
|
||||
|
@ -36,12 +36,15 @@
|
||||
<h1>Gestion des images: logos, signatures, ...</h1>
|
||||
<div class="sco_help">Ces images peuvent être intégrées dans les documents
|
||||
générés par ScoDoc: bulletins, PV, etc.</div>
|
||||
<p><a href="{{url_for('scodoc.configure_logos')}}">configuration des images et logos</a>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.configure_logos')}}">configuration des images et logos</a>
|
||||
</p>
|
||||
|
||||
<h1>Exports Apogée</h1>
|
||||
<p><a href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a></p>
|
||||
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a></p>
|
||||
|
||||
<h1>Utilisateurs</h1>
|
||||
<p><a class="stdlink" href="{{url_for('auth.reset_standard_roles_permissions')}}">remettre les permissions des
|
||||
rôles standards à leurs valeurs par défaut</a> (efface les modifications apportées)</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% import 'bootstrap/wtf.html' as wtf %}
|
||||
|
||||
{% block title %}Une erreur est survenue !{% endblock %}
|
||||
{% block body %}
|
||||
{% block app_content %}
|
||||
<h1>Une erreur est survenue !</h1>
|
||||
<p>Oups...</tt> <span style="color:red;"><b>ScoDoc version
|
||||
<span style="font-size: 120%;">{{SCOVERSION}}</span></b></span>
|
||||
@ -12,10 +12,22 @@
|
||||
|
||||
<p> Si le problème persiste, contacter l'administrateur de votre site,
|
||||
ou écrire la liste "notes" <a href="mailto:notes@listes.univ-paris13.fr">notes@listes.univ-paris13.fr</a> en
|
||||
indiquant la version du logiciel
|
||||
<br />
|
||||
(plus d'informations sur les listes de diffusion <a href="https://scodoc.org/ListesDeDiffusion/">voir
|
||||
cette page</a>).
|
||||
indiquant la version du logiciel.
|
||||
</p>
|
||||
{% if 'scodoc_dept' in g %}
|
||||
<p>Pour aider les développeurs à corriger le problème, nous vous suggérons d'envoyer les données
|
||||
sur votre configuration:
|
||||
<form method="POST" action="{{ url_for( 'scolar.sco_dump_and_send_db', scodoc_dept=g.scodoc_dept ) }}">
|
||||
<input type="hidden" name="request_url" value="{{request_url}}">
|
||||
<input type="submit" value="Envoyer données assistance">
|
||||
<div>Message optionnel: </div>
|
||||
<textarea name="message" cols="40" rows="4"></textarea>
|
||||
|
||||
</form>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>Pour plus d'informations sur les listes de diffusion <a href="https://scodoc.org/ListesDeDiffusion/">voir
|
||||
cette page</a>.
|
||||
</p>
|
||||
|
||||
<p><a href="{{ url_for('scodoc.index') }}">retour à la page d'accueil</a></p>
|
||||
|
50
app/templates/formsemestre_page_title.html
Normal file
50
app/templates/formsemestre_page_title.html
Normal file
@ -0,0 +1,50 @@
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
{# Element HTML decrivant un semestre (barre de menu et infos) #}
|
||||
{# was formsemestre_page_title #}
|
||||
|
||||
<div class="formsemestre_page_title">
|
||||
<div class="infos">
|
||||
<span class="semtitle"><a class="stdlink"
|
||||
title="{{formsemestre.session_id()}}"
|
||||
href="{{url_for('notes.formsemestre_status',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)}}"
|
||||
>{{formsemestre.titre}}</a>
|
||||
{%- if formsemestre.semestre_id != -1 -%}
|
||||
<a
|
||||
title="{{formsemestre.etapes_apo_str()
|
||||
}}">, {{
|
||||
formsemestre.formation.get_parcours().SESSION_NAME}}
|
||||
{{formsemestre.semestre_id}}</a>
|
||||
{%- endif -%}
|
||||
{%- if formsemestre.modalite %} en {{formsemestre.modalite}}
|
||||
{%- endif %}</span><span
|
||||
class="dates"><a
|
||||
title="du {{formsemestre.date_debut.strftime('%d/%m/%Y')}}
|
||||
au {{formsemestre.date_fin.strftime('%d/%m/%Y')}} "
|
||||
>{{formsemestre.mois_debut()}} - {{formsemestre.mois_fin()}}</a></span><span
|
||||
class="resp"><a title="{{formsemestre.responsables_str(abbrev_prenom=False)}}">{{formsemestre.responsables_str()}}</a></span><span
|
||||
class="nbinscrits"><a class="discretelink"
|
||||
href="{{url_for('scolar.groups_view',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}}"
|
||||
>{{formsemestre.etuds_inscriptions|length}} inscrits</a></span><span
|
||||
class="lock">
|
||||
{%-if not formsemestre.etat -%}
|
||||
<a href="{{ url_for( 'notes.formsemestre_change_lock',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )}}">{{
|
||||
scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe
|
||||
}}</a>
|
||||
{%- endif -%}
|
||||
</span><span class="eye"><a href="{{
|
||||
url_for('notes.formsemestre_change_publication_bul',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
|
||||
}}">{%-
|
||||
if formsemestre.bul_hide_xml -%}
|
||||
{{scu.icontag("hide_img", border="0", title="Bulletins NON publiés")|safe}}
|
||||
{%- else -%}
|
||||
{{scu.icontag("eye_img", border="0", title="Bulletins publiés")|safe}}
|
||||
{%- endif -%}
|
||||
</a></span>
|
||||
</div>
|
||||
{{sem_menu_bar|safe}}
|
||||
</div>
|
@ -1,8 +1,11 @@
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
<h2>Édition des coefficients des modules vers les UEs</h2>
|
||||
<h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2>
|
||||
<div class="help">
|
||||
{% if not read_only %}
|
||||
Double-cliquer pour changer une valeur.
|
||||
Les valeurs sont automatiquement enregistrées au fur et à mesure.
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<form class="semestre_selector">Semestre:
|
||||
<select onchange="this.form.submit()"" name="semestre_idx" id="semestre_idx">
|
||||
@ -21,6 +24,7 @@
|
||||
<div class="tableau"></div>
|
||||
|
||||
<script>
|
||||
var read_only={{"true" if read_only else "false"}};
|
||||
$(function () {
|
||||
let data_url = "{{data_source}}";
|
||||
$.getJSON(data_url, function (data) {
|
||||
|
@ -9,6 +9,7 @@
|
||||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
||||
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" />
|
||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||
{# <link href="/ScoDoc/static/css/tooltip.css" rel="stylesheet" type="text/css" /> #}
|
||||
<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css" />
|
||||
{% endblock %}
|
||||
|
||||
|
@ -50,27 +50,29 @@ def close_dept_db_connection(arg):
|
||||
class ScoData:
|
||||
"""Classe utilisée pour passer des valeurs aux vues (templates)"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, etud=None, formsemestre=None):
|
||||
# Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête)
|
||||
self.Permission = Permission
|
||||
self.scu = scu
|
||||
self.SCOVERSION = sco_version.SCOVERSION
|
||||
# -- Informations étudiant courant, si sélectionné:
|
||||
etudid = g.get("etudid", None)
|
||||
if not etudid:
|
||||
if request.method == "GET":
|
||||
etudid = request.args.get("etudid", None)
|
||||
elif request.method == "POST":
|
||||
etudid = request.form.get("etudid", None)
|
||||
|
||||
if etudid:
|
||||
if etud is None:
|
||||
etudid = g.get("etudid", None)
|
||||
if etudid is None:
|
||||
if request.method == "GET":
|
||||
etudid = request.args.get("etudid", None)
|
||||
elif request.method == "POST":
|
||||
etudid = request.form.get("etudid", None)
|
||||
if etudid is not None:
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
self.etud = etud
|
||||
if etud is not None:
|
||||
# Infos sur l'étudiant courant
|
||||
self.etud = Identite.query.get_or_404(etudid)
|
||||
ins = self.etud.inscription_courante()
|
||||
if ins:
|
||||
self.etud_cur_sem = ins.formsemestre
|
||||
self.nbabs, self.nbabsjust = sco_abs.get_abs_count_in_interval(
|
||||
etudid,
|
||||
etud.id,
|
||||
self.etud_cur_sem.date_debut.isoformat(),
|
||||
self.etud_cur_sem.date_fin.isoformat(),
|
||||
)
|
||||
@ -80,16 +82,26 @@ class ScoData:
|
||||
else:
|
||||
self.etud = None
|
||||
# --- Informations sur semestre courant, si sélectionné
|
||||
formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request()
|
||||
if formsemestre_id is None:
|
||||
if formsemestre is None:
|
||||
formsemestre_id = (
|
||||
sco_formsemestre_status.retreive_formsemestre_from_request()
|
||||
)
|
||||
if formsemestre_id is not None:
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre is None:
|
||||
self.sem = None
|
||||
self.sem_menu_bar = None
|
||||
else:
|
||||
self.sem = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
self.sem = formsemestre
|
||||
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
|
||||
self.sem.to_dict()
|
||||
)
|
||||
# --- Préférences
|
||||
# prefs fallback to global pref if sem is None:
|
||||
if formsemestre:
|
||||
formsemestre_id = formsemestre.id
|
||||
else:
|
||||
formsemestre_id = None
|
||||
self.prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
|
||||
|
||||
|
@ -613,7 +613,7 @@ def SignaleAbsenceGrSemestre(
|
||||
"modimpl_id": modimpl["moduleimpl_id"],
|
||||
"modname": (modimpl["module"]["code"] or "")
|
||||
+ " "
|
||||
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]),
|
||||
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or ""),
|
||||
"sel": sel,
|
||||
}
|
||||
)
|
||||
|
@ -32,6 +32,7 @@ Emmanuel Viennet, 2021
|
||||
"""
|
||||
|
||||
from operator import itemgetter
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import flask
|
||||
@ -72,6 +73,7 @@ from app.scodoc.scolog import logdb
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoBugCatcher,
|
||||
ScoException,
|
||||
ScoValueError,
|
||||
ScoInvalidIdType,
|
||||
@ -276,7 +278,7 @@ sco_publish(
|
||||
def formsemestre_bulletinetud(
|
||||
etudid=None,
|
||||
formsemestre_id=None,
|
||||
format="html",
|
||||
format=None,
|
||||
version="long",
|
||||
xml_with_decisions=False,
|
||||
force_publishing=False,
|
||||
@ -284,39 +286,47 @@ def formsemestre_bulletinetud(
|
||||
code_nip=None,
|
||||
code_ine=None,
|
||||
):
|
||||
format = format or "html"
|
||||
if not formsemestre_id:
|
||||
flask.abort(404, "argument manquant: formsemestre_id")
|
||||
if not isinstance(formsemestre_id, int):
|
||||
raise ScoInvalidIdType("formsemestre_id must be an integer !")
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if etudid:
|
||||
etud = models.Identite.query.get_or_404(etudid)
|
||||
elif code_nip:
|
||||
etud = (
|
||||
models.Identite.query.filter_by(code_nip=str(code_nip))
|
||||
.filter_by(dept_id=formsemestre.dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
elif code_ine:
|
||||
etud = (
|
||||
models.Identite.query.filter_by(code_ine=str(code_ine))
|
||||
.filter_by(dept_id=formsemestre.dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
else:
|
||||
raise ScoValueError(
|
||||
"Paramètre manquant: spécifier etudid, code_nip ou code_ine"
|
||||
)
|
||||
if formsemestre.formation.is_apc() and format != "oldjson":
|
||||
if etudid:
|
||||
etud = models.Identite.query.get_or_404(etudid)
|
||||
elif code_nip:
|
||||
etud = (
|
||||
models.Identite.query.filter_by(code_nip=str(code_nip))
|
||||
.filter_by(dept_id=formsemestre.dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
elif code_ine:
|
||||
etud = (
|
||||
models.Identite.query.filter_by(code_ine=str(code_ine))
|
||||
.filter_by(dept_id=formsemestre.dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
else:
|
||||
raise ScoValueError(
|
||||
"Paramètre manquant: spécifier code_nip ou etudid ou code_ine"
|
||||
)
|
||||
if format == "json":
|
||||
r = bulletin_but.BulletinBUT(formsemestre)
|
||||
return jsonify(
|
||||
r.bulletin_etud(etud, formsemestre, force_publishing=force_publishing)
|
||||
r.bulletin_etud(
|
||||
etud,
|
||||
formsemestre,
|
||||
force_publishing=force_publishing,
|
||||
version=version,
|
||||
)
|
||||
)
|
||||
elif format == "html":
|
||||
return render_template(
|
||||
"but/bulletin.html",
|
||||
title=f"Bul. {etud.nom} - BUT",
|
||||
appreciations=models.BulAppreciations.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=formsemestre.id
|
||||
).order_by(models.BulAppreciations.date),
|
||||
bul_url=url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
@ -324,8 +334,23 @@ def formsemestre_bulletinetud(
|
||||
etudid=etudid,
|
||||
format="json",
|
||||
force_publishing=1, # pour ScoDoc lui même
|
||||
version=version,
|
||||
),
|
||||
sco=ScoData(),
|
||||
can_edit_appreciations=formsemestre.est_responsable(current_user)
|
||||
or (current_user.has_permission(Permission.ScoEtudInscrit)),
|
||||
etud=etud,
|
||||
formsemestre=formsemestre,
|
||||
inscription_courante=etud.inscription_courante(),
|
||||
inscription_str=etud.inscription_descr()["inscription_str"],
|
||||
is_apc=formsemestre.formation.is_apc(),
|
||||
menu_autres_operations=sco_bulletins.make_menu_autres_operations(
|
||||
formsemestre, etud, "notes.formsemestre_bulletinetud", version
|
||||
),
|
||||
sco=ScoData(etud=etud),
|
||||
scu=scu,
|
||||
time=time,
|
||||
title=f"Bul. {etud.nom} - BUT",
|
||||
version=version,
|
||||
)
|
||||
|
||||
if not (etudid or code_nip or code_ine):
|
||||
@ -335,7 +360,7 @@ def formsemestre_bulletinetud(
|
||||
if format == "oldjson":
|
||||
format = "json"
|
||||
return sco_bulletins.formsemestre_bulletinetud(
|
||||
etudid=etudid,
|
||||
etud,
|
||||
formsemestre_id=formsemestre_id,
|
||||
format=format,
|
||||
version=version,
|
||||
@ -1796,7 +1821,12 @@ def formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
||||
return scu.sendPDFFile(pdfdoc, filename)
|
||||
|
||||
|
||||
_EXPL_BULL = """Versions des bulletins:<ul><li><bf>courte</bf>: moyennes des modules</li><li><bf>intermédiaire</bf>: moyennes des modules et notes des évaluations sélectionnées</li><li><bf>complète</bf>: toutes les notes</li><ul>"""
|
||||
_EXPL_BULL = """Versions des bulletins:
|
||||
<ul>
|
||||
<li><bf>courte</bf>: moyennes des modules (en BUT: seulement les moyennes d'UE)</li>
|
||||
<li><bf>intermédiaire</bf>: moyennes des modules et notes des évaluations sélectionnées</li>
|
||||
<li><bf>complète</bf>: toutes les notes</li>
|
||||
</ul>"""
|
||||
|
||||
|
||||
@bp.route("/formsemestre_bulletins_pdf_choice")
|
||||
@ -1866,7 +1896,6 @@ def formsemestre_bulletins_choice(
|
||||
formsemestre_id, title="", explanation="", choose_mail=False
|
||||
):
|
||||
"""Choix d'une version de bulletin"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header(title),
|
||||
"""
|
||||
|
@ -37,6 +37,7 @@ from flask import current_app, g, request
|
||||
from flask.templating import render_template
|
||||
from flask_login import current_user
|
||||
from werkzeug.utils import redirect
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
|
||||
from config import Config
|
||||
|
||||
@ -111,7 +112,7 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None):
|
||||
"y": row,
|
||||
"style": style,
|
||||
"data": df[mod.id][ue.id] or "",
|
||||
"editable": True,
|
||||
"editable": mod.ue.type != UE_SPORT,
|
||||
"module_id": mod.id,
|
||||
"ue_id": ue.id,
|
||||
}
|
||||
@ -151,7 +152,7 @@ def set_module_ue_coef():
|
||||
|
||||
@bp.route("/edit_modules_ue_coefs")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoChangeFormation)
|
||||
@permission_required(Permission.ScoView)
|
||||
def edit_modules_ue_coefs():
|
||||
"""Formulaire édition grille coefs EU/modules"""
|
||||
formation_id = int(request.args["formation_id"])
|
||||
@ -194,6 +195,7 @@ def edit_modules_ue_coefs():
|
||||
"notes.set_module_ue_coef",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
),
|
||||
read_only=not current_user.has_permission(Permission.ScoChangeFormation),
|
||||
semestre_idx=semestre_idx,
|
||||
semestre_ids=range(1, formation.get_parcours().NB_SEM + 1),
|
||||
),
|
||||
|
@ -54,6 +54,11 @@ from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.decorators import (
|
||||
admin_required,
|
||||
scodoc7func,
|
||||
scodoc,
|
||||
)
|
||||
from app.forms.main import config_logos, config_main
|
||||
from app.forms.main.create_dept import CreateDeptForm
|
||||
from app.forms.main.config_apo import CodesDecisionsForm
|
||||
@ -63,14 +68,11 @@ from app.models import departements
|
||||
from app.models import FormSemestre, FormSemestreInscription
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import UniteEns
|
||||
from app.scodoc import sco_codes_parcours, sco_logos
|
||||
|
||||
from app.scodoc import sco_find_etud
|
||||
from app.scodoc import sco_logos
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.decorators import (
|
||||
admin_required,
|
||||
scodoc7func,
|
||||
scodoc,
|
||||
)
|
||||
|
||||
from app.scodoc.sco_exceptions import AccessDenied
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import scodoc_bp as bp
|
||||
@ -248,7 +250,7 @@ def about(scodoc_dept=None):
|
||||
# INSTITUTION_ADDRESS = "Web <b>www.sor.bonne.top</b> - 11, rue Simon Crubelier - 75017 Paris"
|
||||
# INSTITUTION_CITY = "Paris"
|
||||
# Textareas:
|
||||
# DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s"
|
||||
# DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s"
|
||||
|
||||
# Booléens
|
||||
# always_require_ine
|
||||
|
@ -30,7 +30,7 @@ issu de ScoDoc7 / ZScolar.py
|
||||
|
||||
Emmanuel Viennet, 2021
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
|
||||
import flask
|
||||
@ -65,23 +65,18 @@ from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
)
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
import app
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_import_etuds
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_archives_etud
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_debouche
|
||||
from app.scodoc import sco_dept
|
||||
from app.scodoc import sco_dump_db
|
||||
from app.scodoc import sco_edt_cal
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_find_etud
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_edit
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_edit
|
||||
@ -92,12 +87,10 @@ from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_portal_apogee
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_report
|
||||
from app.scodoc import sco_synchro_etuds
|
||||
from app.scodoc import sco_trombino
|
||||
from app.scodoc import sco_trombino_tours
|
||||
from app.scodoc import sco_up_to_date
|
||||
from app.scodoc import sco_etud
|
||||
|
||||
|
||||
def sco_publish(route, function, permission, methods=["GET"]):
|
||||
@ -338,6 +331,14 @@ def index_html(showcodes=0, showsemtable=0):
|
||||
return sco_dept.index_html(showcodes=showcodes, showsemtable=showsemtable)
|
||||
|
||||
|
||||
@bp.route("/install_info")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def install_info():
|
||||
"""Information on install status (html str)"""
|
||||
return sco_up_to_date.is_up_to_date()
|
||||
|
||||
|
||||
sco_publish(
|
||||
"/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"]
|
||||
)
|
||||
@ -513,7 +514,7 @@ def etud_info(etudid=None, format="xml"):
|
||||
|
||||
sem = etud["cursem"]
|
||||
if sem:
|
||||
sco_groups.etud_add_group_infos(etud, sem)
|
||||
sco_groups.etud_add_group_infos(etud, sem["formsemestre_id"] if sem else None)
|
||||
d["insemestre"] = [
|
||||
{
|
||||
"current": "1",
|
||||
@ -2190,7 +2191,31 @@ def stat_bac(formsemestre_id):
|
||||
return Bacs
|
||||
|
||||
|
||||
# --- Dump
|
||||
sco_publish(
|
||||
"/sco_dump_and_send_db", sco_dump_db.sco_dump_and_send_db, Permission.ScoView
|
||||
)
|
||||
# --- Dump (assistance)
|
||||
@bp.route("/sco_dump_and_send_db", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@scodoc7func
|
||||
def sco_dump_and_send_db(message="", request_url=""):
|
||||
"Send anonymized data to supervision"
|
||||
|
||||
status_code = sco_dump_db.sco_dump_and_send_db(message, request_url)
|
||||
H = [html_sco_header.sco_header(page_title="Assistance technique")]
|
||||
if 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 status_code == requests.codes.OK: # pylint: disable=no-member
|
||||
H.append("""<p>Opération effectuée.</p>""")
|
||||
else:
|
||||
H.append(
|
||||
f"""<p class="warning">
|
||||
Erreur: code <tt>{status_code}</tt>
|
||||
Merci de contacter <a href="mailto:{scu.SCO_DEV_MAIL}">{scu.SCO_DEV_MAIL}</a></p>"""
|
||||
)
|
||||
flash("Données envoyées au serveur d'assistance")
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
@ -153,7 +153,7 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
|
||||
"form. création ou édition utilisateur"
|
||||
if user_name is not None: # scodoc7func converti en int !
|
||||
user_name = str(user_name)
|
||||
Role.insert_roles() # assure la mise à jour des rôles en base
|
||||
Role.ensure_standard_roles() # assure la présence des rôles en base
|
||||
auth_dept = current_user.dept
|
||||
from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email
|
||||
initvalues = {}
|
||||
|
@ -1,13 +1,14 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "Ent9.2/71"
|
||||
SCOVERSION = "9.1.84e"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
SCONEWS = """
|
||||
<h4>Année 2021</h4>
|
||||
<ul>
|
||||
<li>ScoDoc 9.1.75: bulletins BUT pdf</li>
|
||||
<li>ScoDoc 9.1.50: nombreuses amélioration gestion BUT</li>
|
||||
<li>ScoDoc 9.1: gestion des formations par compétences, type BUT.</li>
|
||||
<li>ScoDoc 9.0: nouvelle architecture logicielle (Flask/Python3/Debian 11)</li>
|
||||
|
19
scodoc.py
19
scodoc.py
@ -14,6 +14,7 @@ import click
|
||||
import flask
|
||||
from flask.cli import with_appcontext
|
||||
from flask.templating import render_template
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
import psycopg2
|
||||
import sqlalchemy
|
||||
|
||||
@ -33,6 +34,7 @@ from app.models.evaluations import Evaluation
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import notes, scolar
|
||||
import tools
|
||||
from tools.fakedatabase import create_test_api_database
|
||||
|
||||
from config import RunningConfig
|
||||
|
||||
@ -44,7 +46,6 @@ cli.register(app)
|
||||
def make_shell_context():
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
import app as mapp # le package app
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@ -84,6 +85,7 @@ def make_shell_context():
|
||||
|
||||
|
||||
# ctx.push()
|
||||
# admin = User.query.filter_by(user_name="admin").first()
|
||||
# login_user(admin)
|
||||
|
||||
|
||||
@ -492,6 +494,21 @@ def clear_cache(sanitize): # clear-cache
|
||||
formation.sanitize_old_formation()
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def init_test_database():
|
||||
"""Initialise les objets en base pour les tests API
|
||||
(à appliquer sur SCODOC_TEST ou SCODOC_DEV)
|
||||
"""
|
||||
click.echo("Initialisation base de test API...")
|
||||
# import app as mapp # le package app
|
||||
|
||||
ctx = app.test_request_context()
|
||||
ctx.push()
|
||||
admin = User.query.filter_by(user_name="admin").first()
|
||||
login_user(admin)
|
||||
create_test_api_database.init_test_database()
|
||||
|
||||
|
||||
def recursive_help(cmd, parent=None):
|
||||
ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
|
||||
print(cmd.get_help(ctx))
|
||||
|
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<formation id="220" titre="BUT R&amp;T" version="1" formation_code="V1RET" dept_id="5" acronyme="BUT R&amp;T" titre_officiel="Bachelor technologique réseaux et télécommunications" type_parcours="700" formation_id="220">
|
||||
<ue acronyme="RT1.1" numero="1" titre="Administrer les réseaux et l’Internet" type="0" ue_code="UCOD11" ects="12.0" is_external="0" code_apogee="" coefficient="0.0" semestre_idx="1" color="#B80004" reference="1896">
|
||||
<matiere titre="Administrer les réseaux et l’Internet" numero="1">
|
||||
<module titre="Initiation aux réseaux informatiques" abbrev="Init aux réseaux informatiques" code="R101" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="10" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="12.0"/>
|
||||
<coefficients ue_reference="1897" coef="4.0"/>
|
||||
<coefficients ue_reference="1898" coef="4.0"/>
|
||||
</module>
|
||||
<module titre="Se sensibiliser à l&apos;hygiène informatique et à la cybersécurité" abbrev="Hygiène informatique" code="SAE11" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="10" code_apogee="" module_type="3">
|
||||
<coefficients ue_reference="1896" coef="16.0"/>
|
||||
</module>
|
||||
<module titre="Principe et architecture des réseaux" abbrev="" code="R102" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="20" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="12.0"/>
|
||||
</module>
|
||||
<module titre="Réseaux locaux et équipements actifs" abbrev="Réseaux locaux" code="R103" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="30" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="8.0"/>
|
||||
<coefficients ue_reference="1897" coef="4.0"/>
|
||||
</module>
|
||||
<module titre="Fondamentaux des systèmes électroniques" abbrev="" code="R104" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="40" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="8.0"/>
|
||||
<coefficients ue_reference="1897" coef="5.0"/>
|
||||
</module>
|
||||
<module titre="Architecture des systèmes numériques et informatiques" abbrev="" code="R106" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="60" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="10.0"/>
|
||||
</module>
|
||||
</matiere>
|
||||
</ue>
|
||||
<ue acronyme="RT2.1" numero="2" titre="Connecter les entreprises et les usagers" type="0" ue_code="UCOD12" ects="8.0" is_external="0" code_apogee="" coefficient="0.0" semestre_idx="1" color="#F97B3D" reference="1897">
|
||||
<matiere titre="Connecter les entreprises et les usagers" numero="1">
|
||||
<module titre="S&apos;initier aux réseaux informatiques" abbrev="" code="SAE12" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="20" code_apogee="" module_type="3">
|
||||
<coefficients ue_reference="1896" coef="33.0"/>
|
||||
</module>
|
||||
<module titre="Découvrir un dispositif de tranmission" abbrev="" code="SAE13" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="30" code_apogee="" module_type="3">
|
||||
<coefficients ue_reference="1897" coef="33.0"/>
|
||||
</module>
|
||||
<module titre="Support de transmission pour les réseaux locaux" abbrev="Support de transmission" code="R105" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="50" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1897" coef="5.0"/>
|
||||
</module>
|
||||
<module titre="Anglais général et init vocabulaire technique" abbrev="" code="R110" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="100" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="3.0"/>
|
||||
<coefficients ue_reference="1897" coef="5.0"/>
|
||||
<coefficients ue_reference="1898" coef="5.0"/>
|
||||
</module>
|
||||
<module titre="Expression-culture-Communication Pro." abbrev="" code="R111" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="110" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="3.0"/>
|
||||
<coefficients ue_reference="1897" coef="5.0"/>
|
||||
<coefficients ue_reference="1898" coef="4.0"/>
|
||||
</module>
|
||||
<module titre="Mathématiques du signal" abbrev="" code="R113" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="130" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="5.0"/>
|
||||
<coefficients ue_reference="1897" coef="8.0"/>
|
||||
</module>
|
||||
<module titre="Mathématiques des transmissions" abbrev="" code="R114" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="140" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="4.0"/>
|
||||
<coefficients ue_reference="1897" coef="8.0"/>
|
||||
</module>
|
||||
</matiere>
|
||||
</ue>
|
||||
<ue acronyme="RT3.1" numero="3" titre="Créer des outils et applications informatiques pour les R&amp;T" type="0" ue_code="UCOD13" ects="10.0" is_external="0" code_apogee="" coefficient="0.0" semestre_idx="1" color="#FEB40B" reference="1898">
|
||||
<matiere titre="Créer des outils et applications informatiques pour les R&amp;T" numero="1">
|
||||
<module titre="Se présenter sur Internet" abbrev="" code="SAE14" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="40" code_apogee="" module_type="3">
|
||||
<coefficients ue_reference="1898" coef="16.0"/>
|
||||
</module>
|
||||
<module titre="Traiter des données" abbrev="" code="SAE15" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="50" code_apogee="" module_type="3">
|
||||
<coefficients ue_reference="1898" coef="26.0"/>
|
||||
</module>
|
||||
<module titre="Portofolio" abbrev="" code="SAE16" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="60" code_apogee="" module_type="3"/>
|
||||
<module titre="Fondamentaux de la programmation" abbrev="" code="R107" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="70" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1898" coef="22.0"/>
|
||||
</module>
|
||||
<module titre="Base des systèmes d&apos;exploitation" abbrev="" code="R108" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="80" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="6.0"/>
|
||||
<coefficients ue_reference="1898" coef="7.0"/>
|
||||
</module>
|
||||
<module titre="Introduction aux technologies Web" abbrev="" code="R109" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="90" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1898" coef="4.0"/>
|
||||
</module>
|
||||
<module titre="PPP" abbrev="" code="R112" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="120" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1896" coef="2.0"/>
|
||||
<coefficients ue_reference="1897" coef="3.0"/>
|
||||
<coefficients ue_reference="1898" coef="4.0"/>
|
||||
</module>
|
||||
<module titre="Gestion de projets" abbrev="" code="R115" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="150" code_apogee="" module_type="2">
|
||||
<coefficients ue_reference="1897" coef="2.0"/>
|
||||
<coefficients ue_reference="1898" coef="4.0"/>
|
||||
</module>
|
||||
</matiere>
|
||||
</ue>
|
||||
</formation>
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
"""XXX OBSOLETE
|
||||
|
||||
Scenario: préparation base de données pour tests Selenium
|
||||
|
||||
S'utilise comme un test avec pytest, mais n'est pas un test !
|
||||
|
@ -40,7 +40,7 @@ def test_roles_permissions(test_client):
|
||||
role.remove_permission(perm)
|
||||
assert not role.has_permission(perm)
|
||||
# Default roles:
|
||||
Role.insert_roles()
|
||||
Role.reset_standard_roles_permissions()
|
||||
# Bien présents ?
|
||||
role_names = [r.name for r in Role.query.filter_by().all()]
|
||||
assert len(role_names) == len(SCO_ROLES_DEFAULTS)
|
||||
|
@ -7,7 +7,7 @@
|
||||
dateext
|
||||
create 0644 scodoc scodoc
|
||||
}
|
||||
/opt/scodoc-datalog/scodoc_exc.log {
|
||||
/opt/scodoc-data/log/scodoc_exc.log {
|
||||
weekly
|
||||
missingok
|
||||
rotate 64
|
||||
|
150
tools/fakedatabase/create_test_api_database.py
Normal file
150
tools/fakedatabase/create_test_api_database.py
Normal file
@ -0,0 +1,150 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Initialise une base pour les tests de l'API ScoDoc 9
|
||||
|
||||
Création des départements, formations, semestres, étudiants, groupes...
|
||||
|
||||
utilisation:
|
||||
1) modifier le .env pour indiquer
|
||||
SCODOC_DATABASE_URI="postgresql:///SCO_TEST_API"
|
||||
|
||||
2) En tant qu'utilisateur scodoc, lancer:
|
||||
tools/create_database.sh SCO_TEST_API
|
||||
flask db upgrade
|
||||
flask sco-db-init --erase
|
||||
flask init-test-database
|
||||
|
||||
3) relancer ScoDoc:
|
||||
flask run --host 0.0.0.0
|
||||
|
||||
4) lancer client de test (ou vérifier dans le navigateur)
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import random
|
||||
|
||||
random.seed(12345678) # tests reproductibles
|
||||
|
||||
from flask_login import login_user
|
||||
|
||||
from app import auth
|
||||
from app import models
|
||||
from app import db
|
||||
from app.scodoc import (
|
||||
sco_formations,
|
||||
sco_formsemestre,
|
||||
sco_formsemestre_inscriptions,
|
||||
sco_groups,
|
||||
)
|
||||
from tools.fakeportal.gen_nomprenoms import nomprenom
|
||||
|
||||
# La formation à utiliser:
|
||||
FORMATION_XML_FILENAME = "tests/ressources/formations/scodoc_formation_RT_BUT_RT_v1.xml"
|
||||
|
||||
|
||||
def init_departement(acronym):
|
||||
"Create dept, and switch context into it."
|
||||
import app as mapp
|
||||
|
||||
dept = models.Departement(acronym=acronym)
|
||||
db.session.add(dept)
|
||||
mapp.set_sco_dept(acronym)
|
||||
db.session.commit()
|
||||
return dept
|
||||
|
||||
|
||||
def import_formation() -> models.Formation:
|
||||
"""Import formation from XML.
|
||||
Returns formation_id
|
||||
"""
|
||||
with open(FORMATION_XML_FILENAME) as f:
|
||||
doc = f.read()
|
||||
# --- Création de la formation
|
||||
f = sco_formations.formation_import_xml(doc)
|
||||
return models.Formation.query.get(f[0])
|
||||
|
||||
|
||||
def create_user(dept):
|
||||
"""créé les utilisaterurs nécessaires aux tests"""
|
||||
user = auth.models.User(
|
||||
user_name="test", nom="Doe", prenom="John", dept=dept.acronym
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
def create_fake_etud(dept):
|
||||
"""Créé un faux étudiant et l'insère dans la base"""
|
||||
civilite = random.choice(("M", "F", "X"))
|
||||
nom, prenom = nomprenom(civilite)
|
||||
etud = models.Identite(civilite=civilite, nom=nom, prenom=prenom, dept_id=dept.id)
|
||||
db.session.add(etud)
|
||||
db.session.commit()
|
||||
adresse = models.Adresse(
|
||||
etudid=etud.id, email=f"{etud.prenom}.{etud.nom}@example.com"
|
||||
)
|
||||
db.session.add(adresse)
|
||||
admission = models.Admission(etudid=etud.id)
|
||||
db.session.add(admission)
|
||||
db.session.commit()
|
||||
return etud
|
||||
|
||||
|
||||
def create_etuds(dept, nb=16):
|
||||
"create nb etuds"
|
||||
return [create_fake_etud(dept) for _ in range(nb)]
|
||||
|
||||
|
||||
def create_formsemestre(formation, user, semestre_idx=1):
|
||||
"""Create formsemestre and moduleimpls"""
|
||||
formsemestre = models.FormSemestre(
|
||||
dept_id=formation.dept_id,
|
||||
semestre_id=semestre_idx,
|
||||
titre="Semestre test",
|
||||
date_debut=datetime.datetime(2021, 9, 1),
|
||||
date_fin=datetime.datetime(2022, 1, 31),
|
||||
modalite="FI",
|
||||
formation=formation,
|
||||
)
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
# Crée un modulimpl par module de ce semestre:
|
||||
for module in formation.modules.filter_by(semestre_id=semestre_idx):
|
||||
modimpl = models.ModuleImpl(
|
||||
module_id=module.id, formsemestre_id=formsemestre.id, responsable_id=user.id
|
||||
)
|
||||
db.session.add(modimpl)
|
||||
db.session.commit()
|
||||
partition_id = sco_groups.partition_create(
|
||||
formsemestre.id, default=True, redirect=False
|
||||
)
|
||||
_group_id = sco_groups.create_group(partition_id, default=True)
|
||||
return formsemestre
|
||||
|
||||
|
||||
def inscrit_etudiants(etuds, formsemestre):
|
||||
"""Inscrit les etudiants aux semestres et à tous ses modules"""
|
||||
for etud in etuds:
|
||||
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
|
||||
formsemestre.id,
|
||||
etud.id,
|
||||
group_ids=[],
|
||||
etat="I",
|
||||
method="init db test",
|
||||
)
|
||||
|
||||
|
||||
def init_test_database():
|
||||
dept = init_departement("TAPI")
|
||||
user = create_user(dept)
|
||||
etuds = create_etuds(dept)
|
||||
formation = import_formation()
|
||||
formsemestre = create_formsemestre(formation, user)
|
||||
inscrit_etudiants(etuds, formsemestre)
|
||||
# à compléter
|
||||
# - groupes
|
||||
# - absences
|
||||
# - notes
|
||||
# - décisions de jury
|
||||
# ...
|
@ -63,7 +63,9 @@ CONFIG.LOGO_HEADER_HEIGHT = 28 # taille verticale dans le document en millimetr
|
||||
# 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"
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
@ -55,7 +55,7 @@ CONFIG = CFG()
|
||||
# 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"
|
||||
|
||||
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user