Compare commits
36 Commits
ed0925419e
...
50d2c91a54
Author | SHA1 | Date | |
---|---|---|---|
50d2c91a54 | |||
d98eb7dc6b | |||
6cd28853bc | |||
07423e08b6 | |||
815d5a71ae | |||
8135038edb | |||
e5a3e3a5a0 | |||
2642a25cf7 | |||
d7f4209a5a | |||
83a5855f3d | |||
8d85edbf1f | |||
0ec9814ac4 | |||
8331d4ff69 | |||
dfca8b07be | |||
e772a29363 | |||
dc009856d6 | |||
dcbf4f32c4 | |||
28e794c089 | |||
1d9c36f8e2 | |||
da93a99069 | |||
7aec0cd2f3 | |||
6aaacbe42e | |||
8b01df0b02 | |||
d1a2e52fef | |||
d97cb6f309 | |||
0895d7b195 | |||
cc7fcded98 | |||
ccc0e58a50 | |||
e2c0de56c3 | |||
fba623a58f | |||
a9bc28a49e | |||
b5d5bb3b91 | |||
4aa5c0e277 | |||
3c5cb3e517 | |||
cb13ea2e31 | |||
eaa82f61a4 |
@ -122,6 +122,7 @@ from app.api import (
|
||||
justificatifs,
|
||||
logos,
|
||||
moduleimpl,
|
||||
operations,
|
||||
partitions,
|
||||
semset,
|
||||
users,
|
||||
|
@ -5,7 +5,8 @@
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -39,6 +40,24 @@ from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
@bp.route("/assiduite/date_time_offset/<string:date_iso>")
|
||||
@api_web_bp.route("/assiduite/date_time_offset/<string:date_iso>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def date_time_offset(date_iso: str):
|
||||
"""L'offset dans le fuseau horaire du serveur pour la date indiquée.
|
||||
Renvoie une chaîne de la forme "+04:00" (ISO 8601)
|
||||
|
||||
Exemple: `/assiduite/date_time_offset/2024-10-01` renvoie `'+02:00'`
|
||||
"""
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_iso):
|
||||
json_error(
|
||||
404,
|
||||
message="date invalide",
|
||||
)
|
||||
return scu.get_local_timezone_offset(date_iso)
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:assiduite_id>")
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||
@scodoc
|
||||
@ -650,7 +669,9 @@ def assiduites_formsemestre_count(
|
||||
@permission_required(Permission.AbsChange)
|
||||
def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Enregistrement d'assiduités pour un étudiant (etudid)
|
||||
Enregistrement d'assiduités pour un étudiant (etudid).
|
||||
|
||||
Si les heures n'ont pas de timezone, elles sont exprimées dans celle du serveur.
|
||||
|
||||
DATA
|
||||
----
|
||||
@ -841,6 +862,10 @@ def _create_one(
|
||||
elif fin.tzinfo is None:
|
||||
fin: datetime = scu.localize_datetime(fin)
|
||||
|
||||
# check duration: min 1 minute
|
||||
if (deb is not None) and (fin is not None) and (fin - deb) < timedelta(seconds=60):
|
||||
errors.append("durée trop courte")
|
||||
|
||||
# cas 4 : desc
|
||||
desc: str = data.get("desc", None)
|
||||
|
||||
|
@ -96,7 +96,7 @@ def departement_get(dept_id: int):
|
||||
/departement/id/1;
|
||||
|
||||
"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
dept = Departement.get_or_404(dept_id)
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@ -212,7 +212,7 @@ def departement_etudiants_by_id(dept_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département d'id donné.
|
||||
"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
dept = Departement.get_or_404(dept_id)
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
|
||||
|
||||
@ -246,7 +246,7 @@ def departement_formsemestres_ids_by_id(dept_id: int):
|
||||
/departement/id/1/formsemestres_ids;
|
||||
|
||||
"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
dept = Departement.get_or_404(dept_id)
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
|
||||
@ -273,7 +273,7 @@ def departement_formsemestres_courants(acronym: str = "", dept_id: int | None =
|
||||
dept = (
|
||||
Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
if acronym
|
||||
else Departement.query.get_or_404(dept_id)
|
||||
else Departement.get_or_404(dept_id)
|
||||
)
|
||||
date_courante = request.args.get("date_courante")
|
||||
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
|
||||
|
@ -215,7 +215,7 @@ def evaluation_create(moduleimpl_id: int):
|
||||
/moduleimpl/1/evaluation/create;{""description"":""Exemple éval.""}
|
||||
|
||||
"""
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||
moduleimpl: ModuleImpl = ModuleImpl.get_or_404(moduleimpl_id)
|
||||
if not moduleimpl.can_edit_evaluation(current_user):
|
||||
return scu.json_error(403, "opération non autorisée")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
|
@ -199,7 +199,7 @@ def ue_set_parcours(ue_id: int):
|
||||
parcours = []
|
||||
else:
|
||||
parcours = [
|
||||
ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
|
||||
ApcParcours.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
|
||||
]
|
||||
log(f"ue_set_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
ok, error_message = ue.set_parcours(parcours)
|
||||
@ -226,7 +226,7 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
niveau: ApcNiveau = ApcNiveau.query.get_or_404(niveau_id)
|
||||
niveau: ApcNiveau = ApcNiveau.get_or_404(niveau_id)
|
||||
ok, error_message = ue.set_niveau_competence(niveau)
|
||||
if not ok:
|
||||
if g.scodoc_dept: # "usage web"
|
||||
|
@ -615,13 +615,13 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
result = []
|
||||
for modimpl_id in nt.modimpls_results:
|
||||
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
||||
modimpl: ModuleImpl = ModuleImpl.get_or_404(modimpl_id)
|
||||
modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
|
||||
|
||||
list_eval = []
|
||||
for evaluation_id in modimpl_results.evaluations_etat:
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
evaluation = Evaluation.get_or_404(evaluation_id)
|
||||
eval_dict = evaluation.to_dict_api()
|
||||
eval_dict["etat"] = eval_etat.to_dict()
|
||||
|
||||
|
@ -277,7 +277,7 @@ def validation_rcue_record(etudid: int):
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid date string")
|
||||
if parcours_id is not None:
|
||||
parcours: ApcParcours = ApcParcours.query.get_or_404(parcours_id)
|
||||
parcours: ApcParcours = ApcParcours.get_or_404(parcours_id)
|
||||
if parcours.referentiel_id != ue1.niveau_competence.competence.referentiel_id:
|
||||
return json_error(API_CLIENT_ERROR, "niveau et parcours incompatibles")
|
||||
|
||||
|
103
app/api/operations.py
Normal file
103
app/api/operations.py
Normal file
@ -0,0 +1,103 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : liste opérations effectuées par un utilisateur
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Operations
|
||||
"""
|
||||
|
||||
from flask import url_for
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.models import NotesNotes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
MAX_QUERY_LENGTH = 10000
|
||||
|
||||
|
||||
@bp.route("/operations/user/<int:uid>/notes")
|
||||
@api_web_bp.route("/operations/user/<int:uid>/notes")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def operations_user_notes(uid: int):
|
||||
"""Liste les opérations de saisie de notes effectuées par utilisateur.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
start: indice de début de la liste
|
||||
length: nombre d'éléments à retourner
|
||||
draw: numéro de la requête (pour pagination, renvoyé tel quel)
|
||||
order[dir]: desc ou asc
|
||||
search[value]: chaîne à chercher (dans évaluation et étudiant)
|
||||
PARAMS
|
||||
-----
|
||||
uid: l'id de l'utilisateur
|
||||
"""
|
||||
# --- Permission: restreint au superadmin ou à l'utilisateur lui-même
|
||||
if not app.current_user.is_administrator() and app.current_user.id != uid:
|
||||
return {"error": "Permission denied"}, 403
|
||||
start = int(app.request.args.get("start", 0))
|
||||
length = min(int(app.request.args.get("length", 10)), MAX_QUERY_LENGTH)
|
||||
order = app.request.args.get("order[dir]", "desc")
|
||||
draw = int(app.request.args.get("draw", 1))
|
||||
search = app.request.args.get("search[value]", "")
|
||||
query = db.session.query(NotesNotes).filter(NotesNotes.uid == uid)
|
||||
if order == "asc":
|
||||
query = query.order_by(NotesNotes.date.asc())
|
||||
else:
|
||||
query = query.order_by(NotesNotes.date.desc())
|
||||
|
||||
# Pour l'efficacité, limite si pas de recherche en python
|
||||
limited_query = query.offset(start).limit(length) if not search else query
|
||||
|
||||
data = []
|
||||
for note in limited_query:
|
||||
obj = {
|
||||
"date": note.date.isoformat(),
|
||||
"date_dmy": note.date.strftime(scu.DATEATIME_FMT),
|
||||
"operation": "Saisie de note",
|
||||
"value": scu.fmt_note(note.value),
|
||||
"id": note.id,
|
||||
"uid": note.uid,
|
||||
"etudiant": note.etudiant.to_dict_short(),
|
||||
"etudiant_link": note.etudiant.html_link_fiche(),
|
||||
"evaluation": note.evaluation.to_dict_api(),
|
||||
"evaluation_link": f"""<a href="{
|
||||
url_for('notes.evaluation_listenotes',
|
||||
scodoc_dept=note.evaluation.moduleimpl.formsemestre.departement.acronym,
|
||||
evaluation_id=note.evaluation_id)
|
||||
}">{note.evaluation.descr()}</a>""",
|
||||
}
|
||||
if search:
|
||||
search = search.lower()
|
||||
if (
|
||||
search not in note.etudiant.nomprenom.lower()
|
||||
and search not in note.evaluation.descr().lower()
|
||||
and search not in obj["date_dmy"]
|
||||
):
|
||||
continue # skip
|
||||
|
||||
data.append(obj)
|
||||
|
||||
result = data[start : start + length] if search else data
|
||||
return {
|
||||
"draw": draw,
|
||||
"recordsTotal": query.count(), # unfiltered
|
||||
"recordsFiltered": len(data) if search else query.count(),
|
||||
"data": result,
|
||||
}
|
@ -159,7 +159,7 @@ def group_etudiants_query(group_id: int):
|
||||
@as_json
|
||||
def group_set_etudiant(group_id: int, etudid: int):
|
||||
"""Affecte l'étudiant au groupe indiqué."""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
etud = Identite.get_or_404(etudid)
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -192,7 +192,7 @@ def group_set_etudiant(group_id: int, etudid: int):
|
||||
@as_json
|
||||
def group_remove_etud(group_id: int, etudid: int):
|
||||
"""Retire l'étudiant de ce groupe. S'il n'y est pas, ne fait rien."""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
etud = Identite.get_or_404(etudid)
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -224,7 +224,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
|
||||
(NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
|
||||
"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
etud = Identite.get_or_404(etudid)
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -533,7 +533,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
message="paramètre liste des partitions invalide",
|
||||
)
|
||||
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
||||
partition = Partition.query.get_or_404(p_id)
|
||||
partition = Partition.get_or_404(p_id)
|
||||
partition.numero = numero
|
||||
db.session.add(partition)
|
||||
db.session.commit()
|
||||
@ -579,7 +579,7 @@ def partition_order_groups(partition_id: int):
|
||||
message="paramètre liste de groupe invalide",
|
||||
)
|
||||
for group_id, numero in zip(group_ids, range(len(group_ids))):
|
||||
group = GroupDescr.query.get_or_404(group_id)
|
||||
group = GroupDescr.get_or_404(group_id)
|
||||
group.numero = numero
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
|
@ -188,7 +188,7 @@ def user_edit(uid: int):
|
||||
```
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user: User = User.query.get_or_404(uid)
|
||||
user: User = User.get_or_404(uid)
|
||||
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
||||
orig_dept = user.dept
|
||||
dest_dept = args.get("dept", False)
|
||||
@ -241,7 +241,7 @@ def user_password(uid: int):
|
||||
/user/3/password;{""password"" : ""rePlaCemeNT456averylongandcomplicated""}
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user: User = User.query.get_or_404(uid)
|
||||
user: User = User.get_or_404(uid)
|
||||
password = data.get("password")
|
||||
if not password:
|
||||
return json_error(404, "user_password: missing password")
|
||||
@ -272,7 +272,7 @@ def user_password(uid: int):
|
||||
@as_json
|
||||
def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
"""Ajoute un rôle à l'utilisateur dans le département donné."""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
user: User = User.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
_ = Departement.query.filter_by(acronym=dept).first_or_404()
|
||||
@ -301,7 +301,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
@as_json
|
||||
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
"""Retire le rôle (dans le département donné) à cet utilisateur."""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
user: User = User.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
_ = Departement.query.filter_by(acronym=dept).first_or_404()
|
||||
|
@ -38,7 +38,7 @@ def after_cas_login():
|
||||
flask.session["scodoc_cas_login_date"] = (
|
||||
datetime.datetime.now().isoformat()
|
||||
)
|
||||
user.cas_last_login = datetime.datetime.utcnow()
|
||||
user.cas_last_login = datetime.datetime.now()
|
||||
if flask.session.get("CAS_EDT_ID"):
|
||||
# essaie de récupérer l'edt_id s'il est présent
|
||||
# cet ID peut être renvoyé par le CAS et extrait par ScoDoc
|
||||
@ -158,7 +158,7 @@ CAS_USER_INFO_COMMENTS = (
|
||||
autorise la connexion via CAS (optionnel, faux par défaut)
|
||||
""",
|
||||
"""cas_allow_scodoc_login
|
||||
autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
|
||||
autorise connexion via ScoDoc même si CAS activé (optionnel, vrai par défaut)
|
||||
""",
|
||||
"""email_institutionnel
|
||||
optionnel, le mail officiel de l'utilisateur.
|
||||
|
@ -9,8 +9,8 @@ from flask import current_app, g, redirect, request, url_for
|
||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||
import flask_login
|
||||
|
||||
from app import db, login
|
||||
from app.auth.models import User
|
||||
from app import db, log, login
|
||||
from app.auth.models import User, Role
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
@ -19,7 +19,7 @@ token_auth = HTTPTokenAuth()
|
||||
|
||||
|
||||
@basic_auth.verify_password
|
||||
def verify_password(username, password):
|
||||
def verify_password(username, password) -> User | None:
|
||||
"""Verify password for this user
|
||||
Appelé lors d'une demande de jeton (normalement via la route /tokens)
|
||||
"""
|
||||
@ -28,6 +28,7 @@ def verify_password(username, password):
|
||||
g.current_user = user
|
||||
# note: est aussi basic_auth.current_user()
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
@basic_auth.error_handler
|
||||
@ -61,7 +62,8 @@ def token_auth_error(status):
|
||||
|
||||
|
||||
@token_auth.get_user_roles
|
||||
def get_user_roles(user):
|
||||
def get_user_roles(user) -> list[Role]:
|
||||
"list roles"
|
||||
return user.roles
|
||||
|
||||
|
||||
@ -82,7 +84,7 @@ def load_user_from_request(req: flask.Request) -> User:
|
||||
@login.unauthorized_handler
|
||||
def unauthorized():
|
||||
"flask-login: si pas autorisé, redirige vers page login, sauf si API"
|
||||
if request.blueprint == "api" or request.blueprint == "apiweb":
|
||||
if request.blueprint in ("api", "apiweb"):
|
||||
return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
@ -92,7 +92,7 @@ class User(UserMixin, ScoDocModel):
|
||||
cas_allow_scodoc_login = db.Column(
|
||||
db.Boolean, default=False, server_default="false", nullable=False
|
||||
)
|
||||
"""Si CAS forcé (cas_force), peut-on se logguer sur ScoDoc directement ?
|
||||
"""Si CAS activé et cas_id renseigné, peut-on se logguer sur ScoDoc directement ?
|
||||
(le rôle ScoSuperAdmin peut toujours, mettre à True pour les utilisateur API)
|
||||
"""
|
||||
cas_last_login = db.Column(db.DateTime, nullable=True)
|
||||
@ -101,10 +101,13 @@ class User(UserMixin, ScoDocModel):
|
||||
"identifiant emplois du temps (unicité non imposée)"
|
||||
password_hash = db.Column(db.Text()) # les hashs modernes peuvent être très longs
|
||||
password_scodoc7 = db.Column(db.String(42))
|
||||
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
date_modif_passwd = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
date_created = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_seen = db.Column(db.DateTime, default=datetime.now)
|
||||
date_modif_passwd = db.Column(db.DateTime, default=datetime.now)
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
date_expiration = db.Column(db.DateTime, default=None)
|
||||
passwd_must_be_changed = db.Column(
|
||||
db.Boolean, nullable=False, server_default="false", default=False
|
||||
)
|
||||
passwd_temp = db.Column(db.Boolean, default=False)
|
||||
"""champ obsolete. Si connexion alors que passwd_temp est vrai,
|
||||
efface mot de passe et redirige vers accueil."""
|
||||
@ -130,7 +133,7 @@ class User(UserMixin, ScoDocModel):
|
||||
self.roles = []
|
||||
self.user_roles = []
|
||||
# check login:
|
||||
if not "user_name" in kwargs:
|
||||
if "user_name" not in kwargs:
|
||||
raise ValueError("missing user_name argument")
|
||||
if not is_valid_user_name(kwargs["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||
@ -177,7 +180,7 @@ class User(UserMixin, ScoDocModel):
|
||||
|
||||
def set_password(self, password):
|
||||
"Set password"
|
||||
current_app.logger.info(f"set_password({self})")
|
||||
log(f"set_password({self})")
|
||||
if password:
|
||||
self.password_hash = generate_password_hash(password)
|
||||
else:
|
||||
@ -185,6 +188,8 @@ class User(UserMixin, ScoDocModel):
|
||||
# La création d'un mot de passe efface l'éventuel mot de passe historique
|
||||
self.password_scodoc7 = None
|
||||
self.passwd_temp = False
|
||||
# Retire le flag
|
||||
self.passwd_must_be_changed = False
|
||||
|
||||
def check_password(self, password: str) -> bool:
|
||||
"""Check given password vs current one.
|
||||
@ -206,10 +211,23 @@ class User(UserMixin, ScoDocModel):
|
||||
send_notif_desactivation_user(self)
|
||||
return False
|
||||
|
||||
# if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login
|
||||
# if CAS activated and cas_id, allow only super-user and users with cas_allow_scodoc_login
|
||||
cas_enabled = ScoDocSiteConfig.is_cas_enabled()
|
||||
if cas_enabled and ScoDocSiteConfig.get("cas_force"):
|
||||
if (not self.is_administrator()) and not self.cas_allow_scodoc_login:
|
||||
if cas_enabled and not self.is_administrator():
|
||||
if not self.cas_allow_scodoc_login:
|
||||
# CAS activé et compte non autorisé à se logguer sur ScoDoc
|
||||
log(
|
||||
f"""auth: login attempt for user {self.user_name}: scodoc login not allowed
|
||||
"""
|
||||
)
|
||||
return False
|
||||
# si CAS activé et forcé et cas_id renseigné, on ne peut pas se logguer
|
||||
if self.cas_id and ScoDocSiteConfig.get("cas_force"):
|
||||
log(
|
||||
f"""auth: login attempt for user {self.user_name
|
||||
} (cas_id='{
|
||||
self.cas_id}'): cas forced and cas_id set: scodoc login not allowed"""
|
||||
)
|
||||
return False
|
||||
|
||||
if not self.password_hash: # user without password can't login
|
||||
@ -250,7 +268,7 @@ class User(UserMixin, ScoDocModel):
|
||||
)
|
||||
except jwt.exceptions.ExpiredSignatureError:
|
||||
log("verify_reset_password_token: token expired")
|
||||
except:
|
||||
except: # pylint: disable=bare-except
|
||||
return None
|
||||
try:
|
||||
user_id = token["reset_password"]
|
||||
@ -282,6 +300,7 @@ class User(UserMixin, ScoDocModel):
|
||||
if self.date_modif_passwd
|
||||
else None
|
||||
),
|
||||
"passwd_must_be_changed": self.passwd_must_be_changed,
|
||||
"date_created": (
|
||||
self.date_created.isoformat() + "Z" if self.date_created else None
|
||||
),
|
||||
@ -347,31 +366,31 @@ class User(UserMixin, ScoDocModel):
|
||||
|
||||
return args_dict
|
||||
|
||||
def from_dict(self, data: dict, new_user=False):
|
||||
def from_dict(self, args: dict, new_user=False):
|
||||
"""Set users' attributes from given dict values.
|
||||
- roles_string : roles, encoded like "Ens_RT, Secr_CJ"
|
||||
- date_expiration is a dateime object.
|
||||
Does not check permissions here.
|
||||
"""
|
||||
if new_user:
|
||||
if "user_name" in data:
|
||||
if "user_name" in args:
|
||||
# never change name of existing users
|
||||
# (see change_user_name method to do that)
|
||||
if not is_valid_user_name(data["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {data['user_name']}")
|
||||
self.user_name = data["user_name"]
|
||||
if "password" in data:
|
||||
self.set_password(data["password"])
|
||||
if not is_valid_user_name(args["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {args['user_name']}")
|
||||
self.user_name = args["user_name"]
|
||||
if "password" in args:
|
||||
self.set_password(args["password"])
|
||||
|
||||
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
||||
if "roles_string" in data:
|
||||
if "roles_string" in args:
|
||||
self.user_roles = []
|
||||
for r_d in data["roles_string"].split(","):
|
||||
for r_d in args["roles_string"].split(","):
|
||||
if r_d:
|
||||
role, dept = UserRole.role_dept_from_string(r_d)
|
||||
self.add_role(role, dept)
|
||||
|
||||
super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
|
||||
super().from_dict(args, excluded={"user_name", "roles_string", "roles"})
|
||||
|
||||
if ScoDocSiteConfig.cas_uid_use_scodoc():
|
||||
self.cas_id = self.user_name
|
||||
@ -385,7 +404,7 @@ class User(UserMixin, ScoDocModel):
|
||||
|
||||
def get_token(self, expires_in=3600):
|
||||
"Un jeton pour cet user. Stocké en base, non commité."
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now()
|
||||
if self.token and self.token_expiration > now + timedelta(seconds=60):
|
||||
return self.token
|
||||
self.token = base64.b64encode(os.urandom(24)).decode("utf-8")
|
||||
@ -395,7 +414,7 @@ class User(UserMixin, ScoDocModel):
|
||||
|
||||
def revoke_token(self):
|
||||
"Révoque le jeton de cet utilisateur"
|
||||
self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
|
||||
self.token_expiration = datetime.now() - timedelta(seconds=1)
|
||||
|
||||
@staticmethod
|
||||
def check_token(token):
|
||||
@ -403,7 +422,7 @@ class User(UserMixin, ScoDocModel):
|
||||
and returns the user object.
|
||||
"""
|
||||
user = User.query.filter_by(token=token).first()
|
||||
if user is None or user.token_expiration < datetime.utcnow():
|
||||
if user is None or user.token_expiration < datetime.now():
|
||||
return None
|
||||
return user
|
||||
|
||||
@ -438,7 +457,10 @@ class User(UserMixin, ScoDocModel):
|
||||
# les role liés à ce département, et les roles avec dept=None (super-admin)
|
||||
roles_in_dept = (
|
||||
UserRole.query.filter_by(user_id=self.id)
|
||||
.filter((UserRole.dept == dept) | (UserRole.dept == None))
|
||||
.filter(
|
||||
(UserRole.dept == dept)
|
||||
| (UserRole.dept == None) # pylint: disable=C0121
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for user_role in roles_in_dept:
|
||||
@ -616,17 +638,19 @@ class User(UserMixin, ScoDocModel):
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
"Notre utilisateur anonyme"
|
||||
|
||||
def has_permission(self, perm, dept=None):
|
||||
def has_permission(self, perm, dept=None): # pylint: disable=unused-argument
|
||||
"always false, anonymous has no permission"
|
||||
return False
|
||||
|
||||
def is_administrator(self):
|
||||
"always false, anonymous is not admin"
|
||||
return False
|
||||
|
||||
|
||||
login.anonymous_user = AnonymousUser
|
||||
|
||||
|
||||
class Role(db.Model):
|
||||
class Role(ScoDocModel):
|
||||
"""Roles for ScoDoc"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -730,7 +754,7 @@ class Role(db.Model):
|
||||
return Role.query.filter_by(name=name).first()
|
||||
|
||||
|
||||
class UserRole(db.Model):
|
||||
class UserRole(ScoDocModel):
|
||||
"""Associate user to role, in a dept.
|
||||
If dept is None, the role applies to all departments (eg super admin).
|
||||
"""
|
||||
|
@ -4,7 +4,7 @@ auth.routes.py
|
||||
"""
|
||||
|
||||
import flask
|
||||
from flask import current_app, flash, render_template
|
||||
from flask import current_app, flash, g, render_template
|
||||
from flask import redirect, url_for, request
|
||||
from flask_login import login_user, current_user
|
||||
from sqlalchemy import func
|
||||
@ -23,6 +23,7 @@ from app.auth.email import send_password_reset_email
|
||||
from app.decorators import admin_required
|
||||
from app.forms.generic import SimpleConfirmationForm
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.models.departements import Departement
|
||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -49,6 +50,24 @@ def _login_form():
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
|
||||
current_app.logger.info("login: success (%s)", form.user_name.data)
|
||||
|
||||
if user.passwd_must_be_changed:
|
||||
# Mot de passe à changer à la première connexion
|
||||
dept = user.dept or getattr(g, "scodoc_dept", None)
|
||||
if not dept:
|
||||
departement = db.session.query(Departement).first()
|
||||
dept = departement.acronym
|
||||
if dept:
|
||||
# Redirect to the password change page
|
||||
flash("Votre mot de passe doit être changé")
|
||||
return redirect(
|
||||
url_for(
|
||||
"users.form_change_password",
|
||||
scodoc_dept=dept,
|
||||
user_name=user.user_name,
|
||||
)
|
||||
)
|
||||
|
||||
return form.redirect("scodoc.index")
|
||||
|
||||
return render_template(
|
||||
|
@ -407,7 +407,9 @@ class BulletinBUT:
|
||||
d = {
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"date": datetime.datetime.now(datetime.timezone.utc)
|
||||
.astimezone()
|
||||
.isoformat(),
|
||||
"publie": not formsemestre.bul_hide_xml,
|
||||
"etat_inscription": etud.inscription_etat(formsemestre.id),
|
||||
"etudiant": etud.to_dict_bul(),
|
||||
|
@ -57,7 +57,7 @@ from app.views import ScoData
|
||||
@permission_required(Permission.ScoView)
|
||||
def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
|
||||
"""Page HTML affichant le bulletin BUT simplifié"""
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
etud: Identite = Identite.get_or_404(etudid)
|
||||
formsemestre: FormSemestre = (
|
||||
FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
.join(FormSemestreInscription)
|
||||
|
@ -508,7 +508,7 @@ def but_validations_ues_parcours(
|
||||
# Les UEs associées au tronc commun (à aucun parcours)
|
||||
# UniteEns.query.filter(~UniteEns.id.in_(UEParcours.query.with_entities(UEParcours.ue_id)))
|
||||
|
||||
parcour = ApcParcours.query.get(parcour_id)
|
||||
parcour: ApcParcours = db.session.get(ApcParcours, parcour_id)
|
||||
if not parcour:
|
||||
raise ScoValueError(f"but_validations_ues_parcours: {parcour_id} inexistant")
|
||||
# Les validations d'UE de ce parcours ou du tronc commun pour cet étudiant:
|
||||
|
@ -1,7 +1,7 @@
|
||||
from app import db
|
||||
from app import db, models
|
||||
|
||||
|
||||
class Entreprise(db.Model):
|
||||
class Entreprise(models.ScoDocModel):
|
||||
__tablename__ = "are_entreprises"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
siret = db.Column(db.Text, index=True, unique=True)
|
||||
@ -45,7 +45,7 @@ class Entreprise(db.Model):
|
||||
}
|
||||
|
||||
|
||||
class EntrepriseSite(db.Model):
|
||||
class EntrepriseSite(models.ScoDocModel):
|
||||
__tablename__ = "are_sites"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
entreprise_id = db.Column(
|
||||
@ -65,7 +65,7 @@ class EntrepriseSite(db.Model):
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
entreprise = Entreprise.query.get_or_404(self.entreprise_id)
|
||||
entreprise = Entreprise.get_or_404(self.entreprise_id)
|
||||
return {
|
||||
"siret_entreprise": entreprise.siret,
|
||||
"id_site": self.id,
|
||||
@ -77,7 +77,7 @@ class EntrepriseSite(db.Model):
|
||||
}
|
||||
|
||||
|
||||
class EntrepriseCorrespondant(db.Model):
|
||||
class EntrepriseCorrespondant(models.ScoDocModel):
|
||||
__tablename__ = "are_correspondants"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
site_id = db.Column(db.Integer, db.ForeignKey("are_sites.id", ondelete="cascade"))
|
||||
@ -92,7 +92,7 @@ class EntrepriseCorrespondant(db.Model):
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
def to_dict(self):
|
||||
site = EntrepriseSite.query.get_or_404(self.site_id)
|
||||
site = EntrepriseSite.get_or_404(self.site_id)
|
||||
return {
|
||||
"id": self.id,
|
||||
"civilite": self.civilite,
|
||||
@ -108,7 +108,7 @@ class EntrepriseCorrespondant(db.Model):
|
||||
}
|
||||
|
||||
|
||||
class EntrepriseContact(db.Model):
|
||||
class EntrepriseContact(models.ScoDocModel):
|
||||
__tablename__ = "are_contacts"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True))
|
||||
@ -119,7 +119,7 @@ class EntrepriseContact(db.Model):
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
|
||||
class EntrepriseOffre(db.Model):
|
||||
class EntrepriseOffre(models.ScoDocModel):
|
||||
__tablename__ = "are_offres"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
entreprise_id = db.Column(
|
||||
@ -147,7 +147,7 @@ class EntrepriseOffre(db.Model):
|
||||
}
|
||||
|
||||
|
||||
class EntrepriseHistorique(db.Model):
|
||||
class EntrepriseHistorique(models.ScoDocModel):
|
||||
__tablename__ = "are_historique"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
@ -158,7 +158,7 @@ class EntrepriseHistorique(db.Model):
|
||||
text = db.Column(db.Text)
|
||||
|
||||
|
||||
class EntrepriseStageApprentissage(db.Model):
|
||||
class EntrepriseStageApprentissage(models.ScoDocModel):
|
||||
__tablename__ = "are_stages_apprentissages"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
entreprise_id = db.Column(
|
||||
@ -176,7 +176,7 @@ class EntrepriseStageApprentissage(db.Model):
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
|
||||
class EntrepriseTaxeApprentissage(db.Model):
|
||||
class EntrepriseTaxeApprentissage(models.ScoDocModel):
|
||||
__tablename__ = "are_taxe_apprentissage"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
entreprise_id = db.Column(
|
||||
@ -187,7 +187,7 @@ class EntrepriseTaxeApprentissage(db.Model):
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
|
||||
class EntrepriseEnvoiOffre(db.Model):
|
||||
class EntrepriseEnvoiOffre(models.ScoDocModel):
|
||||
__tablename__ = "are_envoi_offre"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||
@ -196,7 +196,7 @@ class EntrepriseEnvoiOffre(db.Model):
|
||||
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
|
||||
class EntrepriseEnvoiOffreEtudiant(db.Model):
|
||||
class EntrepriseEnvoiOffreEtudiant(models.ScoDocModel):
|
||||
__tablename__ = "are_envoi_offre_etudiant"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
|
||||
@ -207,14 +207,14 @@ class EntrepriseEnvoiOffreEtudiant(db.Model):
|
||||
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
|
||||
class EntrepriseOffreDepartement(db.Model):
|
||||
class EntrepriseOffreDepartement(models.ScoDocModel):
|
||||
__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):
|
||||
class EntreprisePreferences(models.ScoDocModel):
|
||||
__tablename__ = "are_preferences"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.Text)
|
||||
|
@ -1489,7 +1489,7 @@ def add_stage_apprentissage(entreprise_id):
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
etudiant = Identite.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
@ -1552,7 +1552,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
etudiant = Identite.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
|
@ -50,7 +50,7 @@ from app.scodoc import codes_cursus
|
||||
|
||||
def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||
"""Delete a formation"""
|
||||
formation: Formation = Formation.query.get_or_404(formation_id)
|
||||
formation: Formation = Formation.get_or_404(formation_id)
|
||||
|
||||
H = [
|
||||
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
|
||||
@ -159,7 +159,7 @@ def formation_edit(formation_id=None, create=False):
|
||||
is_locked = False
|
||||
else:
|
||||
# edit an existing formation
|
||||
formation: Formation = Formation.query.get_or_404(formation_id)
|
||||
formation: Formation = Formation.get_or_404(formation_id)
|
||||
form_dict = formation.to_dict()
|
||||
form_dict["commentaire"] = form_dict["commentaire"] or ""
|
||||
initvalues = form_dict
|
||||
@ -347,7 +347,7 @@ def do_formation_edit(args) -> bool:
|
||||
if "formation_code" in args and not args["formation_code"]:
|
||||
del args["formation_code"]
|
||||
|
||||
formation: Formation = Formation.query.get_or_404(args["formation_id"])
|
||||
formation: Formation = Formation.get_or_404(args["formation_id"])
|
||||
# On autorise la modif de la formation meme si elle est verrouillee
|
||||
# car cela ne change que du cosmetique, (sauf eventuellement le code formation ?)
|
||||
# mais si verrouillée on ne peut changer le type de parcours
|
||||
@ -386,7 +386,7 @@ def do_formation_edit(args) -> bool:
|
||||
def module_move(module_id, after=0, redirect=True):
|
||||
"""Move before/after previous one (decrement/increment numero)"""
|
||||
redirect = bool(redirect)
|
||||
module = Module.query.get_or_404(module_id)
|
||||
module = Module.get_or_404(module_id)
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError(f'invalid value for "after" ({after})')
|
||||
@ -430,7 +430,7 @@ def module_move(module_id, after=0, redirect=True):
|
||||
|
||||
def ue_move(ue_id, after=0, redirect=1):
|
||||
"""Move UE before/after previous one (decrement/increment numero)"""
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
ue = UniteEns.get_or_404(ue_id)
|
||||
redirect = int(redirect)
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
|
@ -45,7 +45,7 @@ from app.scodoc.sco_exceptions import (
|
||||
|
||||
def matiere_create(ue_id=None):
|
||||
"""Formulaire création d'une matiere"""
|
||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||
ue: UniteEns = UniteEns.get_or_404(ue_id)
|
||||
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
|
||||
H = [
|
||||
f"""<h2>Création d'une matière dans l'UE {ue.titre or ''} ({ue.acronyme})</h2>
|
||||
|
@ -52,7 +52,7 @@ from app.scodoc import codes_cursus
|
||||
|
||||
def module_delete(module_id=None):
|
||||
"""Formulaire suppression d'un module"""
|
||||
module = Module.query.get_or_404(module_id)
|
||||
module = Module.get_or_404(module_id)
|
||||
|
||||
if not module.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
@ -149,13 +149,13 @@ def module_edit(
|
||||
formation = ue.formation
|
||||
orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id
|
||||
else:
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
formation = Formation.get_or_404(formation_id)
|
||||
module = None
|
||||
unlocked = True
|
||||
else:
|
||||
if not module_id:
|
||||
raise ValueError("missing module_id !")
|
||||
module = models.Module.query.get_or_404(module_id)
|
||||
module = models.Module.get_or_404(module_id)
|
||||
ue = module.ue
|
||||
module_dict = module.to_dict()
|
||||
formation = module.formation
|
||||
@ -714,7 +714,7 @@ def module_edit(
|
||||
old_ue_id = module.ue.id
|
||||
new_ue_id = tf[2]["ue_id"]
|
||||
if (old_ue_id != new_ue_id) and in_use:
|
||||
new_ue = UniteEns.query.get_or_404(new_ue_id)
|
||||
new_ue = UniteEns.get_or_404(new_ue_id)
|
||||
if new_ue.semestre_idx != module.ue.semestre_idx:
|
||||
# pas changer de semestre un module utilisé !
|
||||
raise ScoValueError(
|
||||
@ -808,7 +808,7 @@ def formation_add_malus_modules(
|
||||
):
|
||||
"""Création d'un module de "malus" dans chaque UE d'une formation"""
|
||||
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
formation = Formation.get_or_404(formation_id)
|
||||
|
||||
nb = 0
|
||||
ues = formation.ues
|
||||
|
@ -219,7 +219,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"""Formulaire modification ou création d'une UE"""
|
||||
create = int(create)
|
||||
if not create:
|
||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||
ue: UniteEns = UniteEns.get_or_404(ue_id)
|
||||
ue_dict = ue.to_dict()
|
||||
formation_id = ue.formation_id
|
||||
title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
|
||||
@ -573,7 +573,7 @@ def next_ue_numero(formation_id, semestre_id=None) -> int:
|
||||
|
||||
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
||||
"""Delete an UE"""
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
ue = UniteEns.get_or_404(ue_id)
|
||||
if ue.modules.all():
|
||||
raise ScoValueError(
|
||||
f"""Suppression de l'UE {ue.titre} impossible car
|
||||
@ -1108,7 +1108,7 @@ def _ue_table_ues(
|
||||
if ue.titre != ue.acronyme:
|
||||
acro_titre += " " + (ue.titre or "")
|
||||
H.append(
|
||||
f"""acro_titre <span class="ue_code">(code {ue.ue_code}{ects_str}, coef. {
|
||||
f"""{acro_titre} <span class="ue_code">(code {ue.ue_code}{ects_str}, coef. {
|
||||
(ue.coefficient or 0):3.2f}{code_apogee_str})</span>
|
||||
<span class="ue_coef"></span>
|
||||
"""
|
||||
@ -1370,7 +1370,7 @@ def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None
|
||||
hide_ue_id spécifie un id à retirer de la liste.
|
||||
"""
|
||||
if ue_id is not None:
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
ue = UniteEns.get_or_404(ue_id)
|
||||
if not ue_code:
|
||||
ue_code = ue.ue_code
|
||||
formation_code = ue.formation.formation_code
|
||||
|
@ -653,7 +653,7 @@ def formation_list_table(detail: bool) -> GenTable:
|
||||
|
||||
def formation_create_new_version(formation_id, redirect=True):
|
||||
"duplicate formation, with new version number"
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
formation = Formation.get_or_404(formation_id)
|
||||
resp = formation_export(
|
||||
formation_id, export_ids=True, export_external_ues=True, fmt="xml"
|
||||
)
|
||||
|
@ -60,7 +60,7 @@ def formsemestre_associate_new_version(
|
||||
formsemestre_id: optionnel, formsemestre de départ, qui sera associé à la nouvelle version
|
||||
"""
|
||||
formsemestre_id = int(formsemestre_id) if formsemestre_id else None
|
||||
formation: Formation = Formation.query.get_or_404(formation_id)
|
||||
formation: Formation = Formation.get_or_404(formation_id)
|
||||
other_formsemestre_ids = {int(x) for x in (other_formsemestre_ids or [])}
|
||||
if request.method == "GET":
|
||||
# dresse la liste des semestres non verrouillés de la même formation
|
||||
@ -191,7 +191,7 @@ def do_formsemestres_associate_new_version(
|
||||
log(f"do_formsemestres_associate_new_version {formation_id} {formsemestre_ids}")
|
||||
|
||||
# Check: tous les semestres de la formation
|
||||
formsemestres = [FormSemestre.query.get_or_404(i) for i in formsemestre_ids]
|
||||
formsemestres = [FormSemestre.get_or_404(i) for i in formsemestre_ids]
|
||||
if not all(
|
||||
[formsemestre.formation_id == formation_id for formsemestre in formsemestres]
|
||||
):
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
from flask import abort, g
|
||||
import sqlalchemy
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
import app
|
||||
from app import db
|
||||
|
||||
@ -164,6 +166,16 @@ class ScoDocModel(db.Model):
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
# Compatibilité avec SQLAlchemy 2.0
|
||||
@classmethod
|
||||
def get_or_404(cls, oid: int | str):
|
||||
"""Get instance or abort 404"""
|
||||
stmt = select(cls).where(cls.id == oid)
|
||||
try:
|
||||
return db.session.execute(stmt).scalar_one()
|
||||
except NoResultFound:
|
||||
abort(404)
|
||||
|
||||
|
||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||
from app.models.departements import Departement
|
||||
|
@ -3,10 +3,10 @@
|
||||
"""Gestion des absences
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app import db, models
|
||||
|
||||
|
||||
class Absence(db.Model):
|
||||
class Absence(models.ScoDocModel):
|
||||
"""LEGACY
|
||||
Ce modèle n'est PLUS UTILISE depuis ScoDoc 9.6 et remplacé par assiduité.
|
||||
une absence (sur une demi-journée)
|
||||
@ -45,7 +45,7 @@ class Absence(db.Model):
|
||||
return data
|
||||
|
||||
|
||||
class AbsenceNotification(db.Model):
|
||||
class AbsenceNotification(models.ScoDocModel):
|
||||
"""Notification d'absence émise"""
|
||||
|
||||
__tablename__ = "absences_notifications"
|
||||
@ -67,7 +67,7 @@ class AbsenceNotification(db.Model):
|
||||
)
|
||||
|
||||
|
||||
class BilletAbsence(db.Model):
|
||||
class BilletAbsence(models.ScoDocModel):
|
||||
"""Billet d'absence (signalement par l'étudiant)"""
|
||||
|
||||
__tablename__ = "billet_absence"
|
||||
|
@ -124,19 +124,25 @@ class Assiduite(ScoDocModel):
|
||||
return data
|
||||
|
||||
def __str__(self) -> str:
|
||||
"chaine pour journaux et debug (lisible par humain français)"
|
||||
"chaine pour journaux et debug (lisible par humain français, en timezone serveur)"
|
||||
try:
|
||||
etat_str = EtatAssiduite(self.etat).name.lower().capitalize()
|
||||
except ValueError:
|
||||
etat_str = "Invalide"
|
||||
# passe en timezone serveur
|
||||
d_deb = self.date_debut.astimezone(scu.TIME_ZONE)
|
||||
d_fin = self.date_fin.astimezone(scu.TIME_ZONE)
|
||||
return f"""{etat_str} {
|
||||
"just." if self.est_just else "non just."
|
||||
} de {
|
||||
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
||||
d_deb.strftime("%d/%m/%Y %Hh%M")
|
||||
} à {
|
||||
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
||||
d_fin.strftime("%d/%m/%Y %Hh%M")
|
||||
}"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Assiduite {self.id}: {self.etudiant.nom}: {self.__str__()}>"
|
||||
|
||||
@classmethod
|
||||
def create_assiduite(
|
||||
cls,
|
||||
@ -746,7 +752,7 @@ def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
||||
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
||||
les identifiants, sinon les dict si long est vrai)
|
||||
"""
|
||||
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
||||
assi: Assiduite = Assiduite.get_or_404(assiduite_id)
|
||||
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
|
||||
|
||||
|
||||
|
@ -15,7 +15,7 @@ from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm import class_mapper
|
||||
import sqlalchemy
|
||||
|
||||
from app import db, log
|
||||
from app import db, log, models
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
@ -56,7 +56,7 @@ class XMLModel:
|
||||
return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">'
|
||||
|
||||
|
||||
class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
class ApcReferentielCompetences(models.ScoDocModel, XMLModel):
|
||||
"Référentiel de compétence d'une spécialité"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(
|
||||
@ -76,7 +76,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
"version": "version_orebut",
|
||||
}
|
||||
# ScoDoc specific fields:
|
||||
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.now)
|
||||
scodoc_orig_filename = db.Column(db.Text())
|
||||
# Relations:
|
||||
competences = db.relationship(
|
||||
@ -339,7 +339,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
return doc.get(self.specialite, {})
|
||||
|
||||
|
||||
class ApcCompetence(db.Model, XMLModel):
|
||||
class ApcCompetence(models.ScoDocModel, XMLModel):
|
||||
"Compétence"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
@ -410,7 +410,7 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
}
|
||||
|
||||
|
||||
class ApcSituationPro(db.Model, XMLModel):
|
||||
class ApcSituationPro(models.ScoDocModel, XMLModel):
|
||||
"Situation professionnelle"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
@ -425,7 +425,7 @@ class ApcSituationPro(db.Model, XMLModel):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
|
||||
class ApcComposanteEssentielle(db.Model, XMLModel):
|
||||
class ApcComposanteEssentielle(models.ScoDocModel, XMLModel):
|
||||
"Composante essentielle"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
@ -439,7 +439,7 @@ class ApcComposanteEssentielle(db.Model, XMLModel):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
|
||||
class ApcNiveau(db.Model, XMLModel):
|
||||
class ApcNiveau(models.ScoDocModel, XMLModel):
|
||||
"""Niveau de compétence
|
||||
Chaque niveau peut être associé à deux UE,
|
||||
des semestres impair et pair de la même année.
|
||||
@ -608,7 +608,7 @@ app_critiques_modules = db.Table(
|
||||
)
|
||||
|
||||
|
||||
class ApcAppCritique(db.Model, XMLModel):
|
||||
class ApcAppCritique(models.ScoDocModel, XMLModel):
|
||||
"Apprentissage Critique BUT"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
niveau_id = db.Column(
|
||||
@ -694,7 +694,7 @@ parcours_formsemestre = db.Table(
|
||||
"""Association parcours <-> formsemestre (many-to-many)"""
|
||||
|
||||
|
||||
class ApcParcours(db.Model, XMLModel):
|
||||
class ApcParcours(models.ScoDocModel, XMLModel):
|
||||
"Un parcours BUT"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
@ -749,7 +749,7 @@ class ApcParcours(db.Model, XMLModel):
|
||||
)
|
||||
|
||||
|
||||
class ApcAnneeParcours(db.Model, XMLModel):
|
||||
class ApcAnneeParcours(models.ScoDocModel, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
parcours_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="CASCADE"), nullable=False
|
||||
@ -774,7 +774,7 @@ class ApcAnneeParcours(db.Model, XMLModel):
|
||||
}
|
||||
|
||||
|
||||
class ApcParcoursNiveauCompetence(db.Model):
|
||||
class ApcParcoursNiveauCompetence(models.ScoDocModel):
|
||||
"""Association entre année de parcours et compétence.
|
||||
Le "niveau" de la compétence est donné ici
|
||||
(convention Orébut)
|
||||
|
@ -8,7 +8,7 @@ import re
|
||||
import urllib.parse
|
||||
|
||||
from flask import flash
|
||||
from app import current_app, db, log
|
||||
from app import current_app, db, log, models
|
||||
from app.comp import bonus_spo
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_utils as scu
|
||||
@ -68,7 +68,7 @@ def code_scodoc_to_apo_default(code):
|
||||
return CODES_SCODOC_TO_APO.get(code, "DEF")
|
||||
|
||||
|
||||
class ScoDocSiteConfig(db.Model):
|
||||
class ScoDocSiteConfig(models.ScoDocModel):
|
||||
"""Config. d'un site
|
||||
Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
|
||||
antérieures étaient dans scodoc_config.py
|
||||
|
@ -4,7 +4,7 @@
|
||||
"""
|
||||
import re
|
||||
|
||||
from app import db
|
||||
from app import db, models
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.preferences import ScoPreference
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
@ -12,7 +12,7 @@ from app.scodoc.sco_exceptions import ScoValueError
|
||||
VALID_DEPT_EXP = re.compile(r"^[\w@\\\-\.]+$")
|
||||
|
||||
|
||||
class Departement(db.Model):
|
||||
class Departement(models.ScoDocModel):
|
||||
"""Un département ScoDoc"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -61,7 +61,7 @@ class Departement(db.Model):
|
||||
dept_id = None
|
||||
if dept_id is None:
|
||||
return cls.query.filter_by(acronym=dept_ident).first_or_404()
|
||||
return cls.query.get_or_404(dept_id)
|
||||
return cls.get_or_404(dept_id)
|
||||
|
||||
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||
data = {
|
||||
|
@ -121,6 +121,18 @@ class Identite(models.ScoDocModel):
|
||||
cascade="all, delete-orphan",
|
||||
lazy="dynamic",
|
||||
)
|
||||
notes = db.relationship(
|
||||
"NotesNotes",
|
||||
backref="etudiant",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="dynamic",
|
||||
)
|
||||
notes_log = db.relationship(
|
||||
"NotesNotesLog",
|
||||
backref="etudiant",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="dynamic",
|
||||
)
|
||||
# Relations avec les assiduites et les justificatifs
|
||||
assiduites = db.relationship(
|
||||
"Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
||||
@ -516,7 +528,7 @@ class Identite(models.ScoDocModel):
|
||||
e_dict["date_naissance"] = ndb.DateISOtoDMY(e_dict.get("date_naissance", ""))
|
||||
e_dict["ne"] = self.e
|
||||
e_dict["nomprenom"] = self.nomprenom
|
||||
adresse = self.adresses.first()
|
||||
adresse: Adresse = self.adresses.first()
|
||||
if adresse:
|
||||
e_dict.update(adresse.to_dict(restrict=restrict))
|
||||
if with_inscriptions:
|
||||
|
@ -58,6 +58,19 @@ class Evaluation(models.ScoDocModel):
|
||||
# ordre de presentation (par défaut, le plus petit numero
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
notes = db.relationship(
|
||||
"NotesNotes",
|
||||
backref="evaluation",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="dynamic",
|
||||
)
|
||||
notes_log = db.relationship(
|
||||
"NotesNotesLog",
|
||||
backref="evaluation",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="dynamic",
|
||||
primaryjoin="Evaluation.id == foreign(NotesNotesLog.evaluation_id)",
|
||||
)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
_sco_dept_relations = ("ModuleImpl", "FormSemestre") # accès au dept_id
|
||||
@ -380,6 +393,13 @@ class Evaluation(models.ScoDocModel):
|
||||
return f"""du {self.date_debut.strftime('%d/%m/%Y')} à {
|
||||
_h(self.date_debut)} au {self.date_fin.strftime('%d/%m/%Y')} à {_h(self.date_fin)}"""
|
||||
|
||||
def descr(self) -> str:
|
||||
"Description de l'évaluation pour affichage (avec module et semestre)"
|
||||
return f"""{self.description} {self.descr_date()} en {
|
||||
self.moduleimpl.module.titre_str()} du {
|
||||
self.moduleimpl.formsemestre.titre_formation(with_sem_idx=True)
|
||||
}"""
|
||||
|
||||
def heure_debut(self) -> str:
|
||||
"""L'heure de début (sans la date), en ISO.
|
||||
Chaine vide si non renseignée."""
|
||||
@ -526,7 +546,7 @@ class Evaluation(models.ScoDocModel):
|
||||
)
|
||||
|
||||
|
||||
class EvaluationUEPoids(db.Model):
|
||||
class EvaluationUEPoids(models.ScoDocModel):
|
||||
"""Poids des évaluations (BUT)
|
||||
association many to many
|
||||
"""
|
||||
|
@ -62,7 +62,7 @@ class Scolog(ScoDocModel):
|
||||
}
|
||||
|
||||
|
||||
class ScolarNews(db.Model):
|
||||
class ScolarNews(ScoDocModel):
|
||||
"""Nouvelles pour page d'accueil"""
|
||||
|
||||
NEWS_ABS = "ABS" # saisie absence
|
||||
|
@ -646,9 +646,11 @@ class FormSemestre(models.ScoDocModel):
|
||||
)
|
||||
return [db.session.get(ModuleImpl, modimpl_id) for modimpl_id in cursor]
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
def can_be_edited_by(self, user: User):
|
||||
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
||||
if not user.has_permission(Permission.EditFormSemestre): # pas chef
|
||||
if user.passwd_must_be_changed or not user.has_permission(
|
||||
Permission.EditFormSemestre
|
||||
): # pas chef
|
||||
if not self.resp_can_edit or user.id not in [
|
||||
resp.id for resp in self.responsables
|
||||
]:
|
||||
@ -897,6 +899,8 @@ class FormSemestre(models.ScoDocModel):
|
||||
if not self.etat:
|
||||
return False # semestre verrouillé
|
||||
user = user or current_user
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
if user.has_permission(Permission.EtudChangeGroups):
|
||||
return True # typiquement admin, chef dept
|
||||
return self.est_responsable(user)
|
||||
@ -906,11 +910,15 @@ class FormSemestre(models.ScoDocModel):
|
||||
dans ce semestre: vérifie permission et verrouillage.
|
||||
"""
|
||||
user = user or current_user
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
return self.etat and self.est_chef_or_diretud(user)
|
||||
|
||||
def can_edit_pv(self, user: User = None):
|
||||
"Vrai si utilisateur (par def. current) peut editer un PV de jury de ce semestre"
|
||||
user = user or current_user
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
# Autorise les secrétariats, repérés via la permission EtudChangeAdr
|
||||
return self.est_chef_or_diretud(user) or user.has_permission(
|
||||
Permission.EtudChangeAdr
|
||||
@ -1575,7 +1583,7 @@ class FormSemestreUECoef(models.ScoDocModel):
|
||||
coefficient = db.Column(db.Float, nullable=False)
|
||||
|
||||
|
||||
class FormSemestreUEComputationExpr(db.Model):
|
||||
class FormSemestreUEComputationExpr(models.ScoDocModel):
|
||||
"""Formules utilisateurs pour calcul moyenne UE (désactivées en 9.2+)."""
|
||||
|
||||
__tablename__ = "notes_formsemestre_ue_computation_expr"
|
||||
|
@ -372,11 +372,3 @@ group_membership = db.Table(
|
||||
db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")),
|
||||
db.UniqueConstraint("etudid", "group_id"),
|
||||
)
|
||||
# class GroupMembership(db.Model):
|
||||
# """Association groupe / étudiant"""
|
||||
|
||||
# __tablename__ = "group_membership"
|
||||
# __table_args__ = (db.UniqueConstraint("etudid", "group_id"),)
|
||||
# id = db.Column(db.Integer, primary_key=True)
|
||||
# etudid = db.Column(db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"))
|
||||
# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id"))
|
||||
|
@ -199,6 +199,8 @@ class ModuleImpl(ScoDocModel):
|
||||
"""True if this user can create, delete or edit and evaluation in this modimpl
|
||||
(nb: n'implique pas le droit de saisir ou modifier des notes)
|
||||
"""
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
# acces pour resp. moduleimpl et resp. form semestre (dir etud)
|
||||
if (
|
||||
user.has_permission(Permission.EditAllEvals)
|
||||
@ -222,6 +224,8 @@ class ModuleImpl(ScoDocModel):
|
||||
# was sco_permissions_check.can_edit_notes
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
if not self.formsemestre.etat:
|
||||
return False # semestre verrouillé
|
||||
is_dir_etud = user.id in (u.id for u in self.formsemestre.responsables)
|
||||
@ -247,6 +251,8 @@ class ModuleImpl(ScoDocModel):
|
||||
if raise_exc:
|
||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||
return False
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
# -- check access
|
||||
# admin ou resp. semestre avec flag resp_can_change_resp
|
||||
if user.has_permission(Permission.EditFormSemestre):
|
||||
@ -264,6 +270,8 @@ class ModuleImpl(ScoDocModel):
|
||||
if user is None, current user.
|
||||
"""
|
||||
user = current_user if user is None else user
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
if not self.formsemestre.etat:
|
||||
if raise_exc:
|
||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||
@ -285,6 +293,8 @@ class ModuleImpl(ScoDocModel):
|
||||
Autorise ScoEtudInscrit ou responsables semestre.
|
||||
"""
|
||||
user = current_user if user is None else user
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
if not self.formsemestre.etat:
|
||||
if raise_exc:
|
||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||
|
@ -624,7 +624,7 @@ class Module(models.ScoDocModel):
|
||||
return "", http.HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
class ModuleUECoef(db.Model):
|
||||
class ModuleUECoef(models.ScoDocModel):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
En mode APC, ces coefs remplacent le coefficient "PPN" du module.
|
||||
"""
|
||||
|
@ -83,7 +83,7 @@ class NotesNotes(models.ScoDocModel):
|
||||
} {db.session.get(Evaluation, self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||
|
||||
|
||||
class NotesNotesLog(db.Model):
|
||||
class NotesNotesLog(models.ScoDocModel):
|
||||
"""Historique des modifs sur notes (anciennes entrees de notes_notes)"""
|
||||
|
||||
__tablename__ = "notes_notes_log"
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""évènements scolaires dans la vie d'un étudiant(inscription, ...)
|
||||
"""
|
||||
from app import db
|
||||
|
||||
from app import db, models
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
|
||||
class ScolarEvent(db.Model):
|
||||
class ScolarEvent(models.ScoDocModel):
|
||||
"""Evenement dans le parcours scolaire d'un étudiant"""
|
||||
|
||||
__tablename__ = "scolar_events"
|
||||
|
@ -573,7 +573,7 @@ class UniteEns(models.ScoDocModel):
|
||||
return self.set_parcours(self.parcours + [parcour])
|
||||
|
||||
|
||||
class UEParcours(db.Model):
|
||||
class UEParcours(models.ScoDocModel):
|
||||
"""Association ue <-> parcours, indiquant les ECTS"""
|
||||
|
||||
__tablename__ = "ue_parcours"
|
||||
@ -593,7 +593,7 @@ class UEParcours(db.Model):
|
||||
return f"<UEParcours( ue_id={self.ue_id}, parcours_id={self.parcours_id}, ects={self.ects})>"
|
||||
|
||||
|
||||
class DispenseUE(db.Model):
|
||||
class DispenseUE(models.ScoDocModel):
|
||||
"""Dispense d'UE
|
||||
Utilisé en APC (BUT) pour indiquer
|
||||
- les étudiants redoublants avec une UE capitalisée qu'ils ne refont pas.
|
||||
|
@ -4,8 +4,7 @@
|
||||
"""
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import db, log, models
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.events import Scolog
|
||||
@ -16,7 +15,7 @@ from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import CODES_UE_VALIDES
|
||||
|
||||
|
||||
class ScolarFormSemestreValidation(db.Model):
|
||||
class ScolarFormSemestreValidation(models.ScoDocModel):
|
||||
"""Décisions de jury (sur semestre ou UEs)"""
|
||||
|
||||
__tablename__ = "scolar_formsemestre_validation"
|
||||
@ -158,7 +157,7 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
)
|
||||
|
||||
|
||||
class ScolarAutorisationInscription(db.Model):
|
||||
class ScolarAutorisationInscription(models.ScoDocModel):
|
||||
"""Autorisation d'inscription dans un semestre"""
|
||||
|
||||
__tablename__ = "scolar_autorisation_inscription"
|
||||
|
@ -442,8 +442,9 @@ class TF(object):
|
||||
self.formid}_submit" id="{self.formid}_submit" value="{self.submitlabel
|
||||
}" {' '.join(self.submitbuttonattributes)}/>"""
|
||||
if self.cancelbutton:
|
||||
buttons_markup += f""" <input class="btn btn-default" type="submit" name="cancel" id="{
|
||||
self.formid}_cancel" value="{self.cancelbutton}">"""
|
||||
buttons_markup += f""" <input class="btn btn-default" type="submit"
|
||||
name="{self.formid}_cancel" id="{self.formid}_cancel"
|
||||
value="{self.cancelbutton}">"""
|
||||
buttons_markup += "</div>"
|
||||
|
||||
R = []
|
||||
|
@ -499,11 +499,7 @@ class GenTable:
|
||||
H.append(caption)
|
||||
if self.base_url:
|
||||
H.append('<span class="gt_export_icons">')
|
||||
if self.xls_link:
|
||||
H.append(
|
||||
f""" <a href="{add_query_param(self.base_url, "fmt", "xls")
|
||||
}">{scu.ICON_XLS}</a>"""
|
||||
)
|
||||
H.append(self.xls_export_button())
|
||||
if self.xls_link and self.pdf_link:
|
||||
H.append(" ")
|
||||
if self.pdf_link:
|
||||
@ -517,6 +513,15 @@ class GenTable:
|
||||
H.append(self.html_next_section)
|
||||
return "\n".join(H)
|
||||
|
||||
def xls_export_button(self) -> str:
|
||||
"markup pour export excel"
|
||||
return (
|
||||
f""" <a href="{add_query_param(self.base_url, "fmt", "xls")
|
||||
}">{scu.ICON_XLS}</a>"""
|
||||
if self.xls_link
|
||||
else ""
|
||||
)
|
||||
|
||||
def excel(self, wb=None):
|
||||
"""Simple Excel representation of the table"""
|
||||
if wb is None:
|
||||
|
@ -514,32 +514,6 @@ def DateISOtoDMY(isodate):
|
||||
return "%02d/%02d/%04d" % (day, month, year)
|
||||
|
||||
|
||||
def TimetoISO8601(t, null_is_empty=False) -> str:
|
||||
"convert time string to ISO 8601 (allow 16:03, 16h03, 16)"
|
||||
if isinstance(t, datetime.time):
|
||||
return t.isoformat()
|
||||
if not t and null_is_empty:
|
||||
return ""
|
||||
t = t.strip().upper().replace("H", ":")
|
||||
if t and t.count(":") == 0 and len(t) < 3:
|
||||
t = t + ":00"
|
||||
return t
|
||||
|
||||
|
||||
def TimefromISO8601(t) -> str:
|
||||
"convert time string from ISO 8601 to our display format"
|
||||
if not t:
|
||||
return t
|
||||
# XXX strange bug turnaround...
|
||||
try:
|
||||
t = "%s:%s" % (t.hour(), t.minute())
|
||||
# log('TimefromISO8601: converted isotime to iso !')
|
||||
except:
|
||||
pass
|
||||
fs = str(t).split(":")
|
||||
return fs[0] + "h" + fs[1] # discard seconds
|
||||
|
||||
|
||||
def float_null_is_zero(x):
|
||||
if x is None or x == "":
|
||||
return 0.0
|
||||
|
@ -282,7 +282,7 @@ class ApoEtud(dict):
|
||||
):
|
||||
res = self.autre_res
|
||||
else:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
formsemestre = FormSemestre.get_or_404(sem["formsemestre_id"])
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
if etudid not in res.identdict:
|
||||
@ -761,7 +761,7 @@ class ApoData:
|
||||
if not self.sems_etape:
|
||||
raise ScoValueError("aucun semestre trouvé !")
|
||||
self.formsemestres_etape = [
|
||||
FormSemestre.query.get_or_404(s["formsemestre_id"]) for s in self.sems_etape
|
||||
FormSemestre.get_or_404(s["formsemestre_id"]) for s in self.sems_etape
|
||||
]
|
||||
apcs = {
|
||||
formsemestre.formation.is_apc() for formsemestre in self.formsemestres_etape
|
||||
@ -903,9 +903,7 @@ class ApoData:
|
||||
"""
|
||||
codes_by_sem = {}
|
||||
for sem in self.sems_etape:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(
|
||||
sem["formsemestre_id"]
|
||||
)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(sem["formsemestre_id"])
|
||||
# L'ensemble des codes apo associés aux éléments:
|
||||
codes_semestre = formsemestre.get_codes_apogee()
|
||||
codes_modules = set().union(
|
||||
|
@ -54,9 +54,11 @@ class EtudsArchiver(sco_archives.BaseArchiver):
|
||||
ETUDS_ARCHIVER = EtudsArchiver()
|
||||
|
||||
|
||||
def can_edit_etud_archive(authuser):
|
||||
def can_edit_etud_archive(user):
|
||||
"""True si l'utilisateur peut modifier les archives etudiantes"""
|
||||
return authuser.has_permission(Permission.EtudAddAnnotations)
|
||||
if user.passwd_must_be_changed:
|
||||
return False
|
||||
return user.has_permission(Permission.EtudAddAnnotations)
|
||||
|
||||
|
||||
def etud_list_archives_html(etud: Identite):
|
||||
|
@ -416,7 +416,7 @@ def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
|
||||
|
||||
def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False):
|
||||
"""Delete an archive"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
if not formsemestre.can_edit_pv():
|
||||
raise ScoPermissionDenied(
|
||||
dest_url=url_for(
|
||||
|
@ -159,7 +159,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
|
||||
# Formation et parcours
|
||||
if I["sem"]["formation_id"]:
|
||||
formation_dict = Formation.query.get_or_404(I["sem"]["formation_id"]).to_dict()
|
||||
formation_dict = Formation.get_or_404(I["sem"]["formation_id"]).to_dict()
|
||||
else: # what's the fuck ?
|
||||
formation_dict = {
|
||||
"acronyme": "?",
|
||||
@ -1001,6 +1001,8 @@ def formsemestre_bulletinetud(
|
||||
def can_send_bulletin_by_mail(formsemestre_id):
|
||||
"""True if current user is allowed to send a bulletin (pdf) by mail"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if current_user.passwd_must_be_changed:
|
||||
return False
|
||||
return (
|
||||
sco_preferences.get_preference("bul_mail_allowed_for_all", formsemestre_id)
|
||||
or current_user.has_permission(Permission.EditFormSemestre)
|
||||
|
@ -225,7 +225,7 @@ def get_formsemestre_bulletins_pdf(
|
||||
from app.but import bulletin_but_court
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
versions = (
|
||||
scu.BULLETINS_VERSIONS_BUT
|
||||
if formsemestre.formation.is_apc()
|
||||
|
@ -313,7 +313,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
sont validés. En sortie, sem_idx_set contient ceux qui n'ont pas été validés."""
|
||||
for sem in self.get_semestres():
|
||||
if sem["formation_code"] == self.formation.formation_code:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
formsemestre = FormSemestre.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
decision = nt.get_etud_decision_sem(self.etudid)
|
||||
if decision and code_semestre_validant(decision["code"]):
|
||||
@ -408,7 +408,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
if not sem:
|
||||
code = "" # non inscrit à ce semestre
|
||||
else:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
formsemestre = FormSemestre.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
decision = nt.get_etud_decision_sem(self.etudid)
|
||||
if decision:
|
||||
@ -949,7 +949,7 @@ def do_formsemestre_validate_ue(
|
||||
):
|
||||
"""Ajoute ou change validation UE"""
|
||||
if semestre_id is None:
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
ue = UniteEns.get_or_404(ue_id)
|
||||
semestre_id = ue.semestre_idx
|
||||
args = {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
|
@ -222,18 +222,21 @@ def translate_calendar(
|
||||
if group and group_ids_set and group.id not in group_ids_set:
|
||||
continue # ignore cet évènement
|
||||
modimpl: ModuleImpl | bool = event["modimpl"]
|
||||
params = {
|
||||
"scodoc_dept": g.scodoc_dept,
|
||||
"heure_deb": scu.heure_to_iso8601(event["heure_deb"]),
|
||||
"heure_fin": scu.heure_to_iso8601(event["heure_fin"]),
|
||||
"day": event["jour"],
|
||||
}
|
||||
if group:
|
||||
params["group_ids"] = group.id
|
||||
if modimpl:
|
||||
params["moduleimpl_id"] = modimpl.id
|
||||
elif group:
|
||||
params["formsemestre_id"] = group.partition.formsemestre_id
|
||||
|
||||
url_abs = (
|
||||
url_for(
|
||||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
group_ids=group.id,
|
||||
heure_deb=event["heure_deb"],
|
||||
heure_fin=event["heure_fin"],
|
||||
moduleimpl_id=modimpl.id,
|
||||
day=event["jour"],
|
||||
)
|
||||
if modimpl and group
|
||||
else None
|
||||
url_for("assiduites.signal_assiduites_group", **params) if group else None
|
||||
)
|
||||
match modimpl:
|
||||
case False: # EDT non configuré
|
||||
|
@ -347,7 +347,7 @@ def apo_semset_maq_status(
|
||||
if missing:
|
||||
formation_ids = {sem["formation_id"] for sem in semset.sems}
|
||||
formations = [
|
||||
Formation.query.get_or_404(formation_id) for formation_id in formation_ids
|
||||
Formation.get_or_404(formation_id) for formation_id in formation_ids
|
||||
]
|
||||
H.append(
|
||||
f"""<div class="apo_csv_status_missing_elems">
|
||||
|
@ -137,9 +137,6 @@ _identiteEditor = ndb.EditableTable(
|
||||
(
|
||||
"admission_id",
|
||||
"boursier",
|
||||
"cas_allow_login",
|
||||
"cas_allow_scodoc_login",
|
||||
"cas_id",
|
||||
"civilite_etat_civil",
|
||||
"civilite", # 'M", "F", or "X"
|
||||
"code_ine",
|
||||
|
@ -64,7 +64,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=()):
|
||||
semlist = [dpv["formsemestre"] for dpv in dpv_by_sem.values() if dpv]
|
||||
semlist_parcours = []
|
||||
for sem in semlist:
|
||||
sem["formation"] = Formation.query.get_or_404(sem["formation_id"]).to_dict()
|
||||
sem["formation"] = Formation.get_or_404(sem["formation_id"]).to_dict()
|
||||
sem["parcours"] = codes_cursus.get_cursus_from_code(
|
||||
sem["formation"]["type_parcours"]
|
||||
)
|
||||
|
@ -148,7 +148,7 @@ def _formsemestre_enrich(sem):
|
||||
# imports ici pour eviter refs circulaires
|
||||
from app.scodoc import sco_formsemestre_edit
|
||||
|
||||
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
|
||||
formation: Formation = Formation.get_or_404(sem["formation_id"])
|
||||
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
||||
# 'S1', 'S2', ... ou '' pour les monosemestres
|
||||
if sem["semestre_id"] != NO_SEMESTRE_ID:
|
||||
|
@ -134,15 +134,6 @@ def formsemestre_editwithmodules(formsemestre_id: int):
|
||||
)
|
||||
|
||||
|
||||
def can_edit_sem(formsemestre_id: int = None, sem=None):
|
||||
"""Return sem if user can edit it, False otherwise"""
|
||||
sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if not current_user.has_permission(Permission.EditFormSemestre): # pas chef
|
||||
if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
|
||||
return False
|
||||
return sem
|
||||
|
||||
|
||||
RESP_FIELDS = [
|
||||
"responsable_id",
|
||||
"responsable_id2",
|
||||
@ -181,7 +172,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
formation = formsemestre.formation
|
||||
else:
|
||||
formation_id = int(vals["formation_id"])
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
formation = Formation.get_or_404(formation_id)
|
||||
|
||||
is_apc = formation.is_apc()
|
||||
if not edit:
|
||||
@ -706,8 +697,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
def opt_selected(gid):
|
||||
if gid == vals.get(select_name):
|
||||
return "selected"
|
||||
else:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
if mod.id in module_ids_set:
|
||||
# pas de menu inscription si le module est déjà présent
|
||||
@ -866,7 +856,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
{msg}
|
||||
{tf[1]}
|
||||
"""
|
||||
elif tf[0] == -1:
|
||||
if tf[0] == -1: # Annulation
|
||||
if formsemestre:
|
||||
return redirect(
|
||||
url_for(
|
||||
@ -876,6 +866,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
)
|
||||
)
|
||||
return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
||||
|
||||
# Edition ou modification du semestre
|
||||
tf[2]["gestion_compensation"] = bool(tf[2]["gestion_compensation_lst"])
|
||||
tf[2]["gestion_semestrielle"] = bool(tf[2]["gestion_semestrielle_lst"])
|
||||
@ -890,6 +881,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
# (retire le "MI" du début du nom de champs)
|
||||
module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
|
||||
_formsemestre_check_ue_bonus_unicity(module_ids_checked)
|
||||
ok = False
|
||||
if not edit:
|
||||
if is_apc:
|
||||
_formsemestre_check_module_list(module_ids_checked, tf[2]["semestre_id"])
|
||||
@ -1075,10 +1067,10 @@ def _formsemestre_check_module_list(module_ids, semestre_idx):
|
||||
"""
|
||||
# vérification de la cohérence / modules / semestre
|
||||
mod_sems_idx = {
|
||||
Module.query.get_or_404(module_id).ue.semestre_idx for module_id in module_ids
|
||||
Module.get_or_404(module_id).ue.semestre_idx for module_id in module_ids
|
||||
}
|
||||
if mod_sems_idx and mod_sems_idx != {semestre_idx}:
|
||||
modules = [Module.query.get_or_404(module_id) for module_id in module_ids]
|
||||
modules = [Module.get_or_404(module_id) for module_id in module_ids]
|
||||
log(
|
||||
f"""_formsemestre_check_module_list:
|
||||
{chr(10).join( str(module) + " " + str(module.ue) for module in modules )}
|
||||
@ -1096,7 +1088,7 @@ def _formsemestre_check_module_list(module_ids, semestre_idx):
|
||||
|
||||
def _formsemestre_check_ue_bonus_unicity(module_ids):
|
||||
"""Vérifie qu'il n'y a qu'une seule UE bonus associée aux modules choisis"""
|
||||
ues = [Module.query.get_or_404(module_id).ue for module_id in module_ids]
|
||||
ues = [Module.get_or_404(module_id).ue for module_id in module_ids]
|
||||
ues_bonus = {ue.id for ue in ues if ue.type == codes_cursus.UE_SPORT}
|
||||
if len(ues_bonus) > 1:
|
||||
raise ScoValueError(
|
||||
@ -1293,9 +1285,7 @@ def do_formsemestre_clone(
|
||||
New dates, responsable_id
|
||||
"""
|
||||
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
|
||||
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
|
||||
orig_formsemestre_id
|
||||
)
|
||||
formsemestre_orig: FormSemestre = FormSemestre.get_or_404(orig_formsemestre_id)
|
||||
# 1- create sem
|
||||
args = formsemestre_orig.to_dict()
|
||||
del args["formsemestre_id"]
|
||||
|
@ -61,7 +61,7 @@ def formsemestre_ext_create(etud: Identite | None, sem_params: dict) -> FormSeme
|
||||
sem_params: dict nécessaire à la création du formsemestre
|
||||
"""
|
||||
# Check args
|
||||
_ = Formation.query.get_or_404(sem_params["formation_id"])
|
||||
_ = Formation.get_or_404(sem_params["formation_id"])
|
||||
|
||||
# Create formsemestre
|
||||
sem_params["modalite"] = "EXT"
|
||||
@ -230,7 +230,7 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
|
||||
La moyenne générale indicative du semestre est calculée et affichée,
|
||||
mais pas enregistrée.
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
etud = Identite.get_etud(etudid)
|
||||
ues = formsemestre.formation.ues.filter(UniteEns.type != UE_SPORT).order_by(
|
||||
UniteEns.semestre_idx, UniteEns.numero
|
||||
|
@ -248,9 +248,7 @@ def do_formsemestre_inscription_with_modules(
|
||||
group_ids = [group_ids]
|
||||
# Check that all groups exist before creating the inscription
|
||||
groups = [
|
||||
GroupDescr.query.get_or_404(group_id)
|
||||
for group_id in group_ids
|
||||
if group_id != ""
|
||||
GroupDescr.get_or_404(group_id) for group_id in group_ids if group_id != ""
|
||||
]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
|
||||
# Inscription au semestre
|
||||
@ -377,7 +375,7 @@ def formsemestre_inscription_with_modules(
|
||||
)
|
||||
if multiple_ok:
|
||||
multiple_ok = int(multiple_ok)
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
etud = Identite.get_etud(etudid)
|
||||
if etud.dept_id != formsemestre.dept_id:
|
||||
raise ScoValueError("l'étudiant n'est pas dans ce département")
|
||||
|
@ -1079,7 +1079,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
raise ScoInvalidIdType(
|
||||
"formsemestre_bulletinetud: formsemestre_id must be an integer !"
|
||||
)
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
# S'assure que les groupes de parcours sont à jour:
|
||||
if int(check_parcours):
|
||||
formsemestre.setup_parcours_groups()
|
||||
|
@ -1383,7 +1383,7 @@ def do_formsemestre_validate_previous_ue(
|
||||
cette UE (utile seulement pour les semestres extérieurs).
|
||||
"""
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||
ue: UniteEns = UniteEns.get_or_404(ue_id)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
if ue_coefficient is not None:
|
||||
|
@ -663,7 +663,7 @@ def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
|
||||
(et le désinscrit d'autres groupes de cette partition)
|
||||
Return True si changement, False s'il était déjà dans ce groupe.
|
||||
"""
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
etud: Identite = Identite.get_or_404(etudid)
|
||||
if not group.partition.set_etud_group(etud, group):
|
||||
return # pas de changement
|
||||
|
||||
@ -742,7 +742,7 @@ groupsToDelete={groupsToDelete}
|
||||
except ValueError:
|
||||
log(f"setGroups: ignoring invalid group_id={group_id}")
|
||||
continue
|
||||
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
|
||||
group: GroupDescr = GroupDescr.get_or_404(group_id)
|
||||
# Anciens membres du groupe:
|
||||
old_members_set = {etud.id for etud in group.etuds}
|
||||
# Place dans ce groupe les etudiants indiqués:
|
||||
@ -807,7 +807,7 @@ def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
||||
If default, create default partition (with no name)
|
||||
Obsolete: utiliser Partition.create_group
|
||||
"""
|
||||
partition = Partition.query.get_or_404(partition_id)
|
||||
partition = Partition.get_or_404(partition_id)
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
#
|
||||
@ -840,7 +840,7 @@ def delete_group(group_id, partition_id=None):
|
||||
est bien dans cette partition.
|
||||
S'il s'agit d'un groupe de parcours, affecte l'inscription des étudiants aux parcours.
|
||||
"""
|
||||
group = GroupDescr.query.get_or_404(group_id)
|
||||
group = GroupDescr.get_or_404(group_id)
|
||||
if partition_id:
|
||||
if partition_id != group.partition_id:
|
||||
raise ValueError("inconsistent partition/group")
|
||||
@ -1096,7 +1096,7 @@ def partition_set_attr(partition_id, attr, value):
|
||||
if attr not in {"bul_show_rank", "show_in_lists"}:
|
||||
raise ValueError(f"invalid partition attribute: {attr}")
|
||||
|
||||
partition = Partition.query.get_or_404(partition_id)
|
||||
partition = Partition.get_or_404(partition_id)
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
|
||||
@ -1583,6 +1583,7 @@ def listgroups_abbrev(groups):
|
||||
|
||||
|
||||
# form_group_choice replaces formChoixGroupe
|
||||
# obsolete, utilisé seulement par formsemestre_inscription_with_modules
|
||||
def form_group_choice(
|
||||
formsemestre_id,
|
||||
allow_none=True, # offre un choix vide dans chaque partition
|
||||
|
@ -42,7 +42,7 @@ def affect_groups(partition_id):
|
||||
Permet aussi la creation et la suppression de groupes.
|
||||
"""
|
||||
# réécrit pour 9.0.47 avec un template
|
||||
partition = Partition.query.get_or_404(partition_id)
|
||||
partition = Partition.get_or_404(partition_id)
|
||||
formsemestre = partition.formsemestre
|
||||
if not formsemestre.can_change_groups():
|
||||
raise AccessDenied("vous n'avez pas la permission de modifier les groupes")
|
||||
@ -63,7 +63,7 @@ def affect_groups(partition_id):
|
||||
|
||||
def group_rename(group_id):
|
||||
"""Form to rename a group"""
|
||||
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
|
||||
group: GroupDescr = GroupDescr.get_or_404(group_id)
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.can_change_groups():
|
||||
|
@ -370,7 +370,7 @@ class DisplayedGroupsInfos:
|
||||
|
||||
if not group_ids: # appel sans groupe (eg page accueil)
|
||||
if not formsemestre_id:
|
||||
raise ValueError("missing parameter formsemestre_id or group_ids")
|
||||
raise ScoValueError("missing parameter formsemestre_id or group_ids")
|
||||
if empty_list_select_all:
|
||||
if select_all_when_unspecified:
|
||||
group_ids = [
|
||||
|
@ -85,7 +85,7 @@ COMMENTS = (
|
||||
autorise la connexion via CAS (optionnel, faux par défaut)
|
||||
""",
|
||||
"""cas_allow_scodoc_login
|
||||
autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
|
||||
autorise connexion via ScoDoc même si CAS activé (optionnel, faux par défaut)
|
||||
""",
|
||||
"""email_institutionnel
|
||||
optionnel, le mail officiel de l'utilisateur.
|
||||
|
@ -349,6 +349,7 @@ def formsemestre_inscr_passage(
|
||||
inscrit_groupes=inscrit_groupes,
|
||||
inscrit_parcours=inscrit_parcours,
|
||||
ignore_jury=ignore_jury,
|
||||
with_apo_cols=False,
|
||||
)
|
||||
else:
|
||||
if not dialog_confirmed:
|
||||
@ -447,9 +448,10 @@ def formsemestre_inscr_passage(
|
||||
"""
|
||||
)
|
||||
|
||||
#
|
||||
return render_template(
|
||||
"sco_page.j2", title="Passage des étudiants", content="\n".join(H)
|
||||
"formsemestre/synchro_etuds.j2",
|
||||
title="Passage des étudiants",
|
||||
content="\n".join(H),
|
||||
)
|
||||
|
||||
|
||||
@ -462,6 +464,7 @@ def _build_page(
|
||||
inscrit_groupes=False,
|
||||
inscrit_parcours=False,
|
||||
ignore_jury=False,
|
||||
with_apo_cols: bool = True,
|
||||
):
|
||||
inscrit_groupes = int(inscrit_groupes)
|
||||
inscrit_parcours = int(inscrit_parcours)
|
||||
@ -506,7 +509,7 @@ def _build_page(
|
||||
de ce semestre ({formsemestre.date_debut.strftime(scu.DATE_FMT)}) sont pris en
|
||||
compte.</em>
|
||||
</div>
|
||||
{etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)}
|
||||
{etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs, with_apo_cols=with_apo_cols)}
|
||||
|
||||
<input type="submit" name="submitted" value="Appliquer les modifications"/>
|
||||
|
||||
@ -586,6 +589,7 @@ def etuds_select_boxes(
|
||||
export_cat_xls=None,
|
||||
base_url="",
|
||||
read_only=False,
|
||||
with_apo_cols: bool = True,
|
||||
):
|
||||
"""Boites pour selection étudiants par catégorie
|
||||
auth_etuds_by_cat = { category : { 'info' : {}, 'etuds' : ... }
|
||||
@ -598,22 +602,7 @@ def etuds_select_boxes(
|
||||
return etuds_select_box_xls(auth_etuds_by_cat[export_cat_xls])
|
||||
|
||||
H = [
|
||||
"""<script type="text/javascript">
|
||||
function sem_select(formsemestre_id, state) {
|
||||
var elems = document.getElementById(formsemestre_id).getElementsByTagName("input");
|
||||
for (var i =0; i < elems.length; i++) { elems[i].checked=state; }
|
||||
}
|
||||
function sem_select_inscrits(formsemestre_id) {
|
||||
var elems = document.getElementById(formsemestre_id).getElementsByTagName("input");
|
||||
for (var i =0; i < elems.length; i++) {
|
||||
if (elems[i].parentNode.className.indexOf('inscrit') >= 0) {
|
||||
elems[i].checked=true;
|
||||
} else {
|
||||
elems[i].checked=false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
<div class="etuds_select_boxes">"""
|
||||
] # "
|
||||
# Élimine les boites vides:
|
||||
@ -625,118 +614,218 @@ def etuds_select_boxes(
|
||||
for src_cat in auth_etuds_by_cat.keys():
|
||||
infos = auth_etuds_by_cat[src_cat]["infos"]
|
||||
infos["comment"] = infos.get("comment", "") # commentaire dans sous-titre boite
|
||||
help_txt = infos.get("help", "")
|
||||
etuds = auth_etuds_by_cat[src_cat]["etuds"]
|
||||
etuds.sort(key=itemgetter("nom"))
|
||||
with_checkbox = (not read_only) and auth_etuds_by_cat[src_cat]["infos"].get(
|
||||
"with_checkbox", True
|
||||
)
|
||||
checkbox_name = auth_etuds_by_cat[src_cat]["infos"].get(
|
||||
"checkbox_name", "etuds"
|
||||
)
|
||||
etud_key = auth_etuds_by_cat[src_cat]["infos"].get("etud_key", "etudid")
|
||||
if etuds or show_empty_boxes:
|
||||
infos["nbetuds"] = len(etuds)
|
||||
H.append(
|
||||
"""<div class="pas_sembox" id="%(id)s">
|
||||
<div class="pas_sembox_title"><a href="%(title_target)s" """
|
||||
% infos
|
||||
xls_url = (
|
||||
scu.build_url_query(base_url, export_cat_xls=src_cat)
|
||||
if base_url and etuds
|
||||
else ""
|
||||
)
|
||||
if help_txt: # bubble
|
||||
H.append('title="%s"' % help_txt)
|
||||
H.append(
|
||||
""">%(title)s</a></div>
|
||||
<div class="pas_sembox_subtitle">(%(nbetuds)d étudiants%(comment)s)"""
|
||||
% infos
|
||||
etuds_select_box(
|
||||
etuds,
|
||||
infos,
|
||||
with_checkbox=with_checkbox,
|
||||
sel_inscrits=sel_inscrits,
|
||||
xls_url=xls_url,
|
||||
inscrits_ailleurs=inscrits_ailleurs,
|
||||
with_apo_cols=with_apo_cols,
|
||||
)
|
||||
)
|
||||
if with_checkbox:
|
||||
H.append(
|
||||
""" (Select.
|
||||
<a href="#" class="stdlink" onclick="sem_select('%(id)s', true);">tous</a>
|
||||
<a href="#" class="stdlink" onclick="sem_select('%(id)s', false );">aucun</a>""" # "
|
||||
% infos
|
||||
)
|
||||
if sel_inscrits:
|
||||
H.append(
|
||||
"""<a href="#" class="stdlink" onclick="sem_select_inscrits('%(id)s');">inscrits</a>"""
|
||||
% infos
|
||||
)
|
||||
if with_checkbox or sel_inscrits:
|
||||
H.append(")")
|
||||
if base_url and etuds:
|
||||
url = scu.build_url_query(base_url, export_cat_xls=src_cat)
|
||||
H.append(f'<a href="{url}">{scu.ICON_XLS}</a> ')
|
||||
H.append("</div>")
|
||||
for etud in etuds:
|
||||
if etud.get("inscrit", False):
|
||||
c = " deja-inscrit"
|
||||
checked = 'checked="checked"'
|
||||
else:
|
||||
checked = ""
|
||||
if etud["etudid"] in inscrits_ailleurs:
|
||||
c = " inscrit-ailleurs"
|
||||
else:
|
||||
c = ""
|
||||
sco_etud.format_etud_ident(etud)
|
||||
if etud["etudid"]:
|
||||
elink = f"""<a id="{etud['etudid']}" class="discretelink etudinfo {c}"
|
||||
href="{ url_for(
|
||||
'scolar.fiche_etud',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud['etudid'],
|
||||
)
|
||||
}">{etud['nomprenom']}</a>
|
||||
"""
|
||||
else:
|
||||
# ce n'est pas un etudiant ScoDoc
|
||||
elink = etud["nomprenom"]
|
||||
|
||||
if etud.get("datefinalisationinscription", None):
|
||||
elink += (
|
||||
'<span class="finalisationinscription">'
|
||||
+ " : inscription finalisée le "
|
||||
+ etud["datefinalisationinscription"].strftime(scu.DATE_FMT)
|
||||
+ "</span>"
|
||||
)
|
||||
|
||||
if not etud.get("paiementinscription", True):
|
||||
elink += '<span class="paspaye"> (non paiement)</span>'
|
||||
|
||||
H.append("""<div class="pas_etud%s">""" % c)
|
||||
if "etape" in etud:
|
||||
etape_str = etud["etape"] or ""
|
||||
else:
|
||||
etape_str = ""
|
||||
H.append("""<span class="sp_etape">%s</span>""" % etape_str)
|
||||
if with_checkbox:
|
||||
H.append(
|
||||
"""<input type="checkbox" name="%s:list" value="%s" %s>"""
|
||||
% (checkbox_name, etud[etud_key], checked)
|
||||
)
|
||||
H.append(elink)
|
||||
if with_checkbox:
|
||||
H.append("""</input>""")
|
||||
H.append("</div>")
|
||||
H.append("</div>")
|
||||
|
||||
H.append("</div>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def etuds_select_box(
|
||||
etuds: list[dict],
|
||||
infos: dict,
|
||||
with_checkbox: bool = True,
|
||||
sel_inscrits: bool = True,
|
||||
xls_url: str = "",
|
||||
inscrits_ailleurs: set = None,
|
||||
with_apo_cols: bool = True,
|
||||
) -> str:
|
||||
"""HTML pour une "boite" avec une liste d'étudiants à sélectionner"""
|
||||
inscrits_ailleurs = inscrits_ailleurs or {}
|
||||
box_id = infos["id"]
|
||||
H = []
|
||||
H.append(
|
||||
f"""<div class="pas_sembox" id="{box_id}">
|
||||
<div class="pas_sembox_title"><a href="{infos['title_target']}" """
|
||||
)
|
||||
help_txt = infos.get("help")
|
||||
if help_txt: # bubble
|
||||
H.append(f'title="{help_txt}"')
|
||||
H.append(
|
||||
""">%(title)s</a></div>
|
||||
<div class="pas_sembox_subtitle">(%(nbetuds)d étudiants%(comment)s)"""
|
||||
% infos
|
||||
)
|
||||
if with_checkbox:
|
||||
H.append(
|
||||
f""" (Select.
|
||||
<a href="#" class="stdlink" onclick="sem_select('{box_id}', true);">tous</a>
|
||||
<a href="#" class="stdlink" onclick="sem_select('{box_id}', false );">aucun</a>"""
|
||||
)
|
||||
if sel_inscrits:
|
||||
H.append(
|
||||
f"""<a href="#"
|
||||
class="stdlink" onclick="sem_select_inscrits('{box_id}');"
|
||||
>inscrits</a>"""
|
||||
)
|
||||
if with_checkbox or sel_inscrits:
|
||||
H.append(")")
|
||||
if xls_url:
|
||||
H.append(f'<a href="{xls_url}">{scu.ICON_XLS}</a> ')
|
||||
H.append("</div>")
|
||||
checkbox_title = """<th class="no-sort"></th>""" if with_checkbox else ""
|
||||
ths = (
|
||||
f"<th>Étape</th>{checkbox_title}<th>Nom</th><th>Paiement</th><th>Finalisé</th>"
|
||||
if with_apo_cols
|
||||
else f"{checkbox_title}<th>Nom</th>"
|
||||
)
|
||||
H.append(
|
||||
f"""<table class="etuds-box">
|
||||
<thead>
|
||||
<tr>
|
||||
{ths}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
)
|
||||
for etud in etuds:
|
||||
is_inscrit = etud.get("inscrit", False)
|
||||
extra_class = (
|
||||
"deja-inscrit"
|
||||
if is_inscrit
|
||||
else ("inscrit-ailleurs" if etud["etudid"] in inscrits_ailleurs else "")
|
||||
)
|
||||
H.append(
|
||||
_etud_row(
|
||||
etud,
|
||||
with_checkbox=with_checkbox,
|
||||
checkbox_name=infos.get("checkbox_name", "etuds"),
|
||||
etud_key=infos.get("etud_key", "etudid"),
|
||||
is_inscrit=is_inscrit,
|
||||
extra_class=extra_class,
|
||||
with_apo_cols=with_apo_cols,
|
||||
)
|
||||
)
|
||||
H.append(
|
||||
"""</tbody>
|
||||
</table>"""
|
||||
)
|
||||
|
||||
H.append("</div>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _etud_row(
|
||||
etud: dict,
|
||||
with_checkbox: bool = True,
|
||||
checkbox_name: str = "etuds",
|
||||
etud_key: str = "",
|
||||
is_inscrit: bool = False,
|
||||
extra_class: str = "",
|
||||
with_apo_cols: bool = True,
|
||||
) -> str:
|
||||
"""HTML 'row' for this etud"""
|
||||
H = []
|
||||
nomprenom = scu.format_nomprenom(etud)
|
||||
if etud["etudid"]:
|
||||
elink = f"""<a id="{etud['etudid']}" class="discretelink etudinfo {extra_class}"
|
||||
href="{ url_for(
|
||||
'scolar.fiche_etud',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud['etudid'],
|
||||
)
|
||||
}">{nomprenom}</a>
|
||||
"""
|
||||
else:
|
||||
# ce n'est pas un etudiant ScoDoc
|
||||
elink = nomprenom
|
||||
|
||||
checkbox_cell = (
|
||||
f"""<td><input type="checkbox" name="{checkbox_name}:list"
|
||||
value="{etud[etud_key]}" {'checked="checked"' if is_inscrit else ''}>
|
||||
</td>
|
||||
"""
|
||||
if with_checkbox
|
||||
else ""
|
||||
)
|
||||
paiement = etud.get("paiementinscription", True)
|
||||
datefinalisation = etud.get("datefinalisationinscription")
|
||||
if with_apo_cols:
|
||||
H.append(
|
||||
f"""
|
||||
<tr class="{extra_class}">
|
||||
<td class="etape">{etud.get("etape", "") or ""}</td>
|
||||
{checkbox_cell}
|
||||
<td data-order="{etud.get("nom", "").upper()}">{elink}</td>
|
||||
<td class="paiement {'' if paiement else 'paspaye'}">{
|
||||
'' if paiement else 'non paiement'
|
||||
}</td>
|
||||
<td class="finalise" data-order="{
|
||||
datefinalisation.isoformat() if datefinalisation else ''
|
||||
}">{"inscription finalisée le " + datefinalisation.strftime(scu.DATE_FMT)
|
||||
if datefinalisation else "" }
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
else: # juste checkbox et nom
|
||||
H.append(
|
||||
f"""
|
||||
<tr class="{extra_class}">
|
||||
{checkbox_cell}
|
||||
<td data-order="{etud.get("nom", "").upper()}">{elink}</td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def etuds_select_box_xls(src_cat):
|
||||
"export a box to excel"
|
||||
etuds = src_cat["etuds"]
|
||||
columns_ids = ["etudid", "civilite_str", "nom", "prenom", "etape"]
|
||||
titles = {x: x for x in columns_ids}
|
||||
|
||||
# Ajoute colonne paiement inscription
|
||||
columns_ids.append("paiementinscription_str")
|
||||
titles["paiementinscription_str"] = "paiement inscription"
|
||||
columns_ids = [
|
||||
"etudid",
|
||||
"ine",
|
||||
"nip",
|
||||
"civilite_str",
|
||||
"nom",
|
||||
"prenom",
|
||||
"etape",
|
||||
"paiementinscription_str",
|
||||
"datefinalisationinscription",
|
||||
]
|
||||
titles = {x: x for x in columns_ids} | {
|
||||
"paiementinscription_str": "Paiement inscr.",
|
||||
"datefinalisationinscription": "Finalisation inscr.",
|
||||
}
|
||||
for e in etuds:
|
||||
if not e.get("paiementinscription", True):
|
||||
e["paiementinscription_str"] = "NON"
|
||||
else:
|
||||
e["paiementinscription_str"] = "-"
|
||||
# si e est un étudiant Apo, on a nip et ine
|
||||
# mais si e est ScoDoc, on a code_nip et code_ine:
|
||||
e["nip"] = e.get("nip", e.get("code_nip"))
|
||||
e["ine"] = e.get("ine", e.get("code_ine"))
|
||||
# Pour excel, datefinalisationinscription doit être datetime
|
||||
dat = e.get("datefinalisationinscription")
|
||||
if isinstance(dat, datetime.date):
|
||||
e["datefinalisationinscription"] = datetime.datetime.combine(
|
||||
dat, datetime.time.min
|
||||
)
|
||||
tab = GenTable(
|
||||
caption="%(title)s. %(help)s" % src_cat["infos"],
|
||||
columns_ids=columns_ids,
|
||||
|
@ -399,7 +399,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
||||
H.append(
|
||||
'<h3>Étudiants avec UEs capitalisées (ADM):</h3><ul class="ue_inscr_list">'
|
||||
)
|
||||
ues = [UniteEns.query.get_or_404(ue_id) for ue_id in ues_cap_info.keys()]
|
||||
ues = [UniteEns.get_or_404(ue_id) for ue_id in ues_cap_info.keys()]
|
||||
ues.sort(key=lambda u: u.numero)
|
||||
for ue in ues:
|
||||
H.append(
|
||||
|
@ -884,7 +884,7 @@ def _ligne_evaluation(
|
||||
f"""{gr_moyenne["gr_moy"]} (<a class="stdlink" href="{
|
||||
url_for('notes.evaluation_listenotes',
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id,
|
||||
tf_submitted=1, **{'group_ids:list': gr_moyenne["group_id"]})
|
||||
tf_submitted=1, **{'group_ids': gr_moyenne["group_id"]})
|
||||
}">{gr_moyenne["gr_nb_notes"]} notes</a>"""
|
||||
)
|
||||
if gr_moyenne["gr_nb_att"] > 0:
|
||||
@ -899,7 +899,7 @@ def _ligne_evaluation(
|
||||
H.append(
|
||||
f"""<a class="redlink" href="{url_for('notes.form_saisie_notes',
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id,
|
||||
**{'group_ids:list': gr_moyenne["group_id"]})
|
||||
**{'group_ids': gr_moyenne["group_id"]})
|
||||
}">incomplet : terminer saisie</a></font>]"""
|
||||
)
|
||||
else:
|
||||
|
@ -17,6 +17,8 @@ def can_suppress_annotation(annotation_id):
|
||||
Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer
|
||||
une annotation.
|
||||
"""
|
||||
if current_user.passwd_must_be_changed:
|
||||
return False
|
||||
annotation = (
|
||||
EtudAnnotation.query.filter_by(id=annotation_id)
|
||||
.join(Identite)
|
||||
@ -30,8 +32,10 @@ def can_suppress_annotation(annotation_id):
|
||||
)
|
||||
|
||||
|
||||
def can_edit_suivi():
|
||||
def can_edit_suivi() -> bool:
|
||||
"""Vrai si l'utilisateur peut modifier les informations de suivi sur la page etud" """
|
||||
if current_user.passwd_must_be_changed:
|
||||
return False
|
||||
return current_user.has_permission(Permission.EtudChangeAdr)
|
||||
|
||||
|
||||
|
@ -369,7 +369,7 @@ def copy_portal_photo_to_fs(etudid: int):
|
||||
"""Copy the photo from portal (distant website) to local fs.
|
||||
Returns rel. path or None if copy failed, with a diagnostic message
|
||||
"""
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
etud: Identite = Identite.get_or_404(etudid)
|
||||
url = photo_portal_url(etud.code_nip)
|
||||
if not url:
|
||||
return None, f"""{etud.nomprenom}: pas de code NIP"""
|
||||
|
@ -476,28 +476,37 @@ def get_etapes_apogee_dept():
|
||||
return etapes
|
||||
|
||||
|
||||
def _portal_date_dmy2date(s):
|
||||
"""date inscription renvoyée sous la forme dd/mm/yy
|
||||
renvoie un objet date, ou None
|
||||
def _portal_date_dmy2date(s: str) -> datetime.date | None:
|
||||
"""s est la date inscription fournie par le portail
|
||||
sous la forme dd/mm/yy
|
||||
Renvoie un objet date, ou None
|
||||
Raises ValueError si format invalide.
|
||||
"""
|
||||
s = s.strip()
|
||||
if not s:
|
||||
return None
|
||||
else:
|
||||
d, m, y = [int(x) for x in s.split("/")] # raises ValueError if bad format
|
||||
if y < 100:
|
||||
y += 2000 # 21ème siècle
|
||||
return datetime.date(y, m, d)
|
||||
d, m, y = [int(x) for x in s.split("/")] # raises ValueError if bad format
|
||||
if y < 100:
|
||||
y += 2000 # 21ème siècle
|
||||
return datetime.date(y, m, d)
|
||||
|
||||
|
||||
def _normalize_apo_fields(infolist):
|
||||
"""
|
||||
infolist: liste de dict renvoyés par le portail Apogee
|
||||
|
||||
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
|
||||
ajoute le champ 'paiementinscription_str' : 'ok', 'Non' ou '?'
|
||||
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
|
||||
ajoute le champ 'civilite_etat_civil' (=''), et 'prenom_etat_civil' (='') si non présent.
|
||||
Recode les champs:
|
||||
- paiementinscription (-> booleen)
|
||||
- datefinalisationinscription (date)
|
||||
|
||||
Ajoute le champ
|
||||
- 'paiementinscription_str' : 'ok', 'Non' ou '?'
|
||||
|
||||
S'ils ne sont pas présents, ajoute les champs:
|
||||
- 'etape' (None)
|
||||
- 'prenom' ('')
|
||||
- 'civilite_etat_civil' ('')
|
||||
- 'prenom_etat_civil' ('')
|
||||
"""
|
||||
for infos in infolist:
|
||||
if "paiementinscription" in infos:
|
||||
@ -516,9 +525,11 @@ def _normalize_apo_fields(infolist):
|
||||
infos["datefinalisationinscription"] = _portal_date_dmy2date(
|
||||
infos["datefinalisationinscription"]
|
||||
)
|
||||
infos["datefinalisationinscription_str"] = infos[
|
||||
"datefinalisationinscription"
|
||||
].strftime(scu.DATE_FMT)
|
||||
infos["datefinalisationinscription_str"] = (
|
||||
infos["datefinalisationinscription"].strftime(scu.DATE_FMT)
|
||||
if infos["datefinalisationinscription"]
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
infos["datefinalisationinscription"] = None
|
||||
infos["datefinalisationinscription_str"] = ""
|
||||
|
@ -60,7 +60,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
|
||||
for s in etud["sems"]:
|
||||
if s["semestre_id"] == sem_id:
|
||||
etudid = etud["etudid"]
|
||||
formsemestre = FormSemestre.query.get_or_404(s["formsemestre_id"])
|
||||
formsemestre = FormSemestre.get_or_404(s["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
dec = nt.get_etud_decision_sem(etudid)
|
||||
# Moyennes et rangs des UE
|
||||
|
@ -91,7 +91,7 @@ def dict_pvjury(
|
||||
'decisions_dict' : { etudid : decision (comme ci-dessus) },
|
||||
}
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if etudids is None:
|
||||
etudids = nt.get_etudids()
|
||||
@ -231,7 +231,7 @@ def dict_pvjury(
|
||||
"is_apc": nt.is_apc,
|
||||
"has_prev": has_prev,
|
||||
"semestre_non_terminal": semestre_non_terminal,
|
||||
"formation": Formation.query.get_or_404(sem["formation_id"]).to_dict(),
|
||||
"formation": Formation.get_or_404(sem["formation_id"]).to_dict(),
|
||||
"decisions": decisions,
|
||||
"decisions_dict": D,
|
||||
}
|
||||
|
@ -577,7 +577,7 @@ def descrform_pvjury(formsemestre: FormSemestre):
|
||||
|
||||
def formsemestre_lettres_individuelles(formsemestre_id):
|
||||
"Lettres avis jury en PDF"
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
if request.method == "POST":
|
||||
group_ids = request.form.getlist("group_ids")
|
||||
else:
|
||||
|
@ -33,8 +33,10 @@ import time
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from flask import abort, g, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app.auth.models import Permission
|
||||
from app.but import bulletin_but
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
@ -507,6 +509,7 @@ def gen_formsemestre_recapcomplet_html_table(
|
||||
|
||||
def _gen_formsemestre_recapcomplet_table(
|
||||
res: ResultatsSemestre,
|
||||
include_email_addresses=False,
|
||||
include_evaluations=False,
|
||||
mode_jury=False,
|
||||
convert_values: bool = True,
|
||||
@ -518,6 +521,7 @@ def _gen_formsemestre_recapcomplet_table(
|
||||
table = table_class(
|
||||
res,
|
||||
convert_values=convert_values,
|
||||
include_email_addresses=include_email_addresses,
|
||||
include_evaluations=include_evaluations,
|
||||
mode_jury=mode_jury,
|
||||
read_only=not res.formsemestre.can_edit_jury(),
|
||||
@ -539,8 +543,10 @@ def gen_formsemestre_recapcomplet_excel(
|
||||
Attention: le tableau exporté depuis la page html est celui généré en js par DataTables,
|
||||
et non celui-ci.
|
||||
"""
|
||||
# En excel, ajoute les adresses mail, si on a le droit de les voir.
|
||||
table = _gen_formsemestre_recapcomplet_table(
|
||||
res,
|
||||
include_email_addresses=current_user.has_permission(Permission.ViewEtudData),
|
||||
include_evaluations=include_evaluations,
|
||||
mode_jury=mode_jury,
|
||||
convert_values=False,
|
||||
|
@ -533,7 +533,7 @@ def table_suivi_cohorte(
|
||||
s["members"] = orig_set.intersection(inset)
|
||||
nb_dipl = 0 # combien de diplomes dans ce semestre ?
|
||||
if s["semestre_id"] == nt.parcours.NB_SEM:
|
||||
s_formsemestre = FormSemestre.query.get_or_404(s["formsemestre_id"])
|
||||
s_formsemestre = FormSemestre.get_or_404(s["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(s_formsemestre)
|
||||
for etudid in s["members"]:
|
||||
dec = nt.get_etud_decision_sem(etudid)
|
||||
@ -1114,7 +1114,7 @@ def get_code_cursus_etud(
|
||||
|
||||
if formsemestres is None:
|
||||
formsemestres = [
|
||||
FormSemestre.query.get_or_404(s["formsemestre_id"]) for s in (sems or [])
|
||||
FormSemestre.get_or_404(s["formsemestre_id"]) for s in (sems or [])
|
||||
]
|
||||
|
||||
# élimine les semestres spéciaux hors cursus (LP en 1 sem., ...)
|
||||
@ -1448,7 +1448,7 @@ def graph_cursus(
|
||||
nxt = {}
|
||||
etudid = etud["etudid"]
|
||||
for s in etud["sems"]: # du plus recent au plus ancien
|
||||
s_formsemestre = FormSemestre.query.get_or_404(s["formsemestre_id"])
|
||||
s_formsemestre = FormSemestre.get_or_404(s["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(s_formsemestre)
|
||||
dec = nt.get_etud_decision_sem(etudid)
|
||||
if nxt:
|
||||
|
@ -66,7 +66,7 @@ INDICATEUR_NAMES = {
|
||||
|
||||
def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
|
||||
"""Page avec tableau indicateurs enquête ADIUT BUT 2022"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
|
||||
indicateurs_by_bac = but_indicateurs_by_bac(formsemestre)
|
||||
# finalement on fait une table avec en ligne
|
||||
|
@ -326,7 +326,7 @@ def do_evaluation_set_missing(
|
||||
|
||||
def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
||||
"suppress all notes in this eval"
|
||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
evaluation = Evaluation.get_or_404(evaluation_id)
|
||||
|
||||
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
|
||||
# On a le droit de modifier toutes les notes
|
||||
|
@ -157,7 +157,7 @@ class SemSet(dict):
|
||||
def add(self, formsemestre_id):
|
||||
"Ajoute ce semestre à l'ensemble"
|
||||
# check for valid formsemestre_id
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
|
||||
# check
|
||||
if formsemestre_id in self.formsemestre_ids:
|
||||
return # already there
|
||||
@ -265,7 +265,7 @@ class SemSet(dict):
|
||||
self["jury_nb_missing"] = 0
|
||||
is_apc = None
|
||||
for sem in self.sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
formsemestre = FormSemestre.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if is_apc is not None and is_apc != nt.is_apc:
|
||||
raise ScoValueError(
|
||||
|
@ -299,7 +299,9 @@ def formsemestre_synchro_etuds(
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"sco_page.j2", title="Synchronisation des étudiants", content="\n".join(H)
|
||||
"formsemestre/synchro_etuds.j2",
|
||||
title="Synchronisation des étudiants",
|
||||
content="\n".join(H),
|
||||
)
|
||||
|
||||
|
||||
@ -523,7 +525,7 @@ def list_synch(sem, annee_apogee=None):
|
||||
"etuds": set_to_sorted_list(etuds_ok, is_inscrit=True),
|
||||
"infos": {
|
||||
"id": "etuds_ok",
|
||||
"title": "Étudiants dans Apogée et déjà inscrits",
|
||||
"title": "Étudiants dans Apogée et déjà dans ScoDoc",
|
||||
"help": """Ces etudiants sont inscrits dans le semestre ScoDoc et sont présents dans Apogée:
|
||||
tout est donc correct. Décocher les étudiants que vous souhaitez désinscrire.""",
|
||||
"title_target": "",
|
||||
@ -848,7 +850,7 @@ def formsemestre_import_etud_admission(
|
||||
|
||||
for i in ins:
|
||||
etudid = i["etudid"]
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
etud: Identite = Identite.get_or_404(etudid)
|
||||
code_nip = etud.code_nip
|
||||
if not code_nip:
|
||||
etuds_no_nip.append(etud)
|
||||
|
@ -46,9 +46,11 @@ Opérations:
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from flask import g, request, url_for
|
||||
from flask import g, render_template, request, url_for
|
||||
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.models import Evaluation, FormSemestre, ModuleImpl, NotesNotes, NotesNotesLog
|
||||
from app.scodoc.intervals import intervalmap
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -178,46 +180,119 @@ def evaluation_list_operations(evaluation_id: int):
|
||||
return tab.make_page()
|
||||
|
||||
|
||||
def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
|
||||
def formsemestre_list_notes_intervenants(formsemestre: FormSemestre) -> list[User]:
|
||||
"Liste des comptes ayant saisi au moins une note dans le semestre"
|
||||
q1 = (
|
||||
User.query.join(NotesNotes)
|
||||
.join(Evaluation)
|
||||
.join(ModuleImpl)
|
||||
.filter_by(formsemestre_id=formsemestre.id)
|
||||
.distinct()
|
||||
)
|
||||
q2 = (
|
||||
User.query.join(NotesNotesLog)
|
||||
.join(Evaluation, Evaluation.id == NotesNotesLog.evaluation_id)
|
||||
.join(ModuleImpl)
|
||||
.filter_by(formsemestre_id=formsemestre.id)
|
||||
.distinct()
|
||||
)
|
||||
return sorted(q1.union(q2).all(), key=lambda x: x.sort_key())
|
||||
|
||||
|
||||
def formsemestre_list_saisies_notes(
|
||||
formsemestre_id, only_modifs=False, user_name: str | None = None, fmt="html"
|
||||
):
|
||||
"""Table listant toutes les opérations de saisies de notes, dans toutes
|
||||
les évaluations du semestre.
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
rows = ndb.SimpleDictFetch(
|
||||
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.date_debut,
|
||||
u.user_name, e.id as evaluation_id
|
||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
||||
notes_modules mod, identite i, "user" u
|
||||
WHERE mi.id = e.moduleimpl_id
|
||||
and mi.module_id = mod.id
|
||||
and e.id = n.evaluation_id
|
||||
and i.id = n.etudid
|
||||
and u.id = n.uid
|
||||
and mi.formsemestre_id = %(formsemestre_id)s
|
||||
ORDER BY date desc
|
||||
""",
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
only_modifs = scu.to_bool(only_modifs)
|
||||
model = NotesNotesLog if only_modifs else NotesNotes
|
||||
notes_query = (
|
||||
db.session.query(model)
|
||||
.join(Evaluation, Evaluation.id == model.evaluation_id)
|
||||
.join(ModuleImpl)
|
||||
.filter_by(formsemestre_id=formsemestre.id)
|
||||
.order_by(model.date.desc())
|
||||
)
|
||||
if user_name:
|
||||
user = db.session.query(User).filter_by(user_name=user_name).first()
|
||||
if user:
|
||||
notes_query = notes_query.join(User).filter(model.uid == user.id)
|
||||
# Formate les notes
|
||||
keep_numeric = fmt in scu.FORMATS_NUMERIQUES
|
||||
for row in rows:
|
||||
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
|
||||
row["date_evaluation"] = (
|
||||
row["date_debut"].strftime("%d/%m/%Y %H:%M") if row["date_debut"] else ""
|
||||
)
|
||||
row["_date_evaluation_order"] = (
|
||||
row["date_debut"].isoformat() if row["date_debut"] else ""
|
||||
)
|
||||
rows = []
|
||||
for note in notes_query:
|
||||
ens = User.get_user(note.uid)
|
||||
evaluation = note.evaluation
|
||||
row = {
|
||||
"date": note.date.strftime(scu.DATEATIME_FMT),
|
||||
"_date_order": note.date.isoformat(),
|
||||
"code_nip": note.etudiant.code_nip,
|
||||
"nom": note.etudiant.nom_disp(),
|
||||
"prenom": note.etudiant.prenom_str,
|
||||
"date_evaluation": (
|
||||
evaluation.date_debut.strftime(scu.DATEATIME_FMT)
|
||||
if evaluation and note.evaluation.date_debut
|
||||
else ""
|
||||
),
|
||||
"_date_evaluation_order": (
|
||||
note.evaluation.date_debut.isoformat()
|
||||
if evaluation and note.evaluation.date_debut
|
||||
else ""
|
||||
),
|
||||
"value": scu.fmt_note(note.value, keep_numeric=keep_numeric),
|
||||
"module": (
|
||||
(
|
||||
note.evaluation.moduleimpl.module.code
|
||||
or note.evaluation.moduleimpl.module.titre
|
||||
)
|
||||
if evaluation
|
||||
else ""
|
||||
),
|
||||
"evaluation": note.evaluation.description if evaluation else "",
|
||||
"_evaluation_target": (
|
||||
url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=note.evaluation_id,
|
||||
)
|
||||
if evaluation
|
||||
else ""
|
||||
),
|
||||
"user_name": ens.user_name if ens else "",
|
||||
}
|
||||
|
||||
if only_modifs:
|
||||
# si c'est une modif de note, ajoute une colonne avec la nouvelle valeur
|
||||
new = NotesNotes.query.filter_by(
|
||||
evaluation_id=note.evaluation_id, etudid=note.etudid
|
||||
).first()
|
||||
if new:
|
||||
row["new_value"] = scu.fmt_note(new.value, keep_numeric=keep_numeric)
|
||||
row["old_date"] = row["date"]
|
||||
row["_old_date_order"] = row["_date_order"]
|
||||
row["date"] = new.date.strftime(scu.DATEATIME_FMT)
|
||||
row["_date_order"] = new.date.isoformat()
|
||||
|
||||
rows.append(row)
|
||||
|
||||
columns_ids = (
|
||||
"date",
|
||||
"code_nip",
|
||||
"nom",
|
||||
"prenom",
|
||||
"value",
|
||||
)
|
||||
if only_modifs:
|
||||
columns_ids += (
|
||||
"new_value",
|
||||
"old_date",
|
||||
)
|
||||
columns_ids += (
|
||||
"user_name",
|
||||
"titre",
|
||||
"evaluation_id",
|
||||
"description",
|
||||
"module",
|
||||
"evaluation",
|
||||
"date_evaluation",
|
||||
"comment",
|
||||
)
|
||||
@ -225,16 +300,20 @@ def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
|
||||
"code_nip": "NIP",
|
||||
"nom": "nom",
|
||||
"prenom": "prenom",
|
||||
"date": "Date",
|
||||
"date": "Date modif." if only_modifs else "Date saisie",
|
||||
"old_date": "Date saisie précédente",
|
||||
"value": "Note",
|
||||
"comment": "Remarque",
|
||||
"user_name": "Enseignant",
|
||||
"evaluation_id": "evaluation_id",
|
||||
"titre": "Module",
|
||||
"description": "Evaluation",
|
||||
"module": "Module",
|
||||
"evaluation": "Evaluation",
|
||||
"date_evaluation": "Date éval.",
|
||||
}
|
||||
tab = GenTable(
|
||||
if only_modifs:
|
||||
titles["value"] = "Ancienne note"
|
||||
titles["new_value"] = "Nouvelle note"
|
||||
table = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
@ -244,11 +323,31 @@ def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
|
||||
html_sortable=True,
|
||||
caption=f"Saisies de notes dans {formsemestre.titre_annee()}",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
||||
base_url=f"""{request.base_url}?formsemestre_id={
|
||||
formsemestre_id}&only_modifs={int(only_modifs)}"""
|
||||
+ (f"&user_name={user_name}" if user_name else ""),
|
||||
origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "",
|
||||
table_id="formsemestre_list_saisies_notes",
|
||||
filename=(
|
||||
(
|
||||
f"modifs_notes-S{formsemestre.semestre_id}"
|
||||
if only_modifs
|
||||
else f"saisies_notes_S{formsemestre.semestre_id}"
|
||||
)
|
||||
+ ("-" + user_name if user_name else "")
|
||||
),
|
||||
)
|
||||
return tab.make_page(fmt=fmt)
|
||||
if fmt == "html":
|
||||
return render_template(
|
||||
"formsemestre/list_saisies_notes.j2",
|
||||
table=table,
|
||||
title="Opérations de saisies de notes",
|
||||
only_modifs=only_modifs,
|
||||
formsemestre_id=formsemestre.id,
|
||||
intervenants=formsemestre_list_notes_intervenants(formsemestre),
|
||||
user_name=user_name,
|
||||
)
|
||||
return table.make_page(fmt=fmt, page_title="Opérations de saisies de notes")
|
||||
|
||||
|
||||
def get_note_history(evaluation_id, etudid, fmt=""):
|
||||
|
@ -184,9 +184,11 @@ def list_users(
|
||||
if not current_user.is_administrator():
|
||||
# si non super-admin, ne donne pas la date exacte de derniere connexion
|
||||
d["last_seen"] = _approximate_date(u.last_seen)
|
||||
d["passwd_must_be_changed"] = "OUI" if d["passwd_must_be_changed"] else ""
|
||||
else:
|
||||
d["date_modif_passwd"] = "(non visible)"
|
||||
d["non_migre"] = ""
|
||||
d["passwd_must_be_changed"] = ""
|
||||
if detail_roles:
|
||||
d["roles_set"] = {
|
||||
f"{r.role.name or ''}_{r.dept or ''}" for r in u.user_roles
|
||||
@ -209,6 +211,7 @@ def list_users(
|
||||
"roles_string",
|
||||
"date_expiration",
|
||||
"date_modif_passwd",
|
||||
"passwd_must_be_changed",
|
||||
"non_migre",
|
||||
"status_txt",
|
||||
]
|
||||
@ -240,6 +243,7 @@ def list_users(
|
||||
"roles_string": "Rôles",
|
||||
"date_expiration": "Expiration",
|
||||
"date_modif_passwd": "Modif. mot de passe",
|
||||
"passwd_must_be_changed": "À changer",
|
||||
"last_seen": "Dernière cnx.",
|
||||
"non_migre": "Non migré (!)",
|
||||
"status_txt": "Etat",
|
||||
|
@ -152,6 +152,61 @@ def convert_fr_date(
|
||||
raise ScoValueError("Date (j/m/a) invalide")
|
||||
|
||||
|
||||
# Various implementations of heure_to_iso8601 with timing:
|
||||
# def heure_to_iso8601_v0(t: str | datetime.time = None) -> str: # 682 nsec per loop
|
||||
# """Convert time string or time object to ISO 8601 (allows 16:03, 16h03, 16),
|
||||
# returns hh:mm:ss or empty string
|
||||
# """
|
||||
# if isinstance(t, datetime.time):
|
||||
# return t.isoformat()
|
||||
# if not t:
|
||||
# return ""
|
||||
# parts = t.strip().upper().replace("H", ":").split(":")
|
||||
# # here we can have hh, hh:mm or hh:mm:ss
|
||||
# h = int(parts[0])
|
||||
# m = int(parts[1]) if len(parts) > 1 and parts[1].strip() else 0
|
||||
# s = int(parts[2]) if len(parts) > 2 and parts[1].strip() else 0
|
||||
# return datetime.time(h, m, s).isoformat()
|
||||
|
||||
|
||||
# def heure_to_iso8601_v1(t: str | datetime.time = None) -> str: # 581 nsec per loop
|
||||
# """Convert time string or time object to ISO 8601 (allows 16:03, 16h03, 16),
|
||||
# returns hh:mm:ss or empty string
|
||||
# """
|
||||
# if isinstance(t, datetime.time):
|
||||
# return t.isoformat()
|
||||
# if not t:
|
||||
# return ""
|
||||
# parts = t.strip().upper().replace("H", ":").split(":")
|
||||
# # here we can have hh, hh:mm or hh:mm:ss
|
||||
# h = int(parts[0])
|
||||
# m = int(parts[1]) if len(parts) > 1 and parts[1].strip() else 0
|
||||
# s = int(parts[2]) if len(parts) > 2 and parts[1].strip() else 0
|
||||
# return "%02d:%02d:%02d" % (h, m, s)
|
||||
|
||||
|
||||
def heure_to_iso8601(t: str | datetime.time = None) -> str: # 500 nsec per loop
|
||||
"""Convert time string or time object to ISO 8601 (allows 16:03, 16h03, 16),
|
||||
returns hh:mm:ss or empty string
|
||||
"""
|
||||
if isinstance(t, datetime.time):
|
||||
return t.isoformat()
|
||||
if not t:
|
||||
return ""
|
||||
parts = t.strip().upper().replace("H", ":").split(":")
|
||||
# here we can have hh, hh:mm or hh:mm:ss
|
||||
h = int(parts[0])
|
||||
try:
|
||||
m = int(parts[1])
|
||||
except:
|
||||
m = 0
|
||||
try:
|
||||
s = int(parts[2])
|
||||
except:
|
||||
s = 0
|
||||
return "%02d:%02d:%02d" % (h, m, s)
|
||||
|
||||
|
||||
def print_progress_bar(
|
||||
iteration,
|
||||
total,
|
||||
@ -377,7 +432,6 @@ def localize_datetime(date: datetime.datetime) -> datetime.datetime:
|
||||
Tente de mettre l'offset de la timezone du serveur (ex : UTC+1)
|
||||
Si erreur, mettra l'offset UTC
|
||||
"""
|
||||
|
||||
new_date: datetime.datetime = date
|
||||
if new_date.tzinfo is None:
|
||||
try:
|
||||
@ -387,12 +441,17 @@ def localize_datetime(date: datetime.datetime) -> datetime.datetime:
|
||||
return new_date
|
||||
|
||||
|
||||
def get_local_timezone_offset() -> str:
|
||||
def get_local_timezone_offset(date_iso: str | None = None) -> str:
|
||||
"""Récupère l'offset de la timezone du serveur, sous la forme
|
||||
"+HH:MM"
|
||||
"+HH:MM", pour le jour indiqué (date courante par défaut).
|
||||
Par exemple get_local_timezone_offset("2024-10-30") == "+01:00"
|
||||
"""
|
||||
local_time = datetime.datetime.now().astimezone()
|
||||
utc_offset = local_time.utcoffset()
|
||||
the_time = (
|
||||
datetime.datetime.now()
|
||||
if date_iso is None
|
||||
else datetime.datetime.fromisoformat(date_iso)
|
||||
)
|
||||
utc_offset = the_time.astimezone().utcoffset()
|
||||
total_seconds = int(utc_offset.total_seconds())
|
||||
offset_hours = total_seconds // 3600
|
||||
offset_minutes = (abs(total_seconds) % 3600) // 60
|
||||
|
@ -67,7 +67,7 @@ span.ens-non-reconnu {
|
||||
|
||||
|
||||
#cal_warning {
|
||||
display: inline-block;
|
||||
display: none;
|
||||
color: red;
|
||||
background-color: yellow;
|
||||
font-size: 120%;
|
||||
|
@ -171,6 +171,9 @@
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid rgb(144, 144, 144);
|
||||
border-right: 1px solid rgb(144, 144, 144);
|
||||
}
|
||||
|
||||
.mini-timeline-block {
|
||||
|
@ -3812,11 +3812,14 @@ div.link_defaillance {
|
||||
}
|
||||
|
||||
div.pas_sembox {
|
||||
max-width: var(--sco-content-max-width);
|
||||
min-width: var(--sco-content-min-width);
|
||||
margin-top: 10px;
|
||||
border: 2px solid #a0522d;
|
||||
padding: 5px;
|
||||
margin-right: 10px;
|
||||
font-family: arial, verdana, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
span.sp_etape {
|
||||
@ -3846,8 +3849,48 @@ span.paspaye a {
|
||||
color: #9400d3 !important;
|
||||
}
|
||||
|
||||
table.etuds-box {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.etuds-box th,
|
||||
table.etuds-box tbody tr td {
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.etuds-box th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
/* adapte taille et enlève fleche sur cols no-sort */
|
||||
table.etuds-box thead>tr>th.no-sort {
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
table.etuds-box>thead>tr>th.no-sort span.dt-column-order::before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
table.etuds-box td.etape {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table.etuds-box td.paiement,
|
||||
table.etuds-box td.finalise {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
table.etuds-box td.paiement.paspaye {
|
||||
color: #9400d3 !important;
|
||||
}
|
||||
|
||||
table.etuds-box td.finalise,
|
||||
span.finalisationinscription {
|
||||
color: green;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.pas_sembox_title a {
|
||||
|
@ -2,6 +2,14 @@
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
#app-content>.gtrcontent {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
div.sco-app-content {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* <== Module Assiduité ==> */
|
||||
#ajout-assiduite-etud .description>textarea,
|
||||
#ajout-justificatif-etud .raison>textarea {
|
||||
@ -86,17 +94,19 @@
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar {
|
||||
/* grid-column: 4 !important;
|
||||
grid-row: 1; */
|
||||
/* Ne montre pas la minitimeline sur écrans étroits */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.etud_row .btns_field.single {
|
||||
grid-column: 4 !important;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.etud_row .btns_field.single {
|
||||
grid-column: 3 !important;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.rbtn::before {
|
||||
--size: 35px;
|
||||
--size: 26px;
|
||||
width: var(--size) !important;
|
||||
height: var(--size) !important;
|
||||
}
|
||||
|
@ -460,6 +460,18 @@ async function creerTousLesEtudiants(etuds) {
|
||||
.forEach((etud, index) => {
|
||||
etudsDiv.appendChild(creerLigneEtudiant(etud, index + 1));
|
||||
});
|
||||
// Récupère l'offset timezone serveur pour la date sélectionnée
|
||||
const date_iso = getSelectedDateIso();
|
||||
try {
|
||||
const res = await fetch(`../../api/assiduite/date_time_offset/${date_iso}`);
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const text = await res.text();
|
||||
SERVER_TIMEZONE_OFFSET = text;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -609,7 +621,7 @@ async function actionAssiduite(etud, etat, type, assiduite = null) {
|
||||
const modimpl_id = $("#moduleimpl_select").val();
|
||||
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
||||
|
||||
const { deb, fin } = getPeriodAsDate(true); // en tz server
|
||||
const { deb, fin } = getPeriodAsISO(); // chaines sans timezone pour l'API
|
||||
// génération d'un objet assiduité basique qui sera complété
|
||||
let assiduiteObjet = assiduite ?? {
|
||||
date_debut: deb,
|
||||
@ -722,9 +734,12 @@ function mettreToutLeMonde(etat, el = null) {
|
||||
const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")];
|
||||
|
||||
const { deb, fin } = getPeriodAsDate(true); // tz server
|
||||
const period_iso = getPeriodAsISO(); // chaines sans timezone pour l'API
|
||||
const deb_iso = period_iso.deb;
|
||||
const fin_iso = period_iso.fin;
|
||||
const assiduiteObjet = {
|
||||
date_debut: deb,
|
||||
date_fin: fin,
|
||||
date_debut: deb_iso,
|
||||
date_fin: fin_iso,
|
||||
etat: etat,
|
||||
moduleimpl_id: $("#moduleimpl_select").val(),
|
||||
};
|
||||
@ -741,14 +756,14 @@ function mettreToutLeMonde(etat, el = null) {
|
||||
.filter((e) => e.getAttribute("type") == "edition")
|
||||
.map((e) => Number(e.getAttribute("assiduite_id")));
|
||||
|
||||
// On récupère les assiduités conflictuelles mais qui sont comprisent
|
||||
// Dans la plage de suppression
|
||||
// On récupère les assiduités conflictuelles mais qui sont comprises
|
||||
// dans la plage de suppression
|
||||
const unDeleted = {};
|
||||
lignesEtuds
|
||||
.filter((e) => e.getAttribute("type") == "conflit")
|
||||
.forEach((e) => {
|
||||
const etud = etuds.get(Number(e.getAttribute("etudid")));
|
||||
// On récupère les assiduités couvertent par la plage de suppression
|
||||
// On récupère les assiduités couvertes par la plage de suppression
|
||||
etud.assiduites.forEach((a) => {
|
||||
const date_debut = new Date(a.date_debut);
|
||||
const date_fin = new Date(a.date_fin);
|
||||
@ -756,8 +771,8 @@ function mettreToutLeMonde(etat, el = null) {
|
||||
// (qui intersectent la plage de suppression)
|
||||
if (
|
||||
Date.intersect(
|
||||
{ deb: deb, fin: fin },
|
||||
{ deb: date_debut, fin: date_fin }
|
||||
{ deb: deb, fin: fin }, // la plage, en Date avec timezone serveur
|
||||
{ deb: date_debut, fin: date_fin } // dates de l'assiduité avec leur timezone
|
||||
)
|
||||
) {
|
||||
// Si l'assiduité est couverte par la plage de suppression
|
||||
|
@ -5,7 +5,7 @@
|
||||
function get_etudid_from_elem(e) {
|
||||
// renvoie l'etudid, obtenu a partir de l'id de l'element
|
||||
// qui est soit de la forme xxxx-etudid, soit tout simplement etudid
|
||||
var etudid = e.id.split("-")[1];
|
||||
let etudid = e.id.split("-")[1];
|
||||
if (etudid == undefined) {
|
||||
return e.id;
|
||||
} else {
|
||||
|
@ -5,14 +5,14 @@ Gestion des listes d'assiduités et justificatifs
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import url_for, request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy import desc, literal, union, asc
|
||||
|
||||
from app import db, g
|
||||
from app.auth.models import User
|
||||
from app.models import Assiduite, Identite, Justificatif, Module
|
||||
from app.models import Assiduite, Identite, Justificatif, Module, ScoDocModel
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
from app.scodoc.sco_utils import (
|
||||
@ -715,7 +715,7 @@ class AssiFiltre:
|
||||
if date_fin is not None:
|
||||
self.filtres["date_fin"]: tuple[int, datetime] = date_fin
|
||||
|
||||
def filtrage(self, query: Query, obj_class: db.Model) -> Query:
|
||||
def filtrage(self, query: Query, obj_class: ScoDocModel) -> Query:
|
||||
"""
|
||||
filtrage Filtre la query passée en paramètre et retourne l'objet filtré
|
||||
|
||||
|
@ -51,6 +51,7 @@ class TableRecap(tb.Table):
|
||||
self,
|
||||
res: ResultatsSemestre,
|
||||
convert_values=False,
|
||||
include_email_addresses=False,
|
||||
include_evaluations=False,
|
||||
mode_jury=False,
|
||||
row_class=None,
|
||||
@ -61,6 +62,7 @@ class TableRecap(tb.Table):
|
||||
self.rows: list["RowRecap"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
super().__init__(row_class=row_class or RowRecap, **kwargs)
|
||||
self.res = res
|
||||
self.include_email_addresses = include_email_addresses
|
||||
self.include_evaluations = include_evaluations
|
||||
self.mode_jury = mode_jury
|
||||
self.read_only = read_only # utilisé seulement dans sous-classes
|
||||
@ -603,7 +605,7 @@ class RowRecap(tb.Row):
|
||||
target=url_bulletin,
|
||||
target_attrs={"class": "etudinfo", "id": str(etud.id)},
|
||||
)
|
||||
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
self.add_cell("prenom", "Prénom", etud.prenom_str, "identite_detail")
|
||||
self.add_cell(
|
||||
"nom_short",
|
||||
"Nom",
|
||||
@ -618,6 +620,13 @@ class RowRecap(tb.Row):
|
||||
target=url_bulletin,
|
||||
target_attrs={"class": "etudinfo", "id": str(etud.id)},
|
||||
)
|
||||
if self.table.include_email_addresses:
|
||||
address = etud.adresses.first()
|
||||
if address:
|
||||
self.add_cell("email", "Email", address.email, "identite_detail")
|
||||
self.add_cell(
|
||||
"emailperso", "Email perso", address.emailperso, "identite_detail"
|
||||
)
|
||||
|
||||
def add_abs(self):
|
||||
"Ajoute les colonnes absences"
|
||||
|
@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const SERVER_TIMEZONE_OFFSET = "{{ scu.get_local_timezone_offset() }}";
|
||||
var SERVER_TIMEZONE_OFFSET = "{{ scu.get_local_timezone_offset() }}"; // modifié par creerTousLesEtudiants()
|
||||
const timelineContainer = document.querySelector(".timeline-container");
|
||||
const periodTimeLine = document.querySelector(".period");
|
||||
const t_start = {{ t_start }};
|
||||
@ -336,7 +336,14 @@
|
||||
const [hours, minutes] = time.split(separator).map((el) => Number(el))
|
||||
return hours + minutes / 60
|
||||
}
|
||||
// Renvoie les valeurs de la période sous forme de date
|
||||
// La date ISO du datepicker
|
||||
function getSelectedDateIso() {
|
||||
return $("#date")
|
||||
.datepicker("getDate")
|
||||
.format("yyyy-mm-dd")
|
||||
.substring(0, 10); // récupération que de la date, pas des heures
|
||||
}
|
||||
// Renvoie les valeurs de la période sous forme de Date
|
||||
// Les heures sont récupérées depuis la timeline
|
||||
// la date est récupérée depuis un champ "#date" (datepicker)
|
||||
function getPeriodAsDate(add_server_tz = false) {
|
||||
@ -344,17 +351,30 @@
|
||||
deb = numberToTime(deb);
|
||||
fin = numberToTime(fin);
|
||||
|
||||
const dateStr = getSelectedDateIso();
|
||||
|
||||
// Les heures deb et fin sont telles qu'affichées, c'est à dire
|
||||
// en heure locale DU SERVEUR (des étudiants donc)
|
||||
let offset = add_server_tz ? SERVER_TIMEZONE_OFFSET : "";
|
||||
return {
|
||||
deb: new Date(`${dateStr}T${deb}${offset}`),
|
||||
fin: new Date(`${dateStr}T${fin}${offset}`)
|
||||
}
|
||||
}
|
||||
// Renvoie les valeurs de la période sous forme de chaine ISO sans time zone.
|
||||
function getPeriodAsISO() {
|
||||
let [deb, fin] = getPeriodValues();
|
||||
deb = numberToTime(deb);
|
||||
fin = numberToTime(fin);
|
||||
|
||||
const dateStr = $("#date")
|
||||
.datepicker("getDate")
|
||||
.format("yyyy-mm-dd")
|
||||
.substring(0, 10); // récupération que de la date, pas des heures
|
||||
|
||||
// Les heures deb et fin sont telles qu'affichées, c'est à dire
|
||||
// en heure locale DU SERVEUR (des étudiants donc)
|
||||
let offset = add_server_tz ? SERVER_TIMEZONE_OFFSET : "";
|
||||
// retourne des chaines ISO sans timezone
|
||||
return {
|
||||
deb: new Date(`${dateStr}T${deb}${offset}`),
|
||||
fin: new Date(`${dateStr}T${fin}${offset}`)
|
||||
deb : `${dateStr}T${deb}`,
|
||||
fin : `${dateStr}T${fin}`
|
||||
}
|
||||
}
|
||||
// Sauvegarde les valeurs de la période dans le local storage
|
||||
|
38
app/templates/auth/msg_change_password.j2
Normal file
38
app/templates/auth/msg_change_password.j2
Normal file
@ -0,0 +1,38 @@
|
||||
<style>
|
||||
div.msg-change-passwd {
|
||||
border: 3px solid white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
div.msg-change-passwd, div.msg-change-passwd a {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
div.msg-change-passwd a, div.msg-change-passwd a:visited {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
<div class="msg-change-passwd">
|
||||
Vous devez
|
||||
{% if current_user.dept %}
|
||||
<a class="nav-link" href="{{
|
||||
url_for(
|
||||
'users.form_change_password',
|
||||
scodoc_dept=current_user.dept,
|
||||
user_name=current_user.user_name
|
||||
)
|
||||
}}">
|
||||
{% endif %}
|
||||
changer votre mot de passe
|
||||
{% if current_user.dept %}
|
||||
</a>
|
||||
{% endif %}
|
||||
!
|
||||
</div>
|
@ -18,6 +18,9 @@
|
||||
<br>
|
||||
<b>Nom :</b> {{user.nom or ""}}<br>
|
||||
<b>Prénom :</b> {{user.prenom or ""}}<br>
|
||||
{% if user.passwd_must_be_changed %}
|
||||
<div style="color:white; background-color: red; padding:8px; margin-top: 4px; width: fit-content;">mot de passe à changer</div>
|
||||
{% endif %}
|
||||
<b>Mail :</b> {{user.email}}<br>
|
||||
<b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}<br>
|
||||
<b>Identifiant EDT:</b> {{user.edt_id or ""}}<br>
|
||||
|
@ -25,6 +25,9 @@
|
||||
{% block navbar %}
|
||||
{%- endblock navbar %}
|
||||
<div id="sco_msg" class="head_message"></div>
|
||||
{% if current_user and current_user.passwd_must_be_changed %}
|
||||
{% include "auth/msg_change_password.j2" %}
|
||||
{% endif %}
|
||||
{% block content -%}
|
||||
{%- endblock content %}
|
||||
|
||||
|
@ -46,7 +46,8 @@
|
||||
{% if current_user.is_anonymous %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('auth.login') }}">connexion</a></li>
|
||||
{% else %}
|
||||
<li class="nav-item">{% if current_user.dept %}
|
||||
<li class="nav-item">
|
||||
{% if current_user.dept %}
|
||||
<a class="nav-link" href="{{ url_for('users.user_info_page', scodoc_dept=current_user.dept, user_name=current_user.user_name )
|
||||
}}">{{current_user.user_name}} ({{current_user.dept}})</a>
|
||||
{% else %}
|
||||
@ -89,7 +90,7 @@
|
||||
<script>
|
||||
const SCO_URL = "{% if g.scodoc_dept %}{{
|
||||
url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}{% endif %}";
|
||||
|
||||
|
||||
document.querySelector('.navbar-toggler').addEventListener('click', function() {
|
||||
document.querySelector('.navbar-collapse').classList.toggle('show');
|
||||
});
|
||||
|
71
app/templates/formsemestre/list_saisies_notes.j2
Normal file
71
app/templates/formsemestre/list_saisies_notes.j2
Normal file
@ -0,0 +1,71 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.h-spaced {
|
||||
margin-left: 32px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2 class="formsemestre">{{title_h2}}</h2>
|
||||
|
||||
<div>{{table.get_nb_rows()}}
|
||||
{% if only_modifs %}modifications{% else %}saisies{% endif %}
|
||||
de notes dans ce semestre.
|
||||
</div>
|
||||
<form id="filter-form">
|
||||
<label>
|
||||
<input type="checkbox" id="only-modifs-checkbox" name="only_modifs" value="1"
|
||||
{% if only_modifs %}checked{% endif %}>
|
||||
Lister uniquement les modifications
|
||||
</label>
|
||||
|
||||
<label class="h-spaced" for="user-select">Restreindre à un enseignant :</label>
|
||||
<select id="user-select" name="user_name">
|
||||
<option value="">Choisir...</option>
|
||||
{% for user in intervenants %}
|
||||
<option value="{{ user.user_name }}"
|
||||
{% if user.user_name == user_name %}selected{% endif %}>
|
||||
{{ user.get_nomplogin() }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<span class="h-spaced">{{table.xls_export_button()|safe}} excel</span>
|
||||
</form>
|
||||
|
||||
{{table.html()|safe}}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
|
||||
<script>
|
||||
document.getElementById('only-modifs-checkbox').addEventListener('change', function() {
|
||||
var form = document.getElementById('filter-form');
|
||||
var onlyModifs = this.checked ? '1' : '0';
|
||||
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set('formsemestre_id', {{formsemestre_id}});
|
||||
url.searchParams.set('only_modifs', onlyModifs);
|
||||
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
|
||||
document.getElementById('user-select').addEventListener('change', function() {
|
||||
var form = document.getElementById('filter-form');
|
||||
var userName = this.value;
|
||||
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set('formsemestre_id', {{formsemestre_id}});
|
||||
url.searchParams.set('user_name', userName);
|
||||
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
38
app/templates/formsemestre/synchro_etuds.j2
Normal file
38
app/templates/formsemestre/synchro_etuds.j2
Normal file
@ -0,0 +1,38 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
{% import 'wtf.j2' as wtf %}
|
||||
|
||||
{# Utilisé pour passage d'un semestre à l'autre et pour synchro Apogée #}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
function sem_select(formsemestre_id, state) {
|
||||
let elems = document.getElementById(formsemestre_id).getElementsByTagName("input");
|
||||
for (var i =0; i < elems.length; i++) {
|
||||
elems[i].checked = state;
|
||||
}
|
||||
}
|
||||
function sem_select_inscrits(formsemestre_id) {
|
||||
var elems = document.getElementById(formsemestre_id).getElementsByTagName("input");
|
||||
for (var i =0; i < elems.length; i++) {
|
||||
if (elems[i].parentNode.className.indexOf('inscrit') >= 0) {
|
||||
elems[i].checked=true;
|
||||
} else {
|
||||
elems[i].checked=false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.etuds-box').DataTable({
|
||||
paging: false,
|
||||
autoWidth: false,
|
||||
searching: false,
|
||||
info: false, // Disable the "Showing 1 to X of X entries"
|
||||
columnDefs: [
|
||||
{ targets: 'no-sort', orderable: false } // Disable sorting on columns with class 'no-sort'
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,8 +1,10 @@
|
||||
{# Tableau de bord utilisateur #}
|
||||
|
||||
{% extends "base.j2" %}
|
||||
|
||||
{% block app_content %}
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link type="text/css" rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="{{scu.STATIC_DIR}}/DataTables/datatables.min.css" />
|
||||
<style>
|
||||
.ub-formsemestres {
|
||||
display: flex;
|
||||
@ -60,7 +62,14 @@
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.scobox.saisies-notes {
|
||||
background-color: rgb(243, 255, 255);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="tab-content">
|
||||
<h2>{{user.get_nomcomplet()}}</h2>
|
||||
@ -105,6 +114,63 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="scobox saisies-notes">
|
||||
{% if current_user.is_administrator() or current_user.id == user.id %}
|
||||
<div class="scobox-title">
|
||||
Dernières saisies de notes par {{user.get_prenomnom()}}
|
||||
</div>
|
||||
<table id="saisies-notes" class="display" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Évaluation</th>
|
||||
<th>Étudiant</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data will be loaded dynamically via JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="help">
|
||||
Vous n'avez pas les droits pour voir les notes de cet utilisateur.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock app_content %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#saisies-notes').DataTable({
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": {
|
||||
"url": "{{ url_for('apiweb.operations_user_notes',
|
||||
scodoc_dept=current_user.dept or g.scodoc_dept or fallback_dept.acronym,
|
||||
uid=user.id) }}",
|
||||
"type": "GET"
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "date_dmy", "orderable": false },
|
||||
{ "data": "evaluation_link", "orderable": false },
|
||||
{ "data": "etudiant_link", "orderable": false },
|
||||
{ "data": "value", "orderable": false }
|
||||
],
|
||||
"language": {
|
||||
search: "Chercher (date, titre, étudiant) :", // Change the "Search:" label
|
||||
lengthMenu: "Show _MENU_ records per page"
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -37,7 +37,7 @@ def start_scodoc_request():
|
||||
# current_app.logger.info(f"start_scodoc_request")
|
||||
ndb.open_db_connection()
|
||||
if current_user and current_user.is_authenticated:
|
||||
current_user.last_seen = datetime.datetime.utcnow()
|
||||
current_user.last_seen = datetime.datetime.now()
|
||||
db.session.commit()
|
||||
# caches locaux (durée de vie=la requête en cours)
|
||||
g.stored_get_formsemestre = {}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user