forked from ScoDoc/ScoDoc
Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into new_api
This commit is contained in:
commit
40921efe28
11
README.md
11
README.md
@ -20,10 +20,10 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
|||||||
|
|
||||||
### État actuel (26 jan 22)
|
### État actuel (26 jan 22)
|
||||||
|
|
||||||
- 9.1 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
- 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
||||||
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
|
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
|
||||||
|
|
||||||
- 9.2 (branche refactor_nt) est la version de développement.
|
- 9.2 (branche dev92) est la version de développement.
|
||||||
|
|
||||||
|
|
||||||
### Lignes de commandes
|
### Lignes de commandes
|
||||||
@ -69,7 +69,12 @@ Puis remplacer `/opt/scodoc` par un clone du git.
|
|||||||
cd /opt
|
cd /opt
|
||||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||||
mv ScoDoc scodoc # important !
|
|
||||||
|
# Renommer le répertoire:
|
||||||
|
mv ScoDoc scodoc
|
||||||
|
|
||||||
|
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||||
|
chown -R scodoc.scodoc /opt/scodoc
|
||||||
|
|
||||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||||
|
|
||||||
|
@ -10,10 +10,11 @@ import traceback
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import SMTPHandler, WatchedFileHandler
|
from logging.handlers import SMTPHandler, WatchedFileHandler
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from flask import current_app, g, request
|
from flask import current_app, g, request
|
||||||
from flask import Flask
|
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 import render_template
|
||||||
from flask.logging import default_handler
|
from flask.logging import default_handler
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
@ -27,6 +28,7 @@ import sqlalchemy
|
|||||||
|
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
|
ScoBugCatcher,
|
||||||
ScoGenError,
|
ScoGenError,
|
||||||
ScoValueError,
|
ScoValueError,
|
||||||
APIInvalidParams,
|
APIInvalidParams,
|
||||||
@ -43,11 +45,13 @@ mail = Mail()
|
|||||||
bootstrap = Bootstrap()
|
bootstrap = Bootstrap()
|
||||||
moment = Moment()
|
moment = Moment()
|
||||||
|
|
||||||
cache = Cache( # XXX TODO: configuration file
|
CACHE_TYPE = os.environ.get("CACHE_TYPE")
|
||||||
|
cache = Cache(
|
||||||
config={
|
config={
|
||||||
# see https://flask-caching.readthedocs.io/en/latest/index.html#configuring-flask-caching
|
# see https://flask-caching.readthedocs.io/en/latest/index.html#configuring-flask-caching
|
||||||
"CACHE_TYPE": "RedisCache",
|
"CACHE_TYPE": CACHE_TYPE or "RedisCache",
|
||||||
"CACHE_DEFAULT_TIMEOUT": 0, # by default, never expire
|
# 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
|
return render_template("error_access_denied.html", exc=exc), 403
|
||||||
|
|
||||||
|
|
||||||
def internal_server_error(e):
|
def internal_server_error(exc):
|
||||||
"""Bugs scodoc, erreurs 500"""
|
"""Bugs scodoc, erreurs 500"""
|
||||||
# note that we set the 500 status explicitly
|
# note that we set the 500 status explicitly
|
||||||
return (
|
return (
|
||||||
@ -68,11 +72,35 @@ def internal_server_error(e):
|
|||||||
"error_500.html",
|
"error_500.html",
|
||||||
SCOVERSION=sco_version.SCOVERSION,
|
SCOVERSION=sco_version.SCOVERSION,
|
||||||
date=datetime.datetime.now().isoformat(),
|
date=datetime.datetime.now().isoformat(),
|
||||||
|
exc=exc,
|
||||||
|
request_url=request.url,
|
||||||
),
|
),
|
||||||
500,
|
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):
|
def handle_invalid_usage(error):
|
||||||
response = jsonify(error.to_dict())
|
response = jsonify(error.to_dict())
|
||||||
response.status_code = error.status_code
|
response.status_code = error.status_code
|
||||||
@ -187,10 +215,12 @@ def create_app(config_class=DevConfig):
|
|||||||
moment.init_app(app)
|
moment.init_app(app)
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
sco_cache.CACHE = cache
|
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(ScoGenError, handle_sco_value_error)
|
||||||
app.register_error_handler(ScoValueError, handle_sco_value_error)
|
app.register_error_handler(ScoValueError, handle_sco_value_error)
|
||||||
|
app.register_error_handler(ScoBugCatcher, handle_sco_bug)
|
||||||
app.register_error_handler(AccessDenied, handle_access_denied)
|
app.register_error_handler(AccessDenied, handle_access_denied)
|
||||||
app.register_error_handler(500, internal_server_error)
|
app.register_error_handler(500, internal_server_error)
|
||||||
app.register_error_handler(503, postgresql_server_error)
|
app.register_error_handler(503, postgresql_server_error)
|
||||||
@ -295,10 +325,12 @@ def create_app(config_class=DevConfig):
|
|||||||
|
|
||||||
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
|
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
|
||||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||||
|
from app.but.bulletin_but_pdf import BulletinGeneratorStandardBUT
|
||||||
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
|
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
|
||||||
|
|
||||||
# l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
|
# l'ordre est important, le premier sera le "défaut" pour les nouveaux départements.
|
||||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
|
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
|
||||||
|
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandardBUT)
|
||||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
|
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
|
||||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
|
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
|
||||||
if app.testing or app.debug:
|
if app.testing or app.debug:
|
||||||
@ -334,7 +366,7 @@ def user_db_init():
|
|||||||
|
|
||||||
current_app.logger.info("Init User's db")
|
current_app.logger.info("Init User's db")
|
||||||
# Create roles:
|
# Create roles:
|
||||||
Role.insert_roles()
|
Role.reset_standard_roles_permissions()
|
||||||
current_app.logger.info("created initial roles")
|
current_app.logger.info("created initial roles")
|
||||||
# Ensure that admin exists
|
# Ensure that admin exists
|
||||||
admin_mail = current_app.config.get("SCODOC_ADMIN_MAIL")
|
admin_mail = current_app.config.get("SCODOC_ADMIN_MAIL")
|
||||||
@ -457,15 +489,12 @@ from app.models import Departement
|
|||||||
from app.scodoc import notesdb as ndb, sco_preferences
|
from app.scodoc import notesdb as ndb, sco_preferences
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
|
|
||||||
# admin_role = Role.query.filter_by(name="SuperAdmin").first()
|
|
||||||
# if admin_role:
|
def scodoc_flash_status_messages():
|
||||||
# admin = (
|
"""Should be called on each page: flash messages indicating specific ScoDoc status"""
|
||||||
# User.query.join(UserRole)
|
email_test_mode_address = sco_preferences.get_preference("email_test_mode_address")
|
||||||
# .filter((UserRole.user_id == User.id) & (UserRole.role_id == admin_role.id))
|
if email_test_mode_address:
|
||||||
# .first()
|
flash(
|
||||||
# )
|
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
||||||
# else:
|
category="warning",
|
||||||
# click.echo(
|
)
|
||||||
# "Warning: user database not initialized !\n (use: flask user-db-init)"
|
|
||||||
# )
|
|
||||||
# admin = None
|
|
||||||
|
@ -76,7 +76,9 @@ class User(UserMixin, db.Model):
|
|||||||
"Departement",
|
"Departement",
|
||||||
foreign_keys=[Departement.acronym],
|
foreign_keys=[Departement.acronym],
|
||||||
primaryjoin=(dept == Departement.acronym),
|
primaryjoin=(dept == Departement.acronym),
|
||||||
lazy="dynamic",
|
lazy="select",
|
||||||
|
passive_deletes="all",
|
||||||
|
uselist=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@ -171,7 +173,7 @@ class User(UserMixin, db.Model):
|
|||||||
"id": self.id,
|
"id": self.id,
|
||||||
"active": self.active,
|
"active": self.active,
|
||||||
"status_txt": "actif" if self.active else "fermé",
|
"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
|
"nom": (self.nom or ""), # sco8
|
||||||
"prenom": (self.prenom or ""), # sco8
|
"prenom": (self.prenom or ""), # sco8
|
||||||
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
||||||
@ -236,7 +238,7 @@ class User(UserMixin, db.Model):
|
|||||||
def get_dept_id(self) -> int:
|
def get_dept_id(self) -> int:
|
||||||
"returns user's department id, or None"
|
"returns user's department id, or None"
|
||||||
if self.dept:
|
if self.dept:
|
||||||
return self._departement.first().id
|
return self._departement.id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Permissions management:
|
# Permissions management:
|
||||||
@ -268,6 +270,8 @@ class User(UserMixin, db.Model):
|
|||||||
"""Add a role to this user.
|
"""Add a role to this user.
|
||||||
:param role: Role to add.
|
: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))
|
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
|
||||||
|
|
||||||
def add_roles(self, roles, dept):
|
def add_roles(self, roles, dept):
|
||||||
@ -279,7 +283,9 @@ class User(UserMixin, db.Model):
|
|||||||
|
|
||||||
def set_roles(self, roles, dept):
|
def set_roles(self, roles, dept):
|
||||||
"set roles in the given 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):
|
def get_roles(self):
|
||||||
"iterator on my roles"
|
"iterator on my roles"
|
||||||
@ -290,7 +296,11 @@ class User(UserMixin, db.Model):
|
|||||||
"""string repr. of user's roles (with depts)
|
"""string repr. of user's roles (with depts)
|
||||||
e.g. "Ens_RT, Ens_Info, Secr_CJ"
|
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):
|
def is_administrator(self):
|
||||||
"True if i'm an active SuperAdmin"
|
"True if i'm an active SuperAdmin"
|
||||||
@ -400,20 +410,30 @@ class Role(db.Model):
|
|||||||
return self.permissions & perm == perm
|
return self.permissions & perm == perm
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def insert_roles():
|
def reset_standard_roles_permissions(reset_permissions=True):
|
||||||
"""Create default roles"""
|
"""Create default roles if missing, then, if reset_permissions,
|
||||||
|
reset their permissions to default values.
|
||||||
|
"""
|
||||||
default_role = "Observateur"
|
default_role = "Observateur"
|
||||||
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
|
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
|
||||||
role = Role.query.filter_by(name=role_name).first()
|
role = Role.query.filter_by(name=role_name).first()
|
||||||
if role is None:
|
if role is None:
|
||||||
role = Role(name=role_name)
|
role = Role(name=role_name)
|
||||||
|
role.default = role.name == default_role
|
||||||
|
db.session.add(role)
|
||||||
|
if reset_permissions:
|
||||||
role.reset_permissions()
|
role.reset_permissions()
|
||||||
for perm in permissions:
|
for perm in permissions:
|
||||||
role.add_permission(perm)
|
role.add_permission(perm)
|
||||||
role.default = role.name == default_role
|
|
||||||
db.session.add(role)
|
db.session.add(role)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ensure_standard_roles():
|
||||||
|
"""Create default roles if missing"""
|
||||||
|
Role.reset_standard_roles_permissions(reset_permissions=False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_named_role(name):
|
def get_named_role(name):
|
||||||
"""Returns existing role with given name, or None."""
|
"""Returns existing role with given name, or None."""
|
||||||
|
@ -19,7 +19,7 @@ from app.auth.forms import (
|
|||||||
ResetPasswordForm,
|
ResetPasswordForm,
|
||||||
DeactivateUserForm,
|
DeactivateUserForm,
|
||||||
)
|
)
|
||||||
from app.auth.models import Permission
|
from app.auth.models import Role
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.auth.email import send_password_reset_email
|
from app.auth.email import send_password_reset_email
|
||||||
from app.decorators import admin_required
|
from app.decorators import admin_required
|
||||||
@ -121,3 +121,11 @@ def reset_password(token):
|
|||||||
flash(_("Votre mot de passe a été changé."))
|
flash(_("Votre mot de passe a été changé."))
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
return render_template("auth/reset_password.html", form=form, user=user)
|
return render_template("auth/reset_password.html", form=form, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
|
||||||
|
@admin_required
|
||||||
|
def reset_standard_roles_permissions():
|
||||||
|
Role.reset_standard_roles_permissions()
|
||||||
|
flash("rôles standard réinitialisés !")
|
||||||
|
return redirect(url_for("scodoc.configuration"))
|
||||||
|
1
app/but/__init__.py
Normal file
1
app/but/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# empty but required for pylint
|
@ -7,16 +7,20 @@
|
|||||||
"""Génération bulletin BUT
|
"""Génération bulletin BUT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
|
import numpy as np
|
||||||
from flask import url_for, g
|
from flask import url_for, g
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
|
|
||||||
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.comp.res_but import ResultatsSemestreBUT
|
||||||
|
from app.models import FormSemestre, Identite
|
||||||
|
from app.models.ues import UniteEns
|
||||||
|
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||||
|
from app.scodoc import sco_bulletins_json
|
||||||
|
from app.scodoc import sco_bulletins_pdf
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
||||||
|
from app.scodoc.sco_utils import fmt_note
|
||||||
|
|
||||||
|
|
||||||
class BulletinBUT:
|
class BulletinBUT:
|
||||||
@ -28,6 +32,7 @@ class BulletinBUT:
|
|||||||
def __init__(self, formsemestre: FormSemestre):
|
def __init__(self, formsemestre: FormSemestre):
|
||||||
""" """
|
""" """
|
||||||
self.res = ResultatsSemestreBUT(formsemestre)
|
self.res = ResultatsSemestreBUT(formsemestre)
|
||||||
|
self.prefs = sco_preferences.SemPreferences(formsemestre.id)
|
||||||
|
|
||||||
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
|
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
|
||||||
"dict synthèse résultats dans l'UE pour les modules indiqués"
|
"dict synthèse résultats dans l'UE pour les modules indiqués"
|
||||||
@ -59,18 +64,15 @@ class BulletinBUT:
|
|||||||
# }
|
# }
|
||||||
return d
|
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"
|
"dict synthèse résultats UE"
|
||||||
res = self.res
|
res = self.res
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
"id": ue.id,
|
"id": ue.id,
|
||||||
"titre": ue.titre,
|
"titre": ue.titre,
|
||||||
"numero": ue.numero,
|
"numero": ue.numero,
|
||||||
"type": ue.type,
|
"type": ue.type,
|
||||||
"ECTS": {
|
|
||||||
"acquis": 0, # XXX TODO voir jury #sco92
|
|
||||||
"total": ue.ects,
|
|
||||||
},
|
|
||||||
"color": ue.color,
|
"color": ue.color,
|
||||||
"competence": None, # XXX TODO lien avec référentiel
|
"competence": None, # XXX TODO lien avec référentiel
|
||||||
"moyenne": None,
|
"moyenne": None,
|
||||||
@ -78,13 +80,18 @@ class BulletinBUT:
|
|||||||
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
|
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
|
||||||
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
||||||
else fmt_note(0.0),
|
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
|
"capitalise": None, # "AAAA-MM-JJ" TODO #sco92
|
||||||
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
||||||
"saes": self.etud_ue_mod_results(etud, ue, res.saes),
|
"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 ue.type != UE_SPORT:
|
||||||
if sco_preferences.get_preference("bul_show_ue_rangs", res.formsemestre.id):
|
if self.prefs["bul_show_ue_rangs"]:
|
||||||
rangs, effectif = res.ue_rangs[ue.id]
|
rangs, effectif = res.ue_rangs[ue.id]
|
||||||
rang = rangs[etud.id]
|
rang = rangs[etud.id]
|
||||||
else:
|
else:
|
||||||
@ -109,9 +116,10 @@ class BulletinBUT:
|
|||||||
d["modules"] = self.etud_mods_results(etud, modimpls_spo)
|
d["modules"] = self.etud_mods_results(etud, modimpls_spo)
|
||||||
return d
|
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,
|
"""dict synthèse résultats des modules indiqués,
|
||||||
avec évaluations de chacun."""
|
avec évaluations de chacun (sauf si version == "short")
|
||||||
|
"""
|
||||||
res = self.res
|
res = self.res
|
||||||
d = {}
|
d = {}
|
||||||
# etud_idx = self.etud_index[etud.id]
|
# etud_idx = self.etud_index[etud.id]
|
||||||
@ -152,14 +160,14 @@ class BulletinBUT:
|
|||||||
"evaluations": [
|
"evaluations": [
|
||||||
self.etud_eval_results(etud, e)
|
self.etud_eval_results(etud, e)
|
||||||
for e in modimpl.evaluations
|
for e in modimpl.evaluations
|
||||||
if e.visibulletin
|
if (e.visibulletin or version == "long")
|
||||||
and (
|
and (
|
||||||
modimpl_results.evaluations_etat[e.id].is_complete
|
modimpl_results.evaluations_etat[e.id].is_complete
|
||||||
or sco_preferences.get_preference(
|
or self.prefs["bul_show_all_evals"]
|
||||||
"bul_show_all_evals", res.formsemestre.id
|
|
||||||
)
|
)
|
||||||
)
|
]
|
||||||
],
|
if version != "short"
|
||||||
|
else [],
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@ -168,14 +176,23 @@ class BulletinBUT:
|
|||||||
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
# 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]
|
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
|
||||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
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 = {
|
d = {
|
||||||
"id": e.id,
|
"id": e.id,
|
||||||
"description": e.description,
|
"description": e.description,
|
||||||
"date": e.jour.isoformat() if e.jour else None,
|
"date": e.jour.isoformat() if e.jour else None,
|
||||||
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut 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,
|
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
|
||||||
"coef": e.coefficient,
|
"coef": fmt_note(e.coefficient),
|
||||||
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
|
"poids": poids,
|
||||||
"note": {
|
"note": {
|
||||||
"value": fmt_note(
|
"value": fmt_note(
|
||||||
eval_notes[etud.id],
|
eval_notes[etud.id],
|
||||||
@ -205,7 +222,8 @@ class BulletinBUT:
|
|||||||
details = [
|
details = [
|
||||||
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
|
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
|
||||||
for ue in res.ues
|
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 ue.id in res.bonus_ues
|
||||||
and bonus_vect[ue.id] > 0.0
|
and bonus_vect[ue.id] > 0.0
|
||||||
]
|
]
|
||||||
@ -216,13 +234,23 @@ class BulletinBUT:
|
|||||||
else:
|
else:
|
||||||
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
|
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
|
||||||
|
|
||||||
def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict:
|
def bulletin_etud(
|
||||||
"""Le bulletin de l'étudiant dans ce semestre.
|
self,
|
||||||
Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
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).
|
(bulletins non publiés).
|
||||||
"""
|
"""
|
||||||
res = self.res
|
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]
|
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
||||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||||
d = {
|
d = {
|
||||||
@ -239,7 +267,9 @@ class BulletinBUT:
|
|||||||
},
|
},
|
||||||
"formsemestre_id": formsemestre.id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etat_inscription": etat_inscription,
|
"etat_inscription": etat_inscription,
|
||||||
"options": sco_preferences.bulletin_option_affichage(formsemestre.id),
|
"options": sco_preferences.bulletin_option_affichage(
|
||||||
|
formsemestre.id, self.prefs
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if not published:
|
if not published:
|
||||||
return d
|
return d
|
||||||
@ -253,39 +283,55 @@ class BulletinBUT:
|
|||||||
"numero": formsemestre.semestre_id,
|
"numero": formsemestre.semestre_id,
|
||||||
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
|
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
|
||||||
"groupes": [], # XXX TODO
|
"groupes": [], # XXX TODO
|
||||||
"absences": {
|
|
||||||
"injustifie": nbabsjust,
|
|
||||||
"total": nbabs,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
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(
|
semestre_infos.update(
|
||||||
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
||||||
)
|
)
|
||||||
if etat_inscription == scu.INSCRIT:
|
if etat_inscription == scu.INSCRIT:
|
||||||
semestre_infos.update(
|
# moyenne des moyennes générales du semestre
|
||||||
{
|
semestre_infos["notes"] = {
|
||||||
"notes": { # moyenne des moyennes générales du semestre
|
|
||||||
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
||||||
"min": fmt_note(res.etud_moy_gen.min()),
|
"min": fmt_note(res.etud_moy_gen.min()),
|
||||||
"moy": fmt_note(res.etud_moy_gen.mean()),
|
"moy": fmt_note(res.etud_moy_gen.mean()),
|
||||||
"max": fmt_note(res.etud_moy_gen.max()),
|
"max": fmt_note(res.etud_moy_gen.max()),
|
||||||
},
|
}
|
||||||
"rang": { # classement wrt moyenne général, indicatif
|
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],
|
"value": res.etud_moy_gen_ranks[etud.id],
|
||||||
"total": nb_inscrits,
|
"total": nb_inscrits,
|
||||||
},
|
}
|
||||||
},
|
else:
|
||||||
)
|
semestre_infos["rang"] = {
|
||||||
|
"value": "-",
|
||||||
|
"total": nb_inscrits,
|
||||||
|
}
|
||||||
d.update(
|
d.update(
|
||||||
{
|
{
|
||||||
"ressources": self.etud_mods_results(etud, res.ressources),
|
"ressources": self.etud_mods_results(
|
||||||
"saes": self.etud_mods_results(etud, res.saes),
|
etud, res.ressources, version=version
|
||||||
|
),
|
||||||
|
"saes": self.etud_mods_results(etud, res.saes, version=version),
|
||||||
"ues": {
|
"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
|
for ue in res.ues
|
||||||
if self.res.modimpls_in_ue(
|
# si l'UE comporte des modules auxquels on est inscrit:
|
||||||
ue.id, etud.id
|
if (
|
||||||
) # si l'UE comporte des modules auxquels on est inscrit
|
(ue.type == UE_SPORT)
|
||||||
|
or self.res.modimpls_in_ue(ue.id, etud.id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
"semestre": semestre_infos,
|
"semestre": semestre_infos,
|
||||||
},
|
},
|
||||||
@ -312,3 +358,56 @@ class BulletinBUT:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
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(
|
||||||
|
etud_etat,
|
||||||
|
self.prefs,
|
||||||
|
decision_sem=d["semestre"].get("decision"),
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
||||||
|
d["rang_txt"] = "Rang " + d["rang_nt"]
|
||||||
|
|
||||||
|
# --- Appréciations
|
||||||
|
d.update(
|
||||||
|
sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
|
||||||
|
)
|
||||||
|
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
|
||||||
|
|
||||||
|
return d
|
||||||
|
351
app/but/bulletin_but_pdf.py
Normal file
351
app/but/bulletin_but_pdf.py
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Génération bulletin BUT au format PDF standard
|
||||||
|
"""
|
||||||
|
from reportlab.lib.colors import blue
|
||||||
|
from reportlab.lib.units import cm, mm
|
||||||
|
from reportlab.platypus import Paragraph, Spacer
|
||||||
|
|
||||||
|
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||||
|
from app.scodoc import gen_tables
|
||||||
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
|
|
||||||
|
|
||||||
|
class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
|
"""Génération du bulletin de BUT au format PDF.
|
||||||
|
|
||||||
|
self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur:
|
||||||
|
list_in_menu = False
|
||||||
|
scale_table_in_page = False # pas de mise à l'échelle pleine page auto
|
||||||
|
multi_pages = True # plusieurs pages par bulletins
|
||||||
|
small_fontsize = "8"
|
||||||
|
|
||||||
|
def bul_table(self, format="html"):
|
||||||
|
"""Génère la table centrale du bulletin de notes
|
||||||
|
Renvoie:
|
||||||
|
- en HTML: une chaine
|
||||||
|
- en PDF: une liste d'objets PLATYPUS (eg instance de Table).
|
||||||
|
"""
|
||||||
|
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))
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
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
|
||||||
|
- 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
|
||||||
|
"""
|
||||||
|
col_widths = {
|
||||||
|
"titre": None,
|
||||||
|
"moyenne": 2 * cm,
|
||||||
|
"coef": 2 * cm,
|
||||||
|
}
|
||||||
|
title_bg = tuple(x / 255.0 for x in title_bg)
|
||||||
|
nota_bene = """La moyenne des ressources et SAÉs dans une UE
|
||||||
|
dépend des poids donnés aux évaluations."""
|
||||||
|
# elems pour générer table avec gen_table (liste de dicts)
|
||||||
|
rows = [
|
||||||
|
# Ligne de titres
|
||||||
|
{
|
||||||
|
"titre": "Unités d'enseignement",
|
||||||
|
"moyenne": 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),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
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, 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)
|
etud: Identite = Identite.query.get_or_404(etudid)
|
||||||
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||||
nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT]
|
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
|
etat_inscription = results.formsemestre.etuds_inscriptions[etudid].etat
|
||||||
if (not formsemestre.bul_hide_xml) or force_publishing:
|
if (not formsemestre.bul_hide_xml) or force_publishing:
|
||||||
published = 1
|
published = 1
|
||||||
@ -145,7 +145,7 @@ def bulletin_but_xml_compat(
|
|||||||
doc.append(Element("note_max", value="20")) # notes toujours sur 20
|
doc.append(Element("note_max", value="20")) # notes toujours sur 20
|
||||||
doc.append(Element("bonus_sport_culture", value=str(bonus)))
|
doc.append(Element("bonus_sport_culture", value=str(bonus)))
|
||||||
# Liste les UE / modules /evals
|
# Liste les UE / modules /evals
|
||||||
for ue in results.ues:
|
for ue in results.ues: # avec bonus
|
||||||
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
|
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
|
||||||
nb_inscrits_ue = (
|
nb_inscrits_ue = (
|
||||||
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
|
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
|
||||||
@ -161,14 +161,17 @@ def bulletin_but_xml_compat(
|
|||||||
doc.append(x_ue)
|
doc.append(x_ue)
|
||||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||||
v = results.etud_moy_ue[ue.id][etud.id]
|
v = results.etud_moy_ue[ue.id][etud.id]
|
||||||
|
vmin = results.etud_moy_ue[ue.id].min()
|
||||||
|
vmax = results.etud_moy_ue[ue.id].max()
|
||||||
else:
|
else:
|
||||||
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
|
v = results.bonus or 0.0
|
||||||
|
vmin = vmax = 0.0
|
||||||
x_ue.append(
|
x_ue.append(
|
||||||
Element(
|
Element(
|
||||||
"note",
|
"note",
|
||||||
value=scu.fmt_note(v),
|
value=scu.fmt_note(v),
|
||||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
min=scu.fmt_note(vmin),
|
||||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
max=scu.fmt_note(vmax),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
|
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
|
||||||
@ -179,7 +182,10 @@ def bulletin_but_xml_compat(
|
|||||||
# Liste ici uniquement les modules rattachés à cette UE
|
# Liste ici uniquement les modules rattachés à cette UE
|
||||||
if modimpl.module.ue.id == ue.id:
|
if modimpl.module.ue.id == ue.id:
|
||||||
# mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
|
# mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
|
||||||
|
try:
|
||||||
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
|
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
|
||||||
|
except KeyError:
|
||||||
|
coef = 0.0
|
||||||
x_mod = Element(
|
x_mod = Element(
|
||||||
"module",
|
"module",
|
||||||
id=str(modimpl.id),
|
id=str(modimpl.id),
|
||||||
@ -214,6 +220,7 @@ def bulletin_but_xml_compat(
|
|||||||
note_max_origin=str(e.note_max),
|
note_max_origin=str(e.note_max),
|
||||||
)
|
)
|
||||||
x_mod.append(x_eval)
|
x_mod.append(x_eval)
|
||||||
|
try:
|
||||||
x_eval.append(
|
x_eval.append(
|
||||||
Element(
|
Element(
|
||||||
"note",
|
"note",
|
||||||
@ -225,6 +232,11 @@ def bulletin_but_xml_compat(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
except KeyError:
|
||||||
|
x_eval.append(
|
||||||
|
Element("note", value="", note_max="")
|
||||||
|
)
|
||||||
|
|
||||||
# XXX TODO: Evaluations incomplètes ou futures: XXX
|
# XXX TODO: Evaluations incomplètes ou futures: XXX
|
||||||
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
|
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
|
||||||
|
|
||||||
|
@ -19,10 +19,12 @@ class FormationRefCompForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class RefCompLoadForm(FlaskForm):
|
class RefCompLoadForm(FlaskForm):
|
||||||
|
referentiel_standard = SelectField(
|
||||||
|
"Choisir un référentiel de compétences officiel BUT"
|
||||||
|
)
|
||||||
upload = FileField(
|
upload = FileField(
|
||||||
label="Sélectionner un fichier XML Orébut",
|
label="Ou bien sélectionner un fichier XML au format Orébut",
|
||||||
validators=[
|
validators=[
|
||||||
FileRequired(),
|
|
||||||
FileAllowed(
|
FileAllowed(
|
||||||
[
|
[
|
||||||
"xml",
|
"xml",
|
||||||
@ -33,3 +35,13 @@ class RefCompLoadForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField("Valider")
|
submit = SubmitField("Valider")
|
||||||
cancel = SubmitField("Annuler")
|
cancel = SubmitField("Annuler")
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if not super().validate():
|
||||||
|
return False
|
||||||
|
if (self.referentiel_standard.data == "0") == (not self.upload.data):
|
||||||
|
self.referentiel_standard.errors.append(
|
||||||
|
"Choisir soit un référentiel, soit un fichier xml"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
@ -19,7 +21,7 @@ from app.models.but_refcomp import (
|
|||||||
ApcAnneeParcours,
|
ApcAnneeParcours,
|
||||||
ApcParcoursNiveauCompetence,
|
ApcParcoursNiveauCompetence,
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_exceptions import ScoFormatError
|
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
||||||
@ -27,6 +29,16 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
|||||||
peut lever TypeError ou ScoFormatError
|
peut lever TypeError ou ScoFormatError
|
||||||
Résultat: instance de ApcReferentielCompetences
|
Résultat: instance de ApcReferentielCompetences
|
||||||
"""
|
"""
|
||||||
|
# Vérifie que le même fichier n'a pas déjà été chargé:
|
||||||
|
if ApcReferentielCompetences.query.filter_by(
|
||||||
|
scodoc_orig_filename=orig_filename, dept_id=dept_id
|
||||||
|
).count():
|
||||||
|
raise ScoValueError(
|
||||||
|
f"""Un référentiel a déjà été chargé d'un fichier de même nom.
|
||||||
|
({orig_filename})
|
||||||
|
Supprimez-le ou changer le nom du fichier."""
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
root = ElementTree.XML(xml_data)
|
root = ElementTree.XML(xml_data)
|
||||||
except ElementTree.ParseError as exc:
|
except ElementTree.ParseError as exc:
|
||||||
@ -42,7 +54,16 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
|||||||
if not competences:
|
if not competences:
|
||||||
raise ScoFormatError("élément 'competences' manquant")
|
raise ScoFormatError("élément 'competences' manquant")
|
||||||
for competence in competences.findall("competence"):
|
for competence in competences.findall("competence"):
|
||||||
|
try:
|
||||||
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
|
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
|
||||||
|
db.session.flush()
|
||||||
|
except sqlalchemy.exc.IntegrityError:
|
||||||
|
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
|
||||||
|
db.session.rollback()
|
||||||
|
raise ScoValueError(
|
||||||
|
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
|
||||||
|
"""
|
||||||
|
)
|
||||||
ref.competences.append(c)
|
ref.competences.append(c)
|
||||||
# --- SITUATIONS
|
# --- SITUATIONS
|
||||||
situations = competence.find("situations")
|
situations = competence.find("situations")
|
||||||
|
1
app/comp/__init__.py
Normal file
1
app/comp/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# empty but required for pylint
|
@ -20,6 +20,7 @@ from flask import g
|
|||||||
|
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
|
from app.scodoc.sco_codes_parcours import ParcoursDUT, ParcoursDUTMono
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ class BonusSport:
|
|||||||
etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
|
etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen reste None)
|
# En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen est ajusté pour le prendre en compte)
|
||||||
classic_use_bonus_ues = False
|
classic_use_bonus_ues = False
|
||||||
|
|
||||||
# Attributs virtuels:
|
# Attributs virtuels:
|
||||||
@ -87,7 +88,7 @@ class BonusSport:
|
|||||||
for m in formsemestre.modimpls_sorted
|
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
|
modimpl_mask = np.s_[:] # il n'y a rien, on prend tout donc rien
|
||||||
self.modimpls_spo = [
|
self.modimpls_spo = [
|
||||||
modimpl
|
modimpl
|
||||||
@ -106,6 +107,8 @@ class BonusSport:
|
|||||||
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
|
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
|
||||||
# ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
|
# ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
|
||||||
nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
|
nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
|
||||||
|
if nb_etuds == 0 or nb_mod_sport == 0:
|
||||||
|
return # no bonus at all
|
||||||
# Enlève les NaN du numérateur:
|
# Enlève les NaN du numérateur:
|
||||||
sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)
|
sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)
|
||||||
|
|
||||||
@ -157,7 +160,8 @@ class BonusSport:
|
|||||||
"""Calcul des bonus: méthode virtuelle à écraser.
|
"""Calcul des bonus: méthode virtuelle à écraser.
|
||||||
Arguments:
|
Arguments:
|
||||||
- sem_modimpl_moys_inscrits:
|
- sem_modimpl_moys_inscrits:
|
||||||
ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue_non_bonus)
|
ndarray (nb_etuds, mod_sport)
|
||||||
|
ou en APC (nb_etuds, mods_sport, nb_ue_non_bonus)
|
||||||
les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans.
|
les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans.
|
||||||
- modimpl_coefs_etuds_no_nan:
|
- modimpl_coefs_etuds_no_nan:
|
||||||
les coefficients: float ndarray
|
les coefficients: float ndarray
|
||||||
@ -194,52 +198,66 @@ class BonusSportAdditif(BonusSport):
|
|||||||
à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
|
seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte
|
||||||
|
# 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
|
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):
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
"""calcul du bonus
|
"""calcul du bonus
|
||||||
sem_modimpl_moys_inscrits: les notes de sport
|
sem_modimpl_moys_inscrits: les notes de sport
|
||||||
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
|
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
|
||||||
modimpl_coefs_etuds_no_nan:
|
En classic: ndarray (nb_etuds, nb_mod_sport)
|
||||||
|
modimpl_coefs_etuds_no_nan: même shape, les coefs.
|
||||||
"""
|
"""
|
||||||
if 0 in sem_modimpl_moys_inscrits.shape:
|
if 0 in sem_modimpl_moys_inscrits.shape:
|
||||||
# pas d'étudiants ou pas d'UE ou pas de module...
|
# pas d'étudiants ou pas d'UE ou pas de module...
|
||||||
return
|
return
|
||||||
|
seuil_comptage = (
|
||||||
|
self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
|
||||||
|
)
|
||||||
bonus_moy_arr = np.sum(
|
bonus_moy_arr = np.sum(
|
||||||
np.where(
|
np.where(
|
||||||
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
|
(sem_modimpl_moys_inscrits >= self.seuil_moy_gen)
|
||||||
(sem_modimpl_moys_inscrits - self.seuil_moy_gen)
|
& (modimpl_coefs_etuds_no_nan > 0),
|
||||||
* self.proportion_point,
|
(sem_modimpl_moys_inscrits - seuil_comptage) * self.proportion_point,
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
axis=1,
|
axis=1,
|
||||||
)
|
)
|
||||||
if self.bonus_max is not None:
|
# Seuil: bonus dans [min, max] (défaut [0,20])
|
||||||
# Seuil: bonus limité à bonus_max points (et >= 0)
|
bonus_max = self.bonus_max or 0.0
|
||||||
bonus_moy_arr = np.clip(
|
np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)
|
||||||
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)
|
|
||||||
|
|
||||||
|
self.bonus_additif(bonus_moy_arr)
|
||||||
|
|
||||||
|
def bonus_additif(self, bonus_moy_arr: np.array):
|
||||||
|
"Set bonus_ues et bonus_moy_gen"
|
||||||
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
|
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
|
||||||
if self.formsemestre.formation.is_apc() or self.classic_use_bonus_ues:
|
if self.formsemestre.formation.is_apc():
|
||||||
# Bonus sur les UE et None sur moyenne générale
|
# Bonus sur les UE et None sur moyenne générale
|
||||||
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
||||||
self.bonus_ues = pd.DataFrame(
|
self.bonus_ues = pd.DataFrame(
|
||||||
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
|
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
|
||||||
)
|
)
|
||||||
|
elif self.classic_use_bonus_ues:
|
||||||
|
# Formations classiques apppliquant le bonus sur les UEs
|
||||||
|
# ici bonus_moy_arr = ndarray 1d nb_etuds
|
||||||
|
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
||||||
|
self.bonus_ues = pd.DataFrame(
|
||||||
|
np.stack([bonus_moy_arr] * len(ues_idx)).T,
|
||||||
|
index=self.etuds_idx,
|
||||||
|
columns=ues_idx,
|
||||||
|
dtype=float,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Bonus sur la moyenne générale seulement
|
# Bonus sur la moyenne générale seulement
|
||||||
self.bonus_moy_gen = pd.Series(
|
self.bonus_moy_gen = pd.Series(
|
||||||
bonus_moy_arr, index=self.etuds_idx, dtype=float
|
bonus_moy_arr, index=self.etuds_idx, dtype=float
|
||||||
)
|
)
|
||||||
|
|
||||||
# if len(bonus_moy_arr.shape) > 1:
|
|
||||||
# bonus_moy_arr = bonus_moy_arr.sum(axis=1)
|
|
||||||
# Laisse bonus_moy_gen à None, en APC le bonus moy. gen. sera réparti sur les UEs.
|
|
||||||
|
|
||||||
|
|
||||||
class BonusSportMultiplicatif(BonusSport):
|
class BonusSportMultiplicatif(BonusSport):
|
||||||
"""Bonus sport qui multiplie les moyennes d'UE par un facteur"""
|
"""Bonus sport qui multiplie les moyennes d'UE par un facteur"""
|
||||||
@ -284,6 +302,7 @@ class BonusSportMultiplicatif(BonusSport):
|
|||||||
|
|
||||||
class BonusDirect(BonusSportAdditif):
|
class BonusDirect(BonusSportAdditif):
|
||||||
"""Bonus direct: les points sont directement ajoutés à la moyenne générale.
|
"""Bonus direct: les points sont directement ajoutés à la moyenne générale.
|
||||||
|
|
||||||
Les coefficients sont ignorés: tous les points de bonus sont sommés.
|
Les coefficients sont ignorés: tous les points de bonus sont sommés.
|
||||||
(rappel: la note est ramenée sur 20 avant application).
|
(rappel: la note est ramenée sur 20 avant application).
|
||||||
"""
|
"""
|
||||||
@ -294,8 +313,112 @@ class BonusDirect(BonusSportAdditif):
|
|||||||
proportion_point = 1.0
|
proportion_point = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class BonusAisneStQuentin(BonusSportAdditif):
|
||||||
|
"""Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin
|
||||||
|
|
||||||
|
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
|
de l'Université de St Quentin non rattachés à une unité d'enseignement.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Si la note est >= 10 et < 12.1, bonus de 0.1 point</li>
|
||||||
|
<li>Si la note est >= 12.1 et < 14.1, bonus de 0.2 point</li>
|
||||||
|
<li>Si la note est >= 14.1 et < 16.1, bonus de 0.3 point</li>
|
||||||
|
<li>Si la note est >= 16.1 et < 18.1, bonus de 0.4 point</li>
|
||||||
|
<li>Si la note est >= 18.1, bonus de 0.5 point</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
|
||||||
|
l'étudiant (en BUT, s'ajoute à la moyenne de chaque UE).
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_iutstq"
|
||||||
|
displayed_name = "IUT de Saint-Quentin"
|
||||||
|
|
||||||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
"""calcul du bonus"""
|
||||||
|
if 0 in sem_modimpl_moys_inscrits.shape:
|
||||||
|
# pas d'étudiants ou pas d'UE ou pas de module...
|
||||||
|
return
|
||||||
|
# Calcule moyenne pondérée des notes de sport:
|
||||||
|
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||||
|
bonus_moy_arr = np.sum(
|
||||||
|
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
|
||||||
|
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||||||
|
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
|
||||||
|
|
||||||
|
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
|
||||||
|
bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
|
||||||
|
bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
|
||||||
|
bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3
|
||||||
|
bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2
|
||||||
|
bonus_moy_arr[bonus_moy_arr >= 10] = 0.1
|
||||||
|
|
||||||
|
self.bonus_additif(bonus_moy_arr)
|
||||||
|
|
||||||
|
|
||||||
|
class BonusAmiens(BonusSportAdditif):
|
||||||
|
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
|
||||||
|
|
||||||
|
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
|
||||||
|
sur toutes les moyennes d'UE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_amiens"
|
||||||
|
displayed_name = "IUT d'Amiens"
|
||||||
|
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||||
|
proportion_point = 1e10
|
||||||
|
bonus_max = 0.1
|
||||||
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
|
|
||||||
|
|
||||||
|
# Finalement ils n'en veulent pas.
|
||||||
|
# class BonusAnnecy(BonusSport):
|
||||||
|
# """Calcul bonus modules optionnels (sport), règle IUT d'Annecy.
|
||||||
|
|
||||||
|
# Il peut y avoir plusieurs modules de bonus.
|
||||||
|
# Prend pour chaque étudiant la meilleure de ses notes bonus et
|
||||||
|
# ajoute à chaque UE :<br>
|
||||||
|
# 0.05 point si >=10,<br>
|
||||||
|
# 0.1 point si >=12,<br>
|
||||||
|
# 0.15 point si >=14,<br>
|
||||||
|
# 0.2 point si >=16,<br>
|
||||||
|
# 0.25 point si >=18.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# name = "bonus_iut_annecy"
|
||||||
|
# displayed_name = "IUT d'Annecy"
|
||||||
|
|
||||||
|
# def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
# """calcul du bonus"""
|
||||||
|
# # if math.prod(sem_modimpl_moys_inscrits.shape) == 0:
|
||||||
|
# # return # no etuds or no mod sport
|
||||||
|
# # 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
|
||||||
|
# bonus = np.zeros(note_bonus_max.shape)
|
||||||
|
# bonus[note_bonus_max >= 10.0] = 0.05
|
||||||
|
# bonus[note_bonus_max >= 12.0] = 0.10
|
||||||
|
# bonus[note_bonus_max >= 14.0] = 0.15
|
||||||
|
# bonus[note_bonus_max >= 16.0] = 0.20
|
||||||
|
# bonus[note_bonus_max >= 18.0] = 0.25
|
||||||
|
|
||||||
|
# # Bonus moyenne générale et sur les UE
|
||||||
|
# self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
|
||||||
|
# ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
||||||
|
# nb_ues_no_bonus = len(ues_idx)
|
||||||
|
# self.bonus_ues = pd.DataFrame(
|
||||||
|
# np.stack([bonus] * nb_ues_no_bonus, axis=1),
|
||||||
|
# columns=ues_idx,
|
||||||
|
# index=self.etuds_idx,
|
||||||
|
# dtype=float,
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
class BonusBethune(BonusSportMultiplicatif):
|
class BonusBethune(BonusSportMultiplicatif):
|
||||||
"""Calcul bonus modules optionels (sport), règle IUT de Béthune.
|
"""Calcul bonus modules optionnels (sport), règle IUT de Béthune.
|
||||||
|
|
||||||
Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre.
|
Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre.
|
||||||
Ce bonus est égal au nombre de points divisé par 200 et multiplié par la
|
Ce bonus est égal au nombre de points divisé par 200 et multiplié par la
|
||||||
@ -309,7 +432,7 @@ class BonusBethune(BonusSportMultiplicatif):
|
|||||||
|
|
||||||
|
|
||||||
class BonusBezier(BonusSportAdditif):
|
class BonusBezier(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT de Bézier.
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Bézier.
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
sport , etc) non rattachés à une unité d'enseignement. Les points
|
sport , etc) non rattachés à une unité d'enseignement. Les points
|
||||||
@ -330,28 +453,152 @@ class BonusBezier(BonusSportAdditif):
|
|||||||
|
|
||||||
|
|
||||||
class BonusBordeaux1(BonusSportMultiplicatif):
|
class BonusBordeaux1(BonusSportMultiplicatif):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1,
|
||||||
et UE.
|
sur moyenne générale et UEs.
|
||||||
|
<p>
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
|
de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
|
||||||
|
</p><p>
|
||||||
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
|
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
|
||||||
qui augmente la moyenne de chaque UE et la moyenne générale.
|
qui augmente la moyenne de chaque UE et la moyenne générale.<br>
|
||||||
Formule : le % = points>moyenne / 2
|
Formule : pourcentage = (points au dessus de 10) / 2
|
||||||
|
</p><p>
|
||||||
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
|
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
|
||||||
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_iutBordeaux1"
|
name = "bonus_iutBordeaux1"
|
||||||
displayed_name = "IUT de Bordeaux 1"
|
displayed_name = "IUT de Bordeaux"
|
||||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
seuil_moy_gen = 10.0
|
seuil_moy_gen = 10.0
|
||||||
amplitude = 0.005
|
amplitude = 0.005
|
||||||
|
|
||||||
|
|
||||||
|
# Exactement le même que Bordeaux:
|
||||||
|
class BonusBrest(BonusSportMultiplicatif):
|
||||||
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Brest,
|
||||||
|
sur moyenne générale et UEs.
|
||||||
|
<p>
|
||||||
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
|
de l'Université (sport, théâtre) non rattachés à une unité d'enseignement.
|
||||||
|
</p><p>
|
||||||
|
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
|
||||||
|
qui augmente la moyenne de chaque UE et la moyenne générale.<br>
|
||||||
|
Formule : pourcentage = (points au dessus de 10) / 2
|
||||||
|
</p><p>
|
||||||
|
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_iut_brest"
|
||||||
|
displayed_name = "IUT de Brest"
|
||||||
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
|
seuil_moy_gen = 10.0
|
||||||
|
amplitude = 0.005
|
||||||
|
|
||||||
|
|
||||||
|
class BonusCachan1(BonusSportAdditif):
|
||||||
|
"""Calcul bonus optionnels (sport, culture), règle IUT de Cachan 1.
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
|
||||||
|
bonifie les moyennes d'UE (<b>sauf l'UE41 dont le code est UE41_E</b>) à raison
|
||||||
|
de <em>bonus = (option - 10)/10</em>.
|
||||||
|
</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) * 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.03
|
||||||
|
classic_use_bonus_ues = True
|
||||||
|
|
||||||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
"""calcul du bonus, avec réglage différent suivant le type de formation"""
|
||||||
|
# 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,
|
||||||
|
)
|
||||||
|
else: # --- DUT
|
||||||
|
# pareil mais proportion différente et exclusion d'une UE
|
||||||
|
proportion_point = 0.1
|
||||||
|
bonus_moy_arr = np.where(
|
||||||
|
note_bonus_max > self.seuil_moy_gen,
|
||||||
|
(note_bonus_max - self.seuil_moy_gen) * 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,
|
||||||
|
)
|
||||||
|
# Pas de bonus sur la ou les ue de code "UE41_E"
|
||||||
|
ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"]
|
||||||
|
for ue in ue_exclues:
|
||||||
|
self.bonus_ues[ue.id] = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class BonusCalais(BonusSportAdditif):
|
||||||
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT LCO.
|
||||||
|
|
||||||
|
Les étudiants de l'IUT LCO peuvent suivre des enseignements optionnels non
|
||||||
|
rattachés à une unité d'enseignement. Les points au-dessus de 10
|
||||||
|
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
||||||
|
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
||||||
|
<ul>
|
||||||
|
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
|
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b> (ex : UE2.1BS, UE32BS)
|
||||||
|
</ul>
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_calais"
|
||||||
|
displayed_name = "IUT du Littoral"
|
||||||
|
bonus_max = 0.6
|
||||||
|
seuil_moy_gen = 10.0 # au dessus de 10
|
||||||
|
proportion_point = 0.06 # 6%
|
||||||
|
|
||||||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
parcours = self.formsemestre.formation.get_parcours()
|
||||||
|
# Variantes de DUT ?
|
||||||
|
if (
|
||||||
|
isinstance(parcours, ParcoursDUT)
|
||||||
|
or parcours.TYPE_PARCOURS == ParcoursDUTMono.TYPE_PARCOURS
|
||||||
|
): # DUT
|
||||||
|
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||||
|
else:
|
||||||
|
self.classic_use_bonus_ues = True # pour les LP
|
||||||
|
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||||
|
ues = self.formsemestre.query_ues(with_sport=False).all()
|
||||||
|
ues_sans_bs = [
|
||||||
|
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
||||||
|
] # les 2 derniers cars forcés en majus
|
||||||
|
for ue in ues_sans_bs:
|
||||||
|
self.bonus_ues[ue.id] = 0.0
|
||||||
|
|
||||||
|
|
||||||
class BonusColmar(BonusSportAdditif):
|
class BonusColmar(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Colmar.
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT Colmar.
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non
|
de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non
|
||||||
@ -374,19 +621,21 @@ class BonusColmar(BonusSportAdditif):
|
|||||||
class BonusGrenobleIUT1(BonusSportMultiplicatif):
|
class BonusGrenobleIUT1(BonusSportMultiplicatif):
|
||||||
"""Bonus IUT1 de Grenoble
|
"""Bonus IUT1 de Grenoble
|
||||||
|
|
||||||
|
<p>
|
||||||
À compter de sept. 2021:
|
À compter de sept. 2021:
|
||||||
La note de sport est sur 20, et on calcule une bonification (en %)
|
La note de sport est sur 20, et on calcule une bonification (en %)
|
||||||
qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
|
qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
|
||||||
la formule : bonification (en %) = (note-10)*0,5.
|
la formule : bonification (en %) = (note-10)*0,5.
|
||||||
|
</p><p>
|
||||||
Bonification qui ne s'applique que si la note est >10.
|
<em>La bonification ne s'applique que si la note est supérieure à 10.</em>
|
||||||
|
</p><p>
|
||||||
(Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif)
|
(Une note de 10 donne donc 0% de bonif, et une note de 20 : 5% de bonif)
|
||||||
|
</p><p>
|
||||||
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
|
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
|
||||||
Chaque point correspondait à 0.25% d'augmentation de la moyenne
|
Chaque point correspondait à 0.25% d'augmentation de la moyenne
|
||||||
générale.
|
générale.
|
||||||
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
|
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
|
||||||
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_iut1grenoble_2017"
|
name = "bonus_iut1grenoble_2017"
|
||||||
@ -411,45 +660,72 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
|
|||||||
|
|
||||||
|
|
||||||
class BonusLaRochelle(BonusSportAdditif):
|
class BonusLaRochelle(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT de La Rochelle.
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
|
||||||
|
|
||||||
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.
|
<ul>
|
||||||
Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette
|
<li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
|
||||||
note sur la moyenne générale du semestre (ou sur les UE en BUT).
|
<li>Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette
|
||||||
|
note sur la moyenne générale du semestre (ou sur les UE en BUT).</li>
|
||||||
|
</ul>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_iutlr"
|
name = "bonus_iutlr"
|
||||||
displayed_name = "IUT de La Rochelle"
|
displayed_name = "IUT de La Rochelle"
|
||||||
seuil_moy_gen = 10.0 # tous les points sont comptés
|
seuil_moy_gen = 10.0 # si bonus > 10,
|
||||||
proportion_point = 0.01
|
seuil_comptage = 0.0 # tous les points sont comptés
|
||||||
|
proportion_point = 0.01 # 1%
|
||||||
|
|
||||||
|
|
||||||
class BonusLeHavre(BonusSportMultiplicatif):
|
class BonusLeHavre(BonusSportAdditif):
|
||||||
"""Bonus sport IUT du Havre sur moyenne générale et UE
|
"""Bonus sport IUT du Havre sur les moyennes d'UE
|
||||||
|
|
||||||
Les points des modules bonus au dessus de 10/20 sont ajoutés,
|
<p>
|
||||||
et les moyennes d'UE augmentées de 5% de ces points.
|
Les enseignements optionnels de langue, préprofessionnalisation,
|
||||||
|
PIX (compétences numériques), l'entrepreneuriat étudiant, l'engagement
|
||||||
|
bénévole au sein d’association dès lors qu’une grille d’évaluation des
|
||||||
|
compétences existe ainsi que les activités sportives et culturelles
|
||||||
|
seront traités au niveau semestriel.
|
||||||
|
</p><p>
|
||||||
|
Le maximum de bonification qu’un étudiant peut obtenir sur sa moyenne
|
||||||
|
est plafonné à 0.5 point.
|
||||||
|
</p><p>
|
||||||
|
Lorsqu’un étudiant suit plus de deux matières qui donnent droit à
|
||||||
|
bonification, l’étudiant choisit les deux notes à retenir.
|
||||||
|
</p><p>
|
||||||
|
Les points bonus ne sont acquis que pour une note supérieure à 10/20.
|
||||||
|
</p><p>
|
||||||
|
La bonification est calculée de la manière suivante :<br>
|
||||||
|
|
||||||
|
Pour chaque matière (max. 2) donnant lieu à bonification :<br>
|
||||||
|
|
||||||
|
Bonification = (N-10) x 0,05,
|
||||||
|
N étant la note de l’activité sur 20.
|
||||||
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# note: ScoDoc ne vérifie pas que le nombre de modules avec inscription n'excède pas 2
|
||||||
name = "bonus_iutlh"
|
name = "bonus_iutlh"
|
||||||
displayed_name = "IUT du Havre"
|
displayed_name = "IUT du Havre"
|
||||||
|
classic_use_bonus_ues = True # sur les UE, même en DUT et LP
|
||||||
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
|
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
|
||||||
amplitude = 0.005 # multiplie les points au dessus du seuil
|
proportion_point = 0.05
|
||||||
|
bonus_max = 0.5 #
|
||||||
|
|
||||||
|
|
||||||
class BonusLeMans(BonusSportAdditif):
|
class BonusLeMans(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans.
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans.
|
||||||
|
|
||||||
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
|
<p>Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
|
||||||
optionnelles sont cumulés.
|
optionnelles sont cumulés.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>En BUT: la moyenne de chacune des UE du semestre est augmentée de
|
||||||
|
2% du cumul des points de bonus;</li>
|
||||||
|
|
||||||
|
<li>En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
|
||||||
En BUT: la moyenne de chacune des UE du semestre est augmentée de
|
</li>
|
||||||
2% du cumul des points de bonus,
|
</ul>
|
||||||
|
<p>Dans tous les cas, le bonus est dans la limite de 0,5 point.</p>
|
||||||
En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
|
|
||||||
|
|
||||||
Dans tous les cas, le bonus est dans la limite de 0,5 point.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_iutlemans"
|
name = "bonus_iutlemans"
|
||||||
@ -471,14 +747,15 @@ class BonusLeMans(BonusSportAdditif):
|
|||||||
|
|
||||||
# Bonus simple, mais avec changement de paramètres en 2010 !
|
# Bonus simple, mais avec changement de paramètres en 2010 !
|
||||||
class BonusLille(BonusSportAdditif):
|
class BonusLille(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT Villeneuve d'Ascq
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement.
|
de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement.
|
||||||
|
</p><p>
|
||||||
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
|
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
|
||||||
optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés
|
optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés
|
||||||
s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_lille"
|
name = "bonus_lille"
|
||||||
@ -497,8 +774,25 @@ class BonusLille(BonusSportAdditif):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BonusLimousin(BonusSportAdditif):
|
||||||
|
"""Calcul bonus modules optionnels (sport, culture) à l'IUT du Limousin
|
||||||
|
|
||||||
|
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières optionnelles
|
||||||
|
sont cumulés.
|
||||||
|
|
||||||
|
La moyenne de chacune des UE du semestre pair est augmentée de 5% du
|
||||||
|
cumul des points de bonus.
|
||||||
|
Le maximum de points bonus est de 0,5.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_limousin"
|
||||||
|
displayed_name = "IUT du Limousin"
|
||||||
|
proportion_point = 0.05
|
||||||
|
bonus_max = 0.5
|
||||||
|
|
||||||
|
|
||||||
class BonusLyonProvisoire(BonusSportAdditif):
|
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
|
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
|
optionnelles sont cumulés et 1,8% de ces points cumulés
|
||||||
@ -512,8 +806,36 @@ class BonusLyonProvisoire(BonusSportAdditif):
|
|||||||
bonus_max = 0.5
|
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):
|
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
|
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,
|
5% du cumul des points supérieurs à 10 obtenus en matières optionnelles,
|
||||||
@ -530,17 +852,19 @@ class BonusMulhouse(BonusSportAdditif):
|
|||||||
class BonusNantes(BonusSportAdditif):
|
class BonusNantes(BonusSportAdditif):
|
||||||
"""IUT de Nantes (Septembre 2018)
|
"""IUT de Nantes (Septembre 2018)
|
||||||
|
|
||||||
Nous avons différents types de bonification
|
<p>Nous avons différents types de bonification
|
||||||
(sport, culture, engagement citoyen).
|
(sport, culture, engagement citoyen).
|
||||||
|
</p><p>
|
||||||
Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
|
Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
|
||||||
la bonification totale ne doit pas excéder les 0,5 point.
|
la bonification totale ne doit pas excéder les 0,5 point.
|
||||||
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
|
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
|
||||||
|
</p><p>
|
||||||
Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura des modules
|
Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura
|
||||||
pour chaque activité (Sport, Associations, ...)
|
des modules pour chaque activité (Sport, Associations, ...)
|
||||||
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
|
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20,
|
||||||
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
|
mais en fait ce sera la valeur de la bonification: entrer 0,1/20 signifiera
|
||||||
|
un bonus de 0,1 point la moyenne générale).
|
||||||
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_nantes"
|
name = "bonus_nantes"
|
||||||
@ -550,6 +874,19 @@ class BonusNantes(BonusSportAdditif):
|
|||||||
bonus_max = 0.5 # plafonnement à 0.5 points
|
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):
|
class BonusRoanne(BonusSportAdditif):
|
||||||
"""IUT de Roanne.
|
"""IUT de Roanne.
|
||||||
|
|
||||||
@ -561,11 +898,46 @@ class BonusRoanne(BonusSportAdditif):
|
|||||||
displayed_name = "IUT de Roanne"
|
displayed_name = "IUT de Roanne"
|
||||||
seuil_moy_gen = 0.0
|
seuil_moy_gen = 0.0
|
||||||
bonus_max = 0.6 # plafonnement à 0.6 points
|
bonus_max = 0.6 # plafonnement à 0.6 points
|
||||||
apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP
|
classic_use_bonus_ues = True # sur les UE, même en DUT et LP
|
||||||
|
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 BonusStEtienne(BonusSportAdditif):
|
||||||
|
"""IUT de Saint-Etienne.
|
||||||
|
|
||||||
|
Le bonus est compris entre 0 et 0.6 points.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_iutse"
|
||||||
|
displayed_name = "IUT de Saint-Etienne"
|
||||||
|
seuil_moy_gen = 0.0
|
||||||
|
bonus_max = 0.6 # plafonnement à 0.6 points
|
||||||
|
proportion_point = 1
|
||||||
|
|
||||||
|
|
||||||
class BonusStDenis(BonusSportAdditif):
|
class BonusStDenis(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Saint-Denis
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT Saint-Denis
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
de l'Université Paris 13 (sports, musique, deuxième langue,
|
de l'Université Paris 13 (sports, musique, deuxième langue,
|
||||||
@ -581,16 +953,75 @@ class BonusStDenis(BonusSportAdditif):
|
|||||||
bonus_max = 0.5
|
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):
|
class BonusTours(BonusDirect):
|
||||||
"""Calcul bonus sport & culture IUT Tours.
|
"""Calcul bonus sport & culture IUT Tours.
|
||||||
|
|
||||||
Les notes des UE bonus (ramenées sur 20) sont sommées
|
<p>Les notes des UE bonus (ramenées sur 20) sont sommées
|
||||||
et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale,
|
et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale,
|
||||||
soit pour le BUT à chaque moyenne d'UE.
|
soit pour le BUT à chaque moyenne d'UE.
|
||||||
|
</p><p>
|
||||||
Attention: en GEII, facteur 1/40, ailleurs facteur 1.
|
<em>Attention: en GEII, facteur 1/40, ailleurs facteur 1.</em>
|
||||||
|
</p><p>
|
||||||
Le bonus total est limité à 1 point.
|
Le bonus total est limité à 1 point.
|
||||||
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_tours"
|
name = "bonus_tours"
|
||||||
@ -611,15 +1042,17 @@ class BonusTours(BonusDirect):
|
|||||||
|
|
||||||
|
|
||||||
class BonusVilleAvray(BonusSport):
|
class BonusVilleAvray(BonusSport):
|
||||||
"""Bonus modules optionels (sport, culture), règle IUT Ville d'Avray.
|
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
|
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
|
||||||
Si la note est >= 10 et < 12, bonus de 0.1 point
|
<ul>
|
||||||
Si la note est >= 12 et < 16, bonus de 0.2 point
|
<li>Si la note est >= 10 et < 12, bonus de 0.1 point</li>
|
||||||
Si la note est >= 16, bonus de 0.3 point
|
<li>Si la note est >= 12 et < 16, bonus de 0.2 point</li>
|
||||||
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
|
<li>Si la note est >= 16, bonus de 0.3 point</li>
|
||||||
l'étudiant.
|
</ul>
|
||||||
|
<p>Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
|
||||||
|
l'étudiant.</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_iutva"
|
name = "bonus_iutva"
|
||||||
@ -627,25 +1060,25 @@ class BonusVilleAvray(BonusSport):
|
|||||||
|
|
||||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
"""calcul du bonus"""
|
"""calcul du bonus"""
|
||||||
|
if 0 in sem_modimpl_moys_inscrits.shape:
|
||||||
|
# pas d'étudiants ou pas d'UE ou pas de module...
|
||||||
|
return
|
||||||
# Calcule moyenne pondérée des notes de sport:
|
# Calcule moyenne pondérée des notes de sport:
|
||||||
|
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||||
bonus_moy_arr = np.sum(
|
bonus_moy_arr = np.sum(
|
||||||
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
|
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
|
||||||
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||||||
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
|
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
|
||||||
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
|
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
|
||||||
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
|
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
|
||||||
|
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
|
||||||
|
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
|
||||||
|
|
||||||
# Bonus moyenne générale, et 0 sur les UE
|
self.bonus_additif(bonus_moy_arr)
|
||||||
self.bonus_moy_gen = pd.Series(bonus_moy_arr, index=self.etuds_idx, dtype=float)
|
|
||||||
if self.bonus_max is not None:
|
|
||||||
# Seuil: bonus (sur moy. gen.) limité à bonus_max points
|
|
||||||
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_max)
|
|
||||||
|
|
||||||
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
|
|
||||||
|
|
||||||
|
|
||||||
class BonusIUTV(BonusSportAdditif):
|
class BonusIUTV(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT Villetaneuse
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
de l'Université Paris 13 (sports, musique, deuxième langue,
|
de l'Université Paris 13 (sports, musique, deuxième langue,
|
||||||
@ -657,7 +1090,7 @@ class BonusIUTV(BonusSportAdditif):
|
|||||||
|
|
||||||
name = "bonus_iutv"
|
name = "bonus_iutv"
|
||||||
displayed_name = "IUT de Villetaneuse"
|
displayed_name = "IUT de Villetaneuse"
|
||||||
pass # oui, c'ets le bonus par défaut
|
pass # oui, c'est le bonus par défaut
|
||||||
|
|
||||||
|
|
||||||
def get_bonus_class_dict(start=BonusSport, d=None):
|
def get_bonus_class_dict(start=BonusSport, d=None):
|
||||||
|
@ -3,11 +3,9 @@
|
|||||||
|
|
||||||
"""Matrices d'inscription aux modules d'un semestre
|
"""Matrices d'inscription aux modules d'un semestre
|
||||||
"""
|
"""
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import models
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
|
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
|
||||||
@ -17,7 +15,7 @@ from app import models
|
|||||||
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
||||||
"""Charge la matrice des inscriptions aux modules du semestre
|
"""Charge la matrice des inscriptions aux modules du semestre
|
||||||
rows: etudid (inscrits au semestre, avec DEM et DEF)
|
rows: etudid (inscrits au semestre, avec DEM et DEF)
|
||||||
columns: moduleimpl_id (en chaîne)
|
columns: moduleimpl_id
|
||||||
value: bool (0/1 inscrit ou pas)
|
value: bool (0/1 inscrit ou pas)
|
||||||
"""
|
"""
|
||||||
# méthode la moins lente: une requete par module, merge les dataframes
|
# méthode la moins lente: une requete par module, merge les dataframes
|
||||||
|
@ -16,7 +16,7 @@ from app.scodoc import sco_codes_parcours
|
|||||||
|
|
||||||
|
|
||||||
class ValidationsSemestre(ResultatsCache):
|
class ValidationsSemestre(ResultatsCache):
|
||||||
""" """
|
"""Les décisions de jury pour un semestre"""
|
||||||
|
|
||||||
_cached_attrs = (
|
_cached_attrs = (
|
||||||
"decisions_jury",
|
"decisions_jury",
|
||||||
|
50
app/comp/moy_mat.py
Normal file
50
app/comp/moy_mat.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Calcul des moyennes de matières
|
||||||
|
"""
|
||||||
|
|
||||||
|
# C'est un recalcul (optionnel) effectué _après_ le calcul standard.
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from app.comp import moy_ue
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
def compute_mat_moys_classic(
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
sem_matrix: np.array,
|
||||||
|
ues: list,
|
||||||
|
modimpl_inscr_df: pd.DataFrame,
|
||||||
|
modimpl_coefs: np.array,
|
||||||
|
) -> dict:
|
||||||
|
"""Calcul des moyennes par matières.
|
||||||
|
Result: dict, { matiere_id : Series, index etudid }
|
||||||
|
"""
|
||||||
|
modimpls_std = [
|
||||||
|
m
|
||||||
|
for m in formsemestre.modimpls_sorted
|
||||||
|
if (m.module.module_type == ModuleType.STANDARD)
|
||||||
|
and (m.module.ue.type != UE_SPORT)
|
||||||
|
]
|
||||||
|
matiere_ids = {m.module.matiere.id for m in modimpls_std}
|
||||||
|
matiere_moy = {} # { matiere_id : moy pd.Series, index etudid }
|
||||||
|
for matiere_id in matiere_ids:
|
||||||
|
modimpl_mask = np.array(
|
||||||
|
[m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted]
|
||||||
|
)
|
||||||
|
etud_moy_mat = moy_ue.compute_mat_moys_classic(
|
||||||
|
sem_matrix=sem_matrix,
|
||||||
|
modimpl_inscr_df=modimpl_inscr_df,
|
||||||
|
modimpl_coefs=modimpl_coefs,
|
||||||
|
modimpl_mask=modimpl_mask,
|
||||||
|
)
|
||||||
|
matiere_moy[matiere_id] = etud_moy_mat
|
||||||
|
return matiere_moy
|
@ -335,15 +335,17 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||||||
notes_rat / (eval_rat.note_max / 20.0),
|
notes_rat / (eval_rat.note_max / 20.0),
|
||||||
np.nan,
|
np.nan,
|
||||||
)
|
)
|
||||||
|
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même
|
||||||
|
# pour toutes les UE mais ne remplace que là où elle est supérieure
|
||||||
|
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
|
||||||
# prend le max
|
# prend le max
|
||||||
etuds_use_rattrapage = notes_rat > etuds_moy_module
|
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
|
||||||
etuds_moy_module = np.where(
|
etuds_moy_module = np.where(
|
||||||
etuds_use_rattrapage[:, np.newaxis],
|
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||||
np.tile(notes_rat[:, np.newaxis], nb_ues),
|
|
||||||
etuds_moy_module,
|
|
||||||
)
|
)
|
||||||
|
# Serie indiquant que l'étudiant utilise une note de rattarage sur l'une des UE:
|
||||||
self.etuds_use_rattrapage = pd.Series(
|
self.etuds_use_rattrapage = pd.Series(
|
||||||
etuds_use_rattrapage, index=self.evals_notes.index
|
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||||
)
|
)
|
||||||
self.etuds_moy_module = pd.DataFrame(
|
self.etuds_moy_module = pd.DataFrame(
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
@ -359,6 +361,10 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
Les valeurs manquantes (évaluations sans coef vers des UE) sont
|
Les valeurs manquantes (évaluations sans coef vers des UE) sont
|
||||||
remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon
|
remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon
|
||||||
(sauf pour module bonus, defaut à 1)
|
(sauf pour module bonus, defaut à 1)
|
||||||
|
|
||||||
|
Si le module n'est pas une ressource ou une SAE, ne charge pas de poids
|
||||||
|
et renvoie toujours les poids par défaut.
|
||||||
|
|
||||||
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
||||||
"""
|
"""
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
@ -367,6 +373,10 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
ue_ids = [ue.id for ue in ues]
|
ue_ids = [ue.id for ue in ues]
|
||||||
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||||
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
||||||
|
if (
|
||||||
|
modimpl.module.module_type == ModuleType.RESSOURCE
|
||||||
|
or modimpl.module.module_type == ModuleType.SAE
|
||||||
|
):
|
||||||
for ue_poids in EvaluationUEPoids.query.join(
|
for ue_poids in EvaluationUEPoids.query.join(
|
||||||
EvaluationUEPoids.evaluation
|
EvaluationUEPoids.evaluation
|
||||||
).filter_by(moduleimpl_id=moduleimpl_id):
|
).filter_by(moduleimpl_id=moduleimpl_id):
|
||||||
|
@ -30,8 +30,11 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
from flask import flash, g, Markup, url_for
|
||||||
|
from app.models.formations import Formation
|
||||||
|
|
||||||
def compute_sem_moys_apc(
|
|
||||||
|
def compute_sem_moys_apc_using_coefs(
|
||||||
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||||
) -> pd.Series:
|
) -> pd.Series:
|
||||||
"""Calcule les moyennes générales indicatives de tous les étudiants
|
"""Calcule les moyennes générales indicatives de tous les étudiants
|
||||||
@ -48,14 +51,57 @@ def compute_sem_moys_apc(
|
|||||||
return moy_gen
|
return moy_gen
|
||||||
|
|
||||||
|
|
||||||
def comp_ranks_series(notes: pd.Series) -> dict[int, str]:
|
def compute_sem_moys_apc_using_ects(
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
return moy_gen
|
||||||
|
|
||||||
|
|
||||||
|
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
|
||||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||||
numérique) en tenant compte des ex-aequos.
|
numérique) en tenant compte des ex-aequos.
|
||||||
|
|
||||||
Result: { 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
|
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
|
||||||
rangs = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
|
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
|
||||||
N = len(notes)
|
N = len(notes)
|
||||||
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
|
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
|
||||||
notes_i = notes.iat
|
notes_i = notes.iat
|
||||||
@ -67,6 +113,7 @@ def comp_ranks_series(notes: pd.Series) -> dict[int, str]:
|
|||||||
next = None
|
next = None
|
||||||
val = notes_i[i]
|
val = notes_i[i]
|
||||||
if nb_ex:
|
if nb_ex:
|
||||||
|
rangs_int[etudid] = i + 1 - nb_ex
|
||||||
srang = "%d ex" % (i + 1 - nb_ex)
|
srang = "%d ex" % (i + 1 - nb_ex)
|
||||||
if val == next:
|
if val == next:
|
||||||
nb_ex += 1
|
nb_ex += 1
|
||||||
@ -74,9 +121,11 @@ def comp_ranks_series(notes: pd.Series) -> dict[int, str]:
|
|||||||
nb_ex = 0
|
nb_ex = 0
|
||||||
else:
|
else:
|
||||||
if val == next:
|
if val == next:
|
||||||
|
rangs_int[etudid] = i + 1 - nb_ex
|
||||||
srang = "%d ex" % (i + 1 - nb_ex)
|
srang = "%d ex" % (i + 1 - nb_ex)
|
||||||
nb_ex = 1
|
nb_ex = 1
|
||||||
else:
|
else:
|
||||||
|
rangs_int[etudid] = i + 1
|
||||||
srang = "%d" % (i + 1)
|
srang = "%d" % (i + 1)
|
||||||
rangs[etudid] = srang
|
rangs_str[etudid] = srang
|
||||||
return rangs
|
return rangs_str, rangs_int
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
|
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
|
||||||
"""
|
"""
|
||||||
from re import X
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@ -136,8 +135,13 @@ def df_load_modimpl_coefs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for mod_coef in mod_coefs:
|
for mod_coef in mod_coefs:
|
||||||
modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
|
try:
|
||||||
|
modimpl_coefs_df[mod2impl[mod_coef.module_id]][
|
||||||
|
mod_coef.ue_id
|
||||||
|
] = mod_coef.coef
|
||||||
|
except IndexError:
|
||||||
|
# il peut y avoir en base des coefs sur des modules ou UE qui ont depuis été retirés de la formation
|
||||||
|
pass
|
||||||
# Initialisation des poids non fixés:
|
# Initialisation des poids non fixés:
|
||||||
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
||||||
# sur toutes les UE)
|
# sur toutes les UE)
|
||||||
@ -193,6 +197,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
|||||||
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
||||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||||
modimpls_results[modimpl.id] = mod_results
|
modimpls_results[modimpl.id] = mod_results
|
||||||
|
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||||
modimpls_notes.append(etuds_moy_module)
|
modimpls_notes.append(etuds_moy_module)
|
||||||
if len(modimpls_notes):
|
if len(modimpls_notes):
|
||||||
cube = notes_sem_assemble_cube(modimpls_notes)
|
cube = notes_sem_assemble_cube(modimpls_notes)
|
||||||
@ -213,28 +218,32 @@ def compute_ue_moys_apc(
|
|||||||
ues: list,
|
ues: list,
|
||||||
modimpl_inscr_df: pd.DataFrame,
|
modimpl_inscr_df: pd.DataFrame,
|
||||||
modimpl_coefs_df: pd.DataFrame,
|
modimpl_coefs_df: pd.DataFrame,
|
||||||
|
modimpl_mask: np.array,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
||||||
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
|
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
|
||||||
NI non inscrit à (au moins un) module de cette UE
|
NI non inscrit à (au moins un) module de cette UE
|
||||||
NA pas de notes disponibles
|
NA pas de notes disponibles
|
||||||
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
|
ERR erreur dans une formule utilisateurs (pas gérées ici).
|
||||||
|
|
||||||
sem_cube: notes moyennes aux modules
|
sem_cube: notes moyennes aux modules
|
||||||
ndarray (etuds x modimpls x UEs)
|
ndarray (etuds x modimpls x UEs)
|
||||||
(floats avec des NaN)
|
(floats avec des NaN)
|
||||||
etuds : liste des étudiants (dim. 0 du cube)
|
etuds : liste des étudiants (dim. 0 du cube)
|
||||||
modimpls : liste des modules à considérer (dim. 1 du cube)
|
modimpls : liste des module_impl (dim. 1 du cube)
|
||||||
ues : liste des UE (dim. 2 du cube)
|
ues : liste des UE (dim. 2 du cube)
|
||||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||||
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
|
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
|
||||||
|
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
|
||||||
|
(utilisé pour éliminer les bonus, et pourra servir à cacluler
|
||||||
|
sur des sous-ensembles de modules)
|
||||||
|
|
||||||
Résultat: DataFrame columns UE (sans sport), rows etudid
|
Résultat: DataFrame columns UE (sans bonus), rows etudid
|
||||||
"""
|
"""
|
||||||
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
|
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
|
||||||
nb_ues_tot = len(ues)
|
nb_ues_tot = len(ues)
|
||||||
assert len(modimpls) == nb_modules
|
assert len(modimpls) == nb_modules
|
||||||
if nb_modules == 0 or nb_etuds == 0:
|
if nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
||||||
return pd.DataFrame(
|
return pd.DataFrame(
|
||||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||||
)
|
)
|
||||||
@ -244,7 +253,8 @@ def compute_ue_moys_apc(
|
|||||||
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
|
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
|
||||||
assert modimpl_coefs_df.shape[1] == nb_modules
|
assert modimpl_coefs_df.shape[1] == nb_modules
|
||||||
modimpl_inscr = modimpl_inscr_df.values
|
modimpl_inscr = modimpl_inscr_df.values
|
||||||
modimpl_coefs = modimpl_coefs_df.values
|
# Met à zéro tous les coefs des modules non sélectionnés dans le masque:
|
||||||
|
modimpl_coefs = np.where(modimpl_mask, modimpl_coefs_df.values, 0.0)
|
||||||
|
|
||||||
# Duplique les inscriptions sur les UEs non bonus:
|
# Duplique les inscriptions sur les UEs non bonus:
|
||||||
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2)
|
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2)
|
||||||
@ -261,6 +271,8 @@ def compute_ue_moys_apc(
|
|||||||
)
|
)
|
||||||
# Annule les coefs des modules NaN
|
# Annule les coefs des modules NaN
|
||||||
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
|
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
|
||||||
|
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
|
||||||
|
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||||
#
|
#
|
||||||
# Version vectorisée
|
# Version vectorisée
|
||||||
#
|
#
|
||||||
@ -283,7 +295,8 @@ def compute_ue_moys_classic(
|
|||||||
modimpl_coefs: np.array,
|
modimpl_coefs: np.array,
|
||||||
modimpl_mask: np.array,
|
modimpl_mask: np.array,
|
||||||
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
|
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
|
||||||
"""Calcul de la moyenne d'UE en mode classique.
|
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
|
||||||
|
|
||||||
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
|
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
|
||||||
NI non inscrit à (au moins un) module de cette UE
|
NI non inscrit à (au moins un) module de cette UE
|
||||||
NA pas de notes disponibles
|
NA pas de notes disponibles
|
||||||
@ -343,7 +356,8 @@ def compute_ue_moys_classic(
|
|||||||
modimpl_coefs_etuds_no_nan = np.where(
|
modimpl_coefs_etuds_no_nan = np.where(
|
||||||
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
|
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
|
||||||
)
|
)
|
||||||
|
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
|
||||||
|
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||||
# --------------------- Calcul des moyennes d'UE
|
# --------------------- Calcul des moyennes d'UE
|
||||||
ue_modules = np.array(
|
ue_modules = np.array(
|
||||||
[[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues]
|
[[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues]
|
||||||
@ -351,8 +365,10 @@ def compute_ue_moys_classic(
|
|||||||
modimpl_coefs_etuds_no_nan_stacked = np.stack(
|
modimpl_coefs_etuds_no_nan_stacked = np.stack(
|
||||||
[modimpl_coefs_etuds_no_nan.T] * nb_ues
|
[modimpl_coefs_etuds_no_nan.T] * nb_ues
|
||||||
)
|
)
|
||||||
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions
|
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
|
||||||
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
|
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
|
||||||
|
if coefs.dtype == np.object: # arrive sur des tableaux vides
|
||||||
|
coefs = coefs.astype(np.float)
|
||||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||||
etud_moy_ue = (
|
etud_moy_ue = (
|
||||||
np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2)
|
np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2)
|
||||||
@ -394,6 +410,68 @@ def compute_ue_moys_classic(
|
|||||||
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||||
|
|
||||||
|
|
||||||
|
def compute_mat_moys_classic(
|
||||||
|
sem_matrix: np.array,
|
||||||
|
modimpl_inscr_df: pd.DataFrame,
|
||||||
|
modimpl_coefs: np.array,
|
||||||
|
modimpl_mask: np.array,
|
||||||
|
) -> pd.Series:
|
||||||
|
"""Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE
|
||||||
|
|
||||||
|
La moyenne est un nombre (note/20 ou NaN.
|
||||||
|
|
||||||
|
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
|
||||||
|
permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt).
|
||||||
|
|
||||||
|
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
|
||||||
|
ndarray (etuds x modimpls)
|
||||||
|
(floats avec des NaN)
|
||||||
|
etuds : listes des étudiants (dim. 0 de la matrice)
|
||||||
|
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||||
|
modimpl_coefs: vecteur des coefficients de modules
|
||||||
|
modimpl_mask: masque des modimpls à prendre en compte
|
||||||
|
|
||||||
|
Résultat:
|
||||||
|
- moyennes: pd.Series, index etudid
|
||||||
|
"""
|
||||||
|
if (not len(modimpl_mask)) or (
|
||||||
|
sem_matrix.shape[0] == 0
|
||||||
|
): # aucun module ou aucun étudiant
|
||||||
|
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||||
|
return pd.Series(
|
||||||
|
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
||||||
|
)
|
||||||
|
# Restreint aux modules sélectionnés:
|
||||||
|
sem_matrix = sem_matrix[:, modimpl_mask]
|
||||||
|
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
|
||||||
|
modimpl_coefs = modimpl_coefs[modimpl_mask]
|
||||||
|
|
||||||
|
nb_etuds, nb_modules = sem_matrix.shape
|
||||||
|
assert len(modimpl_coefs) == nb_modules
|
||||||
|
|
||||||
|
# Enlève les NaN du numérateur:
|
||||||
|
sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
|
||||||
|
# Ne prend pas en compte les notes des étudiants non inscrits au module:
|
||||||
|
# Annule les notes:
|
||||||
|
sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0)
|
||||||
|
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
|
||||||
|
modimpl_coefs_etuds = np.where(
|
||||||
|
modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
|
||||||
|
)
|
||||||
|
# Annule les coefs des modules NaN (nb_etuds x nb_mods)
|
||||||
|
modimpl_coefs_etuds_no_nan = np.where(
|
||||||
|
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
|
||||||
|
)
|
||||||
|
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
|
||||||
|
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||||
|
|
||||||
|
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
|
||||||
|
axis=1
|
||||||
|
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
|
||||||
|
|
||||||
|
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
|
||||||
|
|
||||||
|
|
||||||
def compute_malus(
|
def compute_malus(
|
||||||
formsemestre: FormSemestre,
|
formsemestre: FormSemestre,
|
||||||
sem_modimpl_moys: np.array,
|
sem_modimpl_moys: np.array,
|
||||||
|
@ -12,11 +12,13 @@ import pandas as pd
|
|||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||||
from app.comp.res_common import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.comp.bonus_spo import BonusSport
|
from app.comp.bonus_spo import BonusSport
|
||||||
from app.models import ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
|
||||||
class ResultatsSemestreBUT(NotesTableCompat):
|
class ResultatsSemestreBUT(NotesTableCompat):
|
||||||
@ -31,6 +33,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
def __init__(self, formsemestre):
|
def __init__(self, formsemestre):
|
||||||
super().__init__(formsemestre)
|
super().__init__(formsemestre)
|
||||||
|
|
||||||
|
self.sem_cube = None
|
||||||
|
"""ndarray (etuds x modimpl x ue)"""
|
||||||
|
|
||||||
if not self.load_cached():
|
if not self.load_cached():
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
self.compute()
|
self.compute()
|
||||||
@ -38,7 +43,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
self.store()
|
self.store()
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
log(
|
log(
|
||||||
f"ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id
|
||||||
|
} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def compute(self):
|
def compute(self):
|
||||||
@ -56,14 +62,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||||
|
|
||||||
# Elimine les coefs des modimpl bonus sports:
|
# Masque de tous les modules _sauf_ les bonus (sport)
|
||||||
modimpls_sport = [
|
modimpls_mask = [
|
||||||
modimpl
|
modimpl.module.ue.type != UE_SPORT
|
||||||
for modimpl in self.formsemestre.modimpls_sorted
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
if modimpl.module.ue.type == UE_SPORT
|
|
||||||
]
|
]
|
||||||
for modimpl in modimpls_sport:
|
|
||||||
self.modimpl_coefs_df[modimpl.id] = 0
|
|
||||||
|
|
||||||
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||||
self.sem_cube,
|
self.sem_cube,
|
||||||
@ -72,10 +75,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
self.ues,
|
self.ues,
|
||||||
self.modimpl_inscr_df,
|
self.modimpl_inscr_df,
|
||||||
self.modimpl_coefs_df,
|
self.modimpl_coefs_df,
|
||||||
|
modimpls_mask,
|
||||||
)
|
)
|
||||||
# Les coefficients d'UE ne sont pas utilisés en APC
|
# Les coefficients d'UE ne sont pas utilisés en APC
|
||||||
self.etud_coef_ue_df = pd.DataFrame(
|
self.etud_coef_ue_df = pd.DataFrame(
|
||||||
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
|
0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Modules de MALUS sur les UEs
|
# --- Modules de MALUS sur les UEs
|
||||||
@ -85,7 +89,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
self.etud_moy_ue -= self.malus
|
self.etud_moy_ue -= self.malus
|
||||||
|
|
||||||
# --- Bonus Sport & Culture
|
# --- Bonus Sport & Culture
|
||||||
if len(modimpls_sport) > 0:
|
if not all(modimpls_mask): # au moins un module bonus
|
||||||
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
|
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
|
||||||
if bonus_class is not None:
|
if bonus_class is not None:
|
||||||
bonus: BonusSport = bonus_class(
|
bonus: BonusSport = bonus_class(
|
||||||
@ -100,13 +104,23 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
self.bonus_ues = bonus.get_bonus_ues()
|
self.bonus_ues = bonus.get_bonus_ues()
|
||||||
if self.bonus_ues is not None:
|
if self.bonus_ues is not None:
|
||||||
self.etud_moy_ue += self.bonus_ues # somme les dataframes
|
self.etud_moy_ue += self.bonus_ues # somme les dataframes
|
||||||
|
|
||||||
|
# Clippe toutes les moyennes d'UE dans [0,20]
|
||||||
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
||||||
|
|
||||||
# Moyenne générale indicative:
|
# Moyenne générale indicative:
|
||||||
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
||||||
# donc la moyenne indicative)
|
# donc la moyenne indicative)
|
||||||
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
|
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
|
||||||
self.etud_moy_ue, self.modimpl_coefs_df
|
# self.etud_moy_ue, self.modimpl_coefs_df
|
||||||
|
# )
|
||||||
|
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
|
||||||
|
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
|
# --- UE capitalisées
|
||||||
self.apply_capitalisation()
|
self.apply_capitalisation()
|
||||||
@ -134,3 +148,30 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
(ne dépend pas des modules auxquels est inscrit l'étudiant, ).
|
(ne dépend pas des modules auxquels est inscrit l'étudiant, ).
|
||||||
"""
|
"""
|
||||||
return self.modimpl_coefs_df.loc[ue.id].sum()
|
return self.modimpl_coefs_df.loc[ue.id].sum()
|
||||||
|
|
||||||
|
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]:
|
||||||
|
"""Liste des modimpl ayant des coefs non nuls vers cette UE
|
||||||
|
et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant.
|
||||||
|
"""
|
||||||
|
# sert pour l'affichage ou non de l'UE sur le bulletin et la table recap
|
||||||
|
coefs = self.modimpl_coefs_df # row UE, cols modimpl
|
||||||
|
modimpls = [
|
||||||
|
modimpl
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
|
if (coefs[modimpl.id][ue_id] != 0)
|
||||||
|
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||||
|
]
|
||||||
|
if not with_bonus:
|
||||||
|
return [
|
||||||
|
modimpl for modimpl in modimpls if modimpl.module.ue.type != UE_SPORT
|
||||||
|
]
|
||||||
|
return modimpls
|
||||||
|
|
||||||
|
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||||
|
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||||
|
Utile pour stats bottom tableau recap.
|
||||||
|
Résultat: 1d array of float
|
||||||
|
"""
|
||||||
|
i = self.modimpl_coefs_df.columns.get_loc(modimpl_id)
|
||||||
|
j = self.modimpl_coefs_df.index.get_loc(ue_id)
|
||||||
|
return self.sem_cube[:, i, j]
|
||||||
|
@ -11,6 +11,14 @@ from app.models import FormSemestre
|
|||||||
|
|
||||||
|
|
||||||
class ResultatsCache:
|
class ResultatsCache:
|
||||||
|
"""Résultats cachés (via redis)
|
||||||
|
L'attribut _cached_attrs donne la liste des noms des attributs à cacher
|
||||||
|
(doivent être sérialisables facilement, se limiter à des types simples)
|
||||||
|
|
||||||
|
store() enregistre les attributs dans le cache, et
|
||||||
|
load_cached() les recharge.
|
||||||
|
"""
|
||||||
|
|
||||||
_cached_attrs = () # virtual
|
_cached_attrs = () # virtual
|
||||||
|
|
||||||
def __init__(self, formsemestre: FormSemestre, cache_class=None):
|
def __init__(self, formsemestre: FormSemestre, cache_class=None):
|
||||||
|
@ -15,8 +15,8 @@ from flask import g, url_for
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import log
|
from app import log
|
||||||
from app.comp import 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.res_compat import NotesTableCompat
|
||||||
from app.comp.bonus_spo import BonusSport
|
from app.comp.bonus_spo import BonusSport
|
||||||
from app.models import ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
@ -24,6 +24,7 @@ from app.models.formsemestre import FormSemestre
|
|||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
@ -34,10 +35,13 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
"modimpl_coefs",
|
"modimpl_coefs",
|
||||||
"modimpl_idx",
|
"modimpl_idx",
|
||||||
"sem_matrix",
|
"sem_matrix",
|
||||||
|
"mod_rangs",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, formsemestre):
|
def __init__(self, formsemestre):
|
||||||
super().__init__(formsemestre)
|
super().__init__(formsemestre)
|
||||||
|
self.sem_matrix: np.ndarray = None
|
||||||
|
"sem_matrix : 2d-array (etuds x modimpls)"
|
||||||
|
|
||||||
if not self.load_cached():
|
if not self.load_cached():
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@ -46,7 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
self.store()
|
self.store()
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
log(
|
log(
|
||||||
f"ResultatsSemestreClassic: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
f"""ResultatsSemestreClassic: cached formsemestre_id={
|
||||||
|
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||||
)
|
)
|
||||||
# recalculé (aussi rapide que de les cacher)
|
# recalculé (aussi rapide que de les cacher)
|
||||||
self.moy_min = self.etud_moy_gen.min()
|
self.moy_min = self.etud_moy_gen.min()
|
||||||
@ -60,7 +65,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
)
|
)
|
||||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||||
self.modimpl_coefs = np.array(
|
self.modimpl_coefs = np.array(
|
||||||
[m.module.coefficient for m in self.formsemestre.modimpls_sorted]
|
[m.module.coefficient or 0.0 for m in self.formsemestre.modimpls_sorted]
|
||||||
)
|
)
|
||||||
self.modimpl_idx = {
|
self.modimpl_idx = {
|
||||||
m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted)
|
m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted)
|
||||||
@ -113,23 +118,60 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
self.etud_moy_ue += self.bonus_ues # somme les dataframes
|
self.etud_moy_ue += self.bonus_ues # somme les dataframes
|
||||||
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
||||||
bonus_mg = bonus.get_bonus_moy_gen()
|
bonus_mg = bonus.get_bonus_moy_gen()
|
||||||
if bonus_mg is not None:
|
if bonus_mg is None and self.bonus_ues is not None:
|
||||||
|
# pas de bonus explicite sur la moyenne générale
|
||||||
|
# on l'ajuste pour refléter les modifs d'UE, à l'aide des coefs d'UE.
|
||||||
|
bonus_mg = (self.etud_coef_ue_df * self.bonus_ues).sum(
|
||||||
|
axis=1
|
||||||
|
) / self.etud_coef_ue_df.sum(axis=1)
|
||||||
self.etud_moy_gen += bonus_mg
|
self.etud_moy_gen += bonus_mg
|
||||||
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True)
|
elif bonus_mg is not None:
|
||||||
self.bonus = (
|
# Applique le bonus moyenne générale renvoyé
|
||||||
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
|
self.etud_moy_gen += bonus_mg
|
||||||
)
|
|
||||||
|
# compat nt, utilisé pour l'afficher sur les bulletins:
|
||||||
|
self.bonus = bonus_mg
|
||||||
|
|
||||||
# --- UE capitalisées
|
# --- UE capitalisées
|
||||||
self.apply_capitalisation()
|
self.apply_capitalisation()
|
||||||
|
|
||||||
|
# Clippe toutes les moyennes dans [0,20]
|
||||||
|
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
||||||
|
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True)
|
||||||
|
|
||||||
# --- Classements:
|
# --- Classements:
|
||||||
self.compute_rangs()
|
self.compute_rangs()
|
||||||
|
|
||||||
|
# --- En option, moyennes par matières
|
||||||
|
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:
|
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||||
"""La moyenne de l'étudiant dans le moduleimpl
|
"""La moyenne de l'étudiant dans le moduleimpl
|
||||||
Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
|
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:
|
def get_mod_stats(self, moduleimpl_id: int) -> dict:
|
||||||
"""Stats sur les notes obtenues dans un modimpl"""
|
"""Stats sur les notes obtenues dans un modimpl"""
|
||||||
@ -150,41 +192,57 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def modimpl_notes(
|
||||||
|
self,
|
||||||
|
modimpl_id: int,
|
||||||
|
ue_id: int = None,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||||
|
Utile pour stats bottom tableau recap.
|
||||||
|
ue_id n'est pas utilisé ici (formations classiques)
|
||||||
|
Résultat: 1d array of float
|
||||||
|
"""
|
||||||
|
i = self.modimpl_idx[modimpl_id]
|
||||||
|
return self.sem_matrix[:, i]
|
||||||
|
|
||||||
|
def compute_moyennes_matieres(self):
|
||||||
|
"""Calcul les moyennes par matière. Doit être appelée au besoin, en fin de compute."""
|
||||||
|
self.moyennes_matieres = moy_mat.compute_mat_moys_classic(
|
||||||
|
self.formsemestre,
|
||||||
|
self.sem_matrix,
|
||||||
|
self.ues,
|
||||||
|
self.modimpl_inscr_df,
|
||||||
|
self.modimpl_coefs,
|
||||||
|
)
|
||||||
|
|
||||||
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
||||||
"""Détermine le coefficient de l'UE pour cet étudiant.
|
"""Détermine le coefficient de l'UE pour cet étudiant.
|
||||||
N'est utilisé que pour l'injection des UE capitalisées dans la
|
N'est utilisé que pour l'injection des UE capitalisées dans la
|
||||||
moyenne générale.
|
moyenne générale.
|
||||||
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
|
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
|
||||||
"""
|
"""
|
||||||
c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
coef = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
||||||
if c is not None: # inscrit à au moins un module de cette UE
|
if coef is not None: # inscrit à au moins un module de cette UE
|
||||||
return c
|
return coef
|
||||||
# arfff: aucun moyen de déterminer le coefficient de façon sûre
|
# arfff: aucun moyen de déterminer le coefficient de façon sûre
|
||||||
log(
|
log(
|
||||||
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s"
|
f"""* oups: calcul coef UE impossible\nformsemestre_id='{self.formsemestre.id
|
||||||
% (self.formsemestre.id, etudid, ue)
|
}'\netudid='{etudid}'\nue={ue}"""
|
||||||
)
|
)
|
||||||
etud: Identite = Identite.query.get(etudid)
|
etud: Identite = Identite.query.get(etudid)
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
|
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||||
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
|
impossible à déterminer pour l'étudiant <a href="{
|
||||||
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
|
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||||
|
<p>Il faut <a href="{
|
||||||
|
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=self.formsemestre.id, err_ue_id=ue["ue_id"],
|
||||||
|
)
|
||||||
|
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
% (
|
|
||||||
ue.acronyme,
|
|
||||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
|
||||||
etud.nom_disp(),
|
|
||||||
url_for(
|
|
||||||
"notes.formsemestre_edit_uecoefs",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=self.formsemestre.id,
|
|
||||||
err_ue_id=ue["ue_id"],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return 0.0 # ?
|
|
||||||
|
|
||||||
|
|
||||||
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
|
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
|
||||||
@ -215,7 +273,7 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
|
|||||||
(Series rendus par compute_module_moy, index: etud)
|
(Series rendus par compute_module_moy, index: etud)
|
||||||
Resultat: ndarray (etud x module)
|
Resultat: ndarray (etud x module)
|
||||||
"""
|
"""
|
||||||
if not len(modimpls_notes):
|
if not modimpls_notes:
|
||||||
return np.zeros((0, 0), dtype=float)
|
return np.zeros((0, 0), dtype=float)
|
||||||
modimpls_notes_arr = [s.values for s in modimpls_notes]
|
modimpls_notes_arr = [s.values for s in modimpls_notes]
|
||||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||||
|
File diff suppressed because it is too large
Load Diff
462
app/comp/res_compat.py
Normal file
462
app/comp/res_compat.py
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Classe résultats pour compatibilité avec le code ScoDoc 7
|
||||||
|
"""
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from flask import flash, g, Markup, url_for
|
||||||
|
|
||||||
|
from app import log
|
||||||
|
from app.comp import moy_sem
|
||||||
|
from app.comp.aux_stats import StatsMoyenne
|
||||||
|
from app.comp.res_common import ResultatsSemestre
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.models import FormSemestre
|
||||||
|
from app.models import Identite
|
||||||
|
from app.models import ModuleImpl
|
||||||
|
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||||||
|
class NotesTableCompat(ResultatsSemestre):
|
||||||
|
"""Implementation partielle de NotesTable
|
||||||
|
|
||||||
|
Les méthodes définies dans cette classe sont là
|
||||||
|
pour conserver la compatibilité abvec les codes anciens et
|
||||||
|
il n'est pas recommandé de les utiliser dans de nouveaux
|
||||||
|
développements (API malcommode et peu efficace).
|
||||||
|
"""
|
||||||
|
|
||||||
|
_cached_attrs = ResultatsSemestre._cached_attrs + (
|
||||||
|
"malus",
|
||||||
|
"etud_moy_gen_ranks",
|
||||||
|
"etud_moy_gen_ranks_int",
|
||||||
|
"ue_rangs",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, formsemestre: FormSemestre):
|
||||||
|
super().__init__(formsemestre)
|
||||||
|
|
||||||
|
nb_etuds = len(self.etuds)
|
||||||
|
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
|
||||||
|
self.mod_rangs = None # sera surchargé en Classic, mais pas en APC
|
||||||
|
"""{ modimpl_id : (rangs, effectif) }"""
|
||||||
|
self.moy_min = "NA"
|
||||||
|
self.moy_max = "NA"
|
||||||
|
self.moy_moy = "NA"
|
||||||
|
self.expr_diagnostics = ""
|
||||||
|
self.parcours = self.formsemestre.formation.get_parcours()
|
||||||
|
|
||||||
|
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
||||||
|
"""Liste des étudiants inscrits
|
||||||
|
order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative)
|
||||||
|
|
||||||
|
Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace
|
||||||
|
d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]`
|
||||||
|
"""
|
||||||
|
etuds = self.formsemestre.get_inscrits(
|
||||||
|
include_demdef=include_demdef, order=(order_by == "nom")
|
||||||
|
)
|
||||||
|
if order_by == "moy":
|
||||||
|
etuds.sort(
|
||||||
|
key=lambda e: (
|
||||||
|
self.etud_moy_gen_ranks_int.get(e.id, 100000),
|
||||||
|
e.sort_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return etuds
|
||||||
|
|
||||||
|
def get_etudids(self) -> list[int]:
|
||||||
|
"""(deprecated)
|
||||||
|
Liste des etudids inscrits, incluant les démissionnaires.
|
||||||
|
triée par ordre alphabetique de NOM
|
||||||
|
(à éviter: renvoie les etudids, mais est moins efficace que get_inscrits)
|
||||||
|
"""
|
||||||
|
# Note: pour avoir les inscrits non triés,
|
||||||
|
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
|
||||||
|
return [x["etudid"] for x in self.inscrlist]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def sem(self) -> dict:
|
||||||
|
"""le formsemestre, comme un gros et gras dict (nt.sem)"""
|
||||||
|
return self.formsemestre.get_infos_dict()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def inscrlist(self) -> list[dict]: # utilisé par PE
|
||||||
|
"""Liste des inscrits au semestre (avec DEM et DEF),
|
||||||
|
sous forme de dict etud,
|
||||||
|
classée dans l'ordre alphabétique de noms.
|
||||||
|
"""
|
||||||
|
etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True)
|
||||||
|
return [e.to_dict_scodoc7() for e in etuds]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def stats_moy_gen(self):
|
||||||
|
"""Stats (moy/min/max) sur la moyenne générale"""
|
||||||
|
return StatsMoyenne(self.etud_moy_gen)
|
||||||
|
|
||||||
|
def get_ues_stat_dict(
|
||||||
|
self, filter_sport=False, check_apc_ects=True
|
||||||
|
) -> list[dict]: # was get_ues()
|
||||||
|
"""Liste des UEs, ordonnée par numero.
|
||||||
|
Si filter_sport, retire les UE de type SPORT.
|
||||||
|
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
||||||
|
"""
|
||||||
|
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
|
||||||
|
ues_dict = []
|
||||||
|
for ue in ues:
|
||||||
|
d = ue.to_dict()
|
||||||
|
if ue.type != UE_SPORT:
|
||||||
|
moys = self.etud_moy_ue[ue.id]
|
||||||
|
else:
|
||||||
|
moys = None
|
||||||
|
d.update(StatsMoyenne(moys).to_dict())
|
||||||
|
ues_dict.append(d)
|
||||||
|
if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
|
||||||
|
g.checked_apc_ects = True
|
||||||
|
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
|
||||||
|
formation = self.formsemestre.formation
|
||||||
|
ue_sans_ects = [
|
||||||
|
ue for ue in ues if ue.type != UE_SPORT and ue.ects is None
|
||||||
|
]
|
||||||
|
flash(
|
||||||
|
Markup(
|
||||||
|
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||||
|
(dans {' ,'.join([ue.acronyme for ue in ue_sans_ects])}
|
||||||
|
de la formation: <a href="{url_for("notes.ue_table",
|
||||||
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
||||||
|
}">{formation.get_titre_version()}</a>)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
category="danger",
|
||||||
|
)
|
||||||
|
return ues_dict
|
||||||
|
|
||||||
|
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
|
||||||
|
"""Liste des modules pour une UE (ou toutes si ue_id==None),
|
||||||
|
triés par numéros (selon le type de formation)
|
||||||
|
"""
|
||||||
|
modimpls_dict = []
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted:
|
||||||
|
if (ue_id is None) or (modimpl.module.ue.id == ue_id):
|
||||||
|
d = modimpl.to_dict()
|
||||||
|
# compat ScoDoc < 9.2: ajoute matières
|
||||||
|
d["mat"] = modimpl.module.matiere.to_dict()
|
||||||
|
modimpls_dict.append(d)
|
||||||
|
return modimpls_dict
|
||||||
|
|
||||||
|
def compute_rangs(self):
|
||||||
|
"""Calcule les classements
|
||||||
|
Moyenne générale: etud_moy_gen_ranks
|
||||||
|
Par UE (sauf ue bonus)
|
||||||
|
"""
|
||||||
|
(
|
||||||
|
self.etud_moy_gen_ranks,
|
||||||
|
self.etud_moy_gen_ranks_int,
|
||||||
|
) = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||||
|
for ue in self.formsemestre.query_ues():
|
||||||
|
moy_ue = self.etud_moy_ue[ue.id]
|
||||||
|
self.ue_rangs[ue.id] = (
|
||||||
|
moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine
|
||||||
|
int(moy_ue.count()),
|
||||||
|
)
|
||||||
|
# .count() -> nb of non NaN values
|
||||||
|
|
||||||
|
def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]:
|
||||||
|
"""Le rang de l'étudiant dans cette ue
|
||||||
|
Result: rang:str, effectif:str
|
||||||
|
"""
|
||||||
|
rangs, effectif = self.ue_rangs[ue_id]
|
||||||
|
if rangs is not None:
|
||||||
|
rang = rangs[etudid]
|
||||||
|
else:
|
||||||
|
return "", ""
|
||||||
|
return rang, effectif
|
||||||
|
|
||||||
|
def etud_check_conditions_ues(self, etudid):
|
||||||
|
"""Vrai si les conditions sur les UE sont remplies.
|
||||||
|
Ne considère que les UE ayant des notes (moyenne calculée).
|
||||||
|
(les UE sans notes ne sont pas comptées comme sous la barre)
|
||||||
|
Prend en compte les éventuelles UE capitalisées.
|
||||||
|
|
||||||
|
Pour les parcours habituels, cela revient à vérifier que
|
||||||
|
les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
|
||||||
|
|
||||||
|
Pour les parcours non standards (LP2014), cela peut être plus compliqué.
|
||||||
|
|
||||||
|
Return: True|False, message explicatif
|
||||||
|
"""
|
||||||
|
ue_status_list = []
|
||||||
|
for ue in self.formsemestre.query_ues():
|
||||||
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||||
|
if ue_status:
|
||||||
|
ue_status_list.append(ue_status)
|
||||||
|
return self.parcours.check_barre_ues(ue_status_list)
|
||||||
|
|
||||||
|
def all_etuds_have_sem_decisions(self):
|
||||||
|
"""True si tous les étudiants du semestre ont une décision de jury.
|
||||||
|
Ne regarde pas les décisions d'UE.
|
||||||
|
"""
|
||||||
|
for ins in self.formsemestre.inscriptions:
|
||||||
|
if ins.etat != scu.INSCRIT:
|
||||||
|
continue # skip démissionnaires
|
||||||
|
if self.get_etud_decision_sem(ins.etudid) is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def etud_has_decision(self, etudid):
|
||||||
|
"""True s'il y a une décision de jury pour cet étudiant"""
|
||||||
|
return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
|
||||||
|
|
||||||
|
def get_etud_decision_ues(self, etudid: int) -> dict:
|
||||||
|
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
|
||||||
|
Ne tient pas compte des UE capitalisées.
|
||||||
|
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
|
||||||
|
Ne renvoie aucune decision d'UE pour les défaillants
|
||||||
|
"""
|
||||||
|
if self.get_etud_etat(etudid) == DEF:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
if not self.validations:
|
||||||
|
self.validations = res_sem.load_formsemestre_validations(
|
||||||
|
self.formsemestre
|
||||||
|
)
|
||||||
|
return self.validations.decisions_jury_ues.get(etudid, None)
|
||||||
|
|
||||||
|
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||||
|
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||||
|
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
|
||||||
|
Si état défaillant, force le code a DEF
|
||||||
|
"""
|
||||||
|
if self.get_etud_etat(etudid) == DEF:
|
||||||
|
return {
|
||||||
|
"code": DEF,
|
||||||
|
"assidu": False,
|
||||||
|
"event_date": "",
|
||||||
|
"compense_formsemestre_id": None,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if not self.validations:
|
||||||
|
self.validations = res_sem.load_formsemestre_validations(
|
||||||
|
self.formsemestre
|
||||||
|
)
|
||||||
|
return self.validations.decisions_jury.get(etudid, None)
|
||||||
|
|
||||||
|
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
||||||
|
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
|
||||||
|
if not self.moyennes_matieres:
|
||||||
|
return "nd"
|
||||||
|
return (
|
||||||
|
self.moyennes_matieres[matiere_id].get(etudid, "-")
|
||||||
|
if matiere_id in self.moyennes_matieres
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||||
|
"""La moyenne de l'étudiant dans le moduleimpl
|
||||||
|
En APC, il s'agira d'une moyenne indicative sans valeur.
|
||||||
|
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # virtual method
|
||||||
|
|
||||||
|
def get_etud_moy_gen(self, etudid): # -> float | str
|
||||||
|
"""Moyenne générale de cet etudiant dans ce semestre.
|
||||||
|
Prend en compte les UE capitalisées.
|
||||||
|
Si apc, moyenne indicative.
|
||||||
|
Si pas de notes: 'NA'
|
||||||
|
"""
|
||||||
|
return self.etud_moy_gen[etudid]
|
||||||
|
|
||||||
|
def get_etud_ects_pot(self, etudid: int) -> dict:
|
||||||
|
"""
|
||||||
|
Un dict avec les champs
|
||||||
|
ects_pot : (float) nb de crédits ECTS qui seraient validés
|
||||||
|
(sous réserve de validation par le jury)
|
||||||
|
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
|
||||||
|
|
||||||
|
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
|
||||||
|
encore enregistrées).
|
||||||
|
"""
|
||||||
|
# was nt.get_etud_moy_infos
|
||||||
|
# XXX pour compat nt, à remplacer ultérieurement
|
||||||
|
ues = self.get_etud_ue_validables(etudid)
|
||||||
|
ects_pot = 0.0
|
||||||
|
for ue in ues:
|
||||||
|
if (
|
||||||
|
ue.id in self.etud_moy_ue
|
||||||
|
and ue.ects is not None
|
||||||
|
and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
|
||||||
|
):
|
||||||
|
ects_pot += ue.ects
|
||||||
|
return {
|
||||||
|
"ects_pot": ects_pot,
|
||||||
|
"ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_etud_rang(self, etudid: int) -> str:
|
||||||
|
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||||
|
Result: "13" ou "12 ex"
|
||||||
|
"""
|
||||||
|
return self.etud_moy_gen_ranks.get(etudid, 99999)
|
||||||
|
|
||||||
|
def get_etud_rang_group(self, etudid: int, group_id: int):
|
||||||
|
"Le rang de l'étudiant dans ce groupe (NON IMPLEMENTE)"
|
||||||
|
return (None, 0) # XXX unimplemented TODO
|
||||||
|
|
||||||
|
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
||||||
|
"""Liste d'informations (compat NotesTable) sur évaluations completes
|
||||||
|
de ce module.
|
||||||
|
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||||
|
"""
|
||||||
|
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
|
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||||
|
if not modimpl_results:
|
||||||
|
return [] # safeguard
|
||||||
|
evals_results = []
|
||||||
|
for e in modimpl.evaluations:
|
||||||
|
if modimpl_results.evaluations_completes_dict.get(e.id, False):
|
||||||
|
d = e.to_dict()
|
||||||
|
d["heure_debut"] = e.heure_debut # datetime.time
|
||||||
|
d["heure_fin"] = e.heure_fin
|
||||||
|
d["jour"] = e.jour # datetime
|
||||||
|
d["notes"] = {
|
||||||
|
etud.id: {
|
||||||
|
"etudid": etud.id,
|
||||||
|
"value": modimpl_results.evals_notes[e.id][etud.id],
|
||||||
|
}
|
||||||
|
for etud in self.etuds
|
||||||
|
}
|
||||||
|
d["etat"] = {
|
||||||
|
"evalattente": modimpl_results.evaluations_etat[e.id].nb_attente,
|
||||||
|
}
|
||||||
|
evals_results.append(d)
|
||||||
|
elif e.id not in modimpl_results.evaluations_completes_dict:
|
||||||
|
# ne devrait pas arriver ? XXX
|
||||||
|
log(
|
||||||
|
f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?"
|
||||||
|
)
|
||||||
|
return evals_results
|
||||||
|
|
||||||
|
def get_evaluations_etats(self):
|
||||||
|
"""[ {...evaluation et son etat...} ]"""
|
||||||
|
# TODO: à moderniser
|
||||||
|
from app.scodoc import sco_evaluations
|
||||||
|
|
||||||
|
if not hasattr(self, "_evaluations_etats"):
|
||||||
|
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
||||||
|
self.formsemestre.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._evaluations_etats
|
||||||
|
|
||||||
|
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||||
|
"""Liste des états des évaluations de ce module"""
|
||||||
|
# XXX TODO à moderniser: lent, recharge des données que l'on a déjà...
|
||||||
|
return [
|
||||||
|
e
|
||||||
|
for e in self.get_evaluations_etats()
|
||||||
|
if e["moduleimpl_id"] == moduleimpl_id
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_moduleimpls_attente(self):
|
||||||
|
"""Liste des modimpls du semestre ayant des notes en attente"""
|
||||||
|
return [
|
||||||
|
modimpl
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
|
if self.modimpls_results[modimpl.id].en_attente
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_mod_stats(self, moduleimpl_id: int) -> dict:
|
||||||
|
"""Stats sur les notes obtenues dans un modimpl
|
||||||
|
Vide en APC
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"moy": "-",
|
||||||
|
"max": "-",
|
||||||
|
"min": "-",
|
||||||
|
"nb_notes": "-",
|
||||||
|
"nb_missing": "-",
|
||||||
|
"nb_valid_evals": "-",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_nom_short(self, etudid):
|
||||||
|
"formatte nom d'un etud (pour table recap)"
|
||||||
|
etud = self.identdict[etudid]
|
||||||
|
return (
|
||||||
|
(etud["nom_usuel"] or etud["nom"]).upper()
|
||||||
|
+ " "
|
||||||
|
+ etud["prenom"].capitalize()[:2]
|
||||||
|
+ "."
|
||||||
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def T(self):
|
||||||
|
return self.get_table_moyennes_triees()
|
||||||
|
|
||||||
|
def get_table_moyennes_triees(self) -> list:
|
||||||
|
"""Result: liste de tuples
|
||||||
|
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
||||||
|
"""
|
||||||
|
table_moyennes = []
|
||||||
|
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
||||||
|
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
||||||
|
for etudid in etuds_inscriptions:
|
||||||
|
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||||
|
if moy_gen is False:
|
||||||
|
# pas de moyenne: démissionnaire ou def
|
||||||
|
t = (
|
||||||
|
["-"]
|
||||||
|
+ ["0.00"] * len(self.ues)
|
||||||
|
+ ["NI"] * len(self.formsemestre.modimpls_sorted)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
moy_ues = []
|
||||||
|
ue_is_cap = {}
|
||||||
|
for ue in ues:
|
||||||
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||||
|
if ue_status:
|
||||||
|
moy_ues.append(ue_status["moy"])
|
||||||
|
ue_is_cap[ue.id] = ue_status["is_capitalized"]
|
||||||
|
else:
|
||||||
|
moy_ues.append("?")
|
||||||
|
t = [moy_gen] + list(moy_ues)
|
||||||
|
# Moyennes modules:
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted:
|
||||||
|
if ue_is_cap.get(modimpl.module.ue.id, False):
|
||||||
|
val = "-c-"
|
||||||
|
else:
|
||||||
|
val = self.get_etud_mod_moy(modimpl.id, etudid)
|
||||||
|
t.append(val)
|
||||||
|
t.append(etudid)
|
||||||
|
table_moyennes.append(t)
|
||||||
|
# tri par moyennes décroissantes,
|
||||||
|
# en laissant les démissionnaires à la fin, par ordre alphabetique
|
||||||
|
etuds = [ins.etud for ins in etuds_inscriptions.values()]
|
||||||
|
etuds.sort(key=lambda e: e.sort_key)
|
||||||
|
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
|
||||||
|
table_moyennes.sort(key=self._row_key)
|
||||||
|
return table_moyennes
|
||||||
|
|
||||||
|
def _row_key(self, x):
|
||||||
|
"""clé de tri par moyennes décroissantes,
|
||||||
|
en laissant les demissionnaires à la fin, par ordre alphabetique.
|
||||||
|
(moy_gen, rang_alpha)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
moy = -float(x[0])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
moy = 1000.0
|
||||||
|
return (moy, self._rang_alpha[x[-1]])
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def identdict(self) -> dict:
|
||||||
|
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
|
||||||
|
return {
|
||||||
|
ins.etud.id: ins.etud.to_dict_scodoc7()
|
||||||
|
for ins in self.formsemestre.inscriptions
|
||||||
|
}
|
@ -8,11 +8,13 @@
|
|||||||
"""
|
"""
|
||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
|
from app import db
|
||||||
from app.comp.jury import ValidationsSemestre
|
from app.comp.jury import ValidationsSemestre
|
||||||
from app.comp.res_common import ResultatsSemestre
|
from app.comp.res_common import ResultatsSemestre
|
||||||
from app.comp.res_classic import ResultatsSemestreClassic
|
from app.comp.res_classic import ResultatsSemestreClassic
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.scodoc import sco_cache
|
||||||
|
|
||||||
|
|
||||||
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
|
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
|
||||||
@ -23,6 +25,13 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
|
|||||||
Search in local cache (g.formsemestre_result_cache)
|
Search in local cache (g.formsemestre_result_cache)
|
||||||
If not in cache, build it and cache it.
|
If not in cache, build it and cache it.
|
||||||
"""
|
"""
|
||||||
|
is_apc = formsemestre.formation.is_apc()
|
||||||
|
if is_apc and formsemestre.semestre_id == -1:
|
||||||
|
formsemestre.semestre_id = 1
|
||||||
|
db.session.add(formsemestre)
|
||||||
|
db.session.commit()
|
||||||
|
sco_cache.invalidate_formsemestre(formsemestre.id)
|
||||||
|
|
||||||
# --- Try local cache (within the same request context)
|
# --- Try local cache (within the same request context)
|
||||||
if not hasattr(g, "formsemestre_results_cache"):
|
if not hasattr(g, "formsemestre_results_cache"):
|
||||||
g.formsemestre_results_cache = {}
|
g.formsemestre_results_cache = {}
|
||||||
@ -30,11 +39,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
|
|||||||
if formsemestre.id in g.formsemestre_results_cache:
|
if formsemestre.id in g.formsemestre_results_cache:
|
||||||
return g.formsemestre_results_cache[formsemestre.id]
|
return g.formsemestre_results_cache[formsemestre.id]
|
||||||
|
|
||||||
klass = (
|
klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic
|
||||||
ResultatsSemestreBUT
|
|
||||||
if formsemestre.formation.is_apc()
|
|
||||||
else ResultatsSemestreClassic
|
|
||||||
)
|
|
||||||
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
|
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
|
||||||
return g.formsemestre_results_cache[formsemestre.id]
|
return g.formsemestre_results_cache[formsemestre.id]
|
||||||
|
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
"""Decorators for permissions, roles and ScoDoc7 Zope compatibility
|
"""Decorators for permissions, roles and ScoDoc7 Zope compatibility
|
||||||
"""
|
"""
|
||||||
import functools
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import werkzeug
|
import werkzeug
|
||||||
from werkzeug.exceptions import BadRequest
|
|
||||||
import flask
|
import flask
|
||||||
from flask import g, current_app, request
|
from flask import g, current_app, request
|
||||||
from flask import abort, url_for, redirect
|
from flask import abort, url_for, redirect
|
||||||
@ -193,7 +190,7 @@ def scodoc7func(func):
|
|||||||
# necessary for db ids and boolean values
|
# necessary for db ids and boolean values
|
||||||
try:
|
try:
|
||||||
v = int(v)
|
v = int(v)
|
||||||
except ValueError:
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
pos_arg_values.append(v)
|
pos_arg_values.append(v)
|
||||||
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
||||||
|
66
app/email.py
66
app/email.py
@ -1,30 +1,86 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from flask import current_app
|
|
||||||
|
from flask import current_app, g
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
|
|
||||||
from app import mail
|
from app import mail
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
|
||||||
def send_async_email(app, msg):
|
def send_async_email(app, msg):
|
||||||
|
"Send an email, async"
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
def send_email(
|
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,
|
If html_body is specified, build a multipart message with HTML content,
|
||||||
else send a plain text email.
|
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.body = text_body
|
||||||
msg.html = html_body
|
msg.html = html_body
|
||||||
|
if attachments:
|
||||||
|
for attachment in attachments:
|
||||||
|
msg.attach(
|
||||||
|
attachment["filename"], attachment["mimetype"], attachment["data"]
|
||||||
|
)
|
||||||
|
|
||||||
send_message(msg)
|
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(
|
Thread(
|
||||||
target=send_async_email, args=(current_app._get_current_object(), msg)
|
target=send_async_email, args=(current_app._get_current_object(), msg)
|
||||||
).start()
|
).start()
|
||||||
|
@ -4,10 +4,11 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
|
from app.models import Departement
|
||||||
|
|
||||||
bp = Blueprint("entreprises", __name__)
|
bp = Blueprint("entreprises", __name__)
|
||||||
|
|
||||||
LOGS_LEN = 10
|
LOGS_LEN = 5
|
||||||
|
|
||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
@ -21,9 +22,21 @@ def format_nom(s):
|
|||||||
|
|
||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
def get_nomcomplet(s):
|
def get_nomcomplet_by_username(s):
|
||||||
user = User.query.filter_by(user_name=s).first()
|
user = User.query.filter_by(user_name=s).first()
|
||||||
return user.get_nomcomplet()
|
return user.get_nomcomplet()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.app_template_filter()
|
||||||
|
def get_nomcomplet_by_id(id):
|
||||||
|
user = User.query.filter_by(id=id).first()
|
||||||
|
return user.get_nomcomplet()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.app_template_filter()
|
||||||
|
def get_dept_acronym(id):
|
||||||
|
dept = Departement.query.filter_by(id=id).first()
|
||||||
|
return dept.acronym
|
||||||
|
|
||||||
|
|
||||||
from app.entreprises import routes
|
from app.entreprises import routes
|
||||||
|
198
app/entreprises/app_relations_entreprises.py
Normal file
198
app/entreprises/app_relations_entreprises.py
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gestion scolarite IUT
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
import os
|
||||||
|
from config import Config
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import glob
|
||||||
|
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
from app.entreprises.models import (
|
||||||
|
Entreprise,
|
||||||
|
EntrepriseContact,
|
||||||
|
EntrepriseOffre,
|
||||||
|
EntrepriseOffreDepartement,
|
||||||
|
EntreprisePreferences,
|
||||||
|
)
|
||||||
|
|
||||||
|
from app import email
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
from app.models import Departement
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
|
||||||
|
|
||||||
|
def get_depts():
|
||||||
|
"""
|
||||||
|
Retourne une liste contenant les l'id des départements des roles de l'utilisateur courant
|
||||||
|
"""
|
||||||
|
depts = []
|
||||||
|
for role in current_user.user_roles:
|
||||||
|
dept_id = get_dept_id_by_acronym(role.dept)
|
||||||
|
if dept_id is not None:
|
||||||
|
depts.append(dept_id)
|
||||||
|
return depts
|
||||||
|
|
||||||
|
|
||||||
|
def get_dept_id_by_acronym(acronym):
|
||||||
|
"""
|
||||||
|
Retourne l'id d'un departement a l'aide de son acronym
|
||||||
|
"""
|
||||||
|
dept = Departement.query.filter_by(acronym=acronym).first()
|
||||||
|
if dept is not None:
|
||||||
|
return dept.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_offre_depts(depts, offre_depts):
|
||||||
|
"""
|
||||||
|
Retourne vrai si l'utilisateur a le droit de visibilité sur l'offre
|
||||||
|
"""
|
||||||
|
if current_user.has_permission(Permission.RelationsEntreprisesChange, None):
|
||||||
|
return True
|
||||||
|
for offre_dept in offre_depts:
|
||||||
|
if offre_dept.dept_id in depts:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_offre_files_and_depts(offre: EntrepriseOffre, depts: list):
|
||||||
|
"""
|
||||||
|
Retourne l'offre, les fichiers attachés a l'offre et les département liés
|
||||||
|
"""
|
||||||
|
offre_depts = EntrepriseOffreDepartement.query.filter_by(offre_id=offre.id).all()
|
||||||
|
if not offre_depts or check_offre_depts(depts, offre_depts):
|
||||||
|
files = []
|
||||||
|
path = os.path.join(
|
||||||
|
Config.SCODOC_VAR_DIR,
|
||||||
|
"entreprises",
|
||||||
|
f"{offre.entreprise_id}",
|
||||||
|
f"{offre.id}",
|
||||||
|
)
|
||||||
|
if os.path.exists(path):
|
||||||
|
for dir in glob.glob(
|
||||||
|
f"{path}/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
|
||||||
|
):
|
||||||
|
for _file in glob.glob(f"{dir}/*"):
|
||||||
|
file = [os.path.basename(dir), os.path.basename(_file)]
|
||||||
|
files.append(file)
|
||||||
|
return [offre, files, offre_depts]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def send_email_notifications_entreprise(
|
||||||
|
subject, entreprise: Entreprise, contact: EntrepriseContact
|
||||||
|
):
|
||||||
|
txt = [
|
||||||
|
"Une entreprise est en attente de validation",
|
||||||
|
"Entreprise:",
|
||||||
|
f"\tnom: {entreprise.nom}",
|
||||||
|
f"\tsiret: {entreprise.siret}",
|
||||||
|
f"\tadresse: {entreprise.adresse}",
|
||||||
|
f"\tcode postal: {entreprise.codepostal}",
|
||||||
|
f"\tville: {entreprise.ville}",
|
||||||
|
f"\tpays: {entreprise.pays}",
|
||||||
|
"",
|
||||||
|
"Contact:",
|
||||||
|
f"nom: {contact.nom}",
|
||||||
|
f"prenom: {contact.prenom}",
|
||||||
|
f"telephone: {contact.telephone}",
|
||||||
|
f"mail: {contact.mail}",
|
||||||
|
f"poste: {contact.poste}",
|
||||||
|
f"service: {contact.service}",
|
||||||
|
]
|
||||||
|
txt = "\n".join(txt)
|
||||||
|
email.send_email(
|
||||||
|
subject,
|
||||||
|
sco_preferences.get_preference("email_from_addr"),
|
||||||
|
[EntreprisePreferences.get_email_notifications],
|
||||||
|
txt,
|
||||||
|
)
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
|
def verif_contact_data(contact_data):
|
||||||
|
"""
|
||||||
|
Verifie les données d'une ligne Excel (contact)
|
||||||
|
contact_data[0]: nom
|
||||||
|
contact_data[1]: prenom
|
||||||
|
contact_data[2]: telephone
|
||||||
|
contact_data[3]: mail
|
||||||
|
contact_data[4]: poste
|
||||||
|
contact_data[5]: service
|
||||||
|
contact_data[6]: entreprise_id
|
||||||
|
"""
|
||||||
|
# champs obligatoires
|
||||||
|
if contact_data[0] == "" or contact_data[1] == "" or contact_data[6] == "":
|
||||||
|
return False
|
||||||
|
|
||||||
|
# entreprise_id existant
|
||||||
|
entreprise = Entreprise.query.filter_by(siret=contact_data[6]).first()
|
||||||
|
if entreprise is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# contact possède le meme nom et prénom dans la meme entreprise
|
||||||
|
contact = EntrepriseContact.query.filter_by(
|
||||||
|
nom=contact_data[0], prenom=contact_data[1], entreprise_id=entreprise.id
|
||||||
|
).first()
|
||||||
|
if contact is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if contact_data[2] == "" and contact_data[3] == "": # 1 moyen de contact
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def verif_entreprise_data(entreprise_data):
|
||||||
|
"""
|
||||||
|
Verifie les données d'une ligne Excel (entreprise)
|
||||||
|
"""
|
||||||
|
if EntreprisePreferences.get_check_siret():
|
||||||
|
for data in entreprise_data: # champs obligatoires
|
||||||
|
if data == "":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
for data in entreprise_data[1:]: # champs obligatoires
|
||||||
|
if data == "":
|
||||||
|
return False
|
||||||
|
if EntreprisePreferences.get_check_siret():
|
||||||
|
siret = entreprise_data[0].strip() # vérification sur le siret
|
||||||
|
if re.match("^\d{14}$", siret) is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
req = requests.get(
|
||||||
|
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
|
||||||
|
)
|
||||||
|
except requests.ConnectionError:
|
||||||
|
print("no internet")
|
||||||
|
if req.status_code != 200:
|
||||||
|
return False
|
||||||
|
entreprise = Entreprise.query.filter_by(siret=siret).first()
|
||||||
|
if entreprise is not None:
|
||||||
|
return False
|
||||||
|
return True
|
@ -31,74 +31,85 @@ from flask_wtf import FlaskForm
|
|||||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from wtforms import StringField, SubmitField, TextAreaField, SelectField, HiddenField
|
from wtforms import (
|
||||||
from wtforms.fields import EmailField, DateField
|
StringField,
|
||||||
from wtforms.validators import ValidationError, DataRequired, Email
|
SubmitField,
|
||||||
|
TextAreaField,
|
||||||
|
SelectField,
|
||||||
|
HiddenField,
|
||||||
|
SelectMultipleField,
|
||||||
|
DateField,
|
||||||
|
BooleanField,
|
||||||
|
)
|
||||||
|
from wtforms.validators import ValidationError, DataRequired, Email, Optional
|
||||||
|
from wtforms.widgets import ListWidget, CheckboxInput
|
||||||
|
|
||||||
from app.entreprises.models import Entreprise, EntrepriseContact
|
from app.entreprises.models import Entreprise, EntrepriseContact, EntreprisePreferences
|
||||||
from app.models import Identite
|
from app.models import Identite, Departement
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
|
|
||||||
CHAMP_REQUIS = "Ce champ est requis"
|
CHAMP_REQUIS = "Ce champ est requis"
|
||||||
|
SUBMIT_MARGE = {"style": "margin-bottom: 10px;"}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_string_field(label, required=True, render_kw=None):
|
||||||
|
if required:
|
||||||
|
return StringField(
|
||||||
|
label,
|
||||||
|
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||||
|
render_kw=render_kw,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return StringField(label, validators=[Optional()], render_kw=render_kw)
|
||||||
|
|
||||||
|
|
||||||
class EntrepriseCreationForm(FlaskForm):
|
class EntrepriseCreationForm(FlaskForm):
|
||||||
siret = StringField(
|
siret = _build_string_field(
|
||||||
"SIRET",
|
"SIRET (*)",
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
|
||||||
render_kw={"placeholder": "Numéro composé de 14 chiffres", "maxlength": "14"},
|
render_kw={"placeholder": "Numéro composé de 14 chiffres", "maxlength": "14"},
|
||||||
)
|
)
|
||||||
nom_entreprise = StringField(
|
nom_entreprise = _build_string_field("Nom de l'entreprise (*)")
|
||||||
"Nom de l'entreprise",
|
adresse = _build_string_field("Adresse de l'entreprise (*)")
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
codepostal = _build_string_field("Code postal de l'entreprise (*)")
|
||||||
)
|
ville = _build_string_field("Ville de l'entreprise (*)")
|
||||||
adresse = StringField(
|
pays = _build_string_field("Pays de l'entreprise", required=False)
|
||||||
"Adresse de l'entreprise",
|
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
|
||||||
)
|
|
||||||
codepostal = StringField(
|
|
||||||
"Code postal de l'entreprise",
|
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
|
||||||
)
|
|
||||||
ville = StringField(
|
|
||||||
"Ville de l'entreprise",
|
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
|
||||||
)
|
|
||||||
pays = StringField(
|
|
||||||
"Pays de l'entreprise",
|
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
|
||||||
render_kw={"style": "margin-bottom: 50px;"},
|
|
||||||
)
|
|
||||||
|
|
||||||
nom_contact = StringField(
|
nom_contact = _build_string_field("Nom du contact (*)")
|
||||||
"Nom du contact", validators=[DataRequired(message=CHAMP_REQUIS)]
|
prenom_contact = _build_string_field("Prénom du contact (*)")
|
||||||
|
telephone = _build_string_field("Téléphone du contact (*)", required=False)
|
||||||
|
mail = StringField(
|
||||||
|
"Mail du contact (*)",
|
||||||
|
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||||
)
|
)
|
||||||
prenom_contact = StringField(
|
poste = _build_string_field("Poste du contact", required=False)
|
||||||
"Prénom du contact",
|
service = _build_string_field("Service du contact", required=False)
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
validate = True
|
||||||
|
if not FlaskForm.validate(self):
|
||||||
|
validate = False
|
||||||
|
|
||||||
|
if not self.telephone.data and not self.mail.data:
|
||||||
|
self.telephone.errors.append(
|
||||||
|
"Saisir un moyen de contact (mail ou téléphone)"
|
||||||
)
|
)
|
||||||
telephone = StringField(
|
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
|
||||||
"Téléphone du contact",
|
validate = False
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
|
||||||
)
|
return validate
|
||||||
mail = EmailField(
|
|
||||||
"Mail du contact",
|
|
||||||
validators=[
|
|
||||||
DataRequired(message=CHAMP_REQUIS),
|
|
||||||
Email(message="Adresse e-mail invalide"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
poste = StringField("Poste du contact", validators=[])
|
|
||||||
service = StringField("Service du contact", validators=[])
|
|
||||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
|
||||||
|
|
||||||
def validate_siret(self, siret):
|
def validate_siret(self, siret):
|
||||||
|
if EntreprisePreferences.get_check_siret():
|
||||||
siret = siret.data.strip()
|
siret = siret.data.strip()
|
||||||
if re.match("^\d{14}$", siret) == None:
|
if re.match("^\d{14}$", siret) is None:
|
||||||
raise ValidationError("Format incorrect")
|
raise ValidationError("Format incorrect")
|
||||||
|
try:
|
||||||
req = requests.get(
|
req = requests.get(
|
||||||
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
|
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
|
||||||
)
|
)
|
||||||
|
except requests.ConnectionError:
|
||||||
|
print("no internet")
|
||||||
if req.status_code != 200:
|
if req.status_code != 200:
|
||||||
raise ValidationError("SIRET inexistant")
|
raise ValidationError("SIRET inexistant")
|
||||||
entreprise = Entreprise.query.filter_by(siret=siret).first()
|
entreprise = Entreprise.query.filter_by(siret=siret).first()
|
||||||
@ -110,136 +121,191 @@ class EntrepriseCreationForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class EntrepriseModificationForm(FlaskForm):
|
class EntrepriseModificationForm(FlaskForm):
|
||||||
siret = StringField("SIRET", validators=[], render_kw={"disabled": ""})
|
hidden_entreprise_siret = HiddenField()
|
||||||
nom = StringField(
|
siret = StringField("SIRET (*)")
|
||||||
"Nom de l'entreprise",
|
nom = _build_string_field("Nom de l'entreprise (*)")
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
adresse = _build_string_field("Adresse (*)")
|
||||||
)
|
codepostal = _build_string_field("Code postal (*)")
|
||||||
adresse = StringField("Adresse", validators=[DataRequired(message=CHAMP_REQUIS)])
|
ville = _build_string_field("Ville (*)")
|
||||||
codepostal = StringField(
|
pays = _build_string_field("Pays", required=False)
|
||||||
"Code postal", validators=[DataRequired(message=CHAMP_REQUIS)]
|
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||||
)
|
|
||||||
ville = StringField("Ville", validators=[DataRequired(message=CHAMP_REQUIS)])
|
def __init__(self, *args, **kwargs):
|
||||||
pays = StringField("Pays", validators=[DataRequired(message=CHAMP_REQUIS)])
|
super().__init__(*args, **kwargs)
|
||||||
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
|
self.siret.render_kw = {
|
||||||
|
"disabled": "",
|
||||||
|
"value": self.hidden_entreprise_siret.data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MultiCheckboxField(SelectMultipleField):
|
||||||
|
widget = ListWidget(prefix_label=False)
|
||||||
|
option_widget = CheckboxInput()
|
||||||
|
|
||||||
|
|
||||||
class OffreCreationForm(FlaskForm):
|
class OffreCreationForm(FlaskForm):
|
||||||
intitule = StringField("Intitulé", validators=[DataRequired(message=CHAMP_REQUIS)])
|
intitule = _build_string_field("Intitulé (*)")
|
||||||
description = TextAreaField(
|
description = TextAreaField(
|
||||||
"Description", validators=[DataRequired(message=CHAMP_REQUIS)]
|
"Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
)
|
)
|
||||||
type_offre = SelectField(
|
type_offre = SelectField(
|
||||||
"Type de l'offre",
|
"Type de l'offre (*)",
|
||||||
choices=[("Stage"), ("Alternance")],
|
choices=[("Stage"), ("Alternance")],
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||||
)
|
)
|
||||||
missions = TextAreaField(
|
missions = TextAreaField(
|
||||||
"Missions", validators=[DataRequired(message=CHAMP_REQUIS)]
|
"Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
)
|
)
|
||||||
duree = StringField("Durée", validators=[DataRequired(message=CHAMP_REQUIS)])
|
duree = _build_string_field("Durée (*)")
|
||||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
|
||||||
|
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||||
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.depts.choices = [
|
||||||
|
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class OffreModificationForm(FlaskForm):
|
class OffreModificationForm(FlaskForm):
|
||||||
intitule = StringField("Intitulé", validators=[DataRequired(message=CHAMP_REQUIS)])
|
intitule = _build_string_field("Intitulé (*)")
|
||||||
description = TextAreaField(
|
description = TextAreaField(
|
||||||
"Description", validators=[DataRequired(message=CHAMP_REQUIS)]
|
"Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
)
|
)
|
||||||
type_offre = SelectField(
|
type_offre = SelectField(
|
||||||
"Type de l'offre",
|
"Type de l'offre (*)",
|
||||||
choices=[("Stage"), ("Alternance")],
|
choices=[("Stage"), ("Alternance")],
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||||
)
|
)
|
||||||
missions = TextAreaField(
|
missions = TextAreaField(
|
||||||
"Missions", validators=[DataRequired(message=CHAMP_REQUIS)]
|
"Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
)
|
)
|
||||||
duree = StringField("Durée", validators=[DataRequired(message=CHAMP_REQUIS)])
|
duree = _build_string_field("Durée (*)")
|
||||||
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
|
depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
|
||||||
|
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||||
|
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.depts.choices = [
|
||||||
|
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ContactCreationForm(FlaskForm):
|
class ContactCreationForm(FlaskForm):
|
||||||
hidden_entreprise_id = HiddenField()
|
hidden_entreprise_id = HiddenField()
|
||||||
nom = StringField("Nom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
nom = _build_string_field("Nom (*)")
|
||||||
prenom = StringField("Prénom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
prenom = _build_string_field("Prénom (*)")
|
||||||
telephone = StringField(
|
telephone = _build_string_field("Téléphone (*)", required=False)
|
||||||
"Téléphone", validators=[DataRequired(message=CHAMP_REQUIS)]
|
mail = StringField(
|
||||||
|
"Mail (*)",
|
||||||
|
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||||
)
|
)
|
||||||
mail = EmailField(
|
poste = _build_string_field("Poste", required=False)
|
||||||
"Mail",
|
service = _build_string_field("Service", required=False)
|
||||||
validators=[
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
DataRequired(message=CHAMP_REQUIS),
|
|
||||||
Email(message="Adresse e-mail invalide"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
poste = StringField("Poste", validators=[])
|
|
||||||
service = StringField("Service", validators=[])
|
|
||||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
rv = FlaskForm.validate(self)
|
validate = True
|
||||||
if not rv:
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
contact = EntrepriseContact.query.filter_by(
|
contact = EntrepriseContact.query.filter_by(
|
||||||
entreprise_id=self.hidden_entreprise_id.data,
|
entreprise_id=self.hidden_entreprise_id.data,
|
||||||
nom=self.nom.data,
|
nom=self.nom.data,
|
||||||
prenom=self.prenom.data,
|
prenom=self.prenom.data,
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if contact is not None:
|
if contact is not None:
|
||||||
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
|
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
|
||||||
self.prenom.errors.append("")
|
self.prenom.errors.append("")
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
return True
|
if not self.telephone.data and not self.mail.data:
|
||||||
|
self.telephone.errors.append(
|
||||||
|
"Saisir un moyen de contact (mail ou téléphone)"
|
||||||
|
)
|
||||||
|
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
|
||||||
|
validate = False
|
||||||
|
|
||||||
|
return validate
|
||||||
|
|
||||||
|
|
||||||
class ContactModificationForm(FlaskForm):
|
class ContactModificationForm(FlaskForm):
|
||||||
nom = StringField("Nom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
hidden_contact_id = HiddenField()
|
||||||
prenom = StringField("Prénom", validators=[DataRequired(message=CHAMP_REQUIS)])
|
hidden_entreprise_id = HiddenField()
|
||||||
telephone = StringField(
|
nom = _build_string_field("Nom (*)")
|
||||||
"Téléphone", validators=[DataRequired(message=CHAMP_REQUIS)]
|
prenom = _build_string_field("Prénom (*)")
|
||||||
|
telephone = _build_string_field("Téléphone (*)", required=False)
|
||||||
|
mail = StringField(
|
||||||
|
"Mail (*)",
|
||||||
|
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||||
)
|
)
|
||||||
mail = EmailField(
|
poste = _build_string_field("Poste", required=False)
|
||||||
"Mail",
|
service = _build_string_field("Service", required=False)
|
||||||
validators=[
|
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||||
DataRequired(message=CHAMP_REQUIS),
|
|
||||||
Email(message="Adresse e-mail invalide"),
|
def validate(self):
|
||||||
],
|
validate = True
|
||||||
|
if not FlaskForm.validate(self):
|
||||||
|
validate = False
|
||||||
|
|
||||||
|
contact = EntrepriseContact.query.filter(
|
||||||
|
EntrepriseContact.id != self.hidden_contact_id.data,
|
||||||
|
EntrepriseContact.entreprise_id == self.hidden_entreprise_id.data,
|
||||||
|
EntrepriseContact.nom == self.nom.data,
|
||||||
|
EntrepriseContact.prenom == self.prenom.data,
|
||||||
|
).first()
|
||||||
|
if contact is not None:
|
||||||
|
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
|
||||||
|
self.prenom.errors.append("")
|
||||||
|
validate = False
|
||||||
|
|
||||||
|
if not self.telephone.data and not self.mail.data:
|
||||||
|
self.telephone.errors.append(
|
||||||
|
"Saisir un moyen de contact (mail ou téléphone)"
|
||||||
)
|
)
|
||||||
poste = StringField("Poste", validators=[])
|
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
|
||||||
service = StringField("Service", validators=[])
|
validate = False
|
||||||
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
|
|
||||||
|
return validate
|
||||||
|
|
||||||
|
|
||||||
class HistoriqueCreationForm(FlaskForm):
|
class HistoriqueCreationForm(FlaskForm):
|
||||||
etudiant = StringField(
|
etudiant = _build_string_field(
|
||||||
"Étudiant",
|
"Étudiant (*)",
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||||
render_kw={"placeholder": "Tapez le nom de l'étudiant puis selectionnez"},
|
|
||||||
)
|
)
|
||||||
type_offre = SelectField(
|
type_offre = SelectField(
|
||||||
"Type de l'offre",
|
"Type de l'offre (*)",
|
||||||
choices=[("Stage"), ("Alternance")],
|
choices=[("Stage"), ("Alternance")],
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||||
)
|
)
|
||||||
date_debut = DateField(
|
date_debut = DateField(
|
||||||
"Date début", validators=[DataRequired(message=CHAMP_REQUIS)]
|
"Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
)
|
)
|
||||||
date_fin = DateField("Date fin", validators=[DataRequired(message=CHAMP_REQUIS)])
|
date_fin = DateField(
|
||||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
|
)
|
||||||
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
rv = FlaskForm.validate(self)
|
validate = True
|
||||||
if not rv:
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
if self.date_debut.data > self.date_fin.data:
|
if (
|
||||||
|
self.date_debut.data
|
||||||
|
and self.date_fin.data
|
||||||
|
and self.date_debut.data > self.date_fin.data
|
||||||
|
):
|
||||||
self.date_debut.errors.append("Les dates sont incompatibles")
|
self.date_debut.errors.append("Les dates sont incompatibles")
|
||||||
self.date_fin.errors.append("Les dates sont incompatibles")
|
self.date_fin.errors.append("Les dates sont incompatibles")
|
||||||
return False
|
validate = False
|
||||||
return True
|
|
||||||
|
return validate
|
||||||
|
|
||||||
def validate_etudiant(self, etudiant):
|
def validate_etudiant(self, etudiant):
|
||||||
etudiant_data = etudiant.data.upper().strip()
|
etudiant_data = etudiant.data.upper().strip()
|
||||||
@ -254,11 +320,11 @@ class HistoriqueCreationForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class EnvoiOffreForm(FlaskForm):
|
class EnvoiOffreForm(FlaskForm):
|
||||||
responsable = StringField(
|
responsable = _build_string_field(
|
||||||
"Responsable de formation",
|
"Responsable de formation (*)",
|
||||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
render_kw={"placeholder": "Tapez le nom du responsable de formation"},
|
||||||
)
|
)
|
||||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate_responsable(self, responsable):
|
def validate_responsable(self, responsable):
|
||||||
responsable_data = responsable.data.upper().strip()
|
responsable_data = responsable.data.upper().strip()
|
||||||
@ -276,14 +342,38 @@ class EnvoiOffreForm(FlaskForm):
|
|||||||
|
|
||||||
class AjoutFichierForm(FlaskForm):
|
class AjoutFichierForm(FlaskForm):
|
||||||
fichier = FileField(
|
fichier = FileField(
|
||||||
"Fichier",
|
"Fichier (*)",
|
||||||
validators=[
|
validators=[
|
||||||
FileRequired(message=CHAMP_REQUIS),
|
FileRequired(message=CHAMP_REQUIS),
|
||||||
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
|
submit = SubmitField("Ajouter", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
|
||||||
class SuppressionConfirmationForm(FlaskForm):
|
class SuppressionConfirmationForm(FlaskForm):
|
||||||
submit = SubmitField("Supprimer", render_kw={"style": "margin-bottom: 10px;"})
|
submit = SubmitField("Supprimer", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationConfirmationForm(FlaskForm):
|
||||||
|
submit = SubmitField("Valider", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportForm(FlaskForm):
|
||||||
|
fichier = FileField(
|
||||||
|
"Fichier (*)",
|
||||||
|
validators=[
|
||||||
|
FileRequired(message=CHAMP_REQUIS),
|
||||||
|
FileAllowed(["xlsx"], "Fichier .xlsx uniquement"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
submit = SubmitField("Importer", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
|
||||||
|
class PreferencesForm(FlaskForm):
|
||||||
|
mail_entreprise = StringField(
|
||||||
|
"Mail notifications",
|
||||||
|
validators=[Optional(), Email(message="Adresse e-mail invalide")],
|
||||||
|
)
|
||||||
|
check_siret = BooleanField("Vérification SIRET")
|
||||||
|
submit = SubmitField("Valider", render_kw=SUBMIT_MARGE)
|
||||||
|
@ -2,14 +2,15 @@ from app import db
|
|||||||
|
|
||||||
|
|
||||||
class Entreprise(db.Model):
|
class Entreprise(db.Model):
|
||||||
__tablename__ = "entreprises"
|
__tablename__ = "are_entreprises"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
siret = db.Column(db.Text)
|
siret = db.Column(db.Text)
|
||||||
nom = db.Column(db.Text)
|
nom = db.Column(db.Text)
|
||||||
adresse = db.Column(db.Text)
|
adresse = db.Column(db.Text)
|
||||||
codepostal = db.Column(db.Text)
|
codepostal = db.Column(db.Text)
|
||||||
ville = db.Column(db.Text)
|
ville = db.Column(db.Text)
|
||||||
pays = db.Column(db.Text)
|
pays = db.Column(db.Text, default="FRANCE")
|
||||||
|
visible = db.Column(db.Boolean, default=False)
|
||||||
contacts = db.relationship(
|
contacts = db.relationship(
|
||||||
"EntrepriseContact",
|
"EntrepriseContact",
|
||||||
backref="entreprise",
|
backref="entreprise",
|
||||||
@ -26,19 +27,19 @@ class Entreprise(db.Model):
|
|||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
"siret": self.siret,
|
"siret": self.siret,
|
||||||
"nom": self.nom,
|
"nom_entreprise": self.nom,
|
||||||
"adresse": self.adresse,
|
"adresse": self.adresse,
|
||||||
"codepostal": self.codepostal,
|
"code_postal": self.codepostal,
|
||||||
"ville": self.ville,
|
"ville": self.ville,
|
||||||
"pays": self.pays,
|
"pays": self.pays,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EntrepriseContact(db.Model):
|
class EntrepriseContact(db.Model):
|
||||||
__tablename__ = "entreprise_contact"
|
__tablename__ = "are_contacts"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
entreprise_id = db.Column(
|
entreprise_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
|
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||||
)
|
)
|
||||||
nom = db.Column(db.Text)
|
nom = db.Column(db.Text)
|
||||||
prenom = db.Column(db.Text)
|
prenom = db.Column(db.Text)
|
||||||
@ -48,6 +49,7 @@ class EntrepriseContact(db.Model):
|
|||||||
service = db.Column(db.Text)
|
service = db.Column(db.Text)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
entreprise = Entreprise.query.filter_by(id=self.entreprise_id).first()
|
||||||
return {
|
return {
|
||||||
"nom": self.nom,
|
"nom": self.nom,
|
||||||
"prenom": self.prenom,
|
"prenom": self.prenom,
|
||||||
@ -55,31 +57,15 @@ class EntrepriseContact(db.Model):
|
|||||||
"mail": self.mail,
|
"mail": self.mail,
|
||||||
"poste": self.poste,
|
"poste": self.poste,
|
||||||
"service": self.service,
|
"service": self.service,
|
||||||
}
|
"entreprise_siret": entreprise.siret,
|
||||||
|
|
||||||
def to_dict_export(self):
|
|
||||||
entreprise = Entreprise.query.get(self.entreprise_id)
|
|
||||||
return {
|
|
||||||
"nom": self.nom,
|
|
||||||
"prenom": self.prenom,
|
|
||||||
"telephone": self.telephone,
|
|
||||||
"mail": self.mail,
|
|
||||||
"poste": self.poste,
|
|
||||||
"service": self.service,
|
|
||||||
"siret": entreprise.siret,
|
|
||||||
"nom_entreprise": entreprise.nom,
|
|
||||||
"adresse_entreprise": entreprise.adresse,
|
|
||||||
"codepostal": entreprise.codepostal,
|
|
||||||
"ville": entreprise.ville,
|
|
||||||
"pays": entreprise.pays,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EntrepriseOffre(db.Model):
|
class EntrepriseOffre(db.Model):
|
||||||
__tablename__ = "entreprise_offre"
|
__tablename__ = "are_offres"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
entreprise_id = db.Column(
|
entreprise_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
|
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||||
)
|
)
|
||||||
date_ajout = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date_ajout = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
intitule = db.Column(db.Text)
|
intitule = db.Column(db.Text)
|
||||||
@ -87,6 +73,8 @@ class EntrepriseOffre(db.Model):
|
|||||||
type_offre = db.Column(db.Text)
|
type_offre = db.Column(db.Text)
|
||||||
missions = db.Column(db.Text)
|
missions = db.Column(db.Text)
|
||||||
duree = db.Column(db.Text)
|
duree = db.Column(db.Text)
|
||||||
|
expiration_date = db.Column(db.Date)
|
||||||
|
expired = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
@ -99,7 +87,7 @@ class EntrepriseOffre(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class EntrepriseLog(db.Model):
|
class EntrepriseLog(db.Model):
|
||||||
__tablename__ = "entreprise_log"
|
__tablename__ = "are_logs"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
authenticated_user = db.Column(db.Text)
|
authenticated_user = db.Column(db.Text)
|
||||||
@ -108,9 +96,11 @@ class EntrepriseLog(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class EntrepriseEtudiant(db.Model):
|
class EntrepriseEtudiant(db.Model):
|
||||||
__tablename__ = "entreprise_etudiant"
|
__tablename__ = "are_etudiants"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id"))
|
entreprise_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||||
|
)
|
||||||
etudid = db.Column(db.Integer)
|
etudid = db.Column(db.Integer)
|
||||||
type_offre = db.Column(db.Text)
|
type_offre = db.Column(db.Text)
|
||||||
date_debut = db.Column(db.Date)
|
date_debut = db.Column(db.Date)
|
||||||
@ -120,18 +110,78 @@ class EntrepriseEtudiant(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class EntrepriseEnvoiOffre(db.Model):
|
class EntrepriseEnvoiOffre(db.Model):
|
||||||
__tablename__ = "entreprise_envoi_offre"
|
__tablename__ = "are_envoi_offre"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
sender_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||||
receiver_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
receiver_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||||
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
|
offre_id = db.Column(db.Integer, db.ForeignKey("are_offres.id", ondelete="cascade"))
|
||||||
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
|
||||||
class EntrepriseEnvoiOffreEtudiant(db.Model):
|
class EntrepriseEnvoiOffreEtudiant(db.Model):
|
||||||
__tablename__ = "entreprise_envoi_offre_etudiant"
|
__tablename__ = "are_envoi_offre_etudiant"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
sender_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||||
receiver_id = db.Column(db.Integer, db.ForeignKey("identite.id"))
|
receiver_id = db.Column(
|
||||||
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
|
db.Integer, db.ForeignKey("identite.id", ondelete="cascade")
|
||||||
|
)
|
||||||
|
offre_id = db.Column(db.Integer, db.ForeignKey("are_offres.id", ondelete="cascade"))
|
||||||
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class EntrepriseOffreDepartement(db.Model):
|
||||||
|
__tablename__ = "are_offre_departement"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
offre_id = db.Column(db.Integer, db.ForeignKey("are_offres.id", ondelete="cascade"))
|
||||||
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id", ondelete="cascade"))
|
||||||
|
|
||||||
|
|
||||||
|
class EntreprisePreferences(db.Model):
|
||||||
|
__tablename__ = "are_preferences"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.Text)
|
||||||
|
value = db.Column(db.Text)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_email_notifications(cls):
|
||||||
|
mail = EntreprisePreferences.query.filter_by(
|
||||||
|
name="mail_notifications_entreprise"
|
||||||
|
).first()
|
||||||
|
if mail is None:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return mail.value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_email_notifications(cls, mail: str):
|
||||||
|
if mail != cls.get_email_notifications():
|
||||||
|
m = EntreprisePreferences.query.filter_by(
|
||||||
|
name="mail_notifications_entreprise"
|
||||||
|
).first()
|
||||||
|
if m is None:
|
||||||
|
prefs = EntreprisePreferences(
|
||||||
|
name="mail_notifications_entreprise",
|
||||||
|
value=mail,
|
||||||
|
)
|
||||||
|
db.session.add(prefs)
|
||||||
|
else:
|
||||||
|
m.value = mail
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_check_siret(cls):
|
||||||
|
check_siret = EntreprisePreferences.query.filter_by(name="check_siret").first()
|
||||||
|
if check_siret is None:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return int(check_siret.value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_check_siret(cls, check_siret: int):
|
||||||
|
cs = EntreprisePreferences.query.filter_by(name="check_siret").first()
|
||||||
|
if cs is None:
|
||||||
|
prefs = EntreprisePreferences(name="check_siret", value=check_siret)
|
||||||
|
db.session.add(prefs)
|
||||||
|
else:
|
||||||
|
cs.value = check_siret
|
||||||
|
db.session.commit()
|
||||||
|
File diff suppressed because it is too large
Load Diff
1
app/forms/__init__.py
Normal file
1
app/forms/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# empty but required for pylint
|
1
app/forms/main/__init__.py
Normal file
1
app/forms/main/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# empty but required for pylint
|
@ -29,17 +29,13 @@
|
|||||||
Formulaires configuration Exports Apogée (codes)
|
Formulaires configuration Exports Apogée (codes)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import flash, url_for, redirect, render_template
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import SubmitField, validators
|
from wtforms import SubmitField, validators
|
||||||
from wtforms.fields.simple import StringField
|
from wtforms.fields.simple import StringField
|
||||||
|
|
||||||
from app import models
|
|
||||||
from app.models import ScoDocSiteConfig
|
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
|
|
||||||
|
|
||||||
def _build_code_field(code):
|
def _build_code_field(code):
|
||||||
@ -61,6 +57,7 @@ def _build_code_field(code):
|
|||||||
|
|
||||||
|
|
||||||
class CodesDecisionsForm(FlaskForm):
|
class CodesDecisionsForm(FlaskForm):
|
||||||
|
"Formulaire code décisions Apogée"
|
||||||
ADC = _build_code_field("ADC")
|
ADC = _build_code_field("ADC")
|
||||||
ADJ = _build_code_field("ADJ")
|
ADJ = _build_code_field("ADJ")
|
||||||
ADM = _build_code_field("ADM")
|
ADM = _build_code_field("ADM")
|
||||||
|
@ -30,17 +30,15 @@ Formulaires configuration logos
|
|||||||
|
|
||||||
Contrib @jmp, dec 21
|
Contrib @jmp, dec 21
|
||||||
"""
|
"""
|
||||||
import re
|
|
||||||
|
|
||||||
from flask import flash, url_for, redirect, render_template
|
from flask import flash, url_for, redirect, render_template
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileAllowed
|
from flask_wtf.file import FileField, FileAllowed
|
||||||
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
|
from wtforms import SubmitField, FormField, validators, FieldList
|
||||||
|
from wtforms import ValidationError
|
||||||
from wtforms.fields.simple import StringField, HiddenField
|
from wtforms.fields.simple import StringField, HiddenField
|
||||||
|
|
||||||
from app import AccessDenied
|
|
||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
from app.models import ScoDocSiteConfig
|
|
||||||
from app.scodoc import sco_logos, html_sco_header
|
from app.scodoc import sco_logos, html_sco_header
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_config_actions import (
|
from app.scodoc.sco_config_actions import (
|
||||||
@ -49,10 +47,9 @@ from app.scodoc.sco_config_actions import (
|
|||||||
LogoInsert,
|
LogoInsert,
|
||||||
)
|
)
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
|
|
||||||
from app.scodoc.sco_logos import find_logo
|
from app.scodoc.sco_logos import find_logo
|
||||||
|
|
||||||
|
|
||||||
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
|
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
|
||||||
|
|
||||||
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||||
@ -111,6 +108,15 @@ def dept_key_to_id(dept_key):
|
|||||||
return dept_key
|
return dept_key
|
||||||
|
|
||||||
|
|
||||||
|
def logo_name_validator(message=None):
|
||||||
|
def validate_logo_name(form, field):
|
||||||
|
name = field.data if field.data else ""
|
||||||
|
if not scu.is_valid_filename(name):
|
||||||
|
raise ValidationError(message)
|
||||||
|
|
||||||
|
return validate_logo_name
|
||||||
|
|
||||||
|
|
||||||
class AddLogoForm(FlaskForm):
|
class AddLogoForm(FlaskForm):
|
||||||
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
|
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
|
||||||
|
|
||||||
@ -118,11 +124,7 @@ class AddLogoForm(FlaskForm):
|
|||||||
name = StringField(
|
name = StringField(
|
||||||
label="Nom",
|
label="Nom",
|
||||||
validators=[
|
validators=[
|
||||||
validators.regexp(
|
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
|
||||||
r"^[a-zA-Z0-9-_]*$",
|
|
||||||
re.IGNORECASE,
|
|
||||||
"Ne doit comporter que lettres, chiffres, _ ou -",
|
|
||||||
),
|
|
||||||
validators.Length(
|
validators.Length(
|
||||||
max=20, message="Un nom ne doit pas dépasser 20 caractères"
|
max=20, message="Un nom ne doit pas dépasser 20 caractères"
|
||||||
),
|
),
|
||||||
@ -373,11 +375,11 @@ def config_logos():
|
|||||||
if action:
|
if action:
|
||||||
action.execute()
|
action.execute()
|
||||||
flash(action.message)
|
flash(action.message)
|
||||||
return redirect(
|
return redirect(url_for("scodoc.configure_logos"))
|
||||||
url_for(
|
else:
|
||||||
"scodoc.configure_logos",
|
if not form.validate():
|
||||||
)
|
scu.flash_errors(form)
|
||||||
)
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"config_logos.html",
|
"config_logos.html",
|
||||||
scodoc_dept=None,
|
scodoc_dept=None,
|
||||||
|
@ -31,13 +31,14 @@ Formulaires configuration Exports Apogée (codes)
|
|||||||
|
|
||||||
from flask import flash, url_for, redirect, request, render_template
|
from flask import flash, url_for, redirect, request, render_template
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import SelectField, SubmitField
|
from wtforms import BooleanField, SelectField, SubmitField
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app.models import ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class ScoDocConfigurationForm(FlaskForm):
|
class BonusConfigurationForm(FlaskForm):
|
||||||
"Panneau de configuration des logos"
|
"Panneau de configuration des logos"
|
||||||
bonus_sport_func_name = SelectField(
|
bonus_sport_func_name = SelectField(
|
||||||
label="Fonction de calcul des bonus sport&culture",
|
label="Fonction de calcul des bonus sport&culture",
|
||||||
@ -46,31 +47,57 @@ class ScoDocConfigurationForm(FlaskForm):
|
|||||||
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
|
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
submit = SubmitField("Valider")
|
submit_bonus = SubmitField("Valider")
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
|
||||||
|
class ScoDocConfigurationForm(FlaskForm):
|
||||||
|
"Panneau de configuration avancée"
|
||||||
|
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||||
|
submit_scodoc = SubmitField("Valider")
|
||||||
|
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
|
||||||
def configuration():
|
def configuration():
|
||||||
"Page de configuration principale"
|
"Page de configuration principale"
|
||||||
# nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue
|
# nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue
|
||||||
form = ScoDocConfigurationForm(
|
form_bonus = BonusConfigurationForm(
|
||||||
data={
|
data={
|
||||||
"bonus_sport_func_name": ScoDocSiteConfig.get_bonus_sport_class_name(),
|
"bonus_sport_func_name": ScoDocSiteConfig.get_bonus_sport_class_name(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if request.method == "POST" and form.cancel.data: # cancel button
|
form_scodoc = ScoDocConfigurationForm(
|
||||||
|
data={"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled()}
|
||||||
|
)
|
||||||
|
if request.method == "POST" and (
|
||||||
|
form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data
|
||||||
|
): # cancel button
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
if form.validate_on_submit():
|
if form_bonus.submit_bonus.data and form_bonus.validate():
|
||||||
if (
|
if (
|
||||||
form.data["bonus_sport_func_name"]
|
form_bonus.data["bonus_sport_func_name"]
|
||||||
!= ScoDocSiteConfig.get_bonus_sport_class_name()
|
!= ScoDocSiteConfig.get_bonus_sport_class_name()
|
||||||
):
|
):
|
||||||
ScoDocSiteConfig.set_bonus_sport_class(form.data["bonus_sport_func_name"])
|
ScoDocSiteConfig.set_bonus_sport_class(
|
||||||
|
form_bonus.data["bonus_sport_func_name"]
|
||||||
|
)
|
||||||
app.clear_scodoc_cache()
|
app.clear_scodoc_cache()
|
||||||
flash(f"Fonction bonus sport&culture configurée.")
|
flash(f"Fonction bonus sport&culture configurée.")
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
|
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
||||||
|
if ScoDocSiteConfig.enable_entreprises(
|
||||||
|
enabled=form_scodoc.data["enable_entreprises"]
|
||||||
|
):
|
||||||
|
flash(
|
||||||
|
"Module entreprise "
|
||||||
|
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||||
|
)
|
||||||
|
return redirect(url_for("scodoc.index"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"configuration.html",
|
"configuration.html",
|
||||||
form=form,
|
form_bonus=form_bonus,
|
||||||
|
form_scodoc=form_scodoc,
|
||||||
|
scu=scu,
|
||||||
|
title="Configuration",
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,6 @@
|
|||||||
Formulaires création département
|
Formulaires création département
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import flash, url_for, redirect, render_template
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import SubmitField, validators
|
from wtforms import SubmitField, validators
|
||||||
from wtforms.fields.simple import StringField, BooleanField
|
from wtforms.fields.simple import StringField, BooleanField
|
||||||
|
@ -81,6 +81,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
)
|
)
|
||||||
formations = db.relationship("Formation", backref="referentiel_competence")
|
formations = db.relationship("Formation", backref="referentiel_competence")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ApcReferentielCompetences {self.id} {self.specialite}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""Représentation complète du ref. de comp.
|
"""Représentation complète du ref. de comp.
|
||||||
comme un dict.
|
comme un dict.
|
||||||
@ -110,7 +113,8 @@ class ApcCompetence(db.Model, XMLModel):
|
|||||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||||
)
|
)
|
||||||
# les compétences dans Orébut sont identifiées par leur id unique
|
# les compétences dans Orébut sont identifiées par leur id unique
|
||||||
id_orebut = db.Column(db.Text(), nullable=True, index=True, unique=True)
|
# (mais id_orebut n'est pas unique car le même ref. pourra être chargé dans plusieurs depts)
|
||||||
|
id_orebut = db.Column(db.Text(), nullable=True, index=True)
|
||||||
titre = db.Column(db.Text(), nullable=False, index=True)
|
titre = db.Column(db.Text(), nullable=False, index=True)
|
||||||
titre_long = db.Column(db.Text())
|
titre_long = db.Column(db.Text())
|
||||||
couleur = db.Column(db.Text())
|
couleur = db.Column(db.Text())
|
||||||
@ -139,6 +143,9 @@ class ApcCompetence(db.Model, XMLModel):
|
|||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ApcCompetence {self.id} {self.titre}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
"id_orebut": self.id_orebut,
|
"id_orebut": self.id_orebut,
|
||||||
|
@ -69,6 +69,7 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
"INSTITUTION_ADDRESS": str,
|
"INSTITUTION_ADDRESS": str,
|
||||||
"INSTITUTION_CITY": str,
|
"INSTITUTION_CITY": str,
|
||||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||||
|
"enable_entreprises": bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
@ -207,3 +208,27 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
cfg.value = code_apo
|
cfg.value = code_apo
|
||||||
db.session.add(cfg)
|
db.session.add(cfg)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_entreprises_enabled(cls) -> bool:
|
||||||
|
"""True si on doit activer le module entreprise"""
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
|
if (cfg is None) or not cfg.value:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enable_entreprises(cls, enabled=True) -> bool:
|
||||||
|
"""Active (ou déactive) le module entreprises. True si changement."""
|
||||||
|
if enabled != ScoDocSiteConfig.is_entreprises_enabled():
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
|
if cfg is None:
|
||||||
|
cfg = ScoDocSiteConfig(
|
||||||
|
name="enable_entreprises", value="on" if enabled else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cfg.value = "on" if enabled else ""
|
||||||
|
db.session.add(cfg)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
@ -55,6 +55,9 @@ def create_dept(acronym: str, visible=True) -> Departement:
|
|||||||
"Create new departement"
|
"Create new departement"
|
||||||
from app.models import ScoPreference
|
from app.models import ScoPreference
|
||||||
|
|
||||||
|
existing = Departement.query.filter_by(acronym=acronym).count()
|
||||||
|
if existing:
|
||||||
|
raise ValueError(f"acronyme {acronym} déjà existant")
|
||||||
departement = Departement(acronym=acronym, visible=visible)
|
departement = Departement(acronym=acronym, visible=visible)
|
||||||
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
|
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
|
||||||
db.session.add(p1)
|
db.session.add(p1)
|
||||||
|
@ -4,16 +4,19 @@
|
|||||||
et données rattachées (adresses, annotations, ...)
|
et données rattachées (adresses, annotations, ...)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from flask import abort, url_for
|
from flask import abort, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
from sqlalchemy import desc, text
|
||||||
|
|
||||||
from app import db
|
from app import db, log
|
||||||
from app import models
|
from app import models
|
||||||
|
|
||||||
from app.scodoc import notesdb as ndb
|
from app.scodoc import notesdb as ndb
|
||||||
from app.scodoc.sco_bac import Baccalaureat
|
from app.scodoc.sco_bac import Baccalaureat
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class Identite(db.Model):
|
class Identite(db.Model):
|
||||||
@ -74,6 +77,18 @@ class Identite(db.Model):
|
|||||||
"""
|
"""
|
||||||
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
|
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
|
||||||
|
|
||||||
|
def sex_nom(self, no_accents=False) -> str:
|
||||||
|
"'M. DUPONTÉ', ou si no_accents, 'M. DUPONTE'"
|
||||||
|
s = f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()}"
|
||||||
|
if no_accents:
|
||||||
|
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:
|
def nom_disp(self) -> str:
|
||||||
"Nom à afficher"
|
"Nom à afficher"
|
||||||
if self.nom_usuel:
|
if self.nom_usuel:
|
||||||
@ -108,14 +123,22 @@ class Identite(db.Model):
|
|||||||
r.append("-".join([x.lower().capitalize() for x in fields]))
|
r.append("-".join([x.lower().capitalize() for x in fields]))
|
||||||
return " ".join(r)
|
return " ".join(r)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nom_short(self):
|
||||||
|
"Nom et début du prénom pour table recap: 'DUPONT Pi.'"
|
||||||
|
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def sort_key(self) -> tuple:
|
def sort_key(self) -> tuple:
|
||||||
"clé pour tris par ordre alphabétique"
|
"clé pour tris par ordre alphabétique"
|
||||||
return (self.nom_usuel or self.nom).lower(), self.prenom.lower()
|
return (
|
||||||
|
scu.suppress_accents(self.nom_usuel or self.nom or "").lower(),
|
||||||
|
scu.suppress_accents(self.prenom or "").lower(),
|
||||||
|
)
|
||||||
|
|
||||||
def get_first_email(self, field="email") -> str:
|
def get_first_email(self, field="email") -> str:
|
||||||
"Le mail associé à la première adrese de l'étudiant, ou None"
|
"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):
|
def to_dict_scodoc7(self):
|
||||||
"""Représentation dictionnaire,
|
"""Représentation dictionnaire,
|
||||||
@ -126,30 +149,41 @@ class Identite(db.Model):
|
|||||||
# ScoDoc7 output_formators: (backward compat)
|
# ScoDoc7 output_formators: (backward compat)
|
||||||
e["etudid"] = self.id
|
e["etudid"] = self.id
|
||||||
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
||||||
|
e["ne"] = self.e
|
||||||
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
||||||
|
|
||||||
def to_dict_bul(self, include_urls=True):
|
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
|
from app.scodoc import sco_photos
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
"civilite": self.civilite,
|
"civilite": self.civilite,
|
||||||
"code_ine": self.code_ine,
|
"code_ine": self.code_ine or "",
|
||||||
"code_nip": self.code_nip,
|
"code_nip": self.code_nip or "",
|
||||||
"date_naissance": self.date_naissance.isoformat()
|
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||||
if self.date_naissance
|
if self.date_naissance
|
||||||
else None,
|
else "",
|
||||||
"email": self.get_first_email(),
|
"email": self.get_first_email() or "",
|
||||||
"emailperso": self.get_first_email("emailperso"),
|
"emailperso": self.get_first_email("emailperso"),
|
||||||
"etudid": self.id,
|
"etudid": self.id,
|
||||||
"nom": self.nom_disp(),
|
"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:
|
if include_urls:
|
||||||
d["fiche_url"] = url_for(
|
d["fiche_url"] = url_for(
|
||||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
"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
|
return d
|
||||||
|
|
||||||
def inscription_courante(self):
|
def inscription_courante(self):
|
||||||
@ -163,6 +197,23 @@ class Identite(db.Model):
|
|||||||
]
|
]
|
||||||
return r[0] if r else None
|
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):
|
def inscription_courante_date(self, date_debut, date_fin):
|
||||||
"""La première inscription à un formsemestre incluant la
|
"""La première inscription à un formsemestre incluant la
|
||||||
période [date_debut, date_fin]
|
période [date_debut, date_fin]
|
||||||
@ -174,8 +225,8 @@ class Identite(db.Model):
|
|||||||
]
|
]
|
||||||
return r[0] if r else None
|
return r[0] if r else None
|
||||||
|
|
||||||
def etat_inscription(self, formsemestre_id):
|
def inscription_etat(self, formsemestre_id):
|
||||||
"""etat de l'inscription de cet étudiant au semestre:
|
"""État de l'inscription de cet étudiant au semestre:
|
||||||
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
||||||
"""
|
"""
|
||||||
# voir si ce n'est pas trop lent:
|
# voir si ce n'est pas trop lent:
|
||||||
@ -186,6 +237,110 @@ class Identite(db.Model):
|
|||||||
return ins.etat
|
return ins.etat
|
||||||
return False
|
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(
|
def make_etud_args(
|
||||||
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
||||||
@ -209,12 +364,15 @@ def make_etud_args(
|
|||||||
vals = request.args
|
vals = request.args
|
||||||
else:
|
else:
|
||||||
vals = {}
|
vals = {}
|
||||||
|
try:
|
||||||
if "etudid" in vals:
|
if "etudid" in vals:
|
||||||
args = {"etudid": int(vals["etudid"])}
|
args = {"etudid": int(vals["etudid"])}
|
||||||
elif "code_nip" in vals:
|
elif "code_nip" in vals:
|
||||||
args = {"code_nip": str(vals["code_nip"])}
|
args = {"code_nip": str(vals["code_nip"])}
|
||||||
elif "code_ine" in vals:
|
elif "code_ine" in vals:
|
||||||
args = {"code_ine": str(vals["code_ine"])}
|
args = {"code_ine": str(vals["code_ine"])}
|
||||||
|
except ValueError:
|
||||||
|
args = {}
|
||||||
if not args:
|
if not args:
|
||||||
if abort_404:
|
if abort_404:
|
||||||
abort(404, "pas d'étudiant sélectionné")
|
abort(404, "pas d'étudiant sélectionné")
|
||||||
@ -250,6 +408,14 @@ class Adresse(db.Model):
|
|||||||
)
|
)
|
||||||
description = db.Column(db.Text)
|
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):
|
class Admission(db.Model):
|
||||||
"""Informations liées à l'admission d'un étudiant"""
|
"""Informations liées à l'admission d'un étudiant"""
|
||||||
|
@ -59,6 +59,10 @@ class Formation(db.Model):
|
|||||||
"""get l'instance de TypeParcours de cette formation"""
|
"""get l'instance de TypeParcours de cette formation"""
|
||||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
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):
|
def is_apc(self):
|
||||||
"True si formation APC avec SAE (BUT)"
|
"True si formation APC avec SAE (BUT)"
|
||||||
return self.get_parcours().APC_SAE
|
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 APO_CODE_STR_LEN
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
from app.models import UniteEns
|
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.models.ues import UniteEns
|
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 import sco_preferences
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
||||||
|
|
||||||
|
|
||||||
class FormSemestre(db.Model):
|
class FormSemestre(db.Model):
|
||||||
@ -104,6 +104,11 @@ class FormSemestre(db.Model):
|
|||||||
lazy=True,
|
lazy=True,
|
||||||
backref=db.backref("formsemestres", lazy=True),
|
backref=db.backref("formsemestres", lazy=True),
|
||||||
)
|
)
|
||||||
|
partitions = db.relationship(
|
||||||
|
"Partition",
|
||||||
|
backref=db.backref("formsemestre", lazy=True),
|
||||||
|
lazy="dynamic",
|
||||||
|
)
|
||||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||||
@ -117,10 +122,12 @@ class FormSemestre(db.Model):
|
|||||||
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
"dict (compatible ScoDoc7)"
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
# ScoDoc7 output_formators: (backward compat)
|
# ScoDoc7 output_formators: (backward compat)
|
||||||
d["formsemestre_id"] = self.id
|
d["formsemestre_id"] = self.id
|
||||||
|
d["titre_num"] = self.titre_num()
|
||||||
if self.date_debut:
|
if self.date_debut:
|
||||||
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
|
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
|
||||||
d["date_debut_iso"] = self.date_debut.isoformat()
|
d["date_debut_iso"] = self.date_debut.isoformat()
|
||||||
@ -154,10 +161,9 @@ class FormSemestre(db.Model):
|
|||||||
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
|
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
|
||||||
else:
|
else:
|
||||||
d["periode"] = 2 # typiquement, début en février: S2, S4...
|
d["periode"] = 2 # typiquement, début en février: S2, S4...
|
||||||
d["titre_num"] = self.titre_num()
|
|
||||||
d["titreannee"] = self.titre_annee()
|
d["titreannee"] = self.titre_annee()
|
||||||
d["mois_debut"] = f"{self.date_debut.month} {self.date_debut.year}"
|
d["mois_debut"] = self.mois_debut()
|
||||||
d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}"
|
d["mois_fin"] = self.mois_fin()
|
||||||
d["titremois"] = "%s %s (%s - %s)" % (
|
d["titremois"] = "%s %s (%s - %s)" % (
|
||||||
d["titre_num"],
|
d["titre_num"],
|
||||||
self.modalite or "",
|
self.modalite or "",
|
||||||
@ -167,7 +173,6 @@ class FormSemestre(db.Model):
|
|||||||
d["session_id"] = self.session_id()
|
d["session_id"] = self.session_id()
|
||||||
d["etapes"] = self.etapes_apo_vdi()
|
d["etapes"] = self.etapes_apo_vdi()
|
||||||
d["etapes_apo_str"] = self.etapes_apo_str()
|
d["etapes_apo_str"] = self.etapes_apo_str()
|
||||||
d["responsables"] = [u.id for u in self.responsables] # liste des ids
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||||
@ -200,7 +205,11 @@ class FormSemestre(db.Model):
|
|||||||
modimpls = self.modimpls.all()
|
modimpls = self.modimpls.all()
|
||||||
if self.formation.is_apc():
|
if self.formation.is_apc():
|
||||||
modimpls.sort(
|
modimpls.sort(
|
||||||
key=lambda m: (m.module.module_type, m.module.numero, m.module.code)
|
key=lambda m: (
|
||||||
|
m.module.module_type or 0,
|
||||||
|
m.module.numero or 0,
|
||||||
|
m.module.code or 0,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
modimpls.sort(
|
modimpls.sort(
|
||||||
@ -283,6 +292,7 @@ class FormSemestre(db.Model):
|
|||||||
"""chaîne "J. Dupond, X. Martin"
|
"""chaîne "J. Dupond, X. Martin"
|
||||||
ou "Jacques Dupond, Xavier Martin"
|
ou "Jacques Dupond, Xavier Martin"
|
||||||
"""
|
"""
|
||||||
|
# was "nomcomplet"
|
||||||
if not self.responsables:
|
if not self.responsables:
|
||||||
return ""
|
return ""
|
||||||
if abbrev_prenom:
|
if abbrev_prenom:
|
||||||
@ -290,10 +300,22 @@ class FormSemestre(db.Model):
|
|||||||
else:
|
else:
|
||||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
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):
|
def annee_scolaire_str(self):
|
||||||
"2021 - 2022"
|
"2021 - 2022"
|
||||||
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
|
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:
|
def session_id(self) -> str:
|
||||||
"""identifiant externe de semestre de formation
|
"""identifiant externe de semestre de formation
|
||||||
Exemple: RT-DUT-FI-S1-ANNEE
|
Exemple: RT-DUT-FI-S1-ANNEE
|
||||||
@ -354,7 +376,7 @@ class FormSemestre(db.Model):
|
|||||||
|
|
||||||
def get_abs_count(self, etudid):
|
def get_abs_count(self, etudid):
|
||||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
tuple (nb abs non justifiées, nb abs justifiées)
|
tuple (nb abs, nb abs justifiées)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_abs
|
||||||
@ -363,19 +385,23 @@ class FormSemestre(db.Model):
|
|||||||
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_inscrits(self, include_demdef=False) -> list[Identite]:
|
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
||||||
"""Liste des étudiants inscrits à ce semestre
|
"""Liste des étudiants inscrits à ce semestre
|
||||||
Si include_demdef, tous les étudiants, avec les démissionnaires
|
Si include_demdef, tous les étudiants, avec les démissionnaires
|
||||||
et défaillants.
|
et défaillants.
|
||||||
|
Si order, tri par clé sort_key
|
||||||
"""
|
"""
|
||||||
if include_demdef:
|
if include_demdef:
|
||||||
return [ins.etud for ins in self.inscriptions]
|
etuds = [ins.etud for ins in self.inscriptions]
|
||||||
else:
|
else:
|
||||||
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
|
etuds = [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
|
||||||
|
if order:
|
||||||
|
etuds.sort(key=lambda e: e.sort_key)
|
||||||
|
return etuds
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def etudids_actifs(self) -> set:
|
def etudids_actifs(self) -> set:
|
||||||
"Set des etudids inscrits non démissionnaires"
|
"Set des etudids inscrits non démissionnaires et non défaillants"
|
||||||
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
|
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -554,6 +580,9 @@ class FormSemestreInscription(db.Model):
|
|||||||
# etape apogee d'inscription (experimental 2020)
|
# etape apogee d'inscription (experimental 2020)
|
||||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={self.formsemestre_id} etat={self.etat}>"
|
||||||
|
|
||||||
|
|
||||||
class NotesSemSet(db.Model):
|
class NotesSemSet(db.Model):
|
||||||
"""semsets: ensemble de formsemestres pour exports Apogée"""
|
"""semsets: ensemble de formsemestres pour exports Apogée"""
|
||||||
|
@ -31,6 +31,11 @@ class Partition(db.Model):
|
|||||||
show_in_lists = db.Column(
|
show_in_lists = db.Column(
|
||||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||||
)
|
)
|
||||||
|
groups = db.relationship(
|
||||||
|
"GroupDescr",
|
||||||
|
backref=db.backref("partition", lazy=True),
|
||||||
|
lazy="dynamic",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Partition, self).__init__(**kwargs)
|
super(Partition, self).__init__(**kwargs)
|
||||||
@ -42,6 +47,9 @@ class Partition(db.Model):
|
|||||||
else:
|
else:
|
||||||
self.numero = 1
|
self.numero = 1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">"""
|
||||||
|
|
||||||
|
|
||||||
class GroupDescr(db.Model):
|
class GroupDescr(db.Model):
|
||||||
"""Description d'un groupe d'une partition"""
|
"""Description d'un groupe d'une partition"""
|
||||||
@ -55,6 +63,17 @@ class GroupDescr(db.Model):
|
|||||||
# "A", "C2", ... (NULL for 'all'):
|
# "A", "C2", ... (NULL for 'all'):
|
||||||
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
|
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)'}">"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
group_membership = db.Table(
|
group_membership = db.Table(
|
||||||
"group_membership",
|
"group_membership",
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
"""ScoDoc models: moduleimpls
|
"""ScoDoc models: moduleimpls
|
||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import flask_sqlalchemy
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.comp import df_cache
|
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
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
@ -79,7 +81,7 @@ class ModuleImpl(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
"""as a dict, with the same conversions as in ScoDoc7, including module"""
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
# ScoDoc7 output_formators: (backward compat)
|
# ScoDoc7 output_formators: (backward compat)
|
||||||
@ -129,14 +131,38 @@ class ModuleImplInscription(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def nb_inscriptions_dans_ue(
|
def etud_modimpls_in_ue(
|
||||||
cls, formsemestre_id: int, etudid: int, ue_id: int
|
cls, formsemestre_id: int, etudid: int, ue_id: int
|
||||||
) -> int:
|
) -> flask_sqlalchemy.BaseQuery:
|
||||||
"""Nombre de 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(
|
return ModuleImplInscription.query.filter(
|
||||||
ModuleImplInscription.etudid == etudid,
|
ModuleImplInscription.etudid == etudid,
|
||||||
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
|
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
|
||||||
ModuleImpl.formsemestre_id == formsemestre_id,
|
ModuleImpl.formsemestre_id == formsemestre_id,
|
||||||
ModuleImpl.module_id == Module.id,
|
ModuleImpl.module_id == Module.id,
|
||||||
Module.ue_id == ue_id,
|
Module.ue_id == ue_id,
|
||||||
).count()
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def nb_inscriptions_dans_ue(
|
||||||
|
cls, formsemestre_id: int, etudid: int, ue_id: int
|
||||||
|
) -> int:
|
||||||
|
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
|
||||||
|
return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sum_coefs_modimpl_ue(
|
||||||
|
cls, formsemestre_id: int, etudid: int, ue_id: int
|
||||||
|
) -> float:
|
||||||
|
"""Somme des coefficients des modules auxquels l'étudiant est inscrit
|
||||||
|
dans l'UE du semestre indiqué.
|
||||||
|
N'utilise que les coefficients, donc inadapté aux formations APC.
|
||||||
|
"""
|
||||||
|
return sum(
|
||||||
|
[
|
||||||
|
inscr.modimpl.module.coefficient
|
||||||
|
for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@ -33,8 +33,8 @@ class Module(db.Model):
|
|||||||
numero = db.Column(db.Integer) # ordre de présentation
|
numero = db.Column(db.Integer) # ordre de présentation
|
||||||
# id de l'element pedagogique Apogee correspondant:
|
# id de l'element pedagogique Apogee correspondant:
|
||||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||||
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
|
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||||
module_type = db.Column(db.Integer)
|
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
||||||
# Relations:
|
# Relations:
|
||||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
||||||
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
||||||
@ -76,6 +76,11 @@ class Module(db.Model):
|
|||||||
def type_name(self):
|
def type_name(self):
|
||||||
return scu.MODULE_TYPE_NAMES[self.module_type]
|
return scu.MODULE_TYPE_NAMES[self.module_type]
|
||||||
|
|
||||||
|
def type_abbrv(self):
|
||||||
|
""" "mod", "malus", "res", "sae"
|
||||||
|
(utilisées pour style css)"""
|
||||||
|
return scu.ModuleType.get_abbrev(self.module_type)
|
||||||
|
|
||||||
def set_ue_coef(self, ue, coef: float) -> None:
|
def set_ue_coef(self, ue, coef: float) -> None:
|
||||||
"""Set coef module vers cette UE"""
|
"""Set coef module vers cette UE"""
|
||||||
self.update_ue_coef_dict({ue.id: coef})
|
self.update_ue_coef_dict({ue.id: coef})
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import SHORT_STR_LEN
|
|
||||||
from app.models import CODE_STR_LEN
|
import app.scodoc.notesdb as ndb
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class BulAppreciations(db.Model):
|
class BulAppreciations(db.Model):
|
||||||
@ -67,3 +68,29 @@ class NotesNotesLog(db.Model):
|
|||||||
comment = db.Column(db.Text) # texte libre
|
comment = db.Column(db.Text) # texte libre
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
uid = db.Column(db.Integer, db.ForeignKey("user.id"))
|
uid = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||||
|
|
||||||
|
|
||||||
|
def etud_has_notes_attente(etudid, formsemestre_id):
|
||||||
|
"""Vrai si cet etudiant a au moins une note en attente dans ce semestre.
|
||||||
|
(ne compte que les notes en attente dans des évaluations avec coef. non nul).
|
||||||
|
"""
|
||||||
|
cursor = db.session.execute(
|
||||||
|
"""SELECT COUNT(*)
|
||||||
|
FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
|
||||||
|
notes_moduleimpl_inscription i
|
||||||
|
WHERE n.etudid = :etudid
|
||||||
|
and n.value = :code_attente
|
||||||
|
and n.evaluation_id = e.id
|
||||||
|
and e.moduleimpl_id = m.id
|
||||||
|
and m.formsemestre_id = :formsemestre_id
|
||||||
|
and e.coefficient != 0
|
||||||
|
and m.id = i.moduleimpl_id
|
||||||
|
and i.etudid = :etudid
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"formsemestre_id": formsemestre_id,
|
||||||
|
"etudid": etudid,
|
||||||
|
"code_attente": scu.NOTES_ATTENTE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return cursor.fetchone()[0] > 0
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.scodoc import notesdb as ndb
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -54,13 +53,15 @@ class UniteEns(db.Model):
|
|||||||
'EXTERNE' if self.is_external else ''})>"""
|
'EXTERNE' if self.is_external else ''})>"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
"""as a dict, with the same conversions as in ScoDoc7
|
||||||
|
(except ECTS: keep None)
|
||||||
|
"""
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
# ScoDoc7 output_formators
|
# ScoDoc7 output_formators
|
||||||
e["ue_id"] = self.id
|
e["ue_id"] = self.id
|
||||||
e["numero"] = e["numero"] if e["numero"] else 0
|
e["numero"] = e["numero"] if e["numero"] else 0
|
||||||
e["ects"] = e["ects"] if e["ects"] else 0.0
|
e["ects"] = e["ects"]
|
||||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||||
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
||||||
return e
|
return e
|
||||||
|
1
app/pe/__init__.py
Normal file
1
app/pe/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# empty but required for pylint
|
@ -87,7 +87,7 @@ def get_tags_latex(code_latex):
|
|||||||
"""
|
"""
|
||||||
if code_latex:
|
if code_latex:
|
||||||
# changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})"
|
# changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})"
|
||||||
res = re.findall(r"([\*]{2}[^ \t\n\r\f\v\*]+[\*]{2})", code_latex)
|
res = re.findall(r"([\*]{2}[^\t\n\r\f\v\*]+[\*]{2})", code_latex)
|
||||||
return [tag[2:-2] for tag in res]
|
return [tag[2:-2] for tag in res]
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
@ -47,7 +47,7 @@ import os
|
|||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
|
|
||||||
from app.scodoc.gen_tables import GenTable, SeqGenTable
|
from app.scodoc.gen_tables import GenTable, SeqGenTable
|
||||||
@ -322,12 +322,10 @@ class JuryPE(object):
|
|||||||
etudiants = []
|
etudiants = []
|
||||||
for sem in semsListe: # pour chacun des semestres de la liste
|
for sem in semsListe: # pour chacun des semestres de la liste
|
||||||
|
|
||||||
# nt = self.get_notes_d_un_semestre( sem['formsemestre_id'] )
|
|
||||||
nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"])
|
nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"])
|
||||||
# sco_cache.NotesTableCache.get( sem['formsemestre_id'])
|
|
||||||
etudiantsDuSemestre = (
|
etudiantsDuSemestre = (
|
||||||
nt.get_etudids()
|
nt.get_etudids()
|
||||||
) # nt.identdict.keys() # identification des etudiants du semestre
|
) # identification des etudiants du semestre
|
||||||
|
|
||||||
if pe_tools.PE_DEBUG:
|
if pe_tools.PE_DEBUG:
|
||||||
pe_tools.pe_print(
|
pe_tools.pe_print(
|
||||||
|
@ -38,11 +38,10 @@ Created on Fri Sep 9 09:15:05 2016
|
|||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
|
|
||||||
from app.models.ues import UniteEns
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_tag_module
|
from app.scodoc import sco_tag_module
|
||||||
from app.pe import pe_tagtable
|
from app.pe import pe_tagtable
|
||||||
@ -194,12 +193,14 @@ class SemestreTag(pe_tagtable.TableTag):
|
|||||||
return tagdict
|
return tagdict
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def comp_MoyennesTag(self, tag, force=False):
|
def comp_MoyennesTag(self, tag, force=False) -> list:
|
||||||
"""Calcule et renvoie les "moyennes" de tous les étudiants du SemTag (non défaillants)
|
"""Calcule et renvoie les "moyennes" de tous les étudiants du SemTag
|
||||||
à un tag donné, en prenant en compte
|
(non défaillants) à un tag donné, en prenant en compte
|
||||||
tous les modimpl_id concerné par le tag, leur coeff et leur pondération.
|
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.
|
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 = []
|
lesMoyennes = []
|
||||||
for etudid in self.get_etudids():
|
for etudid in self.get_etudids():
|
||||||
|
@ -38,6 +38,7 @@ Created on Thu Sep 8 09:36:33 2016
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from app.scodoc import notes_table
|
from app.scodoc import notes_table
|
||||||
|
|
||||||
@ -287,47 +288,52 @@ 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.
|
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)
|
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)
|
La liste de notes contient soit :
|
||||||
ou "-c-" ue capitalisée, 3) None.
|
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 à
|
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
|
dire s'il y a ou non omission des notes non numériques (auquel cas la moyenne est
|
||||||
notes disponibles) ; sinon renvoie (None, None).
|
calculée sur les notes disponibles) ; sinon renvoie (None, None).
|
||||||
"""
|
"""
|
||||||
# Vérification des paramètres d'entrée
|
# Vérification des paramètres d'entrée
|
||||||
if not isinstance(notes, list) or (
|
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")
|
raise ValueError("Erreur de paramètres dans moyenne_ponderee_terme_a_terme")
|
||||||
|
|
||||||
# Récupération des valeurs des paramètres d'entrée
|
# 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
|
# S'il n'y a pas de notes
|
||||||
if not notes: # Si notes = []
|
if not notes: # Si notes = []
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
notesValides = [
|
# Liste indiquant les notes valides
|
||||||
(1 if isinstance(note, float) or isinstance(note, int) else 0) for note in notes
|
notes_valides = [
|
||||||
] # Liste indiquant les notes valides
|
(isinstance(note, float) and not np.isnan(note)) or isinstance(note, int)
|
||||||
if force == True or (
|
for note in notes
|
||||||
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
|
# Si on force le calcul de la moyenne ou qu'on ne le force pas
|
||||||
(moyenne, ponderation) = (0.0, 0.0)
|
# 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)):
|
for i in range(len(notes)):
|
||||||
if notesValides[i]:
|
if notes_valides[i]:
|
||||||
moyenne += coeffs[i] * notes[i]
|
moyenne += coefs[i] * notes[i]
|
||||||
ponderation += coeffs[i]
|
ponderation += coefs[i]
|
||||||
return (
|
return (
|
||||||
(moyenne / (ponderation * 1.0), ponderation)
|
(moyenne / (ponderation * 1.0), ponderation)
|
||||||
if ponderation != 0
|
if ponderation != 0
|
||||||
else (None, 0)
|
else (None, 0)
|
||||||
)
|
)
|
||||||
else: # Si on ne force pas le calcul de la moyenne
|
# Si on ne force pas le calcul de la moyenne
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import send_file, request
|
from flask import send_file, request
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
@ -50,27 +51,34 @@ from app.pe import pe_avislatex
|
|||||||
def _pe_view_sem_recap_form(formsemestre_id):
|
def _pe_view_sem_recap_form(formsemestre_id):
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
|
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
|
||||||
"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de poursuites d'études.
|
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
||||||
|
poursuites d'études.
|
||||||
<br/>
|
<br/>
|
||||||
De nombreux aspects sont paramétrables:
|
De nombreux aspects sont paramétrables:
|
||||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener noreferrer">
|
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
||||||
voir la documentation</a>.
|
voir la documentation</a>.
|
||||||
</p>
|
</p>
|
||||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form" enctype="multipart/form-data">
|
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
||||||
|
enctype="multipart/form-data">
|
||||||
<div class="pe_template_up">
|
<div class="pe_template_up">
|
||||||
Les templates sont généralement installés sur le serveur ou dans le paramétrage de ScoDoc.<br/>
|
Les templates sont généralement installés sur le serveur ou dans le
|
||||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template (<tt>un_avis.tex</tt>):
|
paramétrage de ScoDoc.
|
||||||
<div class="pe_template_upb">Template: <input type="file" size="30" name="avis_tmpl_file"/></div>
|
<br/>
|
||||||
<div class="pe_template_upb">Pied de page: <input type="file" size="30" name="footer_tmpl_file"/></div>
|
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
||||||
|
(<tt>un_avis.tex</tt>):
|
||||||
|
<div class="pe_template_upb">Template:
|
||||||
|
<input type="file" size="30" name="avis_tmpl_file"/>
|
||||||
|
</div>
|
||||||
|
<div class="pe_template_upb">Pied de page:
|
||||||
|
<input type="file" size="30" name="footer_tmpl_file"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" value="Générer les documents"/>
|
<input type="submit" value="Générer les documents"/>
|
||||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
|
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
|
||||||
</form>
|
</form>
|
||||||
""".format(
|
""",
|
||||||
formsemestre_id=formsemestre_id
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
return "\n".join(H) + html_sco_header.sco_footer()
|
return "\n".join(H) + html_sco_header.sco_footer()
|
||||||
|
|
||||||
@ -97,8 +105,12 @@ def pe_view_sem_recap(
|
|||||||
template_latex = ""
|
template_latex = ""
|
||||||
# template fourni via le formulaire Web
|
# template fourni via le formulaire Web
|
||||||
if avis_tmpl_file:
|
if avis_tmpl_file:
|
||||||
template_latex = avis_tmpl_file.read().decode('utf-8')
|
try:
|
||||||
template_latex = template_latex
|
template_latex = avis_tmpl_file.read().decode("utf-8")
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
raise ScoValueError(
|
||||||
|
"Données (template) invalides (caractères non UTF8 ?)"
|
||||||
|
) from e
|
||||||
else:
|
else:
|
||||||
# template indiqué dans préférences ScoDoc ?
|
# template indiqué dans préférences ScoDoc ?
|
||||||
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
||||||
@ -114,8 +126,7 @@ def pe_view_sem_recap(
|
|||||||
footer_latex = ""
|
footer_latex = ""
|
||||||
# template fourni via le formulaire Web
|
# template fourni via le formulaire Web
|
||||||
if footer_tmpl_file:
|
if footer_tmpl_file:
|
||||||
footer_latex = footer_tmpl_file.read().decode('utf-8')
|
footer_latex = footer_tmpl_file.read().decode("utf-8")
|
||||||
footer_latex = footer_latex
|
|
||||||
else:
|
else:
|
||||||
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
||||||
formsemestre_id, champ="pe_avis_latex_footer"
|
formsemestre_id, champ="pe_avis_latex_footer"
|
||||||
|
@ -254,13 +254,13 @@ class TF(object):
|
|||||||
continue # allowed empty field, skip
|
continue # allowed empty field, skip
|
||||||
# type
|
# type
|
||||||
typ = descr.get("type", "string")
|
typ = descr.get("type", "string")
|
||||||
if val != "" and val != None:
|
if val != "" and val is not None:
|
||||||
# check only non-null values
|
# check only non-null values
|
||||||
if typ[:3] == "int":
|
if typ[:3] == "int":
|
||||||
try:
|
try:
|
||||||
val = int(val)
|
val = int(val)
|
||||||
self.values[field] = val
|
self.values[field] = val
|
||||||
except:
|
except ValueError:
|
||||||
msg.append(
|
msg.append(
|
||||||
"La valeur du champ '%s' doit être un nombre entier" % field
|
"La valeur du champ '%s' doit être un nombre entier" % field
|
||||||
)
|
)
|
||||||
@ -270,35 +270,36 @@ class TF(object):
|
|||||||
try:
|
try:
|
||||||
val = float(val.replace(",", ".")) # allow ,
|
val = float(val.replace(",", ".")) # allow ,
|
||||||
self.values[field] = val
|
self.values[field] = val
|
||||||
except:
|
except ValueError:
|
||||||
msg.append(
|
msg.append(
|
||||||
"La valeur du champ '%s' doit être un nombre" % field
|
"La valeur du champ '%s' doit être un nombre" % field
|
||||||
)
|
)
|
||||||
ok = 0
|
ok = 0
|
||||||
if typ[:3] == "int" or typ == "float" or typ == "real":
|
|
||||||
if (
|
if (
|
||||||
val != ""
|
ok
|
||||||
|
and (typ[:3] == "int" or typ == "float" or typ == "real")
|
||||||
|
and val != ""
|
||||||
and val != None
|
and val != None
|
||||||
and "min_value" in descr
|
|
||||||
and val < descr["min_value"]
|
|
||||||
):
|
):
|
||||||
|
if "min_value" in descr and self.values[field] < descr["min_value"]:
|
||||||
msg.append(
|
msg.append(
|
||||||
"La valeur (%d) du champ '%s' est trop petite (min=%s)"
|
"La valeur (%d) du champ '%s' est trop petite (min=%s)"
|
||||||
% (val, field, descr["min_value"])
|
% (val, field, descr["min_value"])
|
||||||
)
|
)
|
||||||
ok = 0
|
ok = 0
|
||||||
|
if "max_value" in descr and self.values[field] > descr["max_value"]:
|
||||||
if (
|
|
||||||
val != ""
|
|
||||||
and val != None
|
|
||||||
and "max_value" in descr
|
|
||||||
and val > descr["max_value"]
|
|
||||||
):
|
|
||||||
msg.append(
|
msg.append(
|
||||||
"La valeur (%s) du champ '%s' est trop grande (max=%s)"
|
"La valeur (%s) du champ '%s' est trop grande (max=%s)"
|
||||||
% (val, field, descr["max_value"])
|
% (val, field, descr["max_value"])
|
||||||
)
|
)
|
||||||
ok = 0
|
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
|
# allowed values
|
||||||
if "allowed_values" in descr:
|
if "allowed_values" in descr:
|
||||||
|
@ -63,12 +63,15 @@ from app.scodoc.sco_pdf import SU
|
|||||||
from app import log
|
from app import log
|
||||||
|
|
||||||
|
|
||||||
def mark_paras(L, tags):
|
def mark_paras(L, tags) -> list[str]:
|
||||||
"""Put each (string) element of L between <b>"""
|
"""Put each (string) element of L between <tag>...</tag>,
|
||||||
|
for each supplied tag.
|
||||||
|
Leave non string elements untouched.
|
||||||
|
"""
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
b = "<" + tag + ">"
|
start = "<" + tag + ">"
|
||||||
c = "</" + tag.split()[0] + ">"
|
end = "</" + tag.split()[0] + ">"
|
||||||
L = [b + (x or "") + c for x in L]
|
L = [(start + (x or "") + end) if isinstance(x, str) else x for x in L]
|
||||||
return L
|
return L
|
||||||
|
|
||||||
|
|
||||||
@ -233,6 +236,9 @@ class GenTable(object):
|
|||||||
colspan_count -= 1
|
colspan_count -= 1
|
||||||
# if colspan_count > 0:
|
# if colspan_count > 0:
|
||||||
# continue # skip cells after a span
|
# continue # skip cells after a span
|
||||||
|
if pdf_mode:
|
||||||
|
content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or ""
|
||||||
|
else:
|
||||||
content = row.get(cid, "") or "" # nota: None converted to ''
|
content = row.get(cid, "") or "" # nota: None converted to ''
|
||||||
colspan = row.get("_%s_colspan" % cid, 0)
|
colspan = row.get("_%s_colspan" % cid, 0)
|
||||||
if colspan > 1:
|
if colspan > 1:
|
||||||
@ -547,9 +553,16 @@ class GenTable(object):
|
|||||||
omit_hidden_lines=True,
|
omit_hidden_lines=True,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
Pt = [
|
Pt = []
|
||||||
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list
|
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:
|
except ValueError as exc:
|
||||||
raise ScoPDFFormatError(str(exc)) from exc
|
raise ScoPDFFormatError(str(exc)) from exc
|
||||||
pdf_style_list += self.pdf_table_style
|
pdf_style_list += self.pdf_table_style
|
||||||
@ -748,7 +761,7 @@ if __name__ == "__main__":
|
|||||||
doc = io.BytesIO()
|
doc = io.BytesIO()
|
||||||
document = sco_pdf.BaseDocTemplate(doc)
|
document = sco_pdf.BaseDocTemplate(doc)
|
||||||
document.addPageTemplates(
|
document.addPageTemplates(
|
||||||
sco_pdf.ScolarsPageTemplate(
|
sco_pdf.ScoDocPageTemplate(
|
||||||
document,
|
document,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -30,12 +30,12 @@
|
|||||||
|
|
||||||
import html
|
import html
|
||||||
|
|
||||||
from flask import g
|
from flask import render_template
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import scodoc_flash_status_messages
|
||||||
from app.scodoc import html_sidebar
|
from app.scodoc import html_sidebar
|
||||||
import sco_version
|
import sco_version
|
||||||
|
|
||||||
@ -105,7 +105,6 @@ _HTML_BEGIN = """<!DOCTYPE html>
|
|||||||
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
|
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
||||||
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/sorttable.js"></script>
|
|
||||||
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onload=function(){enableTooltips("gtrcontent")};
|
window.onload=function(){enableTooltips("gtrcontent")};
|
||||||
@ -153,13 +152,14 @@ def sco_header(
|
|||||||
"Main HTML page header for ScoDoc"
|
"Main HTML page header for ScoDoc"
|
||||||
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
||||||
|
|
||||||
|
scodoc_flash_status_messages()
|
||||||
|
|
||||||
# Get head message from http request:
|
# Get head message from http request:
|
||||||
if not head_message:
|
if not head_message:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
head_message = request.form.get("head_message", "")
|
head_message = request.form.get("head_message", "")
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
head_message = request.args.get("head_message", "")
|
head_message = request.args.get("head_message", "")
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"page_title": page_title or sco_version.SCONAME,
|
"page_title": page_title or sco_version.SCONAME,
|
||||||
"no_side_bar": no_side_bar,
|
"no_side_bar": no_side_bar,
|
||||||
@ -249,6 +249,9 @@ def sco_header(
|
|||||||
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
|
'<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('<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
|
# JS additionels
|
||||||
for js in javascripts:
|
for js in javascripts:
|
||||||
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
|
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
|
||||||
@ -280,6 +283,9 @@ def sco_header(
|
|||||||
if not no_side_bar:
|
if not no_side_bar:
|
||||||
H.append(html_sidebar.sidebar())
|
H.append(html_sidebar.sidebar())
|
||||||
H.append("""<div id="gtrcontent">""")
|
H.append("""<div id="gtrcontent">""")
|
||||||
|
# En attendant le replacement complet de cette fonction,
|
||||||
|
# inclusion ici des messages flask
|
||||||
|
H.append(render_template("flashed_messages.html"))
|
||||||
#
|
#
|
||||||
# Barre menu semestre:
|
# Barre menu semestre:
|
||||||
H.append(formsemestre_page_title())
|
H.append(formsemestre_page_title())
|
||||||
|
@ -51,8 +51,8 @@ from app.scodoc.sco_formsemestre import (
|
|||||||
from app.scodoc.sco_codes_parcours import (
|
from app.scodoc.sco_codes_parcours import (
|
||||||
DEF,
|
DEF,
|
||||||
UE_SPORT,
|
UE_SPORT,
|
||||||
UE_is_fondamentale,
|
ue_is_fondamentale,
|
||||||
UE_is_professionnelle,
|
ue_is_professionnelle,
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation
|
from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
@ -171,7 +171,7 @@ class NotesTable:
|
|||||||
|
|
||||||
def __init__(self, formsemestre_id):
|
def __init__(self, formsemestre_id):
|
||||||
# log(f"NotesTable( formsemestre_id={formsemestre_id} )")
|
# log(f"NotesTable( formsemestre_id={formsemestre_id} )")
|
||||||
# raise NotImplementedError() # XXX
|
raise NotImplementedError() # XXX
|
||||||
if not formsemestre_id:
|
if not formsemestre_id:
|
||||||
raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
|
raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
|
||||||
self.formsemestre_id = formsemestre_id
|
self.formsemestre_id = formsemestre_id
|
||||||
@ -826,11 +826,11 @@ class NotesTable:
|
|||||||
and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
|
and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
|
||||||
):
|
):
|
||||||
mu["ects_pot"] = ue["ects"] or 0.0
|
mu["ects_pot"] = ue["ects"] or 0.0
|
||||||
if UE_is_fondamentale(ue["type"]):
|
if ue_is_fondamentale(ue["type"]):
|
||||||
mu["ects_pot_fond"] = mu["ects_pot"]
|
mu["ects_pot_fond"] = mu["ects_pot"]
|
||||||
else:
|
else:
|
||||||
mu["ects_pot_fond"] = 0.0
|
mu["ects_pot_fond"] = 0.0
|
||||||
if UE_is_professionnelle(ue["type"]):
|
if ue_is_professionnelle(ue["type"]):
|
||||||
mu["ects_pot_pro"] = mu["ects_pot"]
|
mu["ects_pot_pro"] = mu["ects_pot"]
|
||||||
else:
|
else:
|
||||||
mu["ects_pot_pro"] = 0.0
|
mu["ects_pot_pro"] = 0.0
|
||||||
@ -954,9 +954,12 @@ class NotesTable:
|
|||||||
|
|
||||||
Return: True|False, message explicatif
|
Return: True|False, message explicatif
|
||||||
"""
|
"""
|
||||||
return self.parcours.check_barre_ues(
|
ue_status_list = []
|
||||||
[self.get_etud_ue_status(etudid, ue["ue_id"]) for ue in self._ues]
|
for ue in self._ues:
|
||||||
)
|
ue_status = self.get_etud_ue_status(etudid, ue["ue_id"])
|
||||||
|
if ue_status:
|
||||||
|
ue_status_list.append(ue_status)
|
||||||
|
return self.parcours.check_barre_ues(ue_status_list)
|
||||||
|
|
||||||
def get_table_moyennes_triees(self):
|
def get_table_moyennes_triees(self):
|
||||||
return self.T
|
return self.T
|
||||||
@ -1160,9 +1163,11 @@ class NotesTable:
|
|||||||
nt_cap = sco_cache.NotesTableCache.get(
|
nt_cap = sco_cache.NotesTableCache.get(
|
||||||
ue_cap["formsemestre_id"]
|
ue_cap["formsemestre_id"]
|
||||||
) # > UE capitalisees par un etud
|
) # > UE capitalisees par un etud
|
||||||
moy_ue_cap = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])[
|
ue_cap_status = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])
|
||||||
"moy"
|
if ue_cap_status:
|
||||||
]
|
moy_ue_cap = ue_cap_status["moy"]
|
||||||
|
else:
|
||||||
|
moy_ue_cap = ""
|
||||||
ue_cap["moy_ue"] = moy_ue_cap
|
ue_cap["moy_ue"] = moy_ue_cap
|
||||||
if (
|
if (
|
||||||
isinstance(moy_ue_cap, float)
|
isinstance(moy_ue_cap, float)
|
||||||
|
@ -53,7 +53,11 @@ def _isFarFutur(jour):
|
|||||||
# check si jour est dans le futur "lointain"
|
# check si jour est dans le futur "lointain"
|
||||||
# pour autoriser les saisies dans le futur mais pas a plus de 6 mois
|
# pour autoriser les saisies dans le futur mais pas a plus de 6 mois
|
||||||
y, m, d = [int(x) for x in jour.split("-")]
|
y, m, d = [int(x) for x in jour.split("-")]
|
||||||
|
try:
|
||||||
j = datetime.date(y, m, d)
|
j = datetime.date(y, m, d)
|
||||||
|
except ValueError:
|
||||||
|
# les dates erronées, genre année 20022, sont considéres dans le futur
|
||||||
|
return True
|
||||||
# 6 mois ~ 182 jours:
|
# 6 mois ~ 182 jours:
|
||||||
return j - datetime.date.today() > datetime.timedelta(182)
|
return j - datetime.date.today() > datetime.timedelta(182)
|
||||||
|
|
||||||
@ -225,8 +229,11 @@ def DateRangeISO(date_beg, date_end, workable=1):
|
|||||||
date_end = date_beg
|
date_end = date_beg
|
||||||
r = []
|
r = []
|
||||||
work_saturday = is_work_saturday()
|
work_saturday = is_work_saturday()
|
||||||
|
try:
|
||||||
cur = ddmmyyyy(date_beg, work_saturday=work_saturday)
|
cur = ddmmyyyy(date_beg, work_saturday=work_saturday)
|
||||||
end = ddmmyyyy(date_end, work_saturday=work_saturday)
|
end = ddmmyyyy(date_end, work_saturday=work_saturday)
|
||||||
|
except (AttributeError, ValueError) as e:
|
||||||
|
raise ScoValueError("date invalide !") from e
|
||||||
while cur <= end:
|
while cur <= end:
|
||||||
if (not workable) or cur.iswork():
|
if (not workable) or cur.iswork():
|
||||||
r.append(cur)
|
r.append(cur)
|
||||||
@ -479,7 +486,7 @@ def _get_abs_description(a, cursor=None):
|
|||||||
)
|
)
|
||||||
if Mlist:
|
if Mlist:
|
||||||
M = Mlist[0]
|
M = Mlist[0]
|
||||||
module += "%s " % M["module"]["code"]
|
module += "%s " % (M["module"]["code"] or "(module sans code)")
|
||||||
|
|
||||||
if desc:
|
if desc:
|
||||||
return "(%s) %s" % (desc, module)
|
return "(%s) %s" % (desc, module)
|
||||||
@ -631,7 +638,7 @@ def add_absence(
|
|||||||
):
|
):
|
||||||
"Ajoute une absence dans la bd"
|
"Ajoute une absence dans la bd"
|
||||||
if _isFarFutur(jour):
|
if _isFarFutur(jour):
|
||||||
raise ScoValueError("date absence trop loin dans le futur !")
|
raise ScoValueError("date absence erronée ou trop loin dans le futur !")
|
||||||
estjust = _toboolean(estjust)
|
estjust = _toboolean(estjust)
|
||||||
matin = _toboolean(matin)
|
matin = _toboolean(matin)
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
@ -1037,7 +1044,7 @@ def get_abs_count(etudid, sem):
|
|||||||
|
|
||||||
def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||||
tuple (nb abs non justifiées, nb abs justifiées)
|
tuple (nb abs, nb abs justifiées)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso
|
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso
|
||||||
|
@ -35,6 +35,7 @@ import datetime
|
|||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
@ -55,27 +56,30 @@ def abs_notify(etudid, date):
|
|||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_abs
|
||||||
|
|
||||||
sem = retreive_current_formsemestre(etudid, date)
|
formsemestre = retreive_current_formsemestre(etudid, date)
|
||||||
if not sem:
|
if not formsemestre:
|
||||||
return # non inscrit a la date, pas de notification
|
return # non inscrit a la date, pas de notification
|
||||||
|
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_abs.get_abs_count_in_interval(
|
||||||
do_abs_notify(sem, etudid, date, nbabs, nbabsjust)
|
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."""
|
"""Given new counts of absences, check if notifications are requested and send them."""
|
||||||
# prefs fallback to global pref if sem is None:
|
# prefs fallback to global pref if sem is None:
|
||||||
if sem:
|
if formsemestre:
|
||||||
formsemestre_id = sem["formsemestre_id"]
|
formsemestre_id = formsemestre.id
|
||||||
else:
|
else:
|
||||||
formsemestre_id = None
|
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(
|
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:
|
if not msg:
|
||||||
return # abort
|
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"""
|
"""Returns set of destination emails to be notified"""
|
||||||
formsemestre_id = sem["formsemestre_id"]
|
|
||||||
|
|
||||||
destinations = [] # list of email address to notify
|
destinations = [] # list of email address to notify
|
||||||
|
|
||||||
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id):
|
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre.id):
|
||||||
if sem and prefs["abs_notify_respsem"]:
|
if prefs["abs_notify_respsem"]:
|
||||||
# notifie chaque responsable du semestre
|
# notifie chaque responsable du semestre
|
||||||
for responsable_id in sem["responsables"]:
|
for responsable in formsemestre.responsables:
|
||||||
u = sco_users.user_info(responsable_id)
|
if responsable.email:
|
||||||
if u["email"]:
|
destinations.append(responsable.email)
|
||||||
destinations.append(u["email"])
|
|
||||||
if prefs["abs_notify_chief"] and prefs["email_chefdpt"]:
|
if prefs["abs_notify_chief"] and prefs["email_chefdpt"]:
|
||||||
destinations.append(prefs["email_chefdpt"])
|
destinations.append(prefs["email_chefdpt"])
|
||||||
if prefs["abs_notify_email"]:
|
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
|
# Notification (à chaque fois) des resp. de modules ayant des évaluations
|
||||||
# à cette date
|
# à cette date
|
||||||
# nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas
|
# 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)
|
mods = mod_with_evals_at_date(date, etudid)
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
u = sco_users.user_info(mod["responsable_id"])
|
u = sco_users.user_info(mod["responsable_id"])
|
||||||
@ -232,7 +236,9 @@ def user_nbdays_since_last_notif(email_addr, etudid):
|
|||||||
return None
|
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.
|
"""Mime notification message based on template.
|
||||||
returns a Message instance
|
returns a Message instance
|
||||||
or None if sending should be canceled (empty template).
|
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]
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||||
|
|
||||||
# Variables accessibles dans les balises du template: %(nom_variable)s :
|
# 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["nbabs"] = nbabs
|
||||||
values["nbabsjust"] = nbabsjust
|
values["nbabsjust"] = nbabsjust
|
||||||
values["nbabsnonjust"] = nbabs - nbabsjust
|
values["nbabsnonjust"] = nbabs - nbabsjust
|
||||||
values["url_ficheetud"] = url_for(
|
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"]
|
template = prefs["abs_notification_mail_tmpl"]
|
||||||
@ -264,9 +270,11 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
|
|||||||
return msg
|
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
|
"""Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée
|
||||||
date est une chaine au format ISO (yyyy-mm-dd)
|
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
|
req = """SELECT i.formsemestre_id
|
||||||
FROM notes_formsemestre_inscription i, notes_formsemestre sem
|
FROM notes_formsemestre_inscription i, notes_formsemestre sem
|
||||||
@ -278,8 +286,8 @@ def retreive_current_formsemestre(etudid, cur_date):
|
|||||||
if not r:
|
if not r:
|
||||||
return None
|
return None
|
||||||
# s'il y a plusieurs semestres, prend le premier (rarissime et non significatif):
|
# s'il y a plusieurs semestres, prend le premier (rarissime et non significatif):
|
||||||
sem = sco_formsemestre.get_formsemestre(r[0]["formsemestre_id"])
|
formsemestre = FormSemestre.query.get(r[0]["formsemestre_id"])
|
||||||
return sem
|
return formsemestre
|
||||||
|
|
||||||
|
|
||||||
def mod_with_evals_at_date(date_abs, etudid):
|
def mod_with_evals_at_date(date_abs, etudid):
|
||||||
|
@ -33,7 +33,9 @@ import datetime
|
|||||||
from flask import url_for, g, request, abort
|
from flask import url_for, g, request, abort
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.models import Identite
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models import Identite, FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import notesdb as ndb
|
from app.scodoc import notesdb as ndb
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
@ -118,13 +120,16 @@ def doSignaleAbsence(
|
|||||||
if moduleimpl_id and moduleimpl_id != "NULL":
|
if moduleimpl_id and moduleimpl_id != "NULL":
|
||||||
mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||||
formsemestre_id = mod["formsemestre_id"]
|
formsemestre_id = mod["formsemestre_id"]
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
ues = nt.get_ues_stat_dict()
|
ues = nt.get_ues_stat_dict()
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"])
|
modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"])
|
||||||
for modimpl in modimpls:
|
for modimpl in modimpls:
|
||||||
if modimpl["moduleimpl_id"] == moduleimpl_id:
|
if modimpl["moduleimpl_id"] == moduleimpl_id:
|
||||||
indication_module = "dans le module %s" % modimpl["module"]["code"]
|
indication_module = "dans le module %s" % (
|
||||||
|
modimpl["module"]["code"] or "(pas de code)"
|
||||||
|
)
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
page_title=f"Signalement d'une absence pour {etud.nomprenom}",
|
page_title=f"Signalement d'une absence pour {etud.nomprenom}",
|
||||||
@ -179,11 +184,12 @@ def SignaleAbsenceEtud(): # etudid implied
|
|||||||
menu_module = ""
|
menu_module = ""
|
||||||
else:
|
else:
|
||||||
formsemestre_id = etud["cursem"]["formsemestre_id"]
|
formsemestre_id = etud["cursem"]["formsemestre_id"]
|
||||||
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
ues = nt.get_ues_stat_dict()
|
||||||
require_module = sco_preferences.get_preference(
|
require_module = sco_preferences.get_preference(
|
||||||
"abs_require_module", formsemestre_id
|
"abs_require_module", formsemestre_id
|
||||||
)
|
)
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
|
||||||
ues = nt.get_ues_stat_dict()
|
|
||||||
if require_module:
|
if require_module:
|
||||||
menu_module = """
|
menu_module = """
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@ -214,7 +220,7 @@ def SignaleAbsenceEtud(): # etudid implied
|
|||||||
"""<option value="%(modimpl_id)s">%(modname)s</option>\n"""
|
"""<option value="%(modimpl_id)s">%(modname)s</option>\n"""
|
||||||
% {
|
% {
|
||||||
"modimpl_id": modimpl["moduleimpl_id"],
|
"modimpl_id": modimpl["moduleimpl_id"],
|
||||||
"modname": modimpl["module"]["code"],
|
"modname": modimpl["module"]["code"] or "",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
menu_module += """</select></p>"""
|
menu_module += """</select></p>"""
|
||||||
@ -718,6 +724,7 @@ def CalAbs(etudid, sco_year=None):
|
|||||||
anneescolaire = int(scu.AnneeScolaire(sco_year))
|
anneescolaire = int(scu.AnneeScolaire(sco_year))
|
||||||
datedebut = str(anneescolaire) + "-08-01"
|
datedebut = str(anneescolaire) + "-08-01"
|
||||||
datefin = str(anneescolaire + 1) + "-07-31"
|
datefin = str(anneescolaire + 1) + "-07-31"
|
||||||
|
annee_courante = scu.AnneeScolaire()
|
||||||
nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin)
|
nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin)
|
||||||
nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin)
|
nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin)
|
||||||
events = []
|
events = []
|
||||||
@ -770,7 +777,7 @@ def CalAbs(etudid, sco_year=None):
|
|||||||
"""Année scolaire %s-%s""" % (anneescolaire, anneescolaire + 1),
|
"""Année scolaire %s-%s""" % (anneescolaire, anneescolaire + 1),
|
||||||
""" Changer année: <select name="sco_year" onchange="document.f.submit()">""",
|
""" 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)
|
H.append("""<option value="%s" """ % y)
|
||||||
if y == anneescolaire:
|
if y == anneescolaire:
|
||||||
H.append("selected")
|
H.append("selected")
|
||||||
@ -960,10 +967,10 @@ def _tables_abs_etud(
|
|||||||
ex.append(
|
ex.append(
|
||||||
f"""<a href="{url_for('notes.moduleimpl_status',
|
f"""<a href="{url_for('notes.moduleimpl_status',
|
||||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
|
||||||
">{mod["module"]["code"]}</a>"""
|
">{mod["module"]["code"] or "(module sans code)"}</a>"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ex.append(mod["module"]["code"])
|
ex.append(mod["module"]["code"] or "(module sans code)")
|
||||||
if ex:
|
if ex:
|
||||||
return ", ".join(ex)
|
return ", ".join(ex)
|
||||||
return ""
|
return ""
|
||||||
@ -978,10 +985,10 @@ def _tables_abs_etud(
|
|||||||
ex.append(
|
ex.append(
|
||||||
f"""<a href="{url_for('notes.moduleimpl_status',
|
f"""<a href="{url_for('notes.moduleimpl_status',
|
||||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
|
||||||
">{mod["module"]["code"]}</a>"""
|
">{mod["module"]["code"] or '(module sans code)'}</a>"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ex.append(mod["module"]["code"])
|
ex.append(mod["module"]["code"] or "(module sans code)")
|
||||||
if ex:
|
if ex:
|
||||||
return ", ".join(ex)
|
return ", ".join(ex)
|
||||||
return ""
|
return ""
|
||||||
|
@ -95,9 +95,12 @@ from flask import send_file
|
|||||||
# Pour la détection auto de l'encodage des fichiers Apogée:
|
# Pour la détection auto de l'encodage des fichiers Apogée:
|
||||||
from chardet import detect as chardet_detect
|
from chardet import detect as chardet_detect
|
||||||
|
|
||||||
|
from app import log
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models import FormSemestre, Identite
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
@ -108,7 +111,6 @@ from app.scodoc.sco_codes_parcours import (
|
|||||||
NAR,
|
NAR,
|
||||||
RAT,
|
RAT,
|
||||||
)
|
)
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_parcours_dut
|
from app.scodoc import sco_parcours_dut
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
@ -370,7 +372,9 @@ class ApoEtud(dict):
|
|||||||
dict: with N, B, J, R keys, ou None si elt non trouvé
|
dict: with N, B, J, R keys, ou None si elt non trouvé
|
||||||
"""
|
"""
|
||||||
etudid = self.etud["etudid"]
|
etudid = self.etud["etudid"]
|
||||||
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
|
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
if etudid not in nt.identdict:
|
if etudid not in nt.identdict:
|
||||||
return None # etudiant non inscrit dans ce semestre
|
return None # etudiant non inscrit dans ce semestre
|
||||||
|
|
||||||
@ -419,7 +423,7 @@ class ApoEtud(dict):
|
|||||||
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
|
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
|
||||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||||
return dict(
|
return dict(
|
||||||
N=_apo_fmt_note(ue_status["moy"]),
|
N=_apo_fmt_note(ue_status["moy"] if ue_status else ""),
|
||||||
B=20,
|
B=20,
|
||||||
J="",
|
J="",
|
||||||
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
||||||
@ -449,6 +453,12 @@ class ApoEtud(dict):
|
|||||||
|
|
||||||
def comp_elt_semestre(self, nt, decision, etudid):
|
def comp_elt_semestre(self, nt, decision, etudid):
|
||||||
"""Calcul résultat apo semestre"""
|
"""Calcul résultat apo semestre"""
|
||||||
|
if decision is None:
|
||||||
|
etud = Identite.query.get(etudid)
|
||||||
|
nomprenom = etud.nomprenom if etud else "(inconnu)"
|
||||||
|
raise ScoValueError(
|
||||||
|
f"decision absente pour l'étudiant {nomprenom} ({etudid})"
|
||||||
|
)
|
||||||
# resultat du semestre
|
# resultat du semestre
|
||||||
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
|
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
|
||||||
note = nt.get_etud_moy_gen(etudid)
|
note = nt.get_etud_moy_gen(etudid)
|
||||||
@ -476,7 +486,8 @@ class ApoEtud(dict):
|
|||||||
# l'étudiant n'a pas de semestre courant ?!
|
# l'étudiant n'a pas de semestre courant ?!
|
||||||
log("comp_elt_annuel: etudid %s has no cur_sem" % etudid)
|
log("comp_elt_annuel: etudid %s has no cur_sem" % etudid)
|
||||||
return VOID_APO_RES
|
return VOID_APO_RES
|
||||||
cur_nt = sco_cache.NotesTableCache.get(cur_sem["formsemestre_id"])
|
cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"])
|
||||||
|
cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre)
|
||||||
cur_decision = cur_nt.get_etud_decision_sem(etudid)
|
cur_decision = cur_nt.get_etud_decision_sem(etudid)
|
||||||
if not cur_decision:
|
if not cur_decision:
|
||||||
# pas de decision => pas de résultat annuel
|
# pas de decision => pas de résultat annuel
|
||||||
@ -493,7 +504,10 @@ class ApoEtud(dict):
|
|||||||
|
|
||||||
decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"])
|
decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"])
|
||||||
|
|
||||||
autre_nt = sco_cache.NotesTableCache.get(autre_sem["formsemestre_id"])
|
autre_formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"])
|
||||||
|
autre_nt: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||||
|
autre_formsemestre
|
||||||
|
)
|
||||||
autre_decision = autre_nt.get_etud_decision_sem(etudid)
|
autre_decision = autre_nt.get_etud_decision_sem(etudid)
|
||||||
if not autre_decision:
|
if not autre_decision:
|
||||||
# pas de decision dans l'autre => pas de résultat annuel
|
# pas de decision dans l'autre => pas de résultat annuel
|
||||||
@ -554,7 +568,8 @@ class ApoEtud(dict):
|
|||||||
# prend le plus recent avec decision
|
# prend le plus recent avec decision
|
||||||
cur_sem = None
|
cur_sem = None
|
||||||
for sem in cur_sems:
|
for sem in cur_sems:
|
||||||
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
|
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
decision = nt.get_etud_decision_sem(self.etud["etudid"])
|
decision = nt.get_etud_decision_sem(self.etud["etudid"])
|
||||||
if decision:
|
if decision:
|
||||||
cur_sem = sem
|
cur_sem = sem
|
||||||
@ -614,7 +629,8 @@ class ApoEtud(dict):
|
|||||||
else:
|
else:
|
||||||
autre_sem = None
|
autre_sem = None
|
||||||
for sem in autres_sems:
|
for sem in autres_sems:
|
||||||
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
|
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
decision = nt.get_etud_decision_sem(self.etud["etudid"])
|
decision = nt.get_etud_decision_sem(self.etud["etudid"])
|
||||||
if decision:
|
if decision:
|
||||||
autre_sem = sem
|
autre_sem = sem
|
||||||
@ -947,7 +963,8 @@ class ApoData(object):
|
|||||||
s.add(code)
|
s.add(code)
|
||||||
continue
|
continue
|
||||||
# associé à une UE:
|
# associé à une UE:
|
||||||
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
|
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
for ue in nt.get_ues_stat_dict():
|
for ue in nt.get_ues_stat_dict():
|
||||||
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
|
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
|
||||||
s.add(code)
|
s.add(code)
|
||||||
|
@ -70,13 +70,13 @@ from app.scodoc.sco_exceptions import (
|
|||||||
)
|
)
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_bulletins_pdf
|
from app.scodoc import sco_bulletins_pdf
|
||||||
from app.scodoc import sco_excel
|
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_groups_view
|
from app.scodoc import sco_groups_view
|
||||||
from app.scodoc import sco_permissions_check
|
from app.scodoc import sco_permissions_check
|
||||||
from app.scodoc import sco_pvjury
|
from app.scodoc import sco_pvjury
|
||||||
from app.scodoc import sco_pvpdf
|
from app.scodoc import sco_pvpdf
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
class BaseArchiver(object):
|
class BaseArchiver(object):
|
||||||
@ -254,7 +254,7 @@ class BaseArchiver(object):
|
|||||||
self.initialize()
|
self.initialize()
|
||||||
if not scu.is_valid_filename(filename):
|
if not scu.is_valid_filename(filename):
|
||||||
log('Archiver.get: invalid filename "%s"' % 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)
|
fname = os.path.join(archive_id, filename)
|
||||||
log("reading archive file %s" % fname)
|
log("reading archive file %s" % fname)
|
||||||
with open(fname, "rb") as f:
|
with open(fname, "rb") as f:
|
||||||
|
@ -132,19 +132,29 @@ BACS_S = {t[0]: t[2:] for t in _BACS}
|
|||||||
|
|
||||||
class Baccalaureat:
|
class Baccalaureat:
|
||||||
def __init__(self, bac, specialite=""):
|
def __init__(self, bac, specialite=""):
|
||||||
self.bac = bac
|
self.bac = bac or ""
|
||||||
self.specialite = specialite
|
self.specialite = specialite or ""
|
||||||
self._abbrev, self._type = BACS_SSP.get((bac, specialite), (None, None))
|
self._abbrev, self._type = BACS_SSP.get(
|
||||||
|
(self.bac, self.specialite), (None, None)
|
||||||
|
)
|
||||||
# Parfois, la specialite commence par la serie: essaye
|
# Parfois, la specialite commence par la serie: essaye
|
||||||
if self._type is None and specialite and specialite.startswith(bac):
|
if (
|
||||||
specialite = specialite[len(bac) :].strip(" -")
|
self._type is None
|
||||||
self._abbrev, self._type = BACS_SSP.get((bac, specialite), (None, None))
|
and self.specialite
|
||||||
|
and self.specialite.startswith(self.bac)
|
||||||
|
):
|
||||||
|
specialite = self.specialite[len(self.bac) :].strip(" -")
|
||||||
|
self._abbrev, self._type = BACS_SSP.get(
|
||||||
|
(self.bac, specialite), (None, None)
|
||||||
|
)
|
||||||
# Cherche la forme serie specialite
|
# Cherche la forme serie specialite
|
||||||
if self._type is None and specialite:
|
if self._type is None and specialite:
|
||||||
self._abbrev, self._type = BACS_S.get(bac + " " + specialite, (None, None))
|
self._abbrev, self._type = BACS_S.get(
|
||||||
|
self.bac + " " + specialite, (None, None)
|
||||||
|
)
|
||||||
# Cherche avec juste le bac, sans specialite
|
# Cherche avec juste le bac, sans specialite
|
||||||
if self._type is None:
|
if self._type is None:
|
||||||
self._abbrev, self._type = BACS_S.get(bac, (None, None))
|
self._abbrev, self._type = BACS_S.get(self.bac, (None, None))
|
||||||
|
|
||||||
def abbrev(self):
|
def abbrev(self):
|
||||||
"abbreviation for this bac"
|
"abbreviation for this bac"
|
||||||
|
@ -28,30 +28,19 @@
|
|||||||
"""Génération des bulletins de notes
|
"""Génération des bulletins de notes
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from app.models import formsemestre
|
|
||||||
import time
|
|
||||||
import pprint
|
|
||||||
import email
|
import email
|
||||||
from email.mime.multipart import MIMEMultipart
|
import time
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from email.mime.base import MIMEBase
|
|
||||||
from email.header import Header
|
|
||||||
from reportlab.lib.colors import Color
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask import url_for
|
from flask import flash, render_template, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_mail import Message
|
|
||||||
from app.models.moduleimpls import ModuleImplInscription
|
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
from app import email
|
||||||
from app.scodoc.sco_utils import ModuleType
|
|
||||||
import app.scodoc.notesdb as ndb
|
|
||||||
from app import log
|
from app import log
|
||||||
|
from app.but import bulletin_but
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre, Identite, ModuleImplInscription
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
@ -60,9 +49,9 @@ from app.scodoc import sco_abs
|
|||||||
from app.scodoc import sco_abs_views
|
from app.scodoc import sco_abs_views
|
||||||
from app.scodoc import sco_bulletins_generator
|
from app.scodoc import sco_bulletins_generator
|
||||||
from app.scodoc import sco_bulletins_json
|
from app.scodoc import sco_bulletins_json
|
||||||
|
from app.scodoc import sco_bulletins_pdf
|
||||||
from app.scodoc import sco_bulletins_xml
|
from app.scodoc import sco_bulletins_xml
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
@ -73,7 +62,9 @@ from app.scodoc import sco_photos
|
|||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_pvjury
|
from app.scodoc import sco_pvjury
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
from app import email
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.scodoc.sco_utils import ModuleType, fmt_note
|
||||||
|
import app.scodoc.notesdb as ndb
|
||||||
|
|
||||||
# ----- CLASSES DE BULLETINS DE NOTES
|
# ----- CLASSES DE BULLETINS DE NOTES
|
||||||
from app.scodoc import sco_bulletins_standard
|
from app.scodoc import sco_bulletins_standard
|
||||||
@ -85,33 +76,20 @@ from app.scodoc import sco_bulletins_legacy
|
|||||||
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
|
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
|
"""Construit dictionnaire avec valeurs pour substitution des textes
|
||||||
(preferences bul_pdf_*)
|
(preferences bul_pdf_*)
|
||||||
"""
|
"""
|
||||||
C = sem.copy()
|
C = formsemestre.get_infos_dict()
|
||||||
C["responsable"] = " ,".join(
|
C["responsable"] = formsemestre.responsables_str()
|
||||||
[
|
C["anneesem"] = C["annee"] # backward compat
|
||||||
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.update(etud)
|
C.update(etud)
|
||||||
# copie preferences
|
# copie preferences
|
||||||
# XXX devrait acceder directement à un dict de preferences, à revoir
|
|
||||||
for name in sco_preferences.get_base_preferences().prefs_name:
|
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, ...
|
# 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"]
|
C["groupes"] = etud["groupes"]
|
||||||
n = 0
|
n = 0
|
||||||
for partition_id in etud["partitions"]:
|
for partition_id in etud["partitions"]:
|
||||||
@ -132,7 +110,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
Le contenu du dictionnaire dépend des options (rangs, ...)
|
Le contenu du dictionnaire dépend des options (rangs, ...)
|
||||||
et de la version choisie (short, long, selectedevals).
|
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
|
from app.scodoc import sco_abs
|
||||||
|
|
||||||
@ -140,7 +119,6 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
raise ValueError("invalid version code !")
|
raise ValueError("invalid version code !")
|
||||||
|
|
||||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||||
# nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
|
|
||||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
if not nt.get_etud_etat(etudid):
|
if not nt.get_etud_etat(etudid):
|
||||||
@ -191,39 +169,23 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
show_mention=prefs["bul_show_mention"],
|
show_mention=prefs["bul_show_mention"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if dpv:
|
|
||||||
I["decision_sem"] = dpv["decisions"][0]["decision_sem"]
|
|
||||||
else:
|
|
||||||
I["decision_sem"] = ""
|
|
||||||
I.update(infos)
|
I.update(infos)
|
||||||
|
|
||||||
I["etud_etat_html"] = _get_etud_etat_html(
|
I["etud_etat_html"] = _get_etud_etat_html(
|
||||||
formsemestre.etuds_inscriptions[etudid].etat
|
formsemestre.etuds_inscriptions[etudid].etat
|
||||||
)
|
)
|
||||||
I["etud_etat"] = nt.get_etud_etat(etudid)
|
I["etud_etat"] = nt.get_etud_etat(etudid)
|
||||||
I["filigranne"] = ""
|
I["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||||
|
I["etud_etat"], prefs, decision_sem=I["decision_sem"]
|
||||||
|
)
|
||||||
I["demission"] = ""
|
I["demission"] = ""
|
||||||
if I["etud_etat"] == "D":
|
if I["etud_etat"] == scu.DEMISSION:
|
||||||
I["demission"] = "(Démission)"
|
I["demission"] = "(Démission)"
|
||||||
I["filigranne"] = "Démission"
|
|
||||||
elif I["etud_etat"] == sco_codes_parcours.DEF:
|
elif I["etud_etat"] == sco_codes_parcours.DEF:
|
||||||
I["demission"] = "(Défaillant)"
|
I["demission"] = "(Défaillant)"
|
||||||
I["filigranne"] = "Défaillant"
|
|
||||||
elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[
|
|
||||||
"bul_show_temporary_forced"
|
|
||||||
]:
|
|
||||||
I["filigranne"] = prefs["bul_temporary_txt"]
|
|
||||||
|
|
||||||
# --- Appreciations
|
# --- Appreciations
|
||||||
cnx = ndb.GetDBConnexion()
|
I.update(get_appreciations_list(formsemestre_id, etudid))
|
||||||
apprecs = sco_etud.appreciations_list(
|
|
||||||
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
|
||||||
)
|
|
||||||
I["appreciations_list"] = apprecs
|
|
||||||
I["appreciations_txt"] = [x["date"] + ": " + x["comment"] for x in apprecs]
|
|
||||||
I["appreciations"] = I[
|
|
||||||
"appreciations_txt"
|
|
||||||
] # deprecated / keep it for backward compat in templates
|
|
||||||
|
|
||||||
# --- Notes
|
# --- Notes
|
||||||
ues = nt.get_ues_stat_dict()
|
ues = nt.get_ues_stat_dict()
|
||||||
@ -292,15 +254,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
I["matieres_modules"] = {}
|
I["matieres_modules"] = {}
|
||||||
I["matieres_modules_capitalized"] = {}
|
I["matieres_modules_capitalized"] = {}
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
|
u = ue.copy()
|
||||||
|
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
|
||||||
if (
|
if (
|
||||||
ModuleImplInscription.nb_inscriptions_dans_ue(
|
ModuleImplInscription.nb_inscriptions_dans_ue(
|
||||||
formsemestre_id, etudid, ue["ue_id"]
|
formsemestre_id, etudid, ue["ue_id"]
|
||||||
)
|
)
|
||||||
== 0
|
== 0
|
||||||
):
|
) and not ue_status["is_capitalized"]:
|
||||||
|
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
|
||||||
continue
|
continue
|
||||||
u = ue.copy()
|
|
||||||
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
|
|
||||||
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
|
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
|
||||||
if ue["type"] != sco_codes_parcours.UE_SPORT:
|
if ue["type"] != sco_codes_parcours.UE_SPORT:
|
||||||
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
|
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
|
||||||
@ -315,14 +279,14 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
else:
|
else:
|
||||||
u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs"
|
u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs"
|
||||||
else:
|
else:
|
||||||
u["cur_moy_ue_txt"] = "bonus de %.3g points" % x
|
u["cur_moy_ue_txt"] = f"bonus de {fmt_note(x)} points"
|
||||||
|
if nt.bonus_ues is not None:
|
||||||
|
u["cur_moy_ue_txt"] += " (+ues)"
|
||||||
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
|
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
|
||||||
if ue_status["coef_ue"] != None:
|
if ue_status["coef_ue"] != None:
|
||||||
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
|
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
|
||||||
else:
|
else:
|
||||||
# C'est un bug:
|
u["coef_ue_txt"] = "-"
|
||||||
log("u=" + pprint.pformat(u))
|
|
||||||
raise Exception("invalid None coef for ue")
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dpv
|
dpv
|
||||||
@ -363,10 +327,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
>{u["ue_descr_txt"]} pouet</a>
|
>{u["ue_descr_txt"]} pouet</a>
|
||||||
"""
|
"""
|
||||||
if ue_status["moy"] != "NA" and ue_status["formsemestre_id"]:
|
if ue_status["moy"] != "NA" and ue_status["formsemestre_id"]:
|
||||||
# detail des modules de l'UE capitalisee
|
# détail des modules de l'UE capitalisée
|
||||||
# nt_cap = sco_cache.NotesTableCache.get(
|
|
||||||
# ue_status["formsemestre_id"]
|
|
||||||
# ) # > toutes notes
|
|
||||||
formsemestre_cap = FormSemestre.query.get_or_404(
|
formsemestre_cap = FormSemestre.query.get_or_404(
|
||||||
ue_status["formsemestre_id"]
|
ue_status["formsemestre_id"]
|
||||||
)
|
)
|
||||||
@ -409,13 +370,28 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
|
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)
|
C.update(I)
|
||||||
#
|
#
|
||||||
# log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo
|
# log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo
|
||||||
return C
|
return C
|
||||||
|
|
||||||
|
|
||||||
|
def get_appreciations_list(formsemestre_id: int, etudid: int) -> dict:
|
||||||
|
"""Appréciations pour cet étudiant dans ce semestre"""
|
||||||
|
cnx = ndb.GetDBConnexion()
|
||||||
|
apprecs = sco_etud.appreciations_list(
|
||||||
|
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||||
|
)
|
||||||
|
d = {
|
||||||
|
"appreciations_list": apprecs,
|
||||||
|
"appreciations_txt": [x["date"] + ": " + x["comment"] for x in apprecs],
|
||||||
|
}
|
||||||
|
# deprecated / keep it for backward compat in templates:
|
||||||
|
d["appreciations"] = d["appreciations_txt"]
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
def _get_etud_etat_html(etat: str) -> str:
|
def _get_etud_etat_html(etat: str) -> str:
|
||||||
"""chaine html représentant l'état (backward compat sco7)"""
|
"""chaine html représentant l'état (backward compat sco7)"""
|
||||||
if etat == scu.INSCRIT: # "I"
|
if etat == scu.INSCRIT: # "I"
|
||||||
@ -443,7 +419,9 @@ def _sort_mod_by_matiere(modlist, nt, etudid):
|
|||||||
return matmod
|
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
|
"""Infos sur les modules (et évaluations) dans une UE
|
||||||
(ajoute les informations aux modimpls)
|
(ajoute les informations aux modimpls)
|
||||||
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
|
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
|
||||||
@ -518,7 +496,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
|
|||||||
)
|
)
|
||||||
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
|
||||||
mod["code"] = modimpl["module"]["code"]
|
mod["code"] = modimpl["module"]["code"]
|
||||||
mod["code_html"] = link_mod + mod["code"] + "</a>"
|
mod["code_html"] = link_mod + (mod["code"] or "") + "</a>"
|
||||||
else:
|
else:
|
||||||
mod["code"] = mod["code_html"] = ""
|
mod["code"] = mod["code_html"] = ""
|
||||||
mod["name"] = (
|
mod["name"] = (
|
||||||
@ -536,7 +514,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
|
|||||||
% (modimpl["moduleimpl_id"], mod_descr)
|
% (modimpl["moduleimpl_id"], mod_descr)
|
||||||
)
|
)
|
||||||
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
|
||||||
mod["code_txt"] = modimpl["module"]["code"]
|
mod["code_txt"] = modimpl["module"]["code"] or ""
|
||||||
mod["code_html"] = link_mod + mod["code_txt"] + "</a>"
|
mod["code_html"] = link_mod + mod["code_txt"] + "</a>"
|
||||||
else:
|
else:
|
||||||
mod["code_txt"] = ""
|
mod["code_txt"] = ""
|
||||||
@ -691,6 +669,7 @@ def etud_descr_situation_semestre(
|
|||||||
descr_defaillance : "Défaillant" ou vide si non défaillant.
|
descr_defaillance : "Défaillant" ou vide si non défaillant.
|
||||||
decision_jury : "Validé", "Ajourné", ... (code semestre)
|
decision_jury : "Validé", "Ajourné", ... (code semestre)
|
||||||
descr_decision_jury : "Décision jury: Validé" (une phrase)
|
descr_decision_jury : "Décision jury: Validé" (une phrase)
|
||||||
|
decision_sem :
|
||||||
decisions_ue : noms (acronymes) des UE validées, séparées par des virgules.
|
decisions_ue : noms (acronymes) des UE validées, séparées par des virgules.
|
||||||
descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
|
descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
|
||||||
descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
|
descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
|
||||||
@ -700,7 +679,7 @@ def etud_descr_situation_semestre(
|
|||||||
|
|
||||||
# --- Situation et décisions jury
|
# --- Situation et décisions jury
|
||||||
|
|
||||||
# demission/inscription ?
|
# démission/inscription ?
|
||||||
events = sco_etud.scolar_events_list(
|
events = sco_etud.scolar_events_list(
|
||||||
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||||
)
|
)
|
||||||
@ -767,11 +746,15 @@ def etud_descr_situation_semestre(
|
|||||||
infos["situation"] += " " + infos["descr_defaillance"]
|
infos["situation"] += " " + infos["descr_defaillance"]
|
||||||
|
|
||||||
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
|
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
|
||||||
|
if dpv:
|
||||||
|
infos["decision_sem"] = dpv["decisions"][0]["decision_sem"]
|
||||||
|
else:
|
||||||
|
infos["decision_sem"] = ""
|
||||||
|
|
||||||
if not show_decisions:
|
if not show_decisions:
|
||||||
return infos, dpv
|
return infos, dpv
|
||||||
|
|
||||||
# Decisions de jury:
|
# Décisions de jury:
|
||||||
pv = dpv["decisions"][0]
|
pv = dpv["decisions"][0]
|
||||||
dec = ""
|
dec = ""
|
||||||
if pv["decision_sem_descr"]:
|
if pv["decision_sem_descr"]:
|
||||||
@ -808,27 +791,23 @@ def etud_descr_situation_semestre(
|
|||||||
|
|
||||||
# ------ Page bulletin
|
# ------ Page bulletin
|
||||||
def formsemestre_bulletinetud(
|
def formsemestre_bulletinetud(
|
||||||
etudid=None,
|
etud: Identite = None,
|
||||||
formsemestre_id=None,
|
formsemestre_id=None,
|
||||||
format="html",
|
format=None,
|
||||||
version="long",
|
version="long",
|
||||||
xml_with_decisions=False,
|
xml_with_decisions=False,
|
||||||
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
||||||
prefer_mail_perso=False,
|
prefer_mail_perso=False,
|
||||||
):
|
):
|
||||||
"page bulletin de notes"
|
"page bulletin de notes"
|
||||||
try:
|
format = format or "html"
|
||||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
etudid = etud["etudid"]
|
if not formsemestre:
|
||||||
except:
|
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
|
||||||
sco_etud.log_unknown_etud()
|
|
||||||
raise ScoValueError("étudiant inconnu")
|
|
||||||
# API, donc erreurs admises en ScoValueError
|
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
|
||||||
|
|
||||||
bulletin = do_formsemestre_bulletinetud(
|
bulletin = do_formsemestre_bulletinetud(
|
||||||
formsemestre_id,
|
formsemestre,
|
||||||
etudid,
|
etud.id,
|
||||||
format=format,
|
format=format,
|
||||||
version=version,
|
version=version,
|
||||||
xml_with_decisions=xml_with_decisions,
|
xml_with_decisions=xml_with_decisions,
|
||||||
@ -836,52 +815,25 @@ def formsemestre_bulletinetud(
|
|||||||
prefer_mail_perso=prefer_mail_perso,
|
prefer_mail_perso=prefer_mail_perso,
|
||||||
)[0]
|
)[0]
|
||||||
if format not in {"html", "pdfmail"}:
|
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])
|
return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
|
||||||
|
elif format == "pdfmail":
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
return ""
|
||||||
H = [
|
H = [
|
||||||
_formsemestre_bulletinetud_header_html(
|
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
|
||||||
etud, etudid, sem, formsemestre_id, format, version
|
|
||||||
),
|
|
||||||
bulletin,
|
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)
|
return "".join(H)
|
||||||
|
|
||||||
|
|
||||||
@ -896,23 +848,23 @@ def can_send_bulletin_by_mail(formsemestre_id):
|
|||||||
|
|
||||||
|
|
||||||
def do_formsemestre_bulletinetud(
|
def do_formsemestre_bulletinetud(
|
||||||
formsemestre_id,
|
formsemestre: FormSemestre,
|
||||||
etudid,
|
etudid: int,
|
||||||
version="long", # short, long, selectedevals
|
version="long", # short, long, selectedevals
|
||||||
format="html",
|
format=None,
|
||||||
nohtml=False,
|
xml_with_decisions=False, # force décisions dans XML
|
||||||
xml_with_decisions=False, # force decisions dans XML
|
force_publishing=False, # force publication meme si semestre non publié sur "portail"
|
||||||
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide
|
||||||
prefer_mail_perso=False, # mails envoyes sur adresse perso si non vide
|
|
||||||
):
|
):
|
||||||
"""Génère le bulletin au format demandé.
|
"""Génère le bulletin au format demandé.
|
||||||
Retourne: (bul, filigranne)
|
Retourne: (bul, filigranne)
|
||||||
où bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
|
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").
|
et filigranne est un message à placer en "filigranne" (eg "Provisoire").
|
||||||
"""
|
"""
|
||||||
|
format = format or "html"
|
||||||
if format == "xml":
|
if format == "xml":
|
||||||
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
|
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
|
||||||
formsemestre_id,
|
formsemestre.id,
|
||||||
etudid,
|
etudid,
|
||||||
xml_with_decisions=xml_with_decisions,
|
xml_with_decisions=xml_with_decisions,
|
||||||
force_publishing=force_publishing,
|
force_publishing=force_publishing,
|
||||||
@ -923,7 +875,7 @@ def do_formsemestre_bulletinetud(
|
|||||||
|
|
||||||
elif format == "json":
|
elif format == "json":
|
||||||
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
|
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
|
||||||
formsemestre_id,
|
formsemestre.id,
|
||||||
etudid,
|
etudid,
|
||||||
xml_with_decisions=xml_with_decisions,
|
xml_with_decisions=xml_with_decisions,
|
||||||
force_publishing=force_publishing,
|
force_publishing=force_publishing,
|
||||||
@ -931,7 +883,12 @@ def do_formsemestre_bulletinetud(
|
|||||||
)
|
)
|
||||||
return bul, ""
|
return bul, ""
|
||||||
|
|
||||||
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid)
|
if formsemestre.formation.is_apc():
|
||||||
|
etudiant = Identite.query.get(etudid)
|
||||||
|
r = bulletin_but.BulletinBUT(formsemestre)
|
||||||
|
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":
|
if format == "html":
|
||||||
@ -958,16 +915,9 @@ def do_formsemestre_bulletinetud(
|
|||||||
elif format == "pdfmail":
|
elif format == "pdfmail":
|
||||||
# format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
|
# format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
|
||||||
# check permission
|
# check permission
|
||||||
if not can_send_bulletin_by_mail(formsemestre_id):
|
if not can_send_bulletin_by_mail(formsemestre.id):
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
|
||||||
if nohtml:
|
|
||||||
htm = "" # speed up if html version not needed
|
|
||||||
else:
|
|
||||||
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
|
||||||
I, version=version, format="html"
|
|
||||||
)
|
|
||||||
|
|
||||||
pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
||||||
I, version=version, format="pdf"
|
I, version=version, format="pdf"
|
||||||
)
|
)
|
||||||
@ -975,30 +925,17 @@ def do_formsemestre_bulletinetud(
|
|||||||
if prefer_mail_perso:
|
if prefer_mail_perso:
|
||||||
recipient_addr = etud.get("emailperso", "") or etud.get("email", "")
|
recipient_addr = etud.get("emailperso", "") or etud.get("email", "")
|
||||||
else:
|
else:
|
||||||
recipient_addr = etud["email_default"]
|
recipient_addr = etud.get("email", "") or etud.get("emailperso", "")
|
||||||
|
|
||||||
if not recipient_addr:
|
if not recipient_addr:
|
||||||
if nohtml:
|
flash(f"{etud['nomprenom']} n'a pas d'adresse e-mail !")
|
||||||
h = "" # permet de compter les non-envois
|
return False, I["filigranne"]
|
||||||
else:
|
else:
|
||||||
h = (
|
mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr)
|
||||||
"<div class=\"boldredmsg\">%s n'a pas d'adresse e-mail !</div>"
|
flash(f"mail envoyé à {recipient_addr}")
|
||||||
% etud["nomprenom"]
|
|
||||||
) + htm
|
return True, I["filigranne"]
|
||||||
return h, I["filigranne"]
|
|
||||||
#
|
|
||||||
mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr)
|
|
||||||
emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % (
|
|
||||||
recipient_addr,
|
|
||||||
recipient_addr,
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
('<div class="head_message">Message mail envoyé à %s</div>' % (emaillink))
|
|
||||||
+ htm,
|
|
||||||
I["filigranne"],
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format)
|
raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format)
|
||||||
|
|
||||||
|
|
||||||
@ -1015,11 +952,16 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
|
|||||||
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
|
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
|
||||||
|
|
||||||
if intro_mail:
|
if intro_mail:
|
||||||
|
try:
|
||||||
hea = intro_mail % {
|
hea = intro_mail % {
|
||||||
"nomprenom": etud["nomprenom"],
|
"nomprenom": etud["nomprenom"],
|
||||||
"dept": dept,
|
"dept": dept,
|
||||||
"webmaster": webmaster,
|
"webmaster": webmaster,
|
||||||
}
|
}
|
||||||
|
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:
|
else:
|
||||||
hea = ""
|
hea = ""
|
||||||
|
|
||||||
@ -1035,81 +977,32 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
|
|||||||
bcc = copy_addr.strip()
|
bcc = copy_addr.strip()
|
||||||
else:
|
else:
|
||||||
bcc = ""
|
bcc = ""
|
||||||
msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc])
|
|
||||||
msg.body = hea
|
|
||||||
|
|
||||||
# Attach pdf
|
# Attach pdf
|
||||||
msg.attach(filename, scu.PDF_MIMETYPE, pdfdata)
|
|
||||||
log("mail bulletin a %s" % recipient_addr)
|
log("mail bulletin a %s" % recipient_addr)
|
||||||
email.send_message(msg)
|
email.send_email(
|
||||||
|
subject,
|
||||||
|
sender,
|
||||||
def _formsemestre_bulletinetud_header_html(
|
recipients,
|
||||||
etud,
|
bcc=[bcc],
|
||||||
etudid,
|
text_body=hea,
|
||||||
sem,
|
attachments=[
|
||||||
formsemestre_id=None,
|
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}
|
||||||
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",
|
"title": "Réglages bulletins",
|
||||||
"endpoint": "notes.formsemestre_edit_options",
|
"endpoint": "notes.formsemestre_edit_options",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
# "target_url": url_for(
|
# "target_url": url_for(
|
||||||
# "notes.formsemestre_bulletinetud",
|
# "notes.formsemestre_bulletinetud",
|
||||||
# scodoc_dept=g.scodoc_dept,
|
# scodoc_dept=g.scodoc_dept,
|
||||||
@ -1117,54 +1010,52 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
# etudid=etudid,
|
# etudid=etudid,
|
||||||
# ),
|
# ),
|
||||||
},
|
},
|
||||||
"enabled": (current_user.id in sem["responsables"])
|
"enabled": formsemestre.can_be_edited_by(current_user),
|
||||||
or current_user.has_permission(Permission.ScoImplement),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": 'Version papier (pdf, format "%s")'
|
"title": 'Version papier (pdf, format "%s")'
|
||||||
% sco_bulletins_generator.bulletin_get_class_name_displayed(
|
% sco_bulletins_generator.bulletin_get_class_name_displayed(
|
||||||
formsemestre_id
|
formsemestre.id
|
||||||
),
|
),
|
||||||
"endpoint": endpoint,
|
"endpoint": endpoint,
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
"version": version,
|
"version": version,
|
||||||
"format": "pdf",
|
"format": "pdf",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Envoi par mail à %s" % etud["email"],
|
"title": f"Envoi par mail à {etud_email}",
|
||||||
"endpoint": endpoint,
|
"endpoint": endpoint,
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
"version": version,
|
"version": version,
|
||||||
"format": "pdfmail",
|
"format": "pdfmail",
|
||||||
},
|
},
|
||||||
# possible slt si on a un mail...
|
# 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,
|
"endpoint": endpoint,
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
"version": version,
|
"version": version,
|
||||||
"format": "pdfmail",
|
"format": "pdfmail",
|
||||||
"prefer_mail_perso": 1,
|
"prefer_mail_perso": 1,
|
||||||
},
|
},
|
||||||
# possible slt si on a un mail...
|
# possible slt si on a un mail...
|
||||||
"enabled": etud["emailperso"]
|
"enabled": etud_perso and can_send_bulletin_by_mail(formsemestre.id),
|
||||||
and can_send_bulletin_by_mail(formsemestre_id),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Version json",
|
"title": "Version json",
|
||||||
"endpoint": endpoint,
|
"endpoint": endpoint,
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
"version": version,
|
"version": version,
|
||||||
"format": "json",
|
"format": "json",
|
||||||
},
|
},
|
||||||
@ -1173,8 +1064,8 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
"title": "Version XML",
|
"title": "Version XML",
|
||||||
"endpoint": endpoint,
|
"endpoint": endpoint,
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
"version": version,
|
"version": version,
|
||||||
"format": "xml",
|
"format": "xml",
|
||||||
},
|
},
|
||||||
@ -1183,20 +1074,20 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
"title": "Ajouter une appréciation",
|
"title": "Ajouter une appréciation",
|
||||||
"endpoint": "notes.appreciation_add_form",
|
"endpoint": "notes.appreciation_add_form",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
},
|
},
|
||||||
"enabled": (
|
"enabled": (
|
||||||
(current_user.id in sem["responsables"])
|
formsemestre.can_be_edited_by(current_user)
|
||||||
or (current_user.has_permission(Permission.ScoEtudInscrit))
|
or current_user.has_permission(Permission.ScoEtudInscrit)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Enregistrer un semestre effectué ailleurs",
|
"title": "Enregistrer un semestre effectué ailleurs",
|
||||||
"endpoint": "notes.formsemestre_ext_create_form",
|
"endpoint": "notes.formsemestre_ext_create_form",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
},
|
},
|
||||||
"enabled": current_user.has_permission(Permission.ScoImplement),
|
"enabled": current_user.has_permission(Permission.ScoImplement),
|
||||||
},
|
},
|
||||||
@ -1204,71 +1095,72 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
"title": "Enregistrer une validation d'UE antérieure",
|
"title": "Enregistrer une validation d'UE antérieure",
|
||||||
"endpoint": "notes.formsemestre_validate_previous_ue",
|
"endpoint": "notes.formsemestre_validate_previous_ue",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"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",
|
"title": "Enregistrer note d'une UE externe",
|
||||||
"endpoint": "notes.external_ue_create_form",
|
"endpoint": "notes.external_ue_create_form",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"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",
|
"title": "Entrer décisions jury",
|
||||||
"endpoint": "notes.formsemestre_validation_etud_form",
|
"endpoint": "notes.formsemestre_validation_etud_form",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"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",
|
"endpoint": "notes.formsemestre_pvjury_pdf",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
},
|
},
|
||||||
"enabled": True,
|
"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))
|
def _formsemestre_bulletinetud_header_html(
|
||||||
H.append("""</div></td>""")
|
etud,
|
||||||
H.append(
|
formsemestre: FormSemestre,
|
||||||
'<td> <a href="%s">%s</a></td>'
|
format=None,
|
||||||
% (
|
version=None,
|
||||||
url_for(
|
):
|
||||||
"notes.formsemestre_bulletinetud",
|
H = [
|
||||||
scodoc_dept=g.scodoc_dept,
|
html_sco_header.sco_header(
|
||||||
formsemestre_id=formsemestre_id,
|
page_title=f"Bulletin de {etud.nomprenom}",
|
||||||
etudid=etudid,
|
javascripts=[
|
||||||
format="pdf",
|
"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,
|
version=version,
|
||||||
),
|
),
|
||||||
scu.ICON_PDF,
|
scu=scu,
|
||||||
)
|
time=time,
|
||||||
)
|
version=version,
|
||||||
H.append("""</tr></table>""")
|
),
|
||||||
#
|
]
|
||||||
H.append(
|
return "\n".join(H)
|
||||||
"""</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)
|
|
||||||
|
@ -49,7 +49,14 @@ import traceback
|
|||||||
|
|
||||||
|
|
||||||
import reportlab
|
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 reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
@ -63,48 +70,16 @@ from app.scodoc import sco_pdf
|
|||||||
from app.scodoc.sco_pdf import PDFLOCK
|
from app.scodoc.sco_pdf import PDFLOCK
|
||||||
import sco_version
|
import sco_version
|
||||||
|
|
||||||
# Liste des types des classes de générateurs de bulletins PDF:
|
|
||||||
BULLETIN_CLASSES = collections.OrderedDict()
|
|
||||||
|
|
||||||
|
class BulletinGenerator:
|
||||||
def register_bulletin_class(klass):
|
|
||||||
BULLETIN_CLASSES[klass.__name__] = klass
|
|
||||||
|
|
||||||
|
|
||||||
def bulletin_class_descriptions():
|
|
||||||
return [x.description for x in BULLETIN_CLASSES.values()]
|
|
||||||
|
|
||||||
|
|
||||||
def bulletin_class_names():
|
|
||||||
return list(BULLETIN_CLASSES.keys())
|
|
||||||
|
|
||||||
|
|
||||||
def bulletin_default_class_name():
|
|
||||||
return bulletin_class_names()[0]
|
|
||||||
|
|
||||||
|
|
||||||
def bulletin_get_class(class_name):
|
|
||||||
return BULLETIN_CLASSES[class_name]
|
|
||||||
|
|
||||||
|
|
||||||
def bulletin_get_class_name_displayed(formsemestre_id):
|
|
||||||
"""Le nom du générateur utilisé, en clair"""
|
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
|
|
||||||
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
|
|
||||||
try:
|
|
||||||
gen_class = bulletin_get_class(bul_class_name)
|
|
||||||
return gen_class.description
|
|
||||||
except:
|
|
||||||
return "invalide ! (voir paramètres)"
|
|
||||||
|
|
||||||
|
|
||||||
class BulletinGenerator(object):
|
|
||||||
"Virtual superclass for PDF bulletin generators" ""
|
"Virtual superclass for PDF bulletin generators" ""
|
||||||
# Here some helper methods
|
# Here some helper methods
|
||||||
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
|
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
|
||||||
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
|
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
|
||||||
description = "superclass for bulletins" # description for user interface
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -151,7 +126,7 @@ class BulletinGenerator(object):
|
|||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
"""Build a filename to be proposed to the web client"""
|
"""Build a filename to be proposed to the web client"""
|
||||||
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
|
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):
|
def generate(self, format="", stand_alone=True):
|
||||||
"""Return bulletin in specified format"""
|
"""Return bulletin in specified format"""
|
||||||
@ -187,29 +162,47 @@ class BulletinGenerator(object):
|
|||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
formsemestre_id = self.infos["formsemestre_id"]
|
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
|
# partie haute du bulletin
|
||||||
objects = self.bul_title_pdf() # pylint: disable=no-member
|
story += self.bul_title_pdf() # pylint: disable=no-member
|
||||||
# table des notes
|
index_obj_debut = len(story)
|
||||||
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
|
|
||||||
|
|
||||||
|
# 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
|
# Réduit sur une page
|
||||||
objects = [KeepInFrame(0, 0, objects, mode="shrink")]
|
story = [marque_debut_bulletin, KeepInFrame(0, 0, story, mode="shrink")]
|
||||||
|
else:
|
||||||
|
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
|
||||||
|
story.insert(index_obj_debut, marque_debut_bulletin)
|
||||||
#
|
#
|
||||||
|
# objects.append(sco_pdf.FinBulletin())
|
||||||
if not stand_alone:
|
if not stand_alone:
|
||||||
objects.append(PageBreak()) # insert page break at end
|
if self.multi_pages:
|
||||||
return objects
|
# 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:
|
else:
|
||||||
# Generation du document PDF
|
# Generation du document PDF
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
report = io.BytesIO() # in-memory document, no disk file
|
report = io.BytesIO() # in-memory document, no disk file
|
||||||
document = sco_pdf.BaseDocTemplate(report)
|
document = sco_pdf.BaseDocTemplate(report)
|
||||||
document.addPageTemplates(
|
document.addPageTemplates(
|
||||||
sco_pdf.ScolarsPageTemplate(
|
sco_pdf.ScoDocPageTemplate(
|
||||||
document,
|
document,
|
||||||
author="%s %s (E. Viennet) [%s]"
|
author="%s %s (E. Viennet) [%s]"
|
||||||
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
|
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
|
||||||
@ -222,7 +215,7 @@ class BulletinGenerator(object):
|
|||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
document.build(objects)
|
document.build(story)
|
||||||
data = report.getvalue()
|
data = report.getvalue()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -253,7 +246,7 @@ class BulletinGenerator(object):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def make_formsemestre_bulletinetud(
|
def make_formsemestre_bulletinetud(
|
||||||
infos,
|
infos,
|
||||||
version="long", # short, long, selectedevals
|
version=None, # short, long, selectedevals
|
||||||
format="pdf", # html, pdf
|
format="pdf", # html, pdf
|
||||||
stand_alone=True,
|
stand_alone=True,
|
||||||
):
|
):
|
||||||
@ -265,14 +258,25 @@ def make_formsemestre_bulletinetud(
|
|||||||
"""
|
"""
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
version = version or "long"
|
||||||
if not version in scu.BULLETINS_VERSIONS:
|
if not version in scu.BULLETINS_VERSIONS:
|
||||||
raise ValueError("invalid version code !")
|
raise ValueError("invalid version code !")
|
||||||
|
|
||||||
formsemestre_id = infos["formsemestre_id"]
|
formsemestre_id = infos["formsemestre_id"]
|
||||||
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
|
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
|
||||||
try:
|
|
||||||
|
gen_class = None
|
||||||
|
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)
|
gen_class = bulletin_get_class(bul_class_name)
|
||||||
except:
|
|
||||||
|
if gen_class is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
|
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
|
||||||
)
|
)
|
||||||
@ -313,3 +317,52 @@ def make_formsemestre_bulletinetud(
|
|||||||
filename = bul_generator.get_filename()
|
filename = bul_generator.get_filename()
|
||||||
|
|
||||||
return data, filename
|
return data, filename
|
||||||
|
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
|
# Liste des types des classes de générateurs de bulletins PDF:
|
||||||
|
BULLETIN_CLASSES = collections.OrderedDict()
|
||||||
|
|
||||||
|
|
||||||
|
def register_bulletin_class(klass):
|
||||||
|
BULLETIN_CLASSES[klass.__name__] = klass
|
||||||
|
|
||||||
|
|
||||||
|
def bulletin_class_descriptions():
|
||||||
|
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]:
|
||||||
|
"Liste les noms des classes de bulletins à présenter à l'utilisateur"
|
||||||
|
return [
|
||||||
|
class_name
|
||||||
|
for class_name in BULLETIN_CLASSES
|
||||||
|
if BULLETIN_CLASSES[class_name].list_in_menu
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def bulletin_default_class_name():
|
||||||
|
return bulletin_class_names()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def bulletin_get_class(class_name: str) -> BulletinGenerator:
|
||||||
|
"""La class de génération de bulletin de ce nom,
|
||||||
|
ou None si pas trouvée
|
||||||
|
"""
|
||||||
|
return BULLETIN_CLASSES.get(class_name)
|
||||||
|
|
||||||
|
|
||||||
|
def bulletin_get_class_name_displayed(formsemestre_id):
|
||||||
|
"""Le nom du générateur utilisé, en clair"""
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
|
||||||
|
gen_class = bulletin_get_class(bul_class_name)
|
||||||
|
if gen_class is None:
|
||||||
|
return "invalide ! (voir paramètres)"
|
||||||
|
return gen_class.description
|
||||||
|
@ -33,7 +33,7 @@ import json
|
|||||||
|
|
||||||
from app.but import bulletin_but
|
from app.but import bulletin_but
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||||||
if not published:
|
if not published:
|
||||||
return d # stop !
|
return d # stop !
|
||||||
|
|
||||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||||
if etat_inscription != scu.INSCRIT:
|
if etat_inscription != scu.INSCRIT:
|
||||||
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
|
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
|
||||||
return d
|
return d
|
||||||
@ -209,7 +209,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||||||
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
||||||
titre=scu.quote_xml_attr(ue["titre"]),
|
titre=scu.quote_xml_attr(ue["titre"]),
|
||||||
note=dict(
|
note=dict(
|
||||||
value=scu.fmt_note(ue_status["cur_moy_ue"]),
|
value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""),
|
||||||
min=scu.fmt_note(ue["min"]),
|
min=scu.fmt_note(ue["min"]),
|
||||||
max=scu.fmt_note(ue["max"]),
|
max=scu.fmt_note(ue["max"]),
|
||||||
moy=scu.fmt_note(
|
moy=scu.fmt_note(
|
||||||
|
@ -34,17 +34,19 @@
|
|||||||
CE FORMAT N'EVOLUERA PLUS ET EST CONSIDERE COMME OBSOLETE.
|
CE FORMAT N'EVOLUERA PLUS ET EST CONSIDERE COMME OBSOLETE.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from reportlab.lib.colors import Color, blue
|
||||||
|
from reportlab.lib.units import cm, mm
|
||||||
|
from reportlab.platypus import Paragraph, Spacer, Table
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc import sco_pdf
|
|
||||||
from app.scodoc.sco_pdf import Color, Paragraph, Spacer, Table
|
|
||||||
from app.scodoc.sco_pdf import blue, cm, mm
|
|
||||||
from app.scodoc.sco_pdf import SU
|
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
from app.scodoc import sco_bulletins_generator
|
from app.scodoc import sco_bulletins_generator
|
||||||
from app.scodoc import sco_bulletins_pdf
|
from app.scodoc import sco_bulletins_pdf
|
||||||
|
from app.scodoc import sco_formsemestre
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc import sco_pdf
|
||||||
|
from app.scodoc.sco_pdf import SU
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
|
# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
|
||||||
class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
||||||
@ -476,8 +478,8 @@ def _bulletin_pdf_table_legacy(I, version="long"):
|
|||||||
else:
|
else:
|
||||||
rang_minmax = mod["mod_rang_txt"] # vide si pas option rang
|
rang_minmax = mod["mod_rang_txt"] # vide si pas option rang
|
||||||
t = [
|
t = [
|
||||||
mod["code"],
|
mod["code"] or "",
|
||||||
mod["name"],
|
mod["name"] or "",
|
||||||
rang_minmax,
|
rang_minmax,
|
||||||
mod["mod_moy_txt"],
|
mod["mod_moy_txt"],
|
||||||
mod["mod_coef_txt"],
|
mod["mod_coef_txt"],
|
||||||
|
@ -51,37 +51,39 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import io
|
import io
|
||||||
|
import pprint
|
||||||
|
import pydoc
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from pydoc import html
|
|
||||||
|
|
||||||
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
|
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
from app import log, ScoValueError
|
from app import log, ScoValueError
|
||||||
|
from app.models import FormSemestre
|
||||||
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_pdf
|
from app.scodoc import sco_pdf
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
import sco_version
|
|
||||||
from app.scodoc.sco_logos import find_logo
|
from app.scodoc.sco_logos import find_logo
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
import sco_version
|
||||||
|
|
||||||
|
|
||||||
def pdfassemblebulletins(
|
def assemble_bulletins_pdf(
|
||||||
formsemestre_id,
|
formsemestre_id: int,
|
||||||
objects,
|
story: list,
|
||||||
bul_title,
|
bul_title: str,
|
||||||
infos,
|
infos,
|
||||||
pagesbookmarks,
|
pagesbookmarks=None,
|
||||||
filigranne=None,
|
filigranne=None,
|
||||||
server_name="",
|
server_name="",
|
||||||
):
|
):
|
||||||
"generate PDF document from a list of PLATYPUS objects"
|
"Generate PDF document from a story (list of PLATYPUS objects)."
|
||||||
if not objects:
|
if not story:
|
||||||
return ""
|
return ""
|
||||||
# Paramètres de mise en page
|
# Paramètres de mise en page
|
||||||
margins = (
|
margins = (
|
||||||
@ -90,11 +92,10 @@ def pdfassemblebulletins(
|
|||||||
sco_preferences.get_preference("right_margin", formsemestre_id),
|
sco_preferences.get_preference("right_margin", formsemestre_id),
|
||||||
sco_preferences.get_preference("bottom_margin", formsemestre_id),
|
sco_preferences.get_preference("bottom_margin", formsemestre_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
report = io.BytesIO() # in-memory document, no disk file
|
report = io.BytesIO() # in-memory document, no disk file
|
||||||
document = BaseDocTemplate(report)
|
document = sco_pdf.BulletinDocTemplate(report)
|
||||||
document.addPageTemplates(
|
document.addPageTemplates(
|
||||||
sco_pdf.ScolarsPageTemplate(
|
sco_pdf.ScoDocPageTemplate(
|
||||||
document,
|
document,
|
||||||
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION),
|
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION),
|
||||||
title="Bulletin %s" % bul_title,
|
title="Bulletin %s" % bul_title,
|
||||||
@ -106,7 +107,7 @@ def pdfassemblebulletins(
|
|||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
document.build(objects)
|
document.multiBuild(story)
|
||||||
data = report.getvalue()
|
data = report.getvalue()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -118,7 +119,8 @@ def replacement_function(match):
|
|||||||
if logo is not None:
|
if logo is not None:
|
||||||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||||
raise ScoValueError(
|
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))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -139,7 +141,11 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
|||||||
cdict
|
cdict
|
||||||
) # note that None values are mapped to empty strings
|
) # note that None values are mapped to empty strings
|
||||||
except:
|
except:
|
||||||
log("process_field: invalid format=%s" % field)
|
log(
|
||||||
|
f"""process_field: invalid format. field={field!r}
|
||||||
|
values={pprint.pformat(cdict)}
|
||||||
|
"""
|
||||||
|
)
|
||||||
text = (
|
text = (
|
||||||
"<para><i>format invalide !</i></para><para>"
|
"<para><i>format invalide !</i></para><para>"
|
||||||
+ traceback.format_exc()
|
+ traceback.format_exc()
|
||||||
@ -171,30 +177,23 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
|||||||
|
|
||||||
|
|
||||||
def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
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
|
from app.scodoc import sco_bulletins
|
||||||
|
|
||||||
cached = sco_cache.SemBulletinsPDFCache.get(str(formsemestre_id) + "_" + version)
|
cached = sco_cache.SemBulletinsPDFCache.get(str(formsemestre_id) + "_" + version)
|
||||||
if cached:
|
if cached:
|
||||||
return cached[1], cached[0]
|
return cached[1], cached[0]
|
||||||
fragments = []
|
fragments = []
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
||||||
# Make each bulletin
|
# Make each bulletin
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etudids, get_sexnom
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
bookmarks = {}
|
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
|
||||||
filigrannes = {}
|
frag, _ = sco_bulletins.do_formsemestre_bulletinetud(
|
||||||
i = 1
|
formsemestre,
|
||||||
for etudid in nt.get_etudids():
|
etud.id,
|
||||||
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
|
|
||||||
formsemestre_id,
|
|
||||||
etudid,
|
|
||||||
format="pdfpart",
|
format="pdfpart",
|
||||||
version=version,
|
version=version,
|
||||||
)
|
)
|
||||||
fragments += frag
|
fragments += frag
|
||||||
filigrannes[i] = filigranne
|
|
||||||
bookmarks[i] = scu.suppress_accents(nt.get_sexnom(etudid))
|
|
||||||
i = i + 1
|
|
||||||
#
|
#
|
||||||
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
|
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
|
||||||
if request:
|
if request:
|
||||||
@ -203,20 +202,18 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
|||||||
server_name = ""
|
server_name = ""
|
||||||
try:
|
try:
|
||||||
sco_pdf.PDFLOCK.acquire()
|
sco_pdf.PDFLOCK.acquire()
|
||||||
pdfdoc = pdfassemblebulletins(
|
pdfdoc = assemble_bulletins_pdf(
|
||||||
formsemestre_id,
|
formsemestre_id,
|
||||||
fragments,
|
fragments,
|
||||||
sem["titremois"],
|
formsemestre.titre_mois(),
|
||||||
infos,
|
infos,
|
||||||
bookmarks,
|
|
||||||
filigranne=filigrannes,
|
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
sco_pdf.PDFLOCK.release()
|
sco_pdf.PDFLOCK.release()
|
||||||
#
|
#
|
||||||
dt = time.strftime("%Y-%m-%d")
|
date_iso = time.strftime("%Y-%m-%d")
|
||||||
filename = "bul-%s-%s.pdf" % (sem["titre_num"], dt)
|
filename = "bul-%s-%s.pdf" % (formsemestre.titre_num(), date_iso)
|
||||||
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
|
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
|
||||||
# fill cache
|
# fill cache
|
||||||
sco_cache.SemBulletinsPDFCache.set(
|
sco_cache.SemBulletinsPDFCache.set(
|
||||||
@ -235,8 +232,9 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
|||||||
filigrannes = {}
|
filigrannes = {}
|
||||||
i = 1
|
i = 1
|
||||||
for sem in etud["sems"]:
|
for sem in etud["sems"]:
|
||||||
|
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||||
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
|
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
|
||||||
sem["formsemestre_id"],
|
formsemestre,
|
||||||
etudid,
|
etudid,
|
||||||
format="pdfpart",
|
format="pdfpart",
|
||||||
version=version,
|
version=version,
|
||||||
@ -252,7 +250,7 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
|||||||
server_name = ""
|
server_name = ""
|
||||||
try:
|
try:
|
||||||
sco_pdf.PDFLOCK.acquire()
|
sco_pdf.PDFLOCK.acquire()
|
||||||
pdfdoc = pdfassemblebulletins(
|
pdfdoc = assemble_bulletins_pdf(
|
||||||
None,
|
None,
|
||||||
fragments,
|
fragments,
|
||||||
etud["nomprenom"],
|
etud["nomprenom"],
|
||||||
@ -271,3 +269,16 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return pdfdoc, filename
|
return pdfdoc, filename
|
||||||
|
|
||||||
|
|
||||||
|
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
|
||||||
|
"""Texte à placer en "filigranne" sur le bulletin pdf"""
|
||||||
|
if etud_etat == scu.DEMISSION:
|
||||||
|
return "Démission"
|
||||||
|
elif etud_etat == sco_codes_parcours.DEF:
|
||||||
|
return "Défaillant"
|
||||||
|
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[
|
||||||
|
"bul_show_temporary_forced"
|
||||||
|
]:
|
||||||
|
return prefs["bul_temporary_txt"]
|
||||||
|
return ""
|
||||||
|
@ -46,10 +46,13 @@ de la forme %(XXX)s sont remplacées par la valeur de XXX, pour XXX dans:
|
|||||||
Balises img: actuellement interdites.
|
Balises img: actuellement interdites.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table
|
||||||
|
from reportlab.lib.units import cm, mm
|
||||||
|
from reportlab.lib.colors import Color, blue
|
||||||
|
from app.models import FormSemestre
|
||||||
|
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
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.sco_pdf import SU
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
@ -66,12 +69,13 @@ from app.scodoc import sco_groups
|
|||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import gen_tables
|
from app.scodoc import gen_tables
|
||||||
|
|
||||||
# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
|
# Important: Le nom de la classe ne doit pas changer (bien le choisir),
|
||||||
|
# car il sera stocké en base de données (dans les préférences)
|
||||||
class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
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
|
description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc
|
||||||
supported_formats = ["html", "pdf"]
|
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.
|
"""Génère la partie "titre" du bulletin de notes.
|
||||||
Renvoie une liste d'objets platypus
|
Renvoie une liste d'objets platypus
|
||||||
"""
|
"""
|
||||||
@ -113,11 +117,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
- en PDF: une liste d'objets platypus
|
- en PDF: une liste d'objets platypus
|
||||||
"""
|
"""
|
||||||
H = [] # html
|
H = [] # html
|
||||||
Op = [] # objets platypus
|
story = [] # objets platypus
|
||||||
# ----- ABSENCES
|
# ----- ABSENCES
|
||||||
if self.preferences["bul_show_abs"]:
|
if self.preferences["bul_show_abs"]:
|
||||||
nbabs = self.infos["nbabs"]
|
nbabs = self.infos["nbabs"]
|
||||||
Op.append(Spacer(1, 2 * mm))
|
story.append(Spacer(1, 2 * mm))
|
||||||
if nbabs:
|
if nbabs:
|
||||||
H.append(
|
H.append(
|
||||||
"""<p class="bul_abs">
|
"""<p class="bul_abs">
|
||||||
@ -128,7 +132,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
"""
|
"""
|
||||||
% self.infos
|
% self.infos
|
||||||
)
|
)
|
||||||
Op.append(
|
story.append(
|
||||||
Paragraph(
|
Paragraph(
|
||||||
SU(
|
SU(
|
||||||
"%(nbabs)s absences (1/2 journées), dont %(nbabsjust)s justifiées."
|
"%(nbabs)s absences (1/2 journées), dont %(nbabsjust)s justifiées."
|
||||||
@ -139,7 +143,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append("""<p class="bul_abs">Pas d'absences signalées.</p>""")
|
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
|
# ---- APPRECIATIONS
|
||||||
# le dir. des etud peut ajouter des appreciations,
|
# le dir. des etud peut ajouter des appreciations,
|
||||||
@ -166,10 +170,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
% self.infos
|
% self.infos
|
||||||
)
|
)
|
||||||
H.append("</div>")
|
H.append("</div>")
|
||||||
# Appreciations sur PDF:
|
# Appréciations sur PDF:
|
||||||
if self.infos.get("appreciations_list", False):
|
if self.infos.get("appreciations_list", False):
|
||||||
Op.append(Spacer(1, 3 * mm))
|
story.append(Spacer(1, 3 * mm))
|
||||||
Op.append(
|
story.append(
|
||||||
Paragraph(
|
Paragraph(
|
||||||
SU("Appréciation : " + "\n".join(self.infos["appreciations_txt"])),
|
SU("Appréciation : " + "\n".join(self.infos["appreciations_txt"])),
|
||||||
self.CellStyle,
|
self.CellStyle,
|
||||||
@ -178,7 +182,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
|
|
||||||
# ----- DECISION JURY
|
# ----- DECISION JURY
|
||||||
if self.preferences["bul_show_decision"]:
|
if self.preferences["bul_show_decision"]:
|
||||||
Op += sco_bulletins_pdf.process_field(
|
story += sco_bulletins_pdf.process_field(
|
||||||
self.preferences["bul_pdf_caption"],
|
self.preferences["bul_pdf_caption"],
|
||||||
self.infos,
|
self.infos,
|
||||||
self.FieldStyle,
|
self.FieldStyle,
|
||||||
@ -194,7 +198,12 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
|
|
||||||
# -----
|
# -----
|
||||||
if format == "pdf":
|
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":
|
elif format == "html":
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
@ -263,12 +272,12 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def build_bulletin_table(self):
|
def build_bulletin_table(self):
|
||||||
"""Génère la table centrale du bulletin de notes
|
"""Génère la table centrale du bulletin de notes classique (pas BUT)
|
||||||
Renvoie: colkeys, P, pdf_style, colWidths
|
Renvoie: col_keys, P, pdf_style, col_widths
|
||||||
- colkeys: nom des colonnes de la table (clés)
|
- col_keys: nom des colonnes de la table (clés)
|
||||||
- table (liste de dicts de chaines de caracteres)
|
- table: liste de dicts de chaines de caractères
|
||||||
- style (commandes table Platypus)
|
- pdf_style: commandes table Platypus
|
||||||
- largeurs de colonnes pour PDF
|
- col_widths: largeurs de colonnes pour PDF
|
||||||
"""
|
"""
|
||||||
I = self.infos
|
I = self.infos
|
||||||
P = [] # elems pour générer table avec gen_table (liste de dicts)
|
P = [] # elems pour générer table avec gen_table (liste de dicts)
|
||||||
@ -284,28 +293,28 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
)
|
)
|
||||||
with_col_moypromo = prefs["bul_show_moypromo"]
|
with_col_moypromo = prefs["bul_show_moypromo"]
|
||||||
with_col_rang = prefs["bul_show_rangs"]
|
with_col_rang = prefs["bul_show_rangs"]
|
||||||
with_col_coef = prefs["bul_show_coef"]
|
with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"]
|
||||||
with_col_ects = prefs["bul_show_ects"]
|
with_col_ects = prefs["bul_show_ects"]
|
||||||
|
|
||||||
colkeys = ["titre", "module"] # noms des colonnes à afficher
|
col_keys = ["titre", "module"] # noms des colonnes à afficher
|
||||||
if with_col_rang:
|
if with_col_rang:
|
||||||
colkeys += ["rang"]
|
col_keys += ["rang"]
|
||||||
if with_col_minmax:
|
if with_col_minmax:
|
||||||
colkeys += ["min"]
|
col_keys += ["min"]
|
||||||
if with_col_moypromo:
|
if with_col_moypromo:
|
||||||
colkeys += ["moy"]
|
col_keys += ["moy"]
|
||||||
if with_col_minmax:
|
if with_col_minmax:
|
||||||
colkeys += ["max"]
|
col_keys += ["max"]
|
||||||
colkeys += ["note"]
|
col_keys += ["note"]
|
||||||
if with_col_coef:
|
if with_col_coef:
|
||||||
colkeys += ["coef"]
|
col_keys += ["coef"]
|
||||||
if with_col_ects:
|
if with_col_ects:
|
||||||
colkeys += ["ects"]
|
col_keys += ["ects"]
|
||||||
if with_col_abs:
|
if with_col_abs:
|
||||||
colkeys += ["abs"]
|
col_keys += ["abs"]
|
||||||
colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus)
|
colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus)
|
||||||
i = 0
|
i = 0
|
||||||
for k in colkeys:
|
for k in col_keys:
|
||||||
colidx[k] = i
|
colidx[k] = i
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
@ -313,7 +322,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm
|
bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm
|
||||||
else:
|
else:
|
||||||
bul_pdf_mod_colwidth = None
|
bul_pdf_mod_colwidth = None
|
||||||
colWidths = {
|
col_widths = {
|
||||||
"titre": None,
|
"titre": None,
|
||||||
"module": bul_pdf_mod_colwidth,
|
"module": bul_pdf_mod_colwidth,
|
||||||
"min": 1.5 * cm,
|
"min": 1.5 * cm,
|
||||||
@ -373,10 +382,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
t = {
|
t = {
|
||||||
"titre": "Moyenne générale:",
|
"titre": "Moyenne générale:",
|
||||||
"rang": I["rang_nt"],
|
"rang": I["rang_nt"],
|
||||||
"note": I["moy_gen"],
|
"note": I.get("moy_gen", "-"),
|
||||||
"min": I["moy_min"],
|
"min": I.get("moy_min", "-"),
|
||||||
"max": I["moy_max"],
|
"max": I.get("moy_max", "-"),
|
||||||
"moy": I["moy_moy"],
|
"moy": I.get("moy_moy", "-"),
|
||||||
"abs": "%s / %s" % (nbabs, nbabsjust),
|
"abs": "%s / %s" % (nbabs, nbabsjust),
|
||||||
"_css_row_class": "notes_bulletin_row_gen",
|
"_css_row_class": "notes_bulletin_row_gen",
|
||||||
"_titre_colspan": 2,
|
"_titre_colspan": 2,
|
||||||
@ -409,7 +418,8 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
# Chaque UE:
|
# Chaque UE:
|
||||||
for ue in I["ues"]:
|
for ue in I["ues"]:
|
||||||
ue_type = None
|
ue_type = None
|
||||||
coef_ue = ue["coef_ue_txt"]
|
coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else ""
|
||||||
|
|
||||||
ue_descr = ue["ue_descr_txt"]
|
ue_descr = ue["ue_descr_txt"]
|
||||||
rowstyle = ""
|
rowstyle = ""
|
||||||
plusminus = minuslink #
|
plusminus = minuslink #
|
||||||
@ -471,9 +481,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
ects_txt = str(int(ue["ects"]))
|
ects_txt = str(int(ue["ects"]))
|
||||||
except:
|
except:
|
||||||
ects_txt = "-"
|
ects_txt = "-"
|
||||||
|
titre = f"{ue['acronyme'] or ''} {ue['titre'] or ''}"
|
||||||
t = {
|
t = {
|
||||||
"titre": ue["acronyme"] + " " + ue["titre"],
|
"titre": titre,
|
||||||
"_titre_html": minuslink + ue["acronyme"] + " " + ue["titre"],
|
"_titre_html": minuslink + titre,
|
||||||
"_titre_colspan": 2,
|
"_titre_colspan": 2,
|
||||||
"module": ue["titre"],
|
"module": ue["titre"],
|
||||||
"rang": ue_descr,
|
"rang": ue_descr,
|
||||||
@ -540,7 +551,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
|
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
|
||||||
]
|
]
|
||||||
#
|
#
|
||||||
return colkeys, P, pdf_style, colWidths
|
return col_keys, P, pdf_style, col_widths
|
||||||
|
|
||||||
def _list_modules(
|
def _list_modules(
|
||||||
self,
|
self,
|
||||||
@ -591,7 +602,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
"_titre_colspan": 2,
|
"_titre_colspan": 2,
|
||||||
"rang": mod["mod_rang_txt"], # vide si pas option rang
|
"rang": mod["mod_rang_txt"], # vide si pas option rang
|
||||||
"note": mod["mod_moy_txt"],
|
"note": mod["mod_moy_txt"],
|
||||||
"coef": mod["mod_coef_txt"],
|
"coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "",
|
||||||
"abs": mod.get(
|
"abs": mod.get(
|
||||||
"mod_abs_txt", ""
|
"mod_abs_txt", ""
|
||||||
), # absent si pas option show abs module
|
), # absent si pas option show abs module
|
||||||
@ -655,7 +666,9 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
eval_style = ""
|
eval_style = ""
|
||||||
t = {
|
t = {
|
||||||
"module": '<bullet indent="2mm">•</bullet> ' + e["name"],
|
"module": '<bullet indent="2mm">•</bullet> ' + e["name"],
|
||||||
"coef": "<i>" + e["coef_txt"] + "</i>",
|
"coef": ("<i>" + e["coef_txt"] + "</i>")
|
||||||
|
if prefs["bul_show_coef"]
|
||||||
|
else "",
|
||||||
"_hidden": hidden,
|
"_hidden": hidden,
|
||||||
"_module_target": e["target_html"],
|
"_module_target": e["target_html"],
|
||||||
# '_module_help' : ,
|
# '_module_help' : ,
|
||||||
|
@ -32,16 +32,12 @@ On redéfini la table centrale du bulletin de note et hérite de tout le reste d
|
|||||||
|
|
||||||
E. Viennet, juillet 2011
|
E. Viennet, juillet 2011
|
||||||
"""
|
"""
|
||||||
|
from reportlab.lib.colors import Color
|
||||||
|
from reportlab.lib.units import mm
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
from app.scodoc.sco_pdf import blue, cm, mm
|
|
||||||
from app.scodoc.sco_pdf import Color, Paragraph, Spacer, Table
|
|
||||||
|
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
|
|
||||||
from app.scodoc import sco_bulletins_generator
|
|
||||||
from app.scodoc import sco_bulletins_standard
|
from app.scodoc import sco_bulletins_standard
|
||||||
from app.scodoc import gen_tables
|
from app.scodoc import sco_preferences
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class BulletinGeneratorUCAC(sco_bulletins_standard.BulletinGeneratorStandard):
|
class BulletinGeneratorUCAC(sco_bulletins_standard.BulletinGeneratorStandard):
|
||||||
|
@ -45,7 +45,7 @@ from xml.etree import ElementTree
|
|||||||
from xml.etree.ElementTree import Element
|
from xml.etree.ElementTree import Element
|
||||||
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import log
|
from app import log
|
||||||
@ -217,7 +217,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||||||
)
|
)
|
||||||
doc.append(x_ue)
|
doc.append(x_ue)
|
||||||
if ue["type"] != sco_codes_parcours.UE_SPORT:
|
if ue["type"] != sco_codes_parcours.UE_SPORT:
|
||||||
v = ue_status["cur_moy_ue"]
|
v = ue_status["cur_moy_ue"] if ue_status else ""
|
||||||
else:
|
else:
|
||||||
v = nt.bonus[etudid] if nt.bonus is not None else 0.0
|
v = nt.bonus[etudid] if nt.bonus is not None else 0.0
|
||||||
x_ue.append(
|
x_ue.append(
|
||||||
@ -252,7 +252,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||||||
x_mod = Element(
|
x_mod = Element(
|
||||||
"module",
|
"module",
|
||||||
id=str(modimpl["moduleimpl_id"]),
|
id=str(modimpl["moduleimpl_id"]),
|
||||||
code=str(mod["code"]),
|
code=str(mod["code"] or ""),
|
||||||
coefficient=str(mod["coefficient"]),
|
coefficient=str(mod["coefficient"]),
|
||||||
numero=str(mod["numero"]),
|
numero=str(mod["numero"]),
|
||||||
titre=scu.quote_xml_attr(mod["titre"]),
|
titre=scu.quote_xml_attr(mod["titre"]),
|
||||||
|
@ -33,17 +33,12 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# API ScoDoc8 pour les caches:
|
# API pour les caches:
|
||||||
# sco_cache.NotesTableCache.get( formsemestre_id)
|
# sco_cache.MyCache.get( formsemestre_id)
|
||||||
# => sco_cache.NotesTableCache.get(formsemestre_id)
|
# => sco_cache.MyCache.get(formsemestre_id)
|
||||||
#
|
#
|
||||||
# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None)
|
# sco_cache.MyCache.delete(formsemestre_id)
|
||||||
# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False)
|
# sco_cache.MyCache.delete_many(formsemestre_id_list)
|
||||||
#
|
|
||||||
#
|
|
||||||
# Nouvelles fonctions:
|
|
||||||
# sco_cache.NotesTableCache.delete(formsemestre_id)
|
|
||||||
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
|
|
||||||
#
|
#
|
||||||
# Bulletins PDF:
|
# Bulletins PDF:
|
||||||
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
|
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
|
||||||
@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache):
|
|||||||
duration = 12 * 60 * 60 # ttl 12h
|
duration = 12 * 60 * 60 # ttl 12h
|
||||||
|
|
||||||
|
|
||||||
class NotesTableCache(ScoDocCache):
|
|
||||||
"""Cache pour les NotesTable
|
|
||||||
Clé: formsemestre_id
|
|
||||||
Valeur: NotesTable instance
|
|
||||||
"""
|
|
||||||
|
|
||||||
prefix = "NT"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, formsemestre_id, compute=True):
|
|
||||||
"""Returns NotesTable for this formsemestre
|
|
||||||
Search in local cache (g.nt_cache) or global app cache (eg REDIS)
|
|
||||||
If not in cache:
|
|
||||||
If compute is True, build it and cache it
|
|
||||||
Else return None
|
|
||||||
"""
|
|
||||||
# try local cache (same request)
|
|
||||||
if not hasattr(g, "nt_cache"):
|
|
||||||
g.nt_cache = {}
|
|
||||||
else:
|
|
||||||
if formsemestre_id in g.nt_cache:
|
|
||||||
return g.nt_cache[formsemestre_id]
|
|
||||||
# try REDIS
|
|
||||||
key = cls._get_key(formsemestre_id)
|
|
||||||
nt = CACHE.get(key)
|
|
||||||
if nt:
|
|
||||||
g.nt_cache[formsemestre_id] = nt # cache locally (same request)
|
|
||||||
return nt
|
|
||||||
if not compute:
|
|
||||||
return None
|
|
||||||
# Recompute requested table:
|
|
||||||
from app.scodoc import notes_table
|
|
||||||
|
|
||||||
t0 = time.time()
|
|
||||||
nt = notes_table.NotesTable(formsemestre_id)
|
|
||||||
t1 = time.time()
|
|
||||||
_ = cls.set(formsemestre_id, nt) # cache in REDIS
|
|
||||||
t2 = time.time()
|
|
||||||
log(f"cached formsemestre_id={formsemestre_id} ({(t1-t0):g}s +{(t2-t1):g}s)")
|
|
||||||
g.nt_cache[formsemestre_id] = nt
|
|
||||||
return nt
|
|
||||||
|
|
||||||
|
|
||||||
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
|
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
|
||||||
formsemestre_id=None, pdfonly=False
|
formsemestre_id=None, pdfonly=False
|
||||||
):
|
):
|
||||||
@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||||||
|
|
||||||
if not pdfonly:
|
if not pdfonly:
|
||||||
# Delete cached notes and evaluations
|
# Delete cached notes and evaluations
|
||||||
NotesTableCache.delete_many(formsemestre_ids)
|
|
||||||
if formsemestre_id:
|
if formsemestre_id:
|
||||||
for fid in formsemestre_ids:
|
for fid in formsemestre_ids:
|
||||||
EvaluationCache.invalidate_sem(fid)
|
EvaluationCache.invalidate_sem(fid)
|
||||||
if hasattr(g, "nt_cache") and fid in g.nt_cache:
|
if (
|
||||||
del g.nt_cache[fid]
|
hasattr(g, "formsemestre_results_cache")
|
||||||
|
and fid in g.formsemestre_results_cache
|
||||||
|
):
|
||||||
|
del g.formsemestre_results_cache[fid]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# optimization when we invalidate all evaluations:
|
# optimization when we invalidate all evaluations:
|
||||||
EvaluationCache.invalidate_all_sems()
|
EvaluationCache.invalidate_all_sems()
|
||||||
if hasattr(g, "nt_cache"):
|
if hasattr(g, "formsemestre_results_cache"):
|
||||||
del g.nt_cache
|
del g.formsemestre_results_cache
|
||||||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||||
|
|
||||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
|
||||||
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
||||||
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
||||||
|
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||||
|
|
||||||
|
|
||||||
class DefferedSemCacheManager:
|
class DefferedSemCacheManager:
|
||||||
|
@ -81,11 +81,11 @@ UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
|||||||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||||
|
|
||||||
|
|
||||||
def UE_is_fondamentale(ue_type):
|
def ue_is_fondamentale(ue_type):
|
||||||
return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
|
return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
|
||||||
|
|
||||||
|
|
||||||
def UE_is_professionnelle(ue_type):
|
def ue_is_professionnelle(ue_type):
|
||||||
return (
|
return (
|
||||||
ue_type == UE_PROFESSIONNELLE
|
ue_type == UE_PROFESSIONNELLE
|
||||||
) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
|
) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
|
||||||
@ -211,7 +211,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
|
|||||||
|
|
||||||
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
||||||
|
|
||||||
# Regles gestion parcours
|
# Règles gestion parcours
|
||||||
class DUTRule(object):
|
class DUTRule(object):
|
||||||
def __init__(self, rule_id, premise, conclusion):
|
def __init__(self, rule_id, premise, conclusion):
|
||||||
self.rule_id = rule_id
|
self.rule_id = rule_id
|
||||||
@ -222,13 +222,13 @@ class DUTRule(object):
|
|||||||
def match(self, state):
|
def match(self, state):
|
||||||
"True if state match rule premise"
|
"True if state match rule premise"
|
||||||
assert len(state) == len(self.premise)
|
assert len(state) == len(self.premise)
|
||||||
for i in range(len(state)):
|
for i, stat in enumerate(state):
|
||||||
prem = self.premise[i]
|
prem = self.premise[i]
|
||||||
if isinstance(prem, (list, tuple)):
|
if isinstance(prem, (list, tuple)):
|
||||||
if not state[i] in prem:
|
if not stat in prem:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if prem != ALL and prem != state[i]:
|
if prem not in (ALL, stat):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -244,6 +244,7 @@ class TypeParcours(object):
|
|||||||
COMPENSATION_UE = True # inutilisé
|
COMPENSATION_UE = True # inutilisé
|
||||||
BARRE_MOY = 10.0
|
BARRE_MOY = 10.0
|
||||||
BARRE_UE_DEFAULT = 8.0
|
BARRE_UE_DEFAULT = 8.0
|
||||||
|
BARRE_UE_DISPLAY_WARNING = 8.0
|
||||||
BARRE_UE = {}
|
BARRE_UE = {}
|
||||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||||
NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE # barre sur UE
|
NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE # barre sur UE
|
||||||
@ -282,7 +283,7 @@ class TypeParcours(object):
|
|||||||
return [
|
return [
|
||||||
ue_status
|
ue_status
|
||||||
for ue_status in ues_status
|
for ue_status in ues_status
|
||||||
if ue_status["coef_ue"] > 0
|
if ue_status["coef_ue"]
|
||||||
and isinstance(ue_status["moy"], float)
|
and isinstance(ue_status["moy"], float)
|
||||||
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
|
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
|
||||||
]
|
]
|
||||||
@ -587,7 +588,7 @@ class ParcoursILEPS(TypeParcours):
|
|||||||
# SESSION_ABBRV = 'A' # A1, A2, ...
|
# SESSION_ABBRV = 'A' # A1, A2, ...
|
||||||
COMPENSATION_UE = False
|
COMPENSATION_UE = False
|
||||||
UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
|
UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
|
||||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE]
|
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE, UE_SPORT]
|
||||||
# Barre moy gen. pour validation semestre:
|
# Barre moy gen. pour validation semestre:
|
||||||
BARRE_MOY = 10.0
|
BARRE_MOY = 10.0
|
||||||
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")
|
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")
|
||||||
|
@ -46,7 +46,9 @@ CONFIG.LOGO_HEADER_HEIGHT = 28
|
|||||||
#
|
#
|
||||||
# server_url: URL du serveur ScoDoc
|
# server_url: URL du serveur ScoDoc
|
||||||
# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir sco_version.py)
|
# 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 -------------
|
# ------------- Capitalisation des UEs -------------
|
||||||
|
@ -65,7 +65,7 @@ def formsemestre_table_estim_cost(
|
|||||||
Mod = M["module"]
|
Mod = M["module"]
|
||||||
T.append(
|
T.append(
|
||||||
{
|
{
|
||||||
"code": Mod["code"],
|
"code": Mod["code"] or "",
|
||||||
"titre": Mod["titre"],
|
"titre": Mod["titre"],
|
||||||
"heures_cours": Mod["heures_cours"],
|
"heures_cours": Mod["heures_cours"],
|
||||||
"heures_td": Mod["heures_td"] * n_group_td,
|
"heures_td": Mod["heures_td"] * n_group_td,
|
||||||
|
@ -31,15 +31,17 @@ Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudi
|
|||||||
import http
|
import http
|
||||||
from flask import url_for, g, request
|
from flask import url_for, g, request
|
||||||
|
|
||||||
|
from app import log
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models import FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import log
|
|
||||||
from app.scodoc.sco_exceptions import AccessDenied
|
from app.scodoc.sco_exceptions import AccessDenied
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc import safehtml
|
from app.scodoc import safehtml
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc import sco_permissions_check
|
from app.scodoc import sco_permissions_check
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_tag_module
|
from app.scodoc import sco_tag_module
|
||||||
@ -115,7 +117,7 @@ def get_etudids_with_debouche(start_year):
|
|||||||
|
|
||||||
|
|
||||||
def table_debouche_etudids(etudids, keep_numeric=True):
|
def table_debouche_etudids(etudids, keep_numeric=True):
|
||||||
"""Rapport pour ces etudiants"""
|
"""Rapport pour ces étudiants"""
|
||||||
L = []
|
L = []
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||||
@ -124,7 +126,8 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
|||||||
es = [(s["date_fin_iso"], i) for i, s in enumerate(sems)]
|
es = [(s["date_fin_iso"], i) for i, s in enumerate(sems)]
|
||||||
imax = max(es)[1]
|
imax = max(es)[1]
|
||||||
last_sem = sems[imax]
|
last_sem = sems[imax]
|
||||||
nt = sco_cache.NotesTableCache.get(last_sem["formsemestre_id"])
|
formsemestre = FormSemestre.query.get_or_404(last_sem["formsemestre_id"])
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
row = {
|
row = {
|
||||||
"etudid": etudid,
|
"etudid": etudid,
|
||||||
"civilite": etud["civilite"],
|
"civilite": etud["civilite"],
|
||||||
|
@ -56,7 +56,7 @@ def index_html(showcodes=0, showsemtable=0):
|
|||||||
H.append(sco_news.scolar_news_summary_html())
|
H.append(sco_news.scolar_news_summary_html())
|
||||||
|
|
||||||
# Avertissement de mise à jour:
|
# 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:
|
# Liste de toutes les sessions:
|
||||||
sems = sco_formsemestre.do_formsemestre_list()
|
sems = sco_formsemestre.do_formsemestre_list()
|
||||||
@ -293,7 +293,6 @@ def delete_dept(dept_id: int):
|
|||||||
"create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s",
|
"create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s",
|
||||||
"create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
|
"create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
|
||||||
"create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s",
|
"create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s",
|
||||||
"create temp table entreprises_temp as select id from entreprises where dept_id = %(dept_id)s",
|
|
||||||
"create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s",
|
"create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s",
|
||||||
]
|
]
|
||||||
for r in reqs:
|
for r in reqs:
|
||||||
@ -345,13 +344,9 @@ def delete_dept(dept_id: int):
|
|||||||
"delete from notes_formsemestre where dept_id = %(dept_id)s",
|
"delete from notes_formsemestre where dept_id = %(dept_id)s",
|
||||||
"delete from scolar_news where dept_id = %(dept_id)s",
|
"delete from scolar_news where dept_id = %(dept_id)s",
|
||||||
"delete from notes_semset where dept_id = %(dept_id)s",
|
"delete from notes_semset where dept_id = %(dept_id)s",
|
||||||
"delete from entreprise_contact where entreprise_id in (select id from entreprises_temp) ",
|
|
||||||
"delete from entreprise_correspondant where entreprise_id in (select id from entreprises_temp) ",
|
|
||||||
"delete from entreprises where dept_id = %(dept_id)s",
|
|
||||||
"delete from notes_formations where dept_id = %(dept_id)s",
|
"delete from notes_formations where dept_id = %(dept_id)s",
|
||||||
"delete from departement where id = %(dept_id)s",
|
"delete from departement where id = %(dept_id)s",
|
||||||
"drop table tags_temp",
|
"drop table tags_temp",
|
||||||
"drop table entreprises_temp",
|
|
||||||
"drop table formations_temp",
|
"drop table formations_temp",
|
||||||
"drop table moduleimpls_temp",
|
"drop table moduleimpls_temp",
|
||||||
"drop table etudids_temp",
|
"drop table etudids_temp",
|
||||||
|
@ -51,13 +51,12 @@ import fcntl
|
|||||||
import subprocess
|
import subprocess
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from flask import g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc import html_sco_header
|
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
import sco_version
|
import sco_version
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
@ -65,10 +64,9 @@ from app.scodoc.sco_exceptions import ScoValueError
|
|||||||
SCO_DUMP_LOCK = "/tmp/scodump.lock"
|
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"""
|
"""Dump base de données et l'envoie anonymisée pour debug"""
|
||||||
H = [html_sco_header.sco_header(page_title="Assistance technique")]
|
# get current (dept) DB name:
|
||||||
# get currect (dept) DB name:
|
|
||||||
cursor = ndb.SimpleQuery("SELECT current_database()", {})
|
cursor = ndb.SimpleQuery("SELECT current_database()", {})
|
||||||
db_name = cursor.fetchone()[0]
|
db_name = cursor.fetchone()[0]
|
||||||
ano_db_name = "ANO" + db_name
|
ano_db_name = "ANO" + db_name
|
||||||
@ -94,28 +92,8 @@ def sco_dump_and_send_db():
|
|||||||
_anonymize_db(ano_db_name)
|
_anonymize_db(ano_db_name)
|
||||||
|
|
||||||
# Send
|
# Send
|
||||||
r = _send_db(ano_db_name)
|
r = _send_db(ano_db_name, message, request_url)
|
||||||
if (
|
code = r.status_code
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Drop anonymized database
|
# Drop anonymized database
|
||||||
@ -124,7 +102,8 @@ def sco_dump_and_send_db():
|
|||||||
fcntl.flock(x, fcntl.LOCK_UN)
|
fcntl.flock(x, fcntl.LOCK_UN)
|
||||||
|
|
||||||
log("sco_dump_and_send_db: done.")
|
log("sco_dump_and_send_db: done.")
|
||||||
return "\n".join(H) + html_sco_header.sco_footer()
|
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
def _duplicate_db(db_name, ano_db_name):
|
def _duplicate_db(db_name, ano_db_name):
|
||||||
@ -173,7 +152,7 @@ def _get_scodoc_serial():
|
|||||||
return 0
|
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"""
|
"""Dump this (anonymized) database and send it to tech support"""
|
||||||
log(f"dumping anonymized database {ano_db_name}")
|
log(f"dumping anonymized database {ano_db_name}")
|
||||||
try:
|
try:
|
||||||
@ -182,15 +161,20 @@ def _send_db(ano_db_name):
|
|||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
log(f"sco_dump_and_send_db: exception in anonymisation: {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...")
|
log("uploading anonymized dump...")
|
||||||
files = {"file": (ano_db_name + ".dump", dump)}
|
files = {"file": (ano_db_name + ".dump", dump)}
|
||||||
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
scu.SCO_DUMP_UP_URL,
|
scu.SCO_DUMP_UP_URL,
|
||||||
files=files,
|
files=files,
|
||||||
data={
|
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(),
|
"serial": _get_scodoc_serial(),
|
||||||
"sco_user": str(current_user),
|
"sco_user": str(current_user),
|
||||||
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
|
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
|
||||||
@ -198,6 +182,15 @@ def _send_db(ano_db_name):
|
|||||||
"sco_fullversion": scu.get_scodoc_version(),
|
"sco_fullversion": scu.get_scodoc_version(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except requests.exceptions.ConnectionError as exc:
|
||||||
|
raise ScoValueError(
|
||||||
|
"""
|
||||||
|
Impossible de joindre le serveur d'assistance (scodoc.org).
|
||||||
|
Veuillez contacter le service informatique de votre établissement pour
|
||||||
|
corriger la configuration de ScoDoc. Dans la plupart des cas, il
|
||||||
|
s'agit d'un proxy mal configuré.
|
||||||
|
"""
|
||||||
|
) from exc
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ def html_edit_formation_apc(
|
|||||||
"""
|
"""
|
||||||
parcours = formation.get_parcours()
|
parcours = formation.get_parcours()
|
||||||
assert parcours.APC_SAE
|
assert parcours.APC_SAE
|
||||||
|
|
||||||
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
|
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
|
||||||
Module.semestre_id, Module.numero, Module.code
|
Module.semestre_id, Module.numero, Module.code
|
||||||
)
|
)
|
||||||
@ -68,6 +69,19 @@ def html_edit_formation_apc(
|
|||||||
).order_by(
|
).order_by(
|
||||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ues_by_sem = {}
|
||||||
|
ects_by_sem = {}
|
||||||
|
for semestre_idx in semestre_ids:
|
||||||
|
ues_by_sem[semestre_idx] = formation.ues.filter_by(
|
||||||
|
semestre_idx=semestre_idx
|
||||||
|
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
||||||
|
ects = [ue.ects for ue in ues_by_sem[semestre_idx]]
|
||||||
|
if None in ects:
|
||||||
|
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
||||||
|
else:
|
||||||
|
ects_by_sem[semestre_idx] = sum(ects)
|
||||||
|
|
||||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
@ -93,7 +107,8 @@ def html_edit_formation_apc(
|
|||||||
editable=editable,
|
editable=editable,
|
||||||
tag_editable=tag_editable,
|
tag_editable=tag_editable,
|
||||||
icons=icons,
|
icons=icons,
|
||||||
UniteEns=UniteEns,
|
ues_by_sem=ues_by_sem,
|
||||||
|
ects_by_sem=ects_by_sem,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
for semestre_idx in semestre_ids:
|
for semestre_idx in semestre_ids:
|
||||||
@ -151,7 +166,7 @@ def html_edit_formation_apc(
|
|||||||
|
|
||||||
|
|
||||||
def html_ue_infos(ue):
|
def html_ue_infos(ue):
|
||||||
"""page d'information sur une UE"""
|
"""Page d'information sur une UE"""
|
||||||
from app.views import ScoData
|
from app.views import ScoData
|
||||||
|
|
||||||
formsemestres = (
|
formsemestres = (
|
||||||
@ -174,7 +189,6 @@ def html_ue_infos(ue):
|
|||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"pn/ue_infos.html",
|
"pn/ue_infos.html",
|
||||||
# "pn/tmp.html",
|
|
||||||
titre=f"UE {ue.acronyme} {ue.titre}",
|
titre=f"UE {ue.acronyme} {ue.titre}",
|
||||||
ue=ue,
|
ue=ue,
|
||||||
formsemestres=formsemestres,
|
formsemestres=formsemestres,
|
||||||
|
@ -365,6 +365,7 @@ def ue_move(ue_id, after=0, redirect=1):
|
|||||||
if len({o.numero for o in others}) != len(others):
|
if len({o.numero for o in others}) != len(others):
|
||||||
# il y a des numeros identiques !
|
# il y a des numeros identiques !
|
||||||
scu.objects_renumber(db, others)
|
scu.objects_renumber(db, others)
|
||||||
|
ue.formation.invalidate_cached_sems()
|
||||||
if len(others) > 1:
|
if len(others) > 1:
|
||||||
idx = [u.id for u in others].index(ue.id)
|
idx = [u.id for u in others].index(ue.id)
|
||||||
neigh = None # object to swap with
|
neigh = None # object to swap with
|
||||||
|
@ -88,13 +88,14 @@ def do_matiere_create(args):
|
|||||||
r = _matiereEditor.create(cnx, args)
|
r = _matiereEditor.create(cnx, args)
|
||||||
|
|
||||||
# news
|
# news
|
||||||
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
|
formation = Formation.query.get(ue["formation_id"])
|
||||||
sco_news.add(
|
sco_news.add(
|
||||||
typ=sco_news.NEWS_FORM,
|
typ=sco_news.NEWS_FORM,
|
||||||
object=ue["formation_id"],
|
object=ue["formation_id"],
|
||||||
text="Modification de la formation %(acronyme)s" % F,
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
max_frequency=3,
|
max_frequency=3,
|
||||||
)
|
)
|
||||||
|
formation.invalidate_cached_sems()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@ -195,13 +196,14 @@ def do_matiere_delete(oid):
|
|||||||
_matiereEditor.delete(cnx, oid)
|
_matiereEditor.delete(cnx, oid)
|
||||||
|
|
||||||
# news
|
# news
|
||||||
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
|
formation = Formation.query.get(ue["formation_id"])
|
||||||
sco_news.add(
|
sco_news.add(
|
||||||
typ=sco_news.NEWS_FORM,
|
typ=sco_news.NEWS_FORM,
|
||||||
object=ue["formation_id"],
|
object=ue["formation_id"],
|
||||||
text="Modification de la formation %(acronyme)s" % F,
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
max_frequency=3,
|
max_frequency=3,
|
||||||
)
|
)
|
||||||
|
formation.invalidate_cached_sems()
|
||||||
|
|
||||||
|
|
||||||
def matiere_delete(matiere_id=None):
|
def matiere_delete(matiere_id=None):
|
||||||
|
@ -41,7 +41,6 @@ from app.models import FormSemestre, ModuleImpl
|
|||||||
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.sco_utils import ModuleType
|
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
@ -105,13 +104,14 @@ def do_module_create(args) -> int:
|
|||||||
r = _moduleEditor.create(cnx, args)
|
r = _moduleEditor.create(cnx, args)
|
||||||
|
|
||||||
# news
|
# news
|
||||||
F = sco_formations.formation_list(args={"formation_id": args["formation_id"]})[0]
|
formation = Formation.query.get(args["formation_id"])
|
||||||
sco_news.add(
|
sco_news.add(
|
||||||
typ=sco_news.NEWS_FORM,
|
typ=sco_news.NEWS_FORM,
|
||||||
object=args["formation_id"],
|
object=formation.id,
|
||||||
text="Modification de la formation %(acronyme)s" % F,
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
max_frequency=3,
|
max_frequency=3,
|
||||||
)
|
)
|
||||||
|
formation.invalidate_cached_sems()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@ -196,7 +196,6 @@ def module_create(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
|
||||||
|
|
||||||
if is_apc:
|
if is_apc:
|
||||||
module_types = scu.ModuleType # tous les types
|
module_types = scu.ModuleType # tous les types
|
||||||
@ -396,13 +395,14 @@ def do_module_delete(oid):
|
|||||||
_moduleEditor.delete(cnx, oid)
|
_moduleEditor.delete(cnx, oid)
|
||||||
|
|
||||||
# news
|
# news
|
||||||
F = sco_formations.formation_list(args={"formation_id": mod["formation_id"]})[0]
|
formation = module.formation
|
||||||
sco_news.add(
|
sco_news.add(
|
||||||
typ=sco_news.NEWS_FORM,
|
typ=sco_news.NEWS_FORM,
|
||||||
object=mod["formation_id"],
|
object=mod["formation_id"],
|
||||||
text="Modification de la formation %(acronyme)s" % F,
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
max_frequency=3,
|
max_frequency=3,
|
||||||
)
|
)
|
||||||
|
formation.invalidate_cached_sems()
|
||||||
|
|
||||||
|
|
||||||
def module_delete(module_id=None):
|
def module_delete(module_id=None):
|
||||||
@ -451,8 +451,6 @@ def module_delete(module_id=None):
|
|||||||
|
|
||||||
def do_module_edit(vals: dict) -> None:
|
def do_module_edit(vals: dict) -> None:
|
||||||
"edit a module"
|
"edit a module"
|
||||||
from app.scodoc import sco_edit_formation
|
|
||||||
|
|
||||||
# check
|
# check
|
||||||
mod = module_list({"module_id": vals["module_id"]})[0]
|
mod = module_list({"module_id": vals["module_id"]})[0]
|
||||||
if module_is_locked(mod["module_id"]):
|
if module_is_locked(mod["module_id"]):
|
||||||
@ -502,20 +500,27 @@ def module_edit(module_id=None):
|
|||||||
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx)
|
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx)
|
||||||
|
|
||||||
if is_apc:
|
if is_apc:
|
||||||
|
# ne conserve que la 1ere matière de chaque UE,
|
||||||
|
# et celle à laquelle ce module est rattaché
|
||||||
|
matieres = [
|
||||||
|
mat
|
||||||
|
for mat in matieres
|
||||||
|
if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id
|
||||||
|
]
|
||||||
mat_names = [
|
mat_names = [
|
||||||
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
|
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
|
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
|
||||||
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
|
|
||||||
|
|
||||||
|
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
|
||||||
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
|
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
|
||||||
|
|
||||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||||
|
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
page_title="Modification du module %(titre)s" % module,
|
page_title=f"Modification du module {a_module.code or a_module.titre or ''}",
|
||||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||||
javascripts=[
|
javascripts=[
|
||||||
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
|
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
|
||||||
@ -523,7 +528,7 @@ def module_edit(module_id=None):
|
|||||||
"js/module_tag_editor.js",
|
"js/module_tag_editor.js",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
"""<h2>Modification du module %(titre)s""" % module,
|
f"""<h2>Modification du module {a_module.code or ''} {a_module.titre or ''}""",
|
||||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
|
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
|
||||||
render_template(
|
render_template(
|
||||||
"scodoc/help/modules.html",
|
"scodoc/help/modules.html",
|
||||||
@ -546,14 +551,18 @@ def module_edit(module_id=None):
|
|||||||
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
|
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
|
||||||
module_types = (
|
module_types = (
|
||||||
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
||||||
) | {a_module.module_type}
|
) | {
|
||||||
|
scu.ModuleType(a_module.module_type)
|
||||||
|
if a_module.module_type
|
||||||
|
else scu.ModuleType.STANDARD
|
||||||
|
}
|
||||||
|
|
||||||
descr = [
|
descr = [
|
||||||
(
|
(
|
||||||
"code",
|
"code",
|
||||||
{
|
{
|
||||||
"size": 10,
|
"size": 10,
|
||||||
"explanation": "code du module (doit être unique dans la formation)",
|
"explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)",
|
||||||
"allow_null": False,
|
"allow_null": False,
|
||||||
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
|
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
|
||||||
val, field, formation_id, module_id=module_id
|
val, field, formation_id, module_id=module_id
|
||||||
@ -692,7 +701,10 @@ def module_edit(module_id=None):
|
|||||||
{
|
{
|
||||||
"title": "Code Apogée",
|
"title": "Code Apogée",
|
||||||
"size": 25,
|
"size": 25,
|
||||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
"explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP
|
||||||
|
séparés par des virgules (ce code est propre à chaque établissement, se rapprocher
|
||||||
|
du référent Apogée).
|
||||||
|
""",
|
||||||
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
|
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -736,8 +748,11 @@ def module_edit(module_id=None):
|
|||||||
else:
|
else:
|
||||||
# l'UE de rattachement peut changer
|
# l'UE de rattachement peut changer
|
||||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||||
|
x, y = tf[2]["ue_matiere_id"].split("!")
|
||||||
|
tf[2]["ue_id"] = int(x)
|
||||||
|
tf[2]["matiere_id"] = int(y)
|
||||||
old_ue_id = a_module.ue.id
|
old_ue_id = a_module.ue.id
|
||||||
new_ue_id = int(tf[2]["ue_id"])
|
new_ue_id = tf[2]["ue_id"]
|
||||||
if (old_ue_id != new_ue_id) and in_use:
|
if (old_ue_id != new_ue_id) and in_use:
|
||||||
new_ue = UniteEns.query.get_or_404(new_ue_id)
|
new_ue = UniteEns.query.get_or_404(new_ue_id)
|
||||||
if new_ue.semestre_idx != a_module.ue.semestre_idx:
|
if new_ue.semestre_idx != a_module.ue.semestre_idx:
|
||||||
@ -845,7 +860,7 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
|
|||||||
[
|
[
|
||||||
mod
|
mod
|
||||||
for mod in module_list(args={"ue_id": ue["ue_id"]})
|
for mod in module_list(args={"ue_id": ue["ue_id"]})
|
||||||
if mod["module_type"] == ModuleType.MALUS
|
if mod["module_type"] == scu.ModuleType.MALUS
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if nb_mod_malus == 0:
|
if nb_mod_malus == 0:
|
||||||
@ -897,7 +912,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None):
|
|||||||
"matiere_id": matiere_id,
|
"matiere_id": matiere_id,
|
||||||
"formation_id": ue["formation_id"],
|
"formation_id": ue["formation_id"],
|
||||||
"semestre_id": semestre_id,
|
"semestre_id": semestre_id,
|
||||||
"module_type": ModuleType.MALUS,
|
"module_type": scu.ModuleType.MALUS,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,13 +29,13 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import flask
|
import flask
|
||||||
from flask import url_for, render_template
|
from flask import flash, render_template, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import log
|
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 import Formation, UniteEns, ModuleImpl, Module
|
||||||
from app.models.formations import Matiere
|
from app.models.formations import Matiere
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
|
|||||||
input_formators={
|
input_formators={
|
||||||
"type": ndb.int_null_is_zero,
|
"type": ndb.int_null_is_zero,
|
||||||
"is_external": ndb.bool_or_str,
|
"is_external": ndb.bool_or_str,
|
||||||
|
"ects": ndb.float_null_is_null,
|
||||||
},
|
},
|
||||||
output_formators={
|
output_formators={
|
||||||
"numero": ndb.int_null_is_zero,
|
"numero": ndb.int_null_is_zero,
|
||||||
@ -107,8 +108,6 @@ def ue_list(*args, **kw):
|
|||||||
|
|
||||||
def do_ue_create(args):
|
def do_ue_create(args):
|
||||||
"create an ue"
|
"create an ue"
|
||||||
from app.scodoc import sco_formations
|
|
||||||
|
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
# check duplicates
|
# check duplicates
|
||||||
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
|
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
|
||||||
@ -117,6 +116,14 @@ def do_ue_create(args):
|
|||||||
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
||||||
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
||||||
)
|
)
|
||||||
|
if not "ue_code" in args:
|
||||||
|
# évite les conflits de code
|
||||||
|
while True:
|
||||||
|
cursor = db.session.execute("select notes_newid_ucod();")
|
||||||
|
code = cursor.fetchone()[0]
|
||||||
|
if UniteEns.query.filter_by(ue_code=code).count() == 0:
|
||||||
|
break
|
||||||
|
args["ue_code"] = code
|
||||||
# create
|
# create
|
||||||
ue_id = _ueEditor.create(cnx, args)
|
ue_id = _ueEditor.create(cnx, args)
|
||||||
|
|
||||||
@ -128,13 +135,16 @@ def do_ue_create(args):
|
|||||||
formation = Formation.query.get(args["formation_id"])
|
formation = Formation.query.get(args["formation_id"])
|
||||||
formation.invalidate_module_coefs()
|
formation.invalidate_module_coefs()
|
||||||
# news
|
# news
|
||||||
F = sco_formations.formation_list(args={"formation_id": args["formation_id"]})[0]
|
ue = UniteEns.query.get(ue_id)
|
||||||
|
flash(f"UE créée (code {ue.ue_code})")
|
||||||
|
formation = Formation.query.get(args["formation_id"])
|
||||||
sco_news.add(
|
sco_news.add(
|
||||||
typ=sco_news.NEWS_FORM,
|
typ=sco_news.NEWS_FORM,
|
||||||
object=args["formation_id"],
|
object=args["formation_id"],
|
||||||
text="Modification de la formation %(acronyme)s" % F,
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
max_frequency=3,
|
max_frequency=3,
|
||||||
)
|
)
|
||||||
|
formation.invalidate_cached_sems()
|
||||||
return ue_id
|
return ue_id
|
||||||
|
|
||||||
|
|
||||||
@ -295,7 +305,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
(
|
(
|
||||||
"numero",
|
"numero",
|
||||||
{
|
{
|
||||||
"size": 2,
|
"size": 4,
|
||||||
"explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
|
"explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
},
|
},
|
||||||
@ -337,7 +347,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
"size": 4,
|
"size": 4,
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"title": "ECTS",
|
"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
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -361,7 +372,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
{
|
{
|
||||||
"size": 12,
|
"size": 12,
|
||||||
"title": "Code UE",
|
"title": "Code UE",
|
||||||
"explanation": "code interne (optionnel). 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.""",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -370,7 +384,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
"title": "Code Apogée",
|
"title": "Code Apogée",
|
||||||
"size": 25,
|
"size": 25,
|
||||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||||
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
|
"max_length": APO_CODE_STR_LEN,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -461,8 +475,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
"semestre_id": tf[2]["semestre_idx"],
|
"semestre_id": tf[2]["semestre_idx"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
flash("UE créée")
|
||||||
else:
|
else:
|
||||||
do_ue_edit(tf[2])
|
do_ue_edit(tf[2])
|
||||||
|
flash("UE modifiée")
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"notes.ue_table",
|
"notes.ue_table",
|
||||||
@ -600,7 +616,12 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||||||
_add_ue_semestre_id(ues_externes, is_apc)
|
_add_ue_semestre_id(ues_externes, is_apc)
|
||||||
ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
|
ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
|
||||||
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
|
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
|
||||||
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues)
|
# Codes dupliqués (pour aider l'utilisateur)
|
||||||
|
seen = set()
|
||||||
|
duplicated_codes = {
|
||||||
|
ue["ue_code"] for ue in ues if ue["ue_code"] in seen or seen.add(ue["ue_code"])
|
||||||
|
}
|
||||||
|
ues_with_duplicated_code = [ue for ue in ues if ue["ue_code"] in duplicated_codes]
|
||||||
|
|
||||||
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
|
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
|
||||||
# editable = (not locked) and has_perm_change
|
# editable = (not locked) and has_perm_change
|
||||||
@ -663,11 +684,17 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
if msg:
|
if msg:
|
||||||
H.append('<p class="msg">' + msg + "</p>")
|
H.append('<p class="msg">' + msg + "</p>")
|
||||||
|
|
||||||
if has_duplicate_ue_codes:
|
if ues_with_duplicated_code:
|
||||||
H.append(
|
H.append(
|
||||||
"""<div class="ue_warning"><span>Attention: plusieurs UE de cette
|
f"""<div class="ue_warning"><span>Attention: plusieurs UE de cette
|
||||||
formation ont le même code. Il faut corriger cela ci-dessous,
|
formation ont le même code : <tt>{
|
||||||
sinon les calculs d'ECTS seront erronés !</span></div>"""
|
', '.join([
|
||||||
|
'<a class="stdlink" href="' + url_for( "notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"] )
|
||||||
|
+ '">' + ue["acronyme"] + " (code " + ue["ue_code"] + ")</a>"
|
||||||
|
for ue in ues_with_duplicated_code ])
|
||||||
|
}</tt>.
|
||||||
|
Il faut corriger cela, sinon les capitalisations et ECTS seront
|
||||||
|
erronés !</span></div>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Description de la formation
|
# Description de la formation
|
||||||
@ -698,16 +725,19 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
<a href="{url_for('notes.refcomp_show',
|
<a href="{url_for('notes.refcomp_show',
|
||||||
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}">
|
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}">
|
||||||
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
|
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
|
||||||
</a> """
|
</a> """
|
||||||
msg_refcomp = "changer"
|
msg_refcomp = "changer"
|
||||||
|
H.append(f"""<ul><li>{descr_refcomp}""")
|
||||||
|
if current_user.has_permission(Permission.ScoChangeFormation):
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""<a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
|
||||||
<ul>
|
|
||||||
<li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
|
|
||||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
||||||
}">{msg_refcomp}</a>
|
}">{msg_refcomp}</a>"""
|
||||||
</li>
|
)
|
||||||
<li><a class="stdlink" href="{
|
|
||||||
|
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)
|
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
|
||||||
}">éditer les coefficients des ressources et SAÉs</a>
|
}">éditer les coefficients des ressources et SAÉs</a>
|
||||||
</li>
|
</li>
|
||||||
@ -734,6 +764,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
H.append('<div class="formation_classic_infos">')
|
||||||
H.append(
|
H.append(
|
||||||
_ue_table_ues(
|
_ue_table_ues(
|
||||||
parcours,
|
parcours,
|
||||||
@ -763,7 +794,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
H.append("</div>")
|
||||||
H.append("</div>") # formation_ue_list
|
H.append("</div>") # formation_ue_list
|
||||||
|
|
||||||
if ues_externes:
|
if ues_externes:
|
||||||
@ -912,10 +943,10 @@ def _ue_table_ues(
|
|||||||
cur_ue_semestre_id = None
|
cur_ue_semestre_id = None
|
||||||
iue = 0
|
iue = 0
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
if ue["ects"]:
|
if ue["ects"] is None:
|
||||||
ue["ects_str"] = ", %g ECTS" % ue["ects"]
|
|
||||||
else:
|
|
||||||
ue["ects_str"] = ""
|
ue["ects_str"] = ""
|
||||||
|
else:
|
||||||
|
ue["ects_str"] = ", %g ECTS" % ue["ects"]
|
||||||
if editable:
|
if editable:
|
||||||
klass = "span_apo_edit"
|
klass = "span_apo_edit"
|
||||||
else:
|
else:
|
||||||
@ -929,13 +960,13 @@ def _ue_table_ues(
|
|||||||
|
|
||||||
if cur_ue_semestre_id != ue["semestre_id"]:
|
if cur_ue_semestre_id != ue["semestre_id"]:
|
||||||
cur_ue_semestre_id = ue["semestre_id"]
|
cur_ue_semestre_id = ue["semestre_id"]
|
||||||
if iue > 0:
|
|
||||||
H.append("</ul>")
|
|
||||||
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
|
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
|
||||||
lab = "Pas d'indication de semestre:"
|
lab = "Pas d'indication de semestre:"
|
||||||
else:
|
else:
|
||||||
lab = "Semestre %s:" % ue["semestre_id"]
|
lab = "Semestre %s:" % ue["semestre_id"]
|
||||||
H.append('<div class="ue_list_tit_sem">%s</div>' % lab)
|
H.append(
|
||||||
|
'<div class="ue_list_div"><div class="ue_list_tit_sem">%s</div>' % lab
|
||||||
|
)
|
||||||
H.append('<ul class="notes_ue_list">')
|
H.append('<ul class="notes_ue_list">')
|
||||||
H.append('<li class="notes_ue_list">')
|
H.append('<li class="notes_ue_list">')
|
||||||
if iue != 0 and editable:
|
if iue != 0 and editable:
|
||||||
@ -952,7 +983,6 @@ def _ue_table_ues(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append(arrow_none)
|
H.append(arrow_none)
|
||||||
iue += 1
|
|
||||||
ue["acro_titre"] = str(ue["acronyme"])
|
ue["acro_titre"] = str(ue["acronyme"])
|
||||||
if ue["titre"] != ue["acronyme"]:
|
if ue["titre"] != ue["acronyme"]:
|
||||||
ue["acro_titre"] += " " + str(ue["titre"])
|
ue["acro_titre"] += " " + str(ue["titre"])
|
||||||
@ -1000,6 +1030,16 @@ def _ue_table_ues(
|
|||||||
delete_disabled_icon,
|
delete_disabled_icon,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
|
||||||
|
H.append(
|
||||||
|
f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
|
||||||
|
formation_id=ue['formation_id'], semestre_idx=ue['semestre_id'])
|
||||||
|
}">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
iue += 1
|
||||||
|
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
@ -1267,7 +1307,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
|||||||
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
||||||
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# On ne peut pas supprimer le code UE:
|
# On ne peut pas supprimer le code UE:
|
||||||
if "ue_code" in args and not args["ue_code"]:
|
if "ue_code" in args and not args["ue_code"]:
|
||||||
del args["ue_code"]
|
del args["ue_code"]
|
||||||
|
@ -1,324 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Fonctions sur les entreprises
|
|
||||||
"""
|
|
||||||
# codes anciens déplacés de ZEntreprise
|
|
||||||
import datetime
|
|
||||||
from operator import itemgetter
|
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
import app.scodoc.notesdb as ndb
|
|
||||||
from app.scodoc.notesdb import ScoDocCursor, EditableTable, DateISOtoDMY, DateDMYtoISO
|
|
||||||
|
|
||||||
|
|
||||||
def _format_nom(nom):
|
|
||||||
"formatte nom (filtre en entree db) d'une entreprise"
|
|
||||||
if not nom:
|
|
||||||
return nom
|
|
||||||
return nom[0].upper() + nom[1:]
|
|
||||||
|
|
||||||
|
|
||||||
class EntreprisesEditor(EditableTable):
|
|
||||||
def delete(self, cnx, oid):
|
|
||||||
"delete correspondants and contacts, then self"
|
|
||||||
# first, delete all correspondants and contacts
|
|
||||||
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
|
|
||||||
cursor.execute(
|
|
||||||
"delete from entreprise_contact where entreprise_id=%(entreprise_id)s",
|
|
||||||
{"entreprise_id": oid},
|
|
||||||
)
|
|
||||||
cursor.execute(
|
|
||||||
"delete from entreprise_correspondant where entreprise_id=%(entreprise_id)s",
|
|
||||||
{"entreprise_id": oid},
|
|
||||||
)
|
|
||||||
cnx.commit()
|
|
||||||
EditableTable.delete(self, cnx, oid)
|
|
||||||
|
|
||||||
def list(
|
|
||||||
self,
|
|
||||||
cnx,
|
|
||||||
args={},
|
|
||||||
operator="and",
|
|
||||||
test="=",
|
|
||||||
sortkey=None,
|
|
||||||
sort_on_contact=False,
|
|
||||||
limit="",
|
|
||||||
offset="",
|
|
||||||
):
|
|
||||||
# list, then sort on date of last contact
|
|
||||||
R = EditableTable.list(
|
|
||||||
self,
|
|
||||||
cnx,
|
|
||||||
args=args,
|
|
||||||
operator=operator,
|
|
||||||
test=test,
|
|
||||||
sortkey=sortkey,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
)
|
|
||||||
if sort_on_contact:
|
|
||||||
for r in R:
|
|
||||||
c = do_entreprise_contact_list(
|
|
||||||
args={"entreprise_id": r["entreprise_id"]},
|
|
||||||
disable_formatting=True,
|
|
||||||
)
|
|
||||||
if c:
|
|
||||||
r["date"] = max([x["date"] or datetime.date.min for x in c])
|
|
||||||
else:
|
|
||||||
r["date"] = datetime.date.min
|
|
||||||
# sort
|
|
||||||
R.sort(key=itemgetter("date"))
|
|
||||||
for r in R:
|
|
||||||
r["date"] = DateISOtoDMY(r["date"])
|
|
||||||
return R
|
|
||||||
|
|
||||||
def list_by_etud(
|
|
||||||
self, cnx, args={}, sort_on_contact=False, disable_formatting=False
|
|
||||||
):
|
|
||||||
"cherche rentreprise ayant eu contact avec etudiant"
|
|
||||||
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
|
|
||||||
cursor.execute(
|
|
||||||
"select E.*, I.nom as etud_nom, I.prenom as etud_prenom, C.date from entreprises E, entreprise_contact C, identite I where C.entreprise_id = E.entreprise_id and C.etudid = I.etudid and I.nom ~* %(etud_nom)s ORDER BY E.nom",
|
|
||||||
args,
|
|
||||||
)
|
|
||||||
_, res = [x[0] for x in cursor.description], cursor.dictfetchall()
|
|
||||||
R = []
|
|
||||||
for r in res:
|
|
||||||
r["etud_prenom"] = r["etud_prenom"] or ""
|
|
||||||
d = {}
|
|
||||||
for key in r:
|
|
||||||
v = r[key]
|
|
||||||
# format value
|
|
||||||
if not disable_formatting and key in self.output_formators:
|
|
||||||
v = self.output_formators[key](v)
|
|
||||||
d[key] = v
|
|
||||||
R.append(d)
|
|
||||||
# sort
|
|
||||||
if sort_on_contact:
|
|
||||||
R.sort(key=lambda x: (x["date"] or datetime.date.min))
|
|
||||||
|
|
||||||
for r in R:
|
|
||||||
r["date"] = DateISOtoDMY(r["date"] or datetime.date.min)
|
|
||||||
return R
|
|
||||||
|
|
||||||
|
|
||||||
_entreprisesEditor = EntreprisesEditor(
|
|
||||||
"entreprises",
|
|
||||||
"entreprise_id",
|
|
||||||
(
|
|
||||||
"entreprise_id",
|
|
||||||
"nom",
|
|
||||||
"adresse",
|
|
||||||
"ville",
|
|
||||||
"codepostal",
|
|
||||||
"pays",
|
|
||||||
"contact_origine",
|
|
||||||
"secteur",
|
|
||||||
"privee",
|
|
||||||
"localisation",
|
|
||||||
"qualite_relation",
|
|
||||||
"plus10salaries",
|
|
||||||
"note",
|
|
||||||
"date_creation",
|
|
||||||
),
|
|
||||||
filter_dept=True,
|
|
||||||
sortkey="nom",
|
|
||||||
input_formators={
|
|
||||||
"nom": _format_nom,
|
|
||||||
"plus10salaries": bool,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# ----------- Correspondants
|
|
||||||
_entreprise_correspEditor = EditableTable(
|
|
||||||
"entreprise_correspondant",
|
|
||||||
"entreprise_corresp_id",
|
|
||||||
(
|
|
||||||
"entreprise_corresp_id",
|
|
||||||
"entreprise_id",
|
|
||||||
"civilite",
|
|
||||||
"nom",
|
|
||||||
"prenom",
|
|
||||||
"fonction",
|
|
||||||
"phone1",
|
|
||||||
"phone2",
|
|
||||||
"mobile",
|
|
||||||
"fax",
|
|
||||||
"mail1",
|
|
||||||
"mail2",
|
|
||||||
"note",
|
|
||||||
),
|
|
||||||
sortkey="nom",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ----------- Contacts
|
|
||||||
_entreprise_contactEditor = EditableTable(
|
|
||||||
"entreprise_contact",
|
|
||||||
"entreprise_contact_id",
|
|
||||||
(
|
|
||||||
"entreprise_contact_id",
|
|
||||||
"date",
|
|
||||||
"type_contact",
|
|
||||||
"entreprise_id",
|
|
||||||
"entreprise_corresp_id",
|
|
||||||
"etudid",
|
|
||||||
"description",
|
|
||||||
"enseignant",
|
|
||||||
),
|
|
||||||
sortkey="date",
|
|
||||||
output_formators={"date": DateISOtoDMY},
|
|
||||||
input_formators={"date": DateDMYtoISO},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_create(args):
|
|
||||||
"entreprise_create"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
r = _entreprisesEditor.create(cnx, args)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_delete(oid):
|
|
||||||
"entreprise_delete"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_entreprisesEditor.delete(cnx, oid)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_list(**kw):
|
|
||||||
"entreprise_list"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
return _entreprisesEditor.list(cnx, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_list_by_etud(**kw):
|
|
||||||
"entreprise_list_by_etud"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
return _entreprisesEditor.list_by_etud(cnx, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_edit(*args, **kw):
|
|
||||||
"entreprise_edit"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_entreprisesEditor.edit(cnx, *args, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_correspondant_create(args):
|
|
||||||
"entreprise_correspondant_create"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
r = _entreprise_correspEditor.create(cnx, args)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_correspondant_delete(oid):
|
|
||||||
"entreprise_correspondant_delete"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_entreprise_correspEditor.delete(cnx, oid)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_correspondant_list(**kw):
|
|
||||||
"entreprise_correspondant_list"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
return _entreprise_correspEditor.list(cnx, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_correspondant_edit(*args, **kw):
|
|
||||||
"entreprise_correspondant_edit"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_entreprise_correspEditor.edit(cnx, *args, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_correspondant_listnames(args={}):
|
|
||||||
"-> liste des noms des correspondants (pour affichage menu)"
|
|
||||||
C = do_entreprise_correspondant_list(args=args)
|
|
||||||
return [(x["prenom"] + " " + x["nom"], str(x["entreprise_corresp_id"])) for x in C]
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_contact_delete(oid):
|
|
||||||
"entreprise_contact_delete"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_entreprise_contactEditor.delete(cnx, oid)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_contact_list(**kw):
|
|
||||||
"entreprise_contact_list"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
return _entreprise_contactEditor.list(cnx, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_contact_edit(*args, **kw):
|
|
||||||
"entreprise_contact_edit"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_entreprise_contactEditor.edit(cnx, *args, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_contact_create(args):
|
|
||||||
"entreprise_contact_create"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
r = _entreprise_contactEditor.create(cnx, args)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def do_entreprise_check_etudiant(etudiant):
|
|
||||||
"""Si etudiant est vide, ou un ETUDID valide, ou un nom unique,
|
|
||||||
retourne (1, ETUDID).
|
|
||||||
Sinon, retourne (0, 'message explicatif')
|
|
||||||
"""
|
|
||||||
etudiant = etudiant.strip().translate(
|
|
||||||
str.maketrans("", "", "'()")
|
|
||||||
) # suppress parens and quote from name
|
|
||||||
if not etudiant:
|
|
||||||
return 1, None
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
|
|
||||||
cursor.execute(
|
|
||||||
"select etudid, nom, prenom from identite where upper(nom) ~ upper(%(etudiant)s) or etudid=%(etudiant)s",
|
|
||||||
{"etudiant": etudiant},
|
|
||||||
)
|
|
||||||
r = cursor.fetchall()
|
|
||||||
if len(r) < 1:
|
|
||||||
return 0, 'Aucun etudiant ne correspond à "%s"' % etudiant
|
|
||||||
elif len(r) > 10:
|
|
||||||
return (
|
|
||||||
0,
|
|
||||||
"<b>%d etudiants</b> correspondent à ce nom (utilisez le code)" % len(r),
|
|
||||||
)
|
|
||||||
elif len(r) > 1:
|
|
||||||
e = ['<ul class="entreprise_etud_list">']
|
|
||||||
for x in r:
|
|
||||||
e.append(
|
|
||||||
"<li>%s %s (code %s)</li>" % ((x[1]).upper(), x[2] or "", x[0].strip())
|
|
||||||
)
|
|
||||||
e.append("</ul>")
|
|
||||||
return (
|
|
||||||
0,
|
|
||||||
"Les étudiants suivants correspondent: préciser le nom complet ou le code\n"
|
|
||||||
+ "\n".join(e),
|
|
||||||
)
|
|
||||||
else: # une seule reponse !
|
|
||||||
return 1, r[0][0].strip()
|
|
@ -384,8 +384,8 @@ print apo_csv_list_stored_archives()
|
|||||||
|
|
||||||
groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
|
groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
|
||||||
|
|
||||||
nt = sco_cache.NotesTableCache.get( formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
#
|
#
|
||||||
s = SemSet('NSS29902')
|
s = SemSet('NSS29902')
|
||||||
apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)
|
apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)
|
||||||
|
@ -53,7 +53,7 @@ from app.scodoc.sco_exceptions import ScoValueError
|
|||||||
|
|
||||||
|
|
||||||
def apo_semset_maq_status(
|
def apo_semset_maq_status(
|
||||||
semset_id="",
|
semset_id: int,
|
||||||
allow_missing_apo=False,
|
allow_missing_apo=False,
|
||||||
allow_missing_decisions=False,
|
allow_missing_decisions=False,
|
||||||
allow_missing_csv=False,
|
allow_missing_csv=False,
|
||||||
@ -65,7 +65,7 @@ def apo_semset_maq_status(
|
|||||||
):
|
):
|
||||||
"""Page statut / tableau de bord"""
|
"""Page statut / tableau de bord"""
|
||||||
if not semset_id:
|
if not semset_id:
|
||||||
raise ValueError("invalid null semset_id")
|
raise ScoValueError("invalid null semset_id")
|
||||||
semset = sco_semset.SemSet(semset_id=semset_id)
|
semset = sco_semset.SemSet(semset_id=semset_id)
|
||||||
semset.fill_formsemestres()
|
semset.fill_formsemestres()
|
||||||
# autorise export meme si etudiants Apo manquants:
|
# autorise export meme si etudiants Apo manquants:
|
||||||
|
@ -33,8 +33,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from flask import url_for, g, request
|
from flask import url_for, g
|
||||||
from flask_mail import Message
|
|
||||||
|
|
||||||
from app import email
|
from app import email
|
||||||
from app import log
|
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 safehtml
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
|
||||||
|
|
||||||
|
|
||||||
def format_etud_ident(etud):
|
def format_etud_ident(etud):
|
||||||
@ -451,7 +449,6 @@ _adresseEditor = ndb.EditableTable(
|
|||||||
"telephonemobile",
|
"telephonemobile",
|
||||||
"fax",
|
"fax",
|
||||||
"typeadresse",
|
"typeadresse",
|
||||||
"entreprise_id",
|
|
||||||
"description",
|
"description",
|
||||||
),
|
),
|
||||||
convert_null_outputs_to_empty=True,
|
convert_null_outputs_to_empty=True,
|
||||||
@ -860,18 +857,14 @@ def list_scolog(etudid):
|
|||||||
return cursor.dictfetchall()
|
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)
|
"""etuds est une liste d'etudiants (mappings)
|
||||||
Pour chaque etudiant, ajoute ou formatte les champs
|
Pour chaque etudiant, ajoute ou formatte les champs
|
||||||
-> informations pour fiche etudiant ou listes diverses
|
-> informations pour fiche etudiant ou listes diverses
|
||||||
|
|
||||||
Si add_admission: ajoute au dict le schamps "admission" s'il n'y sont pas déjà.
|
Si add_admission: ajoute au dict le schamps "admission" s'il n'y sont pas déjà.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
|
||||||
|
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
# open('/tmp/t','w').write( str(etuds) )
|
|
||||||
for etud in etuds:
|
for etud in etuds:
|
||||||
etudid = etud["etudid"]
|
etudid = etud["etudid"]
|
||||||
etud["dept"] = g.scodoc_dept
|
etud["dept"] = g.scodoc_dept
|
||||||
@ -894,49 +887,7 @@ def fill_etuds_info(etuds, add_admission=True):
|
|||||||
etud.update(adr)
|
etud.update(adr)
|
||||||
format_etud_ident(etud)
|
format_etud_ident(etud)
|
||||||
|
|
||||||
# Semestres dans lesquel il est inscrit
|
etud.update(etud_inscriptions_infos(etudid, etud["ne"]))
|
||||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
|
||||||
{"etudid": etudid}
|
|
||||||
)
|
|
||||||
etud["ins"] = ins
|
|
||||||
sems = []
|
|
||||||
cursem = None # semestre "courant" ou il est inscrit
|
|
||||||
for i in ins:
|
|
||||||
sem = sco_formsemestre.get_formsemestre(i["formsemestre_id"])
|
|
||||||
if sco_formsemestre.sem_est_courant(sem):
|
|
||||||
cursem = sem
|
|
||||||
curi = i
|
|
||||||
sem["ins"] = i
|
|
||||||
sems.append(sem)
|
|
||||||
# trie les semestres par date de debut, le plus recent d'abord
|
|
||||||
# (important, ne pas changer (suivi cohortes))
|
|
||||||
sems.sort(key=itemgetter("dateord"), reverse=True)
|
|
||||||
etud["sems"] = sems
|
|
||||||
etud["cursem"] = cursem
|
|
||||||
if cursem:
|
|
||||||
etud["inscription"] = cursem["titremois"]
|
|
||||||
etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"]
|
|
||||||
etud["inscription_formsemestre_id"] = cursem["formsemestre_id"]
|
|
||||||
etud["etatincursem"] = curi["etat"]
|
|
||||||
etud["situation"] = descr_situation_etud(etudid, etud["ne"])
|
|
||||||
# XXX est-ce utile ? sco_groups.etud_add_group_infos( etud, cursem)
|
|
||||||
else:
|
|
||||||
if etud["sems"]:
|
|
||||||
if etud["sems"][0]["dateord"] > time.strftime(
|
|
||||||
"%Y-%m-%d", time.localtime()
|
|
||||||
):
|
|
||||||
etud["inscription"] = "futur"
|
|
||||||
etud["situation"] = "futur élève"
|
|
||||||
else:
|
|
||||||
etud["inscription"] = "ancien"
|
|
||||||
etud["situation"] = "ancien élève"
|
|
||||||
else:
|
|
||||||
etud["inscription"] = "non inscrit"
|
|
||||||
etud["situation"] = etud["inscription"]
|
|
||||||
etud["inscriptionstr"] = etud["inscription"]
|
|
||||||
etud["inscription_formsemestre_id"] = None
|
|
||||||
# XXXetud['partitions'] = {} # ne va pas chercher les groupes des anciens semestres
|
|
||||||
etud["etatincursem"] = "?"
|
|
||||||
|
|
||||||
# nettoyage champs souvent vides
|
# nettoyage champs souvent vides
|
||||||
if etud.get("nomlycee"):
|
if etud.get("nomlycee"):
|
||||||
@ -974,8 +925,59 @@ def fill_etuds_info(etuds, add_admission=True):
|
|||||||
etud["telephonemobilestr"] = ""
|
etud["telephonemobilestr"] = ""
|
||||||
|
|
||||||
|
|
||||||
def descr_situation_etud(etudid, ne=""):
|
def etud_inscriptions_infos(etudid: int, ne="") -> dict:
|
||||||
"""chaine decrivant la situation actuelle de l'etudiant"""
|
"""Dict avec les informations sur les semestres passés et courant"""
|
||||||
|
from app.scodoc import sco_formsemestre
|
||||||
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
|
|
||||||
|
etud = {}
|
||||||
|
# Semestres dans lesquel il est inscrit
|
||||||
|
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||||
|
{"etudid": etudid}
|
||||||
|
)
|
||||||
|
etud["ins"] = ins
|
||||||
|
sems = []
|
||||||
|
cursem = None # semestre "courant" ou il est inscrit
|
||||||
|
for i in ins:
|
||||||
|
sem = sco_formsemestre.get_formsemestre(i["formsemestre_id"])
|
||||||
|
if sco_formsemestre.sem_est_courant(sem):
|
||||||
|
cursem = sem
|
||||||
|
curi = i
|
||||||
|
sem["ins"] = i
|
||||||
|
sems.append(sem)
|
||||||
|
# trie les semestres par date de debut, le plus recent d'abord
|
||||||
|
# (important, ne pas changer (suivi cohortes))
|
||||||
|
sems.sort(key=itemgetter("dateord"), reverse=True)
|
||||||
|
etud["sems"] = sems
|
||||||
|
etud["cursem"] = cursem
|
||||||
|
if cursem:
|
||||||
|
etud["inscription"] = cursem["titremois"]
|
||||||
|
etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"]
|
||||||
|
etud["inscription_formsemestre_id"] = cursem["formsemestre_id"]
|
||||||
|
etud["etatincursem"] = curi["etat"]
|
||||||
|
etud["situation"] = descr_situation_etud(etudid, ne)
|
||||||
|
else:
|
||||||
|
if etud["sems"]:
|
||||||
|
if etud["sems"][0]["dateord"] > time.strftime("%Y-%m-%d", time.localtime()):
|
||||||
|
etud["inscription"] = "futur"
|
||||||
|
etud["situation"] = "futur élève"
|
||||||
|
else:
|
||||||
|
etud["inscription"] = "ancien"
|
||||||
|
etud["situation"] = "ancien élève"
|
||||||
|
else:
|
||||||
|
etud["inscription"] = "non inscrit"
|
||||||
|
etud["situation"] = etud["inscription"]
|
||||||
|
etud["inscriptionstr"] = etud["inscription"]
|
||||||
|
etud["inscription_formsemestre_id"] = None
|
||||||
|
etud["etatincursem"] = "?"
|
||||||
|
return etud
|
||||||
|
|
||||||
|
|
||||||
|
def descr_situation_etud(etudid: int, ne="") -> str:
|
||||||
|
"""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
|
from app.scodoc import sco_formsemestre
|
||||||
|
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
@ -992,7 +994,7 @@ def descr_situation_etud(etudid, ne=""):
|
|||||||
)
|
)
|
||||||
r = cursor.dictfetchone()
|
r = cursor.dictfetchone()
|
||||||
if not r:
|
if not r:
|
||||||
situation = "non inscrit"
|
situation = "non inscrit" + ne
|
||||||
else:
|
else:
|
||||||
sem = sco_formsemestre.get_formsemestre(r["formsemestre_id"])
|
sem = sco_formsemestre.get_formsemestre(r["formsemestre_id"])
|
||||||
if r["etat"] == "I":
|
if r["etat"] == "I":
|
||||||
|
@ -237,7 +237,11 @@ def formsemestre_check_absences_html(formsemestre_id):
|
|||||||
if evals:
|
if evals:
|
||||||
H.append(
|
H.append(
|
||||||
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
|
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
|
||||||
% (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"])
|
% (
|
||||||
|
M["moduleimpl_id"],
|
||||||
|
M["module"]["code"] or "",
|
||||||
|
M["module"]["abbrev"] or "",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for E in evals:
|
for E in evals:
|
||||||
H.append(
|
H.append(
|
||||||
|
@ -175,6 +175,7 @@ def do_evaluation_create(
|
|||||||
|
|
||||||
# news
|
# news
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||||
|
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||||
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||||
|
@ -122,7 +122,7 @@ def evaluation_create_form(
|
|||||||
#
|
#
|
||||||
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
|
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
|
||||||
moduleimpl_id,
|
moduleimpl_id,
|
||||||
mod["code"],
|
mod["code"] or "module sans code",
|
||||||
mod["titre"],
|
mod["titre"],
|
||||||
link,
|
link,
|
||||||
)
|
)
|
||||||
|
@ -39,7 +39,7 @@ from flask import request
|
|||||||
from app import log
|
from app import log
|
||||||
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
@ -403,8 +403,8 @@ def do_evaluation_etat_in_mod(nt, moduleimpl_id):
|
|||||||
|
|
||||||
def formsemestre_evaluations_cal(formsemestre_id):
|
def formsemestre_evaluations_cal(formsemestre_id):
|
||||||
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
evals = nt.get_evaluations_etats()
|
evals = nt.get_evaluations_etats()
|
||||||
nb_evals = len(evals)
|
nb_evals = len(evals)
|
||||||
@ -415,8 +415,8 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||||||
|
|
||||||
today = time.strftime("%Y-%m-%d")
|
today = time.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
year = int(sem["annee_debut"])
|
year = formsemestre.date_debut.year
|
||||||
if sem["mois_debut_ord"] < 8:
|
if formsemestre.date_debut.month < 8:
|
||||||
year -= 1 # calendrier septembre a septembre
|
year -= 1 # calendrier septembre a septembre
|
||||||
events = {} # (day, halfday) : event
|
events = {} # (day, halfday) : event
|
||||||
for e in evals:
|
for e in evals:
|
||||||
@ -536,10 +536,10 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
|||||||
"""Experimental: un tableau indiquant pour chaque évaluation
|
"""Experimental: un tableau indiquant pour chaque évaluation
|
||||||
le nombre de jours avant la publication des notes.
|
le nombre de jours avant la publication des notes.
|
||||||
|
|
||||||
N'indique pas les évaluations de ratrapage ni celles des modules de bonus/malus.
|
N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
|
||||||
"""
|
"""
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
evals = nt.get_evaluations_etats()
|
evals = nt.get_evaluations_etats()
|
||||||
T = []
|
T = []
|
||||||
@ -605,7 +605,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
|||||||
origin="Généré par %s le " % sco_version.SCONAME
|
origin="Généré par %s le " % sco_version.SCONAME
|
||||||
+ scu.timedate_human_repr()
|
+ scu.timedate_human_repr()
|
||||||
+ "",
|
+ "",
|
||||||
filename=scu.make_filename("evaluations_delais_" + sem["titreannee"]),
|
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
||||||
)
|
)
|
||||||
return tab.make_page(format=format)
|
return tab.make_page(format=format)
|
||||||
|
|
||||||
@ -633,9 +633,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
|
|||||||
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
|
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
|
||||||
% moduleimpl_id
|
% moduleimpl_id
|
||||||
)
|
)
|
||||||
mod_descr = (
|
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' % (
|
||||||
'<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
|
moduleimpl_id,
|
||||||
% (moduleimpl_id, Mod["code"], Mod["titre"], nomcomplet, resp, link)
|
Mod["code"] or "",
|
||||||
|
Mod["titre"] or "?",
|
||||||
|
nomcomplet,
|
||||||
|
resp,
|
||||||
|
link,
|
||||||
)
|
)
|
||||||
|
|
||||||
etit = E["description"] or ""
|
etit = E["description"] or ""
|
||||||
|
@ -47,9 +47,12 @@ class ScoValueError(ScoException):
|
|||||||
self.dest_url = dest_url
|
self.dest_url = dest_url
|
||||||
|
|
||||||
|
|
||||||
|
class ScoBugCatcher(ScoException):
|
||||||
|
"bug avec enquete en cours"
|
||||||
|
|
||||||
|
|
||||||
class NoteProcessError(ScoValueError):
|
class NoteProcessError(ScoValueError):
|
||||||
"Valeurs notes invalides"
|
"Valeurs notes invalides"
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidEtudId(NoteProcessError):
|
class InvalidEtudId(NoteProcessError):
|
||||||
@ -112,8 +115,9 @@ class ScoNonEmptyFormationObject(ScoValueError):
|
|||||||
|
|
||||||
|
|
||||||
class ScoInvalidIdType(ScoValueError):
|
class ScoInvalidIdType(ScoValueError):
|
||||||
"""Pour les clients qui s'obstinnent à utiliser des bookmarks ou
|
"""Pour les clients qui s'obstinent à utiliser des bookmarks
|
||||||
historiques anciens avec des ID ScoDoc7"""
|
ou historiques anciens avec des ID ScoDoc7.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, msg=""):
|
def __init__(self, msg=""):
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
@ -29,6 +29,9 @@
|
|||||||
"""
|
"""
|
||||||
from flask import url_for, g, request
|
from flask import url_for, g, request
|
||||||
|
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models import FormSemestre
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import log
|
||||||
@ -77,7 +80,8 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
|
|||||||
{}
|
{}
|
||||||
) # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
|
) # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
|
||||||
for formsemestre_id in formsemestre_ids_parcours:
|
for formsemestre_id in formsemestre_ids_parcours:
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etudids
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
etudids = nt.get_etudids()
|
etudids = nt.get_etudids()
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
if etudid not in etuds_infos: # pas encore traité ?
|
if etudid not in etuds_infos: # pas encore traité ?
|
||||||
|
@ -39,6 +39,7 @@ from app.scodoc.gen_tables import GenTable
|
|||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
@ -179,7 +180,9 @@ def search_etud_in_dept(expnom=""):
|
|||||||
e["_nomprenom_target"] = target
|
e["_nomprenom_target"] = target
|
||||||
e["inscription_target"] = target
|
e["inscription_target"] = target
|
||||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
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(
|
tab = GenTable(
|
||||||
columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
|
columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
|
||||||
@ -221,7 +224,10 @@ def search_etuds_infos(expnom=None, code_nip=None):
|
|||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
if expnom and not may_be_nip:
|
if expnom and not may_be_nip:
|
||||||
expnom = expnom.upper() # les noms dans la BD sont en uppercase
|
expnom = expnom.upper() # les noms dans la BD sont en uppercase
|
||||||
|
try:
|
||||||
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
|
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
|
||||||
|
except ScoException:
|
||||||
|
etuds = []
|
||||||
else:
|
else:
|
||||||
code_nip = code_nip or expnom
|
code_nip = code_nip or expnom
|
||||||
if code_nip:
|
if code_nip:
|
||||||
|
@ -151,8 +151,14 @@ def formation_export(
|
|||||||
if mod["ects"] is None:
|
if mod["ects"] is None:
|
||||||
del mod["ects"]
|
del mod["ects"]
|
||||||
|
|
||||||
|
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
|
||||||
return scu.sendResult(
|
return scu.sendResult(
|
||||||
F, name="formation", format=format, force_outer_xml_tag=False, attached=True
|
F,
|
||||||
|
name="formation",
|
||||||
|
format=format,
|
||||||
|
force_outer_xml_tag=False,
|
||||||
|
attached=True,
|
||||||
|
filename=filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -322,11 +328,15 @@ def formation_list_table(formation_id=None, args={}):
|
|||||||
"session_id)s<a> " % s
|
"session_id)s<a> " % s
|
||||||
for s in f["sems"]
|
for s in f["sems"]
|
||||||
]
|
]
|
||||||
+ [
|
+ (
|
||||||
|
[
|
||||||
'<a class="stdlink" id="add-semestre-%s" '
|
'<a class="stdlink" id="add-semestre-%s" '
|
||||||
'href="formsemestre_createwithmodules?formation_id=%s&semestre_id=1">ajouter</a> '
|
'href="formsemestre_createwithmodules?formation_id=%s&semestre_id=1">ajouter</a> '
|
||||||
% (f["acronyme"].lower().replace(" ", "-"), f["formation_id"])
|
% (f["acronyme"].lower().replace(" ", "-"), f["formation_id"])
|
||||||
]
|
]
|
||||||
|
if current_user.has_permission(Permission.ScoImplement)
|
||||||
|
else []
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if f["sems"]:
|
if f["sems"]:
|
||||||
f["date_fin_dernier_sem"] = max([s["date_fin_iso"] for s in f["sems"]])
|
f["date_fin_dernier_sem"] = max([s["date_fin_iso"] for s in f["sems"]])
|
||||||
|
@ -78,7 +78,7 @@ def formsemestre_createwithmodules():
|
|||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
page_title="Création d'un semestre",
|
page_title="Création d'un semestre",
|
||||||
javascripts=["libjs/AutoSuggest.js"],
|
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
|
||||||
cssstyles=["css/autosuggest_inquisitor.css"],
|
cssstyles=["css/autosuggest_inquisitor.css"],
|
||||||
bodyOnLoad="init_tf_form('')",
|
bodyOnLoad="init_tf_form('')",
|
||||||
),
|
),
|
||||||
@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id):
|
|||||||
H = [
|
H = [
|
||||||
html_sco_header.html_sem_header(
|
html_sco_header.html_sem_header(
|
||||||
"Modification du semestre",
|
"Modification du semestre",
|
||||||
javascripts=["libjs/AutoSuggest.js"],
|
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
|
||||||
cssstyles=["css/autosuggest_inquisitor.css"],
|
cssstyles=["css/autosuggest_inquisitor.css"],
|
||||||
bodyOnLoad="init_tf_form('')",
|
bodyOnLoad="init_tf_form('')",
|
||||||
)
|
)
|
||||||
@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
# en APC, ne permet pas de changer de semestre
|
# en APC, ne permet pas de changer de semestre
|
||||||
semestre_id_list = [formsemestre.semestre_id]
|
semestre_id_list = [formsemestre.semestre_id]
|
||||||
else:
|
else:
|
||||||
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
|
semestre_id_list = list(range(1, NB_SEM + 1))
|
||||||
|
if not formation.is_apc():
|
||||||
|
# propose "pas de semestre" seulement en classique
|
||||||
|
semestre_id_list.insert(0, -1)
|
||||||
|
|
||||||
semestre_id_labels = []
|
semestre_id_labels = []
|
||||||
for sid in semestre_id_list:
|
for sid in semestre_id_list:
|
||||||
@ -341,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
|
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
|
||||||
if formation.is_apc()
|
if formation.is_apc()
|
||||||
else "",
|
else "",
|
||||||
|
"attributes": ['onchange="change_semestre_id();"']
|
||||||
|
if formation.is_apc()
|
||||||
|
else "",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -493,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
{
|
{
|
||||||
"input_type": "boolcheckbox",
|
"input_type": "boolcheckbox",
|
||||||
"title": "",
|
"title": "",
|
||||||
"explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations",
|
"explanation": """Autoriser tous les enseignants associés
|
||||||
|
à un module à y créer des évaluations""",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -534,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
]
|
]
|
||||||
|
|
||||||
nbmod = 0
|
nbmod = 0
|
||||||
if edit:
|
|
||||||
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"
|
|
||||||
else:
|
|
||||||
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td></tr>"
|
|
||||||
for semestre_id in semestre_ids:
|
for semestre_id in semestre_ids:
|
||||||
|
if formation.is_apc():
|
||||||
|
# pour restreindre l'édition aux module du semestre sélectionné
|
||||||
|
tr_class = f'class="sem{semestre_id}"'
|
||||||
|
else:
|
||||||
|
tr_class = ""
|
||||||
|
if edit:
|
||||||
|
templ_sep = f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"""
|
||||||
|
else:
|
||||||
|
templ_sep = (
|
||||||
|
f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td></tr>"""
|
||||||
|
)
|
||||||
modform.append(
|
modform.append(
|
||||||
(
|
(
|
||||||
"sep",
|
"sep",
|
||||||
@ -588,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
)
|
)
|
||||||
fcg += "</select>"
|
fcg += "</select>"
|
||||||
itemtemplate = (
|
itemtemplate = (
|
||||||
"""<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
|
f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
|
||||||
+ fcg
|
+ fcg
|
||||||
+ "</td></tr>"
|
+ "</td></tr>"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
itemtemplate = """<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
|
itemtemplate = f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
|
||||||
modform.append(
|
modform.append(
|
||||||
(
|
(
|
||||||
"MI" + str(mod["module_id"]),
|
"MI" + str(mod["module_id"]),
|
||||||
@ -601,7 +616,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
"input_type": "text_suggest",
|
"input_type": "text_suggest",
|
||||||
"size": 50,
|
"size": 50,
|
||||||
"withcheckbox": True,
|
"withcheckbox": True,
|
||||||
"title": "%s %s" % (mod["code"], mod["titre"]),
|
"title": "%s %s" % (mod["code"] or "", mod["titre"] or ""),
|
||||||
"allowed_values": allowed_user_names,
|
"allowed_values": allowed_user_names,
|
||||||
"template": itemtemplate,
|
"template": itemtemplate,
|
||||||
"text_suggest_options": {
|
"text_suggest_options": {
|
||||||
@ -633,7 +648,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
# 'allowed_values' : ['X'], 'labels' : [ '' ],
|
# 'allowed_values' : ['X'], 'labels' : [ '' ],
|
||||||
# 'title' : '' ,
|
# 'title' : '' ,
|
||||||
# 'explanation' : 'inscrire tous les étudiants du semestre aux modules ajoutés'}) )
|
# 'explanation' : 'inscrire tous les étudiants du semestre aux modules ajoutés'}) )
|
||||||
submitlabel = "Modifier ce semestre de formation"
|
submitlabel = "Modifier ce semestre"
|
||||||
else:
|
else:
|
||||||
submitlabel = "Créer ce semestre de formation"
|
submitlabel = "Créer ce semestre de formation"
|
||||||
#
|
#
|
||||||
@ -802,7 +817,9 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
}
|
}
|
||||||
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
|
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
|
||||||
mod = sco_edit_module.module_list({"module_id": module_id})[0]
|
mod = sco_edit_module.module_list({"module_id": module_id})[0]
|
||||||
msg += ["création de %s (%s)" % (mod["code"], mod["titre"])]
|
msg += [
|
||||||
|
"création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?")
|
||||||
|
]
|
||||||
# INSCRIPTIONS DES ETUDIANTS
|
# INSCRIPTIONS DES ETUDIANTS
|
||||||
log(
|
log(
|
||||||
'inscription module: %s = "%s"'
|
'inscription module: %s = "%s"'
|
||||||
@ -824,7 +841,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
)
|
)
|
||||||
msg += [
|
msg += [
|
||||||
"inscription de %d étudiants au module %s"
|
"inscription de %d étudiants au module %s"
|
||||||
% (len(etudids), mod["code"])
|
% (len(etudids), mod["code"] or "(module sans code)")
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
log(
|
log(
|
||||||
@ -919,11 +936,19 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
|
|||||||
if evals:
|
if evals:
|
||||||
msg += [
|
msg += [
|
||||||
'<b>impossible de supprimer %s (%s) car il y a %d évaluations définies (<a href="moduleimpl_status?moduleimpl_id=%s" class="stdlink">supprimer les d\'abord</a>)</b>'
|
'<b>impossible de supprimer %s (%s) car il y a %d évaluations définies (<a href="moduleimpl_status?moduleimpl_id=%s" class="stdlink">supprimer les d\'abord</a>)</b>'
|
||||||
% (mod["code"], mod["titre"], len(evals), moduleimpl_id)
|
% (
|
||||||
|
mod["code"] or "(module sans code)",
|
||||||
|
mod["titre"],
|
||||||
|
len(evals),
|
||||||
|
moduleimpl_id,
|
||||||
|
)
|
||||||
]
|
]
|
||||||
ok = False
|
ok = False
|
||||||
else:
|
else:
|
||||||
msg += ["suppression de %s (%s)" % (mod["code"], mod["titre"])]
|
msg += [
|
||||||
|
"suppression de %s (%s)"
|
||||||
|
% (mod["code"] or "(module sans code)", mod["titre"] or "")
|
||||||
|
]
|
||||||
sco_moduleimpl.do_moduleimpl_delete(
|
sco_moduleimpl.do_moduleimpl_delete(
|
||||||
moduleimpl_id, formsemestre_id=formsemestre_id
|
moduleimpl_id, formsemestre_id=formsemestre_id
|
||||||
)
|
)
|
||||||
|
@ -37,6 +37,9 @@ import flask
|
|||||||
from flask import url_for, g, request
|
from flask import url_for, g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models import FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import log
|
from app import log
|
||||||
@ -260,7 +263,8 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
|
|||||||
|
|
||||||
|
|
||||||
def _make_page(etud, sem, tf, message=""):
|
def _make_page(etud, sem, tf, message=""):
|
||||||
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
|
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
moy_gen = nt.get_etud_moy_gen(etud["etudid"])
|
moy_gen = nt.get_etud_moy_gen(etud["etudid"])
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
|
@ -32,14 +32,16 @@ import time
|
|||||||
import flask
|
import flask
|
||||||
from flask import url_for, g, request
|
from flask import url_for, g, request
|
||||||
|
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models import FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
from app.scodoc.sco_codes_parcours import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
|
from app.scodoc.sco_codes_parcours import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
from app.scodoc import sco_find_etud
|
from app.scodoc import sco_find_etud
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
@ -186,7 +188,9 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
|||||||
raise ScoValueError("desinscription impossible: semestre verrouille")
|
raise ScoValueError("desinscription impossible: semestre verrouille")
|
||||||
|
|
||||||
# -- Si decisions de jury, desinscription interdite
|
# -- Si decisions de jury, desinscription interdite
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
if nt.etud_has_decision(etudid):
|
if nt.etud_has_decision(etudid):
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"desinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire)"
|
"desinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire)"
|
||||||
@ -475,7 +479,8 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
|
|||||||
raise ScoValueError("Modification impossible: semestre verrouille")
|
raise ScoValueError("Modification impossible: semestre verrouille")
|
||||||
|
|
||||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etud_ue_status
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
F = html_sco_header.sco_footer()
|
F = html_sco_header.sco_footer()
|
||||||
H = [
|
H = [
|
||||||
@ -503,7 +508,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
|
|||||||
modimpls_by_ue_ids[ue_id].append(mod["moduleimpl_id"])
|
modimpls_by_ue_ids[ue_id].append(mod["moduleimpl_id"])
|
||||||
|
|
||||||
modimpls_by_ue_names[ue_id].append(
|
modimpls_by_ue_names[ue_id].append(
|
||||||
"%s %s" % (mod["module"]["code"], mod["module"]["titre"])
|
"%s %s" % (mod["module"]["code"] or "", mod["module"]["titre"] or "")
|
||||||
)
|
)
|
||||||
vals = scu.get_request_args()
|
vals = scu.get_request_args()
|
||||||
if not vals.get("tf_submitted", False):
|
if not vals.get("tf_submitted", False):
|
||||||
@ -527,7 +532,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
|
|||||||
if ue["type"] != UE_STANDARD:
|
if ue["type"] != UE_STANDARD:
|
||||||
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue["type"]]
|
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue["type"]]
|
||||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||||
if ue_status["is_capitalized"]:
|
if ue_status and ue_status["is_capitalized"]:
|
||||||
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
|
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
|
||||||
ue_descr += ' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)' % (
|
ue_descr += ' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)' % (
|
||||||
sem_origin["formsemestre_id"],
|
sem_origin["formsemestre_id"],
|
||||||
@ -648,7 +653,7 @@ function chkbx_select(field_id, state) {
|
|||||||
"%s (%s)"
|
"%s (%s)"
|
||||||
% (
|
% (
|
||||||
modsdict[x]["module"]["titre"],
|
modsdict[x]["module"]["titre"],
|
||||||
modsdict[x]["module"]["code"],
|
modsdict[x]["module"]["code"] or "(module sans code)",
|
||||||
)
|
)
|
||||||
for x in a_desinscrire
|
for x in a_desinscrire
|
||||||
]
|
]
|
||||||
@ -667,7 +672,7 @@ function chkbx_select(field_id, state) {
|
|||||||
"%s (%s)"
|
"%s (%s)"
|
||||||
% (
|
% (
|
||||||
modsdict[x]["module"]["titre"],
|
modsdict[x]["module"]["titre"],
|
||||||
modsdict[x]["module"]["code"],
|
modsdict[x]["module"]["code"] or "(module sans code)",
|
||||||
)
|
)
|
||||||
for x in a_inscrire
|
for x in a_inscrire
|
||||||
]
|
]
|
||||||
@ -785,7 +790,9 @@ def list_inscrits_ailleurs(formsemestre_id):
|
|||||||
Pour chacun, donne la liste des semestres.
|
Pour chacun, donne la liste des semestres.
|
||||||
{ etudid : [ liste de sems ] }
|
{ etudid : [ liste de sems ] }
|
||||||
"""
|
"""
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etudids
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
etudids = nt.get_etudids()
|
etudids = nt.get_etudids()
|
||||||
d = {}
|
d = {}
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
|
@ -31,13 +31,13 @@
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask import url_for
|
from flask import render_template, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import Module
|
from app.models import Module
|
||||||
from app.models import formsemestre
|
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
@ -51,7 +51,6 @@ from app.scodoc import sco_archives
|
|||||||
from app.scodoc import sco_bulletins
|
from app.scodoc import sco_bulletins
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_compute_moy
|
from app.scodoc import sco_compute_moy
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
@ -412,7 +411,7 @@ def formsemestre_status_menubar(sem):
|
|||||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
"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",
|
"endpoint": "notes.formsemestre_archive",
|
||||||
"args": {"formsemestre_id": formsemestre_id},
|
"args": {"formsemestre_id": formsemestre_id},
|
||||||
"enabled": sco_permissions_check.can_edit_pv(formsemestre_id),
|
"enabled": sco_permissions_check.can_edit_pv(formsemestre_id),
|
||||||
@ -446,6 +445,7 @@ def retreive_formsemestre_from_request() -> int:
|
|||||||
"""Cherche si on a de quoi déduire le semestre affiché à partir des
|
"""Cherche si on a de quoi déduire le semestre affiché à partir des
|
||||||
arguments de la requête:
|
arguments de la requête:
|
||||||
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
|
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
|
||||||
|
Returns None si pas défini.
|
||||||
"""
|
"""
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
args = request.args
|
args = request.args
|
||||||
@ -506,34 +506,17 @@ def formsemestre_page_title():
|
|||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
formsemestre_id = int(formsemestre_id)
|
formsemestre_id = int(formsemestre_id)
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy()
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
except:
|
except:
|
||||||
log("can't find formsemestre_id %s" % formsemestre_id)
|
log("can't find formsemestre_id %s" % formsemestre_id)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
fill_formsemestre(sem)
|
h = render_template(
|
||||||
|
"formsemestre_page_title.html",
|
||||||
h = f"""<div class="formsemestre_page_title">
|
formsemestre=formsemestre,
|
||||||
<div class="infos">
|
scu=scu,
|
||||||
<span class="semtitle"><a class="stdlink" title="{sem['session_id']}"
|
sem_menu_bar=formsemestre_status_menubar(formsemestre.to_dict()),
|
||||||
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>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
|
||||||
@ -596,10 +579,12 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
"""Description du semestre sous forme de table exportable
|
"""Description du semestre sous forme de table exportable
|
||||||
Liste des modules et de leurs coefficients
|
Liste des modules et de leurs coefficients
|
||||||
"""
|
"""
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[
|
||||||
|
0
|
||||||
|
]
|
||||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
||||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
|
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||||
formsemestre_id=formsemestre_id, sort_by_ue=True
|
formsemestre_id=formsemestre_id, sort_by_ue=True
|
||||||
@ -638,7 +623,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
)
|
)
|
||||||
l = {
|
l = {
|
||||||
"UE": M["ue"]["acronyme"],
|
"UE": M["ue"]["acronyme"],
|
||||||
"Code": M["module"]["code"],
|
"Code": M["module"]["code"] or "",
|
||||||
"Module": M["module"]["abbrev"] or M["module"]["titre"],
|
"Module": M["module"]["abbrev"] or M["module"]["titre"],
|
||||||
"_Module_class": "scotext",
|
"_Module_class": "scotext",
|
||||||
"Inscrits": len(ModInscrits),
|
"Inscrits": len(ModInscrits),
|
||||||
@ -709,7 +694,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
titles["coefficient"] = "Coef. éval."
|
titles["coefficient"] = "Coef. éval."
|
||||||
titles["evalcomplete_str"] = "Complète"
|
titles["evalcomplete_str"] = "Complète"
|
||||||
titles["publish_incomplete_str"] = "Toujours Utilisée"
|
titles["publish_incomplete_str"] = "Toujours Utilisée"
|
||||||
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), sem["titremois"])
|
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois())
|
||||||
|
|
||||||
return GenTable(
|
return GenTable(
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
@ -986,12 +971,10 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
|
|||||||
def formsemestre_status(formsemestre_id=None):
|
def formsemestre_status(formsemestre_id=None):
|
||||||
"""Tableau de bord semestre HTML"""
|
"""Tableau de bord semestre HTML"""
|
||||||
# porté du DTML
|
# porté du DTML
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||||
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||||
formsemestre_id=formsemestre_id
|
formsemestre_id=formsemestre_id
|
||||||
)
|
)
|
||||||
# nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
|
||||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
nt = res_sem.load_formsemestre_results(formsemestre)
|
nt = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
@ -1078,7 +1061,7 @@ def formsemestre_status(formsemestre_id=None):
|
|||||||
"</p>",
|
"</p>",
|
||||||
]
|
]
|
||||||
|
|
||||||
if use_ue_coefs:
|
if use_ue_coefs and not formsemestre.formation.is_apc():
|
||||||
H.append(
|
H.append(
|
||||||
"""
|
"""
|
||||||
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
|
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
|
||||||
@ -1187,8 +1170,10 @@ def formsemestre_tableau_modules(
|
|||||||
H.append('<tr class="formsemestre_status%s">' % fontorange)
|
H.append('<tr class="formsemestre_status%s">' % fontorange)
|
||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
'<td class="formsemestre_status_code"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="stdlink">%s</a></td>'
|
f"""<td class="formsemestre_status_code""><a
|
||||||
% (modimpl["moduleimpl_id"], mod_descr, mod.code)
|
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(
|
H.append(
|
||||||
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
|
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user