forked from ScoDoc/ScoDoc
Compare commits
69 Commits
42f5b32624
...
12583814cb
Author | SHA1 | Date | |
---|---|---|---|
|
12583814cb | ||
174baf326e | |||
b1a45b34f5 | |||
a6bc24aba2 | |||
f645c878a8 | |||
007f915d52 | |||
7c25098387 | |||
af94a2a727 | |||
730f2e9cb7 | |||
e355c2f780 | |||
da827e50ad | |||
a9303f6274 | |||
e0eb57300e | |||
aae94b4f10 | |||
154e0e5cab | |||
31856c857c | |||
b93e76f99c | |||
9ba44f5285 | |||
53118e2446 | |||
0f78a1288d | |||
72dfcc086d | |||
598e037e38 | |||
6d71b116b5 | |||
ce69df4e08 | |||
1f7d13c6cf | |||
b349ff3d79 | |||
466daad0c0 | |||
a1e6465224 | |||
a94686957c | |||
e679f23065 | |||
1a2a15d7f6 | |||
6ae31c3e9f | |||
36f75ab0c4 | |||
e36a83a48a | |||
68e37a2ccd | |||
c731e194ef | |||
363e7e2952 | |||
332e1a306a | |||
559f1882d1 | |||
6357dd999d | |||
04b7ff7658 | |||
e3da4d51a5 | |||
3bfeebbcb2 | |||
ffca42917d | |||
4cd3b71cfc | |||
a390cffe57 | |||
38714e5d2a | |||
a533c40267 | |||
657b1e1f1e | |||
8bd5d83af0 | |||
36a0784897 | |||
0948734ff2 | |||
3c16f44ed3 | |||
16149ea6bc | |||
59d10a01f7 | |||
36abc54f7f | |||
94c9035c4e | |||
c23738e5dc | |||
bfa6973d4e | |||
66a565d64a | |||
7361f2fa1a | |||
e6595bdf30 | |||
4f9638582a | |||
cf778eba85 | |||
63603463f1 | |||
39466bf9b5 | |||
3ba5318b17 | |||
87aab32d2e | |||
f40fae3463 |
@ -3,6 +3,7 @@
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
@ -12,16 +13,20 @@ import traceback
|
||||
import logging
|
||||
from logging.handlers import SMTPHandler, WatchedFileHandler
|
||||
from threading import Thread
|
||||
import warnings
|
||||
import warnings
|
||||
|
||||
from flask import current_app, g, request
|
||||
from flask import Flask
|
||||
from flask import abort, flash, has_request_context, jsonify
|
||||
from flask import abort, flash, has_request_context
|
||||
from flask import render_template
|
||||
from flask.json import JSONEncoder
|
||||
|
||||
# from flask.json import JSONEncoder
|
||||
from flask.logging import default_handler
|
||||
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_caching import Cache
|
||||
from flask_json import FlaskJSON, json_response
|
||||
from flask_login import LoginManager, current_user
|
||||
from flask_mail import Mail
|
||||
from flask_migrate import Migrate
|
||||
@ -29,9 +34,10 @@ from flask_moment import Moment
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from jinja2 import select_autoescape
|
||||
import sqlalchemy
|
||||
import sqlalchemy as sa
|
||||
|
||||
from flask_cas import CAS
|
||||
import werkzeug.debug
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
@ -42,6 +48,8 @@ from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
APIInvalidParams,
|
||||
)
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
|
||||
from config import DevConfig
|
||||
import sco_version
|
||||
|
||||
@ -134,18 +142,22 @@ def _async_dump(app, request_url: str):
|
||||
|
||||
|
||||
def handle_invalid_usage(error):
|
||||
response = jsonify(error.to_dict())
|
||||
response = json_response(data_=error.to_dict())
|
||||
response.status_code = error.status_code
|
||||
return response
|
||||
|
||||
|
||||
# JSON ENCODING
|
||||
class ScoDocJSONEncoder(JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, (datetime.datetime, datetime.date)):
|
||||
# used by some internal finctions
|
||||
# the API is now using flask_son, NOT THIS ENCODER
|
||||
class ScoDocJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o): # pylint: disable=E0202
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return o.isoformat()
|
||||
|
||||
return super().default(o)
|
||||
elif isinstance(o, ApoEtapeVDI):
|
||||
return str(o)
|
||||
else:
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
def render_raw_html(template_filename: str, **args) -> str:
|
||||
@ -244,17 +256,33 @@ class ReverseProxied(object):
|
||||
|
||||
def create_app(config_class=DevConfig):
|
||||
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
|
||||
app.config.from_object(config_class)
|
||||
from app.auth import cas
|
||||
|
||||
CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration)
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
app.json_encoder = ScoDocJSONEncoder
|
||||
FlaskJSON(app)
|
||||
|
||||
# Pour conserver l'ordre des objets dans les JSON:
|
||||
# e.g. l'ordre des UE dans les bulletins
|
||||
app.json.sort_keys = False
|
||||
|
||||
app.config.from_object(config_class)
|
||||
# Evite de logguer toutes les requetes dans notre log
|
||||
logging.getLogger("werkzeug").disabled = True
|
||||
app.logger.setLevel(app.config["LOG_LEVEL"])
|
||||
|
||||
if app.config["TESTING"] or app.config["DEBUG"]:
|
||||
# S'arrête sur tous les warnings, sauf
|
||||
# flask_sqlalchemy/query (pb deprecation du model.get())
|
||||
warnings.filterwarnings("error", module="flask_sqlalchemy/query")
|
||||
# warnings.filterwarnings("ignore", module="json/provider.py") xxx sans effet en test
|
||||
if app.config["DEBUG"]:
|
||||
# comme on a désactivé ci-dessus les logs de werkzeug,
|
||||
# on affiche nous même le PIN en mode debug:
|
||||
print(
|
||||
f""" * Debugger is active!
|
||||
* Debugger PIN: {werkzeug.debug.get_pin_and_cookie_name(app)[0]}
|
||||
"""
|
||||
)
|
||||
# Vérifie/crée lien sym pour les URL statiques
|
||||
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
|
||||
if not os.path.exists(link_filename):
|
||||
@ -409,7 +437,7 @@ def create_app(config_class=DevConfig):
|
||||
with app.app_context():
|
||||
try:
|
||||
set_cas_configuration(app)
|
||||
except sqlalchemy.exc.ProgrammingError:
|
||||
except sa.exc.ProgrammingError:
|
||||
# Si la base n'a pas été upgradée (arrive durrant l'install)
|
||||
# il se peut que la table scodoc_site_config n'existe pas encore.
|
||||
pass
|
||||
@ -421,7 +449,7 @@ def set_sco_dept(scodoc_dept: str, open_cnx=True):
|
||||
# Check that dept exists
|
||||
try:
|
||||
dept = Departement.query.filter_by(acronym=scodoc_dept).first()
|
||||
except sqlalchemy.exc.OperationalError:
|
||||
except sa.exc.OperationalError:
|
||||
abort(503)
|
||||
if not dept:
|
||||
raise ScoValueError(f"Invalid dept: {scodoc_dept}")
|
||||
@ -499,13 +527,14 @@ def truncate_database():
|
||||
"""
|
||||
# use a stored SQL function, see createtables.sql
|
||||
try:
|
||||
db.session.execute("SELECT truncate_tables('scodoc');")
|
||||
db.session.execute(sa.text("SELECT truncate_tables('scodoc');"))
|
||||
db.session.commit()
|
||||
except:
|
||||
db.session.rollback()
|
||||
raise
|
||||
# Remet les compteurs (séquences sql) à zéro
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
|
||||
DECLARE
|
||||
@ -523,6 +552,7 @@ def truncate_database():
|
||||
SELECT reset_sequences('scodoc');
|
||||
"""
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""api.__init__
|
||||
"""
|
||||
|
||||
from flask_json import as_json
|
||||
from flask import Blueprint
|
||||
from flask import request, g, jsonify
|
||||
from flask import request, g
|
||||
from app import db
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
@ -35,6 +35,7 @@ def requested_format(default_format="json", allowed_formats=None):
|
||||
return None
|
||||
|
||||
|
||||
@as_json
|
||||
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||
"""
|
||||
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||
@ -48,7 +49,7 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model
|
||||
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
||||
unique: model_cls = query.first_or_404()
|
||||
|
||||
return jsonify(unique.to_dict(format_api=True))
|
||||
return unique.to_dict(format_api=True)
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
|
@ -6,7 +6,7 @@
|
||||
"""ScoDoc 9 API : Absences
|
||||
"""
|
||||
|
||||
from flask import jsonify
|
||||
from flask_json import as_json
|
||||
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.scodoc.sco_utils import json_error
|
||||
@ -19,10 +19,12 @@ from app.scodoc import sco_abs
|
||||
from app.scodoc.sco_groups import get_group_members
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
# TODO XXX revoir routes web API et calcul des droits
|
||||
@bp.route("/absences/etudid/<int:etudid>", methods=["GET"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def absences(etudid: int = None):
|
||||
"""
|
||||
Liste des absences de cet étudiant
|
||||
@ -57,12 +59,13 @@ def absences(etudid: int = None):
|
||||
abs_list = sco_abs.list_abs_date(etud.id)
|
||||
for absence in abs_list:
|
||||
absence["jour"] = absence["jour"].isoformat()
|
||||
return jsonify(abs_list)
|
||||
return abs_list
|
||||
|
||||
|
||||
@bp.route("/absences/etudid/<int:etudid>/just", methods=["GET"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def absences_just(etudid: int = None):
|
||||
"""
|
||||
Retourne la liste des absences justifiées d'un étudiant donné
|
||||
@ -103,7 +106,7 @@ def absences_just(etudid: int = None):
|
||||
]
|
||||
for absence in abs_just:
|
||||
absence["jour"] = absence["jour"].isoformat()
|
||||
return jsonify(abs_just)
|
||||
return abs_just
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -116,6 +119,7 @@ def absences_just(etudid: int = None):
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None):
|
||||
"""
|
||||
Liste des absences d'un groupe (possibilité de choisir entre deux dates)
|
||||
@ -167,7 +171,7 @@ def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None):
|
||||
}
|
||||
data.append(absence)
|
||||
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
||||
|
||||
# XXX TODO EV: A REVOIR (data json dans le POST + modifier les routes)
|
||||
|
@ -6,7 +6,8 @@
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
from datetime import datetime
|
||||
from flask import g, jsonify, request
|
||||
from flask_json import as_json
|
||||
from flask import g, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
@ -52,6 +53,7 @@ def assiduite(assiduite_id: int = None):
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
@ -109,11 +111,9 @@ def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||
if with_query:
|
||||
metric, filtered = _count_manager(request)
|
||||
|
||||
return jsonify(
|
||||
scass.get_assiduites_stats(
|
||||
return scass.get_assiduites_stats(
|
||||
assiduites=etud.assiduites, metric=metric, filtered=filtered
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||
@ -122,6 +122,7 @@ def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
@ -178,13 +179,14 @@ def assiduites(etudid: int = None, with_query: bool = False):
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
return data_set
|
||||
|
||||
|
||||
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_group(with_query: bool = False):
|
||||
"""
|
||||
@ -252,7 +254,7 @@ def assiduites_group(with_query: bool = False):
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.get(data["etudid"]).append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
return data_set
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -271,6 +273,7 @@ def assiduites_group(with_query: bool = False):
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne toutes les assiduités du formsemestre"""
|
||||
@ -291,7 +294,7 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
return data_set
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -312,6 +315,7 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def count_assiduites_formsemestre(
|
||||
formsemestre_id: int = None, with_query: bool = False
|
||||
@ -334,12 +338,13 @@ def count_assiduites_formsemestre(
|
||||
if with_query:
|
||||
metric, filtered = _count_manager(request)
|
||||
|
||||
return jsonify(scass.get_assiduites_stats(assiduites_query, metric, filtered))
|
||||
return scass.get_assiduites_stats(assiduites_query, metric, filtered)
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@scodoc
|
||||
@as_json
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
@ -382,12 +387,13 @@ def assiduite_create(etudid: int = None):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
return {"errors": errors, "success": success}
|
||||
|
||||
|
||||
@bp.route("/assiduites/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduites/create", methods=["POST"])
|
||||
@scodoc
|
||||
@as_json
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
@ -435,7 +441,7 @@ def assiduites_create():
|
||||
else:
|
||||
success[i] = obj
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
return {"errors": errors, "success": success}
|
||||
|
||||
|
||||
def _create_singular(
|
||||
@ -515,6 +521,7 @@ def _create_singular(
|
||||
@api_web_bp.route("/assiduite/delete", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_delete():
|
||||
@ -543,7 +550,7 @@ def assiduite_delete():
|
||||
else:
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
db.session.commit()
|
||||
return jsonify(output)
|
||||
return output
|
||||
|
||||
|
||||
def _delete_singular(assiduite_id: int, database):
|
||||
@ -558,6 +565,7 @@ def _delete_singular(assiduite_id: int, database):
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_edit(assiduite_id: int):
|
||||
@ -625,13 +633,14 @@ def assiduite_edit(assiduite_id: int):
|
||||
|
||||
db.session.add(assiduite_unique)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
return {"OK": True}
|
||||
|
||||
|
||||
@bp.route("/assiduites/edit", methods=["POST"])
|
||||
@api_web_bp.route("/assiduites/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduites_edit():
|
||||
@ -666,7 +675,7 @@ def assiduites_edit():
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
return {"errors": errors, "success": success}
|
||||
|
||||
|
||||
def _edit_singular(assiduite_unique, data):
|
||||
|
@ -8,7 +8,8 @@
|
||||
API : billets d'absences
|
||||
"""
|
||||
|
||||
from flask import g, jsonify, request
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
from app import db
|
||||
@ -26,10 +27,11 @@ from app.scodoc.sco_permissions import Permission
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def billets_absence_etudiant(etudid: int):
|
||||
"""Liste des billets d'absence pour cet étudiant"""
|
||||
billets = sco_abs_billets.query_billets_etud(etudid)
|
||||
return jsonify([billet.to_dict() for billet in billets])
|
||||
return [billet.to_dict() for billet in billets]
|
||||
|
||||
|
||||
@bp.route("/billets_absence/create", methods=["POST"])
|
||||
@ -37,6 +39,7 @@ def billets_absence_etudiant(etudid: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoAbsAddBillet)
|
||||
@as_json
|
||||
def billets_absence_create():
|
||||
"""Ajout d'un billet d'absence"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -60,7 +63,7 @@ def billets_absence_create():
|
||||
)
|
||||
db.session.add(billet)
|
||||
db.session.commit()
|
||||
return jsonify(billet.to_dict())
|
||||
return billet.to_dict()
|
||||
|
||||
|
||||
@bp.route("/billets_absence/<int:billet_id>/delete", methods=["POST"])
|
||||
@ -68,6 +71,7 @@ def billets_absence_create():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoAbsAddBillet)
|
||||
@as_json
|
||||
def billets_absence_delete(billet_id: int):
|
||||
"""Suppression d'un billet d'absence"""
|
||||
query = BilletAbsence.query.filter_by(id=billet_id)
|
||||
@ -77,4 +81,4 @@ def billets_absence_delete(billet_id: int):
|
||||
billet = query.first_or_404()
|
||||
db.session.delete(billet)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
return {"OK": True}
|
||||
|
@ -12,7 +12,8 @@
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import jsonify, request
|
||||
from flask import request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
@ -41,24 +42,27 @@ def get_departement(dept_ident: str) -> Departement:
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_list():
|
||||
"""Liste les départements"""
|
||||
return jsonify([dept.to_dict(with_dept_name=True) for dept in Departement.query])
|
||||
return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
|
||||
|
||||
|
||||
@bp.route("/departements_ids")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_ids():
|
||||
"""Liste des ids de départements"""
|
||||
return jsonify([dept.id for dept in Departement.query])
|
||||
return [dept.id for dept in Departement.query]
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement(acronym: str):
|
||||
"""
|
||||
Info sur un département. Accès par acronyme.
|
||||
@ -74,25 +78,27 @@ def departement(acronym: str):
|
||||
}
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return jsonify(dept.to_dict(with_dept_name=True))
|
||||
return dept.to_dict(with_dept_name=True)
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_by_id(dept_id: int):
|
||||
"""
|
||||
Info sur un département. Accès par id.
|
||||
"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
return jsonify(dept.to_dict())
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@bp.route("/departement/create", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def departement_create():
|
||||
"""
|
||||
Création d'un département.
|
||||
@ -111,13 +117,14 @@ def departement_create():
|
||||
dept = departements.create_dept(acronym, visible=visible)
|
||||
except ScoValueError as exc:
|
||||
return json_error(500, exc.args[0] if exc.args else "")
|
||||
return jsonify(dept.to_dict())
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def departement_edit(acronym):
|
||||
"""
|
||||
Edition d'un département: seul visible peut être modifié
|
||||
@ -135,7 +142,7 @@ def departement_edit(acronym):
|
||||
dept.visible = visible
|
||||
db.session.add(dept)
|
||||
db.session.commit()
|
||||
return jsonify(dept.to_dict())
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/delete", methods=["POST"])
|
||||
@ -149,13 +156,14 @@ def departement_delete(acronym):
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
db.session.delete(dept)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
return {"OK": True}
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/etudiants", methods=["GET"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_etudiants(acronym: str):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département
|
||||
@ -179,45 +187,49 @@ def dept_etudiants(acronym: str):
|
||||
]
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return jsonify([etud.to_dict_short() for etud in dept.etudiants])
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/etudiants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_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)
|
||||
return jsonify([etud.to_dict_short() for etud in dept.etudiants])
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/formsemestres_ids")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_formsemestres_ids(acronym: str):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return jsonify([formsemestre.id for formsemestre in dept.formsemestres])
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_ids")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_formsemestres_ids_by_id(dept_id: int):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
return jsonify([formsemestre.id for formsemestre in dept.formsemestres])
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/formsemestres_courants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_formsemestres_courants(acronym: str):
|
||||
"""
|
||||
Liste des semestres actifs d'un département d'acronyme donné
|
||||
@ -269,13 +281,14 @@ def dept_formsemestres_courants(acronym: str):
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||
return [d.to_dict_api() for d in formsemestres]
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_formsemestres_courants_by_id(dept_id: int):
|
||||
"""
|
||||
Liste des semestres actifs d'un département d'id donné
|
||||
@ -294,4 +307,4 @@ def dept_formsemestres_courants_by_id(dept_id: int):
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
|
||||
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||
return [d.to_dict_api() for d in formsemestres]
|
||||
|
@ -9,7 +9,8 @@
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import abort, g, jsonify, request
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import desc, or_
|
||||
@ -38,11 +39,11 @@ import app.scodoc.sco_photos as sco_photos
|
||||
# @login_required
|
||||
# @scodoc
|
||||
# @permission_required(Permission.ScoView)
|
||||
# @as_json
|
||||
# def api_function(arg: int):
|
||||
# """Une fonction quelconque de l'API"""
|
||||
# return jsonify(
|
||||
# {"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept}
|
||||
# )
|
||||
# return {"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept}
|
||||
#
|
||||
|
||||
|
||||
@bp.route("/etudiants/courants", defaults={"long": False})
|
||||
@ -52,6 +53,7 @@ import app.scodoc.sco_photos as sco_photos
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_courants(long=False):
|
||||
"""
|
||||
La liste des étudiants des semestres "courants" (tous départements)
|
||||
@ -97,7 +99,7 @@ def etudiants_courants(long=False):
|
||||
data = [etud.to_dict_api() for etud in etuds]
|
||||
else:
|
||||
data = [etud.to_dict_short() for etud in etuds]
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>")
|
||||
@ -109,6 +111,7 @@ def etudiants_courants(long=False):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
|
||||
@ -128,7 +131,7 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
return jsonify(etud.to_dict_api())
|
||||
return etud.to_dict_api()
|
||||
|
||||
|
||||
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||
@ -175,6 +178,7 @@ def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
@api_web_bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie
|
||||
@ -200,7 +204,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
etuds = etuds.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
return jsonify([etud.to_dict_api() for etud in query])
|
||||
return [etud.to_dict_api() for etud in query]
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@ -211,6 +215,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
@api_web_bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique.
|
||||
@ -243,7 +248,7 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
||||
|
||||
formsemestres = query.order_by(FormSemestre.date_debut)
|
||||
|
||||
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
|
||||
return [formsemestre.to_dict_api() for formsemestre in formsemestres]
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -302,7 +307,7 @@ def bulletin(
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||
return json_error(404, "formsemestre inexistant")
|
||||
return json_error(404, "formsemestre inexistant", as_response=True)
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
if code_type == "nip":
|
||||
@ -340,6 +345,7 @@ def bulletin(
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
"""
|
||||
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
|
||||
@ -389,4 +395,4 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
app.set_sco_dept(dept.acronym)
|
||||
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
||||
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
@ -8,7 +8,8 @@
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
"""
|
||||
|
||||
from flask import g, jsonify
|
||||
from flask import g
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
@ -26,7 +27,8 @@ import app.scodoc.sco_utils as scu
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluation(evaluation_id: int):
|
||||
@as_json
|
||||
def the_eval(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
{
|
||||
@ -56,7 +58,7 @@ def evaluation(evaluation_id: int):
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
e = query.first_or_404()
|
||||
return jsonify(e.to_dict_api())
|
||||
return e.to_dict_api()
|
||||
|
||||
|
||||
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
||||
@ -64,6 +66,7 @@ def evaluation(evaluation_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des évaluations d'un moduleimpl
|
||||
@ -79,7 +82,7 @@ def evaluations(moduleimpl_id: int):
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
return jsonify([e.to_dict_api() for e in query])
|
||||
return [e.to_dict_api() for e in query]
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||
@ -87,6 +90,7 @@ def evaluations(moduleimpl_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Retourne la liste des notes à partir de l'id d'une évaluation donnée
|
||||
@ -124,8 +128,8 @@ def evaluation_notes(evaluation_id: int):
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
evaluation = query.first_or_404()
|
||||
dept = evaluation.moduleimpl.formsemestre.departement
|
||||
the_eval = query.first_or_404()
|
||||
dept = the_eval.moduleimpl.formsemestre.departement
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
@ -133,7 +137,7 @@ def evaluation_notes(evaluation_id: int):
|
||||
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
|
||||
note = notes[etudid]
|
||||
note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
|
||||
note["note_max"] = evaluation.note_max
|
||||
note["note_max"] = the_eval.note_max
|
||||
del note["id"]
|
||||
|
||||
return jsonify(notes)
|
||||
return notes
|
||||
|
@ -8,16 +8,23 @@
|
||||
ScoDoc 9 API : accès aux formations
|
||||
"""
|
||||
|
||||
from flask import g, jsonify
|
||||
from flask import flash, g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
ModuleImpl,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
@ -27,6 +34,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formations():
|
||||
"""
|
||||
Retourne la liste de toutes les formations (tous départements)
|
||||
@ -35,7 +43,7 @@ def formations():
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
return jsonify([d.to_dict() for d in query])
|
||||
return [d.to_dict() for d in query]
|
||||
|
||||
|
||||
@bp.route("/formations_ids")
|
||||
@ -43,6 +51,7 @@ def formations():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formations_ids():
|
||||
"""
|
||||
Retourne la liste de toutes les id de formations (tous départements)
|
||||
@ -52,7 +61,7 @@ def formations_ids():
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
return jsonify([d.id for d in query])
|
||||
return [d.id for d in query]
|
||||
|
||||
|
||||
@bp.route("/formation/<int:formation_id>")
|
||||
@ -60,6 +69,7 @@ def formations_ids():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formation_by_id(formation_id: int):
|
||||
"""
|
||||
La formation d'id donné
|
||||
@ -84,7 +94,7 @@ def formation_by_id(formation_id: int):
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
return jsonify(query.first_or_404().to_dict())
|
||||
return query.first_or_404().to_dict()
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -106,6 +116,7 @@ def formation_by_id(formation_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
"""
|
||||
Retourne la formation, avec UE, matières, modules
|
||||
@ -174,7 +185,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
]
|
||||
},
|
||||
{
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9",
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
@ -212,7 +223,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
except ValueError:
|
||||
return json_error(500, message="Erreur inconnue")
|
||||
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
||||
|
||||
@bp.route("/formation/<int:formation_id>/referentiel_competences")
|
||||
@ -220,6 +231,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def referentiel_competences(formation_id: int):
|
||||
"""
|
||||
Retourne le référentiel de compétences
|
||||
@ -233,8 +245,8 @@ def referentiel_competences(formation_id: int):
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formation = query.first_or_404(formation_id)
|
||||
if formation.referentiel_competence is None:
|
||||
return jsonify(None)
|
||||
return jsonify(formation.referentiel_competence.to_dict())
|
||||
return None
|
||||
return formation.referentiel_competence.to_dict()
|
||||
|
||||
|
||||
@bp.route("/moduleimpl/<int:moduleimpl_id>")
|
||||
@ -242,6 +254,7 @@ def referentiel_competences(formation_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def moduleimpl(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne un moduleimpl en fonction de son id
|
||||
@ -281,4 +294,92 @@ def moduleimpl(moduleimpl_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
modimpl: ModuleImpl = query.first_or_404()
|
||||
return jsonify(modimpl.to_dict(convert_objects=True))
|
||||
return modimpl.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoChangeFormation)
|
||||
@as_json
|
||||
def set_ue_parcours(ue_id: int):
|
||||
"""Associe UE et parcours BUT.
|
||||
La liste des ids de parcours est passée en argument JSON.
|
||||
JSON arg: [parcour_id1, parcour_id2, ...]
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
parcours_ids = request.get_json(force=True) or [] # may raise 400 Bad Request
|
||||
if parcours_ids == [""]:
|
||||
parcours = []
|
||||
else:
|
||||
parcours = [
|
||||
ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
|
||||
]
|
||||
log(f"set_ue_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
ok, error_message = ue.set_parcours(parcours)
|
||||
if not ok:
|
||||
return json_error(404, error_message)
|
||||
return {"status": ok, "message": error_message}
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoChangeFormation)
|
||||
@as_json
|
||||
def assoc_ue_niveau(ue_id: int, niveau_id: int):
|
||||
"""Associe l'UE au niveau de compétence"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
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)
|
||||
ok, error_message = ue.set_niveau_competence(niveau)
|
||||
if not ok:
|
||||
if g.scodoc_dept: # "usage web"
|
||||
flash(error_message, "error")
|
||||
return json_error(404, error_message)
|
||||
if g.scodoc_dept: # "usage web"
|
||||
flash(f"""{ue.acronyme} associée au niveau "{niveau.libelle}" """)
|
||||
return {"status": 0}
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoChangeFormation)
|
||||
@as_json
|
||||
def desassoc_ue_niveau(ue_id: int):
|
||||
"""Désassocie cette UE de son niveau de compétence
|
||||
(si elle n'est pas associée, ne fait rien)
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
ue.niveau_competence = None
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
log(f"desassoc_ue_niveau: {ue}")
|
||||
if g.scodoc_dept:
|
||||
# "usage web"
|
||||
flash(f"UE {ue.acronyme} dé-associée")
|
||||
return {"status": 0}
|
||||
|
@ -7,10 +7,14 @@
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formsemestres
|
||||
"""
|
||||
from flask import g, jsonify, request
|
||||
from operator import attrgetter, itemgetter
|
||||
|
||||
from flask import g, make_response, request
|
||||
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, API_CLIENT_ERROR
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
@ -27,6 +31,7 @@ from app.models import (
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
)
|
||||
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
|
||||
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -40,6 +45,7 @@ from app.tables.recap import TableRecap
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_infos(formsemestre_id: int):
|
||||
"""
|
||||
Information sur le formsemestre indiqué.
|
||||
@ -81,7 +87,7 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
return jsonify(formsemestre.to_dict_api())
|
||||
return formsemestre.to_dict_api()
|
||||
|
||||
|
||||
@bp.route("/formsemestres/query")
|
||||
@ -89,6 +95,7 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestres_query():
|
||||
"""
|
||||
Retourne les formsemestres filtrés par
|
||||
@ -144,7 +151,7 @@ def formsemestres_query():
|
||||
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
||||
formsemestres = formsemestres.filter_by(code_ine=ine)
|
||||
|
||||
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
|
||||
return [formsemestre.to_dict_api() for formsemestre in formsemestres]
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@ -154,6 +161,7 @@ def formsemestres_query():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
"""
|
||||
Retourne les bulletins d'un formsemestre donné
|
||||
@ -177,7 +185,7 @@ def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
)
|
||||
data.append(bul_etu.json)
|
||||
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/programme")
|
||||
@ -185,6 +193,7 @@ def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_programme(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste des Ues, ressources et SAE d'un semestre
|
||||
@ -254,7 +263,7 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
ues = formsemestre.query_ues()
|
||||
ues = formsemestre.get_ues()
|
||||
m_list = {
|
||||
ModuleType.RESSOURCE: [],
|
||||
ModuleType.SAE: [],
|
||||
@ -264,15 +273,13 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
d = modimpl.to_dict(convert_objects=True)
|
||||
m_list[modimpl.module.module_type].append(d)
|
||||
return jsonify(
|
||||
{
|
||||
return {
|
||||
"ues": [ue.to_dict(convert_objects=True) for ue in ues],
|
||||
"ressources": m_list[ModuleType.RESSOURCE],
|
||||
"saes": m_list[ModuleType.SAE],
|
||||
"modules": m_list[ModuleType.STANDARD],
|
||||
"malus": m_list[ModuleType.MALUS],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -310,6 +317,7 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_etudiants(
|
||||
formsemestre_id: int, with_query: bool = False, long: bool = False
|
||||
):
|
||||
@ -345,7 +353,7 @@ def formsemestre_etudiants(
|
||||
etud["id"], formsemestre_id, exclude_default=True
|
||||
)
|
||||
|
||||
return jsonify(sorted(etuds, key=lambda e: e["sort_key"]))
|
||||
return sorted(etuds, key=itemgetter("sort_key"))
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
||||
@ -353,6 +361,7 @@ def formsemestre_etudiants(
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etat_evals(formsemestre_id: int):
|
||||
"""
|
||||
Informations sur l'état des évaluations d'un formsemestre.
|
||||
@ -432,7 +441,7 @@ def etat_evals(formsemestre_id: int):
|
||||
# Si il y a plus d'une note saisie pour l'évaluation
|
||||
if len(notes) >= 1:
|
||||
# Tri des notes en fonction de leurs dates
|
||||
notes_sorted = sorted(notes, key=lambda note: note.date)
|
||||
notes_sorted = sorted(notes, key=attrgetter("date"))
|
||||
|
||||
date_debut = notes_sorted[0].date
|
||||
date_fin = notes_sorted[-1].date
|
||||
@ -454,7 +463,7 @@ def etat_evals(formsemestre_id: int):
|
||||
|
||||
modimpl_dict["evaluations"] = list_eval
|
||||
result.append(modimpl_dict)
|
||||
return jsonify(result)
|
||||
return result
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")
|
||||
@ -462,6 +471,7 @@ def etat_evals(formsemestre_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_resultat(formsemestre_id: int):
|
||||
"""Tableau récapitulatif des résultats
|
||||
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
|
||||
@ -487,4 +497,45 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||
for row in rows:
|
||||
row["partitions"] = etud_groups.get(row["etudid"], {})
|
||||
|
||||
return jsonify(rows)
|
||||
return rows
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def get_groups_auto_assignment(formsemestre_id: int):
|
||||
"""rend les données"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
response = make_response(formsemestre.groups_auto_assignment_data or b"")
|
||||
response.headers["Content-Type"] = scu.JSON_MIMETYPE
|
||||
return response
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def save_groups_auto_assignment(formsemestre_id: int):
|
||||
"""enregistre les données"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
|
||||
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
|
||||
return json_error(413, "data too large")
|
||||
formsemestre.groups_auto_assignment_data = request.data
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
|
@ -8,7 +8,7 @@
|
||||
ScoDoc 9 API : jury WIP
|
||||
"""
|
||||
|
||||
from flask import jsonify
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
@ -25,6 +25,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def decisions_jury(formsemestre_id: int):
|
||||
"""Décisions du jury des étudiants du formsemestre."""
|
||||
# APC, pair:
|
||||
@ -32,6 +33,6 @@ def decisions_jury(formsemestre_id: int):
|
||||
if formsemestre.formation.is_apc():
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||
return jsonify(rows)
|
||||
return rows
|
||||
else:
|
||||
raise ScoException("non implemente")
|
||||
|
@ -7,6 +7,7 @@
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask_json import as_json
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
@ -57,6 +58,7 @@ def justificatif(justif_id: int = None):
|
||||
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
@ -100,13 +102,14 @@ def justificatifs(etudid: int = None, with_query: bool = False):
|
||||
data = just.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
return data_set
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_create(etudid: int = None):
|
||||
@ -145,7 +148,7 @@ def justif_create(etudid: int = None):
|
||||
else:
|
||||
success[i] = obj
|
||||
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
return {"errors": errors, "success": success}
|
||||
|
||||
|
||||
def _create_singular(
|
||||
@ -221,6 +224,7 @@ def _create_singular(
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_edit(justif_id: int):
|
||||
@ -296,8 +300,7 @@ def justif_edit(justif_id: int):
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
return {
|
||||
"couverture": {
|
||||
"avant": avant_ids,
|
||||
"après": compute_assiduites_justified(
|
||||
@ -306,13 +309,13 @@ def justif_edit(justif_id: int):
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/justificatif/delete", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/delete", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_delete():
|
||||
@ -342,7 +345,7 @@ def justif_delete():
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(output)
|
||||
return output
|
||||
|
||||
|
||||
def _delete_singular(justif_id: int, database):
|
||||
@ -371,6 +374,7 @@ def _delete_singular(justif_id: int, database):
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_import(justif_id: int = None):
|
||||
@ -407,7 +411,7 @@ def justif_import(justif_id: int = None):
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"filename": fname})
|
||||
return {"filename": fname}
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
@ -447,6 +451,7 @@ def justif_export(justif_id: int = None, filename: str = None):
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_remove(justif_id: int = None):
|
||||
@ -504,13 +509,14 @@ def justif_remove(justif_id: int = None):
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
return jsonify({"response": "removed"})
|
||||
return {"response": "removed"}
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_list(justif_id: int = None):
|
||||
@ -534,7 +540,7 @@ def justif_list(justif_id: int = None):
|
||||
archive_name, justificatif_unique.etudid
|
||||
)
|
||||
|
||||
return jsonify(filenames)
|
||||
return filenames
|
||||
|
||||
|
||||
# Partie justification
|
||||
@ -542,6 +548,7 @@ def justif_list(justif_id: int = None):
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_justifies(justif_id: int = None):
|
||||
@ -557,7 +564,7 @@ def justif_justifies(justif_id: int = None):
|
||||
|
||||
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||
|
||||
return jsonify(assiduites_list)
|
||||
return assiduites_list
|
||||
|
||||
|
||||
# -- Utils --
|
||||
|
@ -30,11 +30,10 @@ Contrib @jmp
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from flask import jsonify, g, send_file
|
||||
from flask_login import login_required
|
||||
from flask import Response, send_file
|
||||
from flask_json import as_json
|
||||
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import requested_format
|
||||
from app.api import api_bp as bp
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import Departement
|
||||
from app.scodoc.sco_logos import list_logos, find_logo
|
||||
@ -47,10 +46,11 @@ from app.scodoc.sco_permissions import Permission
|
||||
@bp.route("/logos")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def api_get_glob_logos():
|
||||
"""Liste tous les logos"""
|
||||
logos = list_logos()[None]
|
||||
return jsonify(list(logos.keys()))
|
||||
return list(logos.keys())
|
||||
|
||||
|
||||
@bp.route("/logo/<string:logoname>")
|
||||
@ -68,27 +68,29 @@ def api_get_glob_logo(logoname):
|
||||
)
|
||||
|
||||
|
||||
def core_get_logos(dept_id):
|
||||
def _core_get_logos(dept_id) -> list:
|
||||
logos = list_logos().get(dept_id, dict())
|
||||
return jsonify(list(logos.keys()))
|
||||
return list(logos.keys())
|
||||
|
||||
|
||||
@bp.route("/departement/<string:departement>/logos")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def api_get_local_logos_by_acronym(departement):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return core_get_logos(dept_id)
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/logos")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def api_get_local_logos_by_id(dept_id):
|
||||
return core_get_logos(dept_id)
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
|
||||
def core_get_logo(dept_id, logoname):
|
||||
def _core_get_logo(dept_id, logoname) -> Response:
|
||||
logo = find_logo(logoname=logoname, dept_id=dept_id)
|
||||
if logo is None:
|
||||
return json_error(404, message="logo not found")
|
||||
@ -105,11 +107,11 @@ def core_get_logo(dept_id, logoname):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def api_get_local_logo_dept_by_acronym(departement, logoname):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return core_get_logo(dept_id, logoname)
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def api_get_local_logo_dept_by_id(dept_id, logoname):
|
||||
return core_get_logo(dept_id, logoname)
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
@ -7,7 +7,10 @@
|
||||
"""
|
||||
ScoDoc 9 API : partitions
|
||||
"""
|
||||
from flask import g, jsonify, request
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
@ -29,6 +32,7 @@ from app.scodoc import sco_utils as scu
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_info(partition_id: int):
|
||||
"""Info sur une partition.
|
||||
|
||||
@ -53,7 +57,7 @@ def partition_info(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition = query.first_or_404()
|
||||
return jsonify(partition.to_dict(with_groups=True))
|
||||
return partition.to_dict(with_groups=True)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/partitions")
|
||||
@ -61,6 +65,7 @@ def partition_info(partition_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_partitions(formsemestre_id: int):
|
||||
"""Liste de toutes les partitions d'un formsemestre
|
||||
|
||||
@ -85,14 +90,12 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
partitions = sorted(formsemestre.partitions, key=lambda p: p.numero or 0)
|
||||
return jsonify(
|
||||
{
|
||||
partition.id: partition.to_dict(with_groups=True)
|
||||
partitions = sorted(formsemestre.partitions, key=attrgetter("numero"))
|
||||
return {
|
||||
str(partition.id): partition.to_dict(with_groups=True, str_keys=True)
|
||||
for partition in partitions
|
||||
if partition.partition_name is not None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/etudiants")
|
||||
@ -100,6 +103,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etud_in_group(group_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants dans un groupe
|
||||
@ -126,7 +130,7 @@ def etud_in_group(group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
return jsonify([etud.to_dict_short() for etud in group.etuds])
|
||||
return [etud.to_dict_short() for etud in group.etuds]
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/etudiants/query")
|
||||
@ -134,6 +138,7 @@ def etud_in_group(group_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etud_in_group_query(group_id: int):
|
||||
"""Étudiants du groupe, filtrés par état"""
|
||||
etat = request.args.get("etat")
|
||||
@ -154,7 +159,7 @@ def etud_in_group_query(group_id: int):
|
||||
|
||||
query = query.join(group_membership).filter_by(group_id=group_id)
|
||||
|
||||
return jsonify([etud.to_dict_short() for etud in query])
|
||||
return [etud.to_dict_short() for etud in query]
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/set_etudiant/<int:etudid>", methods=["POST"])
|
||||
@ -162,6 +167,7 @@ def etud_in_group_query(group_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def set_etud_group(etudid: int, group_id: int):
|
||||
"""Affecte l'étudiant au groupe indiqué"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
@ -180,7 +186,7 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
etudid, group_id, group.partition.to_dict()
|
||||
)
|
||||
|
||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||
return {"group_id": group_id, "etudid": etudid}
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/remove_etudiant/<int:etudid>", methods=["POST"])
|
||||
@ -190,6 +196,7 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@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)
|
||||
@ -213,7 +220,7 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
# Update parcours
|
||||
group.partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||
return {"group_id": group_id, "etudid": etudid}
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -225,6 +232,7 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def partition_remove_etud(partition_id: int, etudid: int):
|
||||
"""Enlève l'étudiant de tous les groupes de cette partition
|
||||
(NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
|
||||
@ -254,7 +262,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
||||
return jsonify({"partition_id": partition_id, "etudid": etudid})
|
||||
return {"partition_id": partition_id, "etudid": etudid}
|
||||
|
||||
|
||||
@bp.route("/partition/<int:partition_id>/group/create", methods=["POST"])
|
||||
@ -262,6 +270,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def group_create(partition_id: int):
|
||||
"""Création d'un groupe dans une partition
|
||||
|
||||
@ -292,7 +301,7 @@ def group_create(partition_id: int):
|
||||
log(f"created group {group}")
|
||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
||||
return jsonify(group.to_dict(with_partition=True))
|
||||
return group.to_dict(with_partition=True)
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/delete", methods=["POST"])
|
||||
@ -300,6 +309,7 @@ def group_create(partition_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def group_delete(group_id: int):
|
||||
"""Suppression d'un groupe"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
@ -318,7 +328,7 @@ def group_delete(group_id: int):
|
||||
db.session.commit()
|
||||
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||
return jsonify({"OK": True})
|
||||
return {"OK": True}
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/edit", methods=["POST"])
|
||||
@ -326,6 +336,7 @@ def group_delete(group_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def group_edit(group_id: int):
|
||||
"""Edit a group"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
@ -350,7 +361,7 @@ def group_edit(group_id: int):
|
||||
log(f"modified {group}")
|
||||
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
return jsonify(group.to_dict(with_partition=True))
|
||||
return group.to_dict(with_partition=True)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
|
||||
@ -360,6 +371,7 @@ def group_edit(group_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def partition_create(formsemestre_id: int):
|
||||
"""Création d'une partition dans un semestre
|
||||
|
||||
@ -412,7 +424,7 @@ def partition_create(formsemestre_id: int):
|
||||
log(f"created partition {partition}")
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||
return jsonify(partition.to_dict(with_groups=True))
|
||||
return partition.to_dict(with_groups=True)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/partitions/order", methods=["POST"])
|
||||
@ -422,6 +434,7 @@ def partition_create(formsemestre_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def formsemestre_order_partitions(formsemestre_id: int):
|
||||
"""Modifie l'ordre des partitions du formsemestre
|
||||
JSON args: [partition_id1, partition_id2, ...]
|
||||
@ -441,19 +454,17 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
||||
message="paramètre liste des partitions invalide",
|
||||
)
|
||||
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
||||
p = Partition.query.get_or_404(p_id)
|
||||
p.numero = numero
|
||||
db.session.add(p)
|
||||
partition = Partition.query.get_or_404(p_id)
|
||||
partition.numero = numero
|
||||
db.session.add(partition)
|
||||
db.session.commit()
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||
return jsonify(
|
||||
[
|
||||
return [
|
||||
partition.to_dict()
|
||||
for partition in formsemestre.partitions.order_by(Partition.numero)
|
||||
if partition.partition_name is not None
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/partition/<int:partition_id>/groups/order", methods=["POST"])
|
||||
@ -461,6 +472,7 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def partition_order_groups(partition_id: int):
|
||||
"""Modifie l'ordre des groupes de la partition
|
||||
JSON args: [group_id1, group_id2, ...]
|
||||
@ -487,7 +499,7 @@ def partition_order_groups(partition_id: int):
|
||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
||||
log(f"partition_order_groups: {partition} : {group_ids}")
|
||||
return jsonify(partition.to_dict(with_groups=True))
|
||||
return partition.to_dict(with_groups=True)
|
||||
|
||||
|
||||
@bp.route("/partition/<int:partition_id>/edit", methods=["POST"])
|
||||
@ -495,6 +507,7 @@ def partition_order_groups(partition_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def partition_edit(partition_id: int):
|
||||
"""Modification d'une partition dans un semestre
|
||||
|
||||
@ -556,7 +569,7 @@ def partition_edit(partition_id: int):
|
||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
||||
|
||||
return jsonify(partition.to_dict(with_groups=True))
|
||||
return partition.to_dict(with_groups=True)
|
||||
|
||||
|
||||
@bp.route("/partition/<int:partition_id>/delete", methods=["POST"])
|
||||
@ -564,6 +577,7 @@ def partition_edit(partition_id: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def partition_delete(partition_id: int):
|
||||
"""Suppression d'une partition (et de tous ses groupes).
|
||||
|
||||
@ -591,4 +605,4 @@ def partition_delete(partition_id: int):
|
||||
sco_cache.invalidate_formsemestre(formsemestre.id)
|
||||
if is_parcours:
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
return jsonify({"OK": True})
|
||||
return {"OK": True}
|
||||
|
@ -7,33 +7,34 @@
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formsemestres
|
||||
"""
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required
|
||||
# from flask import g, jsonify, request
|
||||
# from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models.formsemestre import NotesSemSet
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
# import app
|
||||
# from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
# from app.decorators import scodoc, permission_required
|
||||
# from app.scodoc.sco_utils import json_error
|
||||
# from app.models.formsemestre import NotesSemSet
|
||||
# from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/semset/set_periode/<int:semset_id>", methods=["POST"])
|
||||
@api_web_bp.route("/semset/set_periode/<int:semset_id>", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEditApo)
|
||||
# TODO à modifier pour utiliser @as_json
|
||||
def semset_set_periode(semset_id: int):
|
||||
"Change la période d'un semset"
|
||||
query = NotesSemSet.query.filter_by(semset_id=semset_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
semset: NotesSemSet = query.first_or_404()
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
try:
|
||||
periode = int(data)
|
||||
semset.set_periode(periode)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid periode value")
|
||||
return jsonify({"OK": True})
|
||||
# Impossible de changer la période à cause des archives
|
||||
# @bp.route("/semset/set_periode/<int:semset_id>", methods=["POST"])
|
||||
# @api_web_bp.route("/semset/set_periode/<int:semset_id>", methods=["POST"])
|
||||
# @login_required
|
||||
# @scodoc
|
||||
# @permission_required(Permission.ScoEditApo)
|
||||
# # TODO à modifier pour utiliser @as_json
|
||||
# def semset_set_periode(semset_id: int):
|
||||
# "Change la période d'un semset"
|
||||
# query = NotesSemSet.query.filter_by(semset_id=semset_id)
|
||||
# if g.scodoc_dept:
|
||||
# query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
# semset: NotesSemSet = query.first_or_404()
|
||||
# data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
# try:
|
||||
# periode = int(data)
|
||||
# semset.set_periode(periode)
|
||||
# except ValueError:
|
||||
# return json_error(API_CLIENT_ERROR, "invalid periode value")
|
||||
# return jsonify({"OK": True})
|
||||
|
@ -1,4 +1,4 @@
|
||||
from flask import jsonify
|
||||
from flask_json import as_json
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp
|
||||
from app.auth.logic import basic_auth, token_auth
|
||||
@ -6,12 +6,13 @@ from app.auth.logic import basic_auth, token_auth
|
||||
|
||||
@bp.route("/tokens", methods=["POST"])
|
||||
@basic_auth.login_required
|
||||
@as_json
|
||||
def get_token():
|
||||
"renvoie un jeton jwt pour l'utilisateur courant"
|
||||
token = basic_auth.current_user().get_token()
|
||||
log(f"API: giving token to {basic_auth.current_user()}")
|
||||
db.session.commit()
|
||||
return jsonify({"token": token})
|
||||
return {"token": token}
|
||||
|
||||
|
||||
@bp.route("/tokens", methods=["DELETE"])
|
||||
|
@ -9,7 +9,8 @@
|
||||
"""
|
||||
|
||||
|
||||
from flask import g, jsonify, request
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from app import db
|
||||
@ -29,6 +30,7 @@ from app.scodoc import sco_utils as scu
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoUsersView)
|
||||
@as_json
|
||||
def user_info(uid: int):
|
||||
"""
|
||||
Info sur un compte utilisateur scodoc
|
||||
@ -41,7 +43,7 @@ def user_info(uid: int):
|
||||
if (None not in allowed_depts) and (user.dept not in allowed_depts):
|
||||
return json_error(404, "user not found")
|
||||
|
||||
return jsonify(user.to_dict())
|
||||
return user.to_dict()
|
||||
|
||||
|
||||
@bp.route("/users/query")
|
||||
@ -49,6 +51,7 @@ def user_info(uid: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def users_info_query():
|
||||
"""Utilisateurs, filtrés par dept, active ou début nom
|
||||
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
|
||||
@ -79,7 +82,7 @@ def users_info_query():
|
||||
)
|
||||
|
||||
query = query.order_by(User.user_name)
|
||||
return jsonify([user.to_dict() for user in query])
|
||||
return [user.to_dict() for user in query]
|
||||
|
||||
|
||||
@bp.route("/user/create", methods=["POST"])
|
||||
@ -87,6 +90,7 @@ def users_info_query():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoUsersAdmin)
|
||||
@as_json
|
||||
def user_create():
|
||||
"""Création d'un utilisateur
|
||||
The request content type should be "application/json":
|
||||
@ -121,7 +125,7 @@ def user_create():
|
||||
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict())
|
||||
return user.to_dict()
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>/edit", methods=["POST"])
|
||||
@ -129,6 +133,7 @@ def user_create():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoUsersAdmin)
|
||||
@as_json
|
||||
def user_edit(uid: int):
|
||||
"""Modification d'un utilisateur
|
||||
Champs modifiables:
|
||||
@ -165,7 +170,7 @@ def user_edit(uid: int):
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict())
|
||||
return user.to_dict()
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>/password", methods=["POST"])
|
||||
@ -173,6 +178,7 @@ def user_edit(uid: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoUsersAdmin)
|
||||
@as_json
|
||||
def user_password(uid: int):
|
||||
"""Modification du mot de passe d'un utilisateur
|
||||
Champs modifiables:
|
||||
@ -194,7 +200,7 @@ def user_password(uid: int):
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict())
|
||||
return user.to_dict()
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>/role/<string:role_name>/add", methods=["POST"])
|
||||
@ -210,6 +216,7 @@ def user_password(uid: int):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
"""Add a role to the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
@ -222,7 +229,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
user.add_role(role, dept)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict())
|
||||
return user.to_dict()
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>/role/<string:role_name>/remove", methods=["POST"])
|
||||
@ -238,6 +245,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
"""Remove the role from the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
@ -256,7 +264,7 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
db.session.delete(user_role)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict())
|
||||
return user.to_dict()
|
||||
|
||||
|
||||
@bp.route("/permissions")
|
||||
@ -264,9 +272,10 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoUsersView)
|
||||
@as_json
|
||||
def list_permissions():
|
||||
"""Liste des noms de permissions définies"""
|
||||
return jsonify(list(Permission.permission_by_name.keys()))
|
||||
return list(Permission.permission_by_name.keys())
|
||||
|
||||
|
||||
@bp.route("/role/<string:role_name>")
|
||||
@ -274,9 +283,10 @@ def list_permissions():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoUsersView)
|
||||
@as_json
|
||||
def list_role(role_name: str):
|
||||
"""Un rôle"""
|
||||
return jsonify(Role.query.filter_by(name=role_name).first_or_404().to_dict())
|
||||
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
|
||||
|
||||
|
||||
@bp.route("/roles")
|
||||
@ -284,9 +294,10 @@ def list_role(role_name: str):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoUsersView)
|
||||
@as_json
|
||||
def list_roles():
|
||||
"""Tous les rôles définis"""
|
||||
return jsonify([role.to_dict() for role in Role.query])
|
||||
return [role.to_dict() for role in Role.query]
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -300,6 +311,7 @@ def list_roles():
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_add(role_name: str, perm_name: str):
|
||||
"""Add permission to role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
@ -309,7 +321,7 @@ def role_permission_add(role_name: str, perm_name: str):
|
||||
role.add_permission(permission)
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
return jsonify(role.to_dict())
|
||||
return role.to_dict()
|
||||
|
||||
|
||||
@bp.route(
|
||||
@ -323,6 +335,7 @@ def role_permission_add(role_name: str, perm_name: str):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_remove(role_name: str, perm_name: str):
|
||||
"""Remove permission from role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
@ -332,7 +345,7 @@ def role_permission_remove(role_name: str, perm_name: str):
|
||||
role.remove_permission(permission)
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
return jsonify(role.to_dict())
|
||||
return role.to_dict()
|
||||
|
||||
|
||||
@bp.route("/role/create/<string:role_name>", methods=["POST"])
|
||||
@ -340,6 +353,7 @@ def role_permission_remove(role_name: str, perm_name: str):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_create(role_name: str):
|
||||
"""Create a new role with permissions.
|
||||
{
|
||||
@ -359,7 +373,7 @@ def role_create(role_name: str):
|
||||
return json_error(404, "role_create: invalid permissions")
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
return jsonify(role.to_dict())
|
||||
return role.to_dict()
|
||||
|
||||
|
||||
@bp.route("/role/<string:role_name>/edit", methods=["POST"])
|
||||
@ -367,6 +381,7 @@ def role_create(role_name: str):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_edit(role_name: str):
|
||||
"""Edit a role. On peut spécifier un nom et/ou des permissions.
|
||||
{
|
||||
@ -390,7 +405,7 @@ def role_edit(role_name: str):
|
||||
role.name = role_name
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
return jsonify(role.to_dict())
|
||||
return role.to_dict()
|
||||
|
||||
|
||||
@bp.route("/role/<string:role_name>/delete", methods=["POST"])
|
||||
@ -398,9 +413,10 @@ def role_edit(role_name: str):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_delete(role_name: str):
|
||||
"""Delete a role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
return {"OK": True}
|
||||
|
@ -30,7 +30,7 @@ def after_cas_login():
|
||||
flask.session.get("CAS_USERNAME"),
|
||||
)
|
||||
if cas_id is not None:
|
||||
user: User = User.query.filter_by(cas_id=cas_id).first()
|
||||
user: User = User.query.filter_by(cas_id=str(cas_id)).first()
|
||||
if user and user.active:
|
||||
if user.cas_allow_login:
|
||||
current_app.logger.info(f"CAS: login {user.user_name}")
|
||||
|
@ -8,68 +8,69 @@
|
||||
Edition associations UE <-> Ref. Compétence
|
||||
"""
|
||||
from flask import g, url_for
|
||||
from app.models import ApcReferentielCompetences, Formation, UniteEns
|
||||
|
||||
from app.models import ApcReferentielCompetences, UniteEns
|
||||
from app.scodoc import codes_cursus
|
||||
|
||||
|
||||
def form_ue_choix_niveau(ue: UniteEns) -> str:
|
||||
"""Form. HTML pour associer une UE à un niveau de compétence.
|
||||
Le menu select lui meême est vide et rempli en JS par appel à get_ue_niveaux_options_html
|
||||
def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||
"""Form. HTML pour associer une UE à ses parcours.
|
||||
Le menu select lui même est vide et rempli en JS par appel à get_ue_niveaux_options_html
|
||||
"""
|
||||
if ue.type != codes_cursus.UE_STANDARD:
|
||||
return ""
|
||||
ref_comp = ue.formation.referentiel_competence
|
||||
if ref_comp is None:
|
||||
return f"""<div class="ue_choix_niveau">
|
||||
return f"""<div class="ue_advanced">
|
||||
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
|
||||
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
|
||||
}">associer un référentiel de compétence</a>
|
||||
</div>
|
||||
</div>"""
|
||||
# Les parcours:
|
||||
parcours_options = []
|
||||
for parcour in ref_comp.parcours:
|
||||
parcours_options.append(
|
||||
f"""<option value="{parcour.id}" {
|
||||
'selected' if ue.parcour == parcour else ''}
|
||||
>{parcour.libelle} ({parcour.code})
|
||||
</option>"""
|
||||
)
|
||||
|
||||
newline = "\n"
|
||||
return f"""
|
||||
<div class="ue_choix_niveau">
|
||||
<form class="form_ue_choix_niveau">
|
||||
<div class="cont_ue_choix_niveau">
|
||||
<div>
|
||||
<b>Parcours :</b>
|
||||
<select class="select_parcour"
|
||||
onchange="set_ue_parcour(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
<option value="" {
|
||||
'selected' if ue.parcour is None else ''
|
||||
}>Tous</option>
|
||||
{newline.join(parcours_options)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<b>Niveau de compétence :</b>
|
||||
<select class="select_niveau_ue"
|
||||
onchange="set_ue_niveau_competence(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
H = [
|
||||
"""
|
||||
<div class="ue_advanced">
|
||||
<h3>Parcours du BUT</h3>
|
||||
"""
|
||||
]
|
||||
# Choix des parcours
|
||||
ue_pids = [p.id for p in ue.parcours]
|
||||
H.append("""<form id="choix_parcours">""")
|
||||
|
||||
ects_differents = {
|
||||
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
||||
} != {None}
|
||||
for parcour in ref_comp.parcours:
|
||||
ects_parcour = ue.get_ects(parcour)
|
||||
ects_parcour_txt = (
|
||||
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
|
||||
)
|
||||
H.append(
|
||||
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
|
||||
{'checked' if parcour.id in ue_pids else ""}
|
||||
onclick="set_ue_parcour(this);"
|
||||
data-setter="{url_for("apiweb.set_ue_parcours",
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
|
||||
>{parcour.code}{ects_parcour_txt}</label>"""
|
||||
)
|
||||
H.append("""</form>""")
|
||||
#
|
||||
H.append(
|
||||
f"""
|
||||
<ul>
|
||||
<li>
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.ue_parcours_ects",
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
||||
}">définir des ECTS différents dans chaque parcours</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||
@ -85,9 +86,7 @@ def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||
return ""
|
||||
# Les niveaux:
|
||||
annee = ue.annee() # 1, 2, 3
|
||||
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(
|
||||
annee, parcour=ue.parcour
|
||||
)
|
||||
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee, ue.parcours)
|
||||
|
||||
# Les niveaux déjà associés à d'autres UE du même semestre
|
||||
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||
@ -101,7 +100,7 @@ def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||
options.append(
|
||||
f"""<option value="{n.id}" {
|
||||
'selected' if ue.niveau_competence == n else ''}
|
||||
>{n.annee} {n.competence.titre_long}
|
||||
>{n.annee} {n.competence.titre} / {n.competence.titre_long}
|
||||
niveau {n.ordre}</option>"""
|
||||
)
|
||||
options.append("""</optgroup>""")
|
||||
@ -116,7 +115,7 @@ def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||
options.append(
|
||||
f"""<option value="{n.id}" {'selected'
|
||||
if ue.niveau_competence == n else ''}
|
||||
{disabled}>{n.annee} {n.competence.titre_long}
|
||||
{disabled}>{n.annee} {n.competence.titre} / {n.competence.titre_long}
|
||||
niveau {n.ordre}</option>"""
|
||||
)
|
||||
options.append("""</optgroup>""")
|
||||
|
@ -285,9 +285,9 @@ class BulletinBUT:
|
||||
eval_notes[etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min()),
|
||||
"max": fmt_note(notes_ok.max()),
|
||||
"moy": fmt_note(notes_ok.mean()),
|
||||
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
||||
},
|
||||
"poids": poids,
|
||||
"url": url_for(
|
||||
@ -484,6 +484,7 @@ class BulletinBUT:
|
||||
d["etudid"] = etud.id
|
||||
d["etud"] = d["etudiant"]
|
||||
d["etud"]["nomprenom"] = etud.nomprenom
|
||||
d["etud"]["etat_civil"] = etud.etat_civil
|
||||
d.update(self.res.sem)
|
||||
etud_etat = self.res.get_etud_etat(etud.id)
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
|
@ -24,7 +24,6 @@ from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.models import formsemestre
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
@ -32,6 +31,7 @@ from app.models.but_refcomp import (
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
ApcReferentielCompetences,
|
||||
)
|
||||
from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
@ -109,7 +109,7 @@ class EtudCursusBUT:
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
annee, self.parcour
|
||||
annee, [self.parcour] if self.parcour else None
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
@ -170,6 +170,7 @@ class EtudCursusBUT:
|
||||
}
|
||||
}
|
||||
"""
|
||||
# XXX lent, provisoirement utilisé par TableJury.add_but_competences()
|
||||
return {
|
||||
competence.id: {
|
||||
annee: self.validation_par_competence_et_annee.get(
|
||||
@ -204,3 +205,211 @@ class EtudCursusBUT:
|
||||
validation_rcue.to_dict_codes() if validation_rcue else None
|
||||
)
|
||||
return d
|
||||
|
||||
|
||||
class FormSemestreCursusBUT:
|
||||
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
|
||||
Permet d'obtenir pour chacun liste des niveaux validés/à valider
|
||||
"""
|
||||
|
||||
def __init__(self, res: ResultatsSemestreBUT):
|
||||
"""res indique le formsemestre de référence,
|
||||
qui donne la liste des étudiants et le référentiel de compétence.
|
||||
"""
|
||||
self.res = res
|
||||
self.formsemestre = res.formsemestre
|
||||
if not res.formsemestre.formation.referentiel_competence:
|
||||
raise ScoNoReferentielCompetences(formation=res.formsemestre.formation)
|
||||
# Données cachées pour accélerer les accès:
|
||||
self.referentiel_competences_id: int = (
|
||||
self.res.formsemestre.formation.referentiel_competence_id
|
||||
)
|
||||
self.ue_ids: set[int] = set()
|
||||
"set of ue_ids known to belong to our cursus"
|
||||
self.parcours_by_id: dict[int, ApcParcours] = {}
|
||||
"cache des parcours"
|
||||
self.niveaux_by_parcour_by_annee: dict[int, dict[int, list[ApcNiveau]]] = {}
|
||||
"cache { parcour_id : { annee : [ parcour] } }"
|
||||
self.niveaux_by_id: dict[int, ApcNiveau] = {}
|
||||
"cache niveaux"
|
||||
|
||||
def get_niveaux_parcours_etud(self, etud: Identite) -> dict[int, list[ApcNiveau]]:
|
||||
"""Les niveaux compétences que doit valider cet étudiant.
|
||||
Le parcour considéré est celui de l'inscription dans le semestre courant.
|
||||
Si on est en début de cursus, on peut être en tronc commun sans avoir choisi
|
||||
de parcours. Dans ce cas, on n'aura que les compétences de tronc commun.
|
||||
Il faudra donc, avant de diplômer, s'assurer que les compétences du parcours
|
||||
du dernier semestre (S6) sont validées (avec parcour non NULL).
|
||||
"""
|
||||
parcour_id = self.res.etuds_parcour_id.get(etud.id)
|
||||
if parcour_id is None:
|
||||
parcour = None
|
||||
else:
|
||||
if parcour_id not in self.parcours_by_id:
|
||||
self.parcours_by_id[parcour_id] = ApcParcours.query.get(parcour_id)
|
||||
parcour = self.parcours_by_id[parcour_id]
|
||||
|
||||
return self.get_niveaux_parcours_by_annee(parcour)
|
||||
|
||||
def get_niveaux_parcours_by_annee(
|
||||
self, parcour: ApcParcours
|
||||
) -> dict[int, list[ApcNiveau]]:
|
||||
"""La liste des niveaux de compétences du parcours, par année BUT.
|
||||
{ 1 : [ niveau, ... ] }
|
||||
Si parcour est None, donne uniquement les niveaux tronc commun
|
||||
(cas utile par exemple en 1ere année, mais surtout pas pour donner un diplôme!)
|
||||
"""
|
||||
parcour_id = None if parcour is None else parcour.id
|
||||
if parcour_id in self.niveaux_by_parcour_by_annee:
|
||||
return self.niveaux_by_parcour_by_annee[parcour_id]
|
||||
|
||||
ref_comp: ApcReferentielCompetences = (
|
||||
self.res.formsemestre.formation.referentiel_competence
|
||||
)
|
||||
niveaux_by_annee = {}
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = ref_comp.get_niveaux_by_parcours(
|
||||
annee, [parcour] if parcour else None
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
niveaux_d[parcour.id] if parcour else []
|
||||
)
|
||||
self.niveaux_by_parcour_by_annee[parcour_id] = niveaux_by_annee
|
||||
self.niveaux_by_id.update(
|
||||
{niveau.id: niveau for niveau in niveaux_by_annee[annee]}
|
||||
)
|
||||
return niveaux_by_annee
|
||||
|
||||
def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
validation_par_competence_et_annee = {}
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# On s'assurer qu'elle concerne notre cursus !
|
||||
ue = validation_rcue.ue2
|
||||
if ue.id not in self.ue_ids:
|
||||
if (
|
||||
ue.formation.referentiel_competences_id
|
||||
== self.referentiel_competences_id
|
||||
):
|
||||
self.ue_ids = ue.id
|
||||
else:
|
||||
continue # skip this validation
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in validation_par_competence_et_annee:
|
||||
validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
return validation_par_competence_et_annee
|
||||
|
||||
def list_etud_inscriptions(self, etud: Identite):
|
||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
self.niveaux_by_annee = {}
|
||||
"{ annee : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
niveaux_d[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
self.niveaux.update(
|
||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
)
|
||||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
|
||||
self.competences = {
|
||||
competence.id: competence
|
||||
for competence in (
|
||||
self.parcour.query_competences()
|
||||
if self.parcour
|
||||
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
)
|
||||
}
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
|
||||
def formsemestre_warning_apc_setup(
|
||||
formsemestre: FormSemestre, res: ResultatsSemestreBUT
|
||||
) -> str:
|
||||
"""Vérifie que la formation est OK pour un BUT:
|
||||
- ref. compétence associé
|
||||
- tous les niveaux des parcours du semestre associés à des UEs du formsemestre
|
||||
- pas d'UE non associée à un niveau
|
||||
Renvoie fragment de HTML.
|
||||
"""
|
||||
if not formsemestre.formation.is_apc():
|
||||
return ""
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
return f"""<div class="formsemestre_status_warning">
|
||||
La <a class="stdlink" href="{
|
||||
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
|
||||
}">formation n'est pas associée à un référentiel de compétence.</a>
|
||||
</div>
|
||||
"""
|
||||
# Vérifie les niveaux de chaque parcours
|
||||
H = []
|
||||
for parcour in formsemestre.parcours or [None]:
|
||||
annee = (formsemestre.semestre_id + 1) // 2
|
||||
niveaux_ids = {
|
||||
niveau.id
|
||||
for niveau in ApcNiveau.niveaux_annee_de_parcours(
|
||||
parcour, annee, formsemestre.formation.referentiel_competence
|
||||
)
|
||||
}
|
||||
ues_parcour = formsemestre.formation.query_ues_parcour(parcour).filter(
|
||||
UniteEns.semestre_idx == formsemestre.semestre_id
|
||||
)
|
||||
ues_niveaux_ids = {
|
||||
ue.niveau_competence.id for ue in ues_parcour if ue.niveau_competence
|
||||
}
|
||||
if niveaux_ids != ues_niveaux_ids:
|
||||
H.append(
|
||||
f"""Parcours {parcour.code if parcour else "Tronc commun"} :
|
||||
{len(ues_niveaux_ids)} UE avec niveaux
|
||||
mais {len(niveaux_ids)} niveaux à valider !
|
||||
"""
|
||||
)
|
||||
if not H:
|
||||
return ""
|
||||
return f"""<div class="formsemestre_status_warning">
|
||||
Problème dans la configuration de la formation:
|
||||
<ul>
|
||||
<li>{ '</li><li>'.join(H) }</li>
|
||||
</ul>
|
||||
<p class="help">Vérifiez les parcours cochés pour ce semestre,
|
||||
et les associations entre UE et niveaux <a class="stdlink" href="{
|
||||
url_for("notes.parcour_formation", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
|
||||
}">dans la formation.</a>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
@ -38,8 +38,8 @@ class RefCompLoadForm(FlaskForm):
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
def validate(self):
|
||||
if not super().validate():
|
||||
def validate(self, extra_validators=None):
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
if (self.referentiel_standard.data == "0") == (not self.upload.data):
|
||||
self.referentiel_standard.errors.append(
|
||||
|
@ -324,7 +324,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
parcours,
|
||||
niveaux_by_parcours,
|
||||
) = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
self.annee_but, self.parcour
|
||||
self.annee_but, [self.parcour] if self.parcour else None
|
||||
)
|
||||
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
||||
niveaux_by_parcours[self.parcour.id] if self.parcour else []
|
||||
@ -421,7 +421,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
+ '</div><div class="warning">'.join(messages)
|
||||
+ "</div>"
|
||||
)
|
||||
#
|
||||
|
||||
# WIP TODO XXX def get_moyenne_annuelle(self)
|
||||
|
||||
def infos(self) -> str:
|
||||
"""informations, for debugging purpose."""
|
||||
@ -521,7 +522,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
ramène [ listes des UE du semestre impair, liste des UE du semestre pair ].
|
||||
"""
|
||||
ues_sems = []
|
||||
for (formsemestre, res) in (
|
||||
for formsemestre, res in (
|
||||
(self.formsemestre_impair, self.res_impair),
|
||||
(self.formsemestre_pair, self.res_pair),
|
||||
):
|
||||
@ -1003,7 +1004,7 @@ def list_ue_parcour_etud(
|
||||
parcour = ApcParcours.query.get(res.etuds_parcour_id[etud.id])
|
||||
ues = (
|
||||
formsemestre.formation.query_ues_parcour(parcour)
|
||||
.filter_by(semestre_idx=formsemestre.semestre_id)
|
||||
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
|
@ -106,6 +106,8 @@ class BonusSport:
|
||||
if formsemestre.formation.is_apc():
|
||||
# BUT
|
||||
nb_ues_no_bonus = sem_modimpl_moys.shape[2]
|
||||
if nb_ues_no_bonus == 0: # aucune UE...
|
||||
return # no bonus at all
|
||||
# Duplique les inscriptions sur les UEs non bonus:
|
||||
modimpl_inscr_spo_stacked = np.stack(
|
||||
[modimpl_inscr_spo] * nb_ues_no_bonus, axis=2
|
||||
@ -228,14 +230,14 @@ class BonusSportAdditif(BonusSport):
|
||||
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
|
||||
if self.formsemestre.formation.is_apc():
|
||||
# Bonus sur les UE et None sur moyenne générale
|
||||
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
||||
ues_idx = [ue.id for ue in self.formsemestre.get_ues(with_sport=False)]
|
||||
self.bonus_ues = pd.DataFrame(
|
||||
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
|
||||
)
|
||||
elif self.classic_use_bonus_ues:
|
||||
# Formations classiques apppliquant le bonus sur les UEs
|
||||
# ici bonus_moy_arr = ndarray 1d nb_etuds
|
||||
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
||||
ues_idx = [ue.id for ue in self.formsemestre.get_ues(with_sport=False)]
|
||||
self.bonus_ues = pd.DataFrame(
|
||||
np.stack([bonus_moy_arr] * len(ues_idx)).T,
|
||||
index=self.etuds_idx,
|
||||
@ -420,7 +422,7 @@ class BonusAmiens(BonusSportAdditif):
|
||||
|
||||
# # Bonus moyenne générale et sur les UE
|
||||
# self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
|
||||
# ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
||||
# ues_idx = [ue.id for ue in self.formsemestre.get_ues(with_sport=False)]
|
||||
# nb_ues_no_bonus = len(ues_idx)
|
||||
# self.bonus_ues = pd.DataFrame(
|
||||
# np.stack([bonus] * nb_ues_no_bonus, axis=1),
|
||||
@ -597,7 +599,7 @@ class BonusCachan1(BonusSportAdditif):
|
||||
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
|
||||
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
|
||||
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
|
||||
ues = self.formsemestre.query_ues(with_sport=False).all()
|
||||
ues = self.formsemestre.get_ues(with_sport=False)
|
||||
ues_idx = [ue.id for ue in ues]
|
||||
|
||||
if self.formsemestre.formation.is_apc(): # --- BUT
|
||||
@ -687,7 +689,7 @@ class BonusCalais(BonusSportAdditif):
|
||||
else:
|
||||
self.classic_use_bonus_ues = True # pour les LP
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
ues = self.formsemestre.query_ues(with_sport=False).all()
|
||||
ues = self.formsemestre.get_ues(with_sport=False)
|
||||
ues_sans_bs = [
|
||||
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
||||
] # les 2 derniers cars forcés en majus
|
||||
@ -788,7 +790,7 @@ class BonusIUTRennes1(BonusSportAdditif):
|
||||
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
|
||||
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
|
||||
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
|
||||
nb_ues = self.formsemestre.query_ues(with_sport=False).count()
|
||||
nb_ues = len(self.formsemestre.get_ues(with_sport=False))
|
||||
|
||||
bonus_moy_arr = np.where(
|
||||
note_bonus_max > self.seuil_moy_gen,
|
||||
|
@ -4,6 +4,7 @@
|
||||
"""Matrices d'inscription aux modules d'un semestre
|
||||
"""
|
||||
import pandas as pd
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
|
||||
@ -12,6 +13,13 @@ from app import db
|
||||
# sur test debug 116 etuds, 18 modules, on est autour de 250ms.
|
||||
# On a testé trois approches, ci-dessous (et retenu la 1ere)
|
||||
#
|
||||
_load_modimpl_inscr_q = sa.text(
|
||||
"""SELECT etudid, 1 AS ":moduleimpl_id"
|
||||
FROM notes_moduleimpl_inscription
|
||||
WHERE moduleimpl_id=:moduleimpl_id"""
|
||||
)
|
||||
|
||||
|
||||
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
||||
"""Charge la matrice des inscriptions aux modules du semestre
|
||||
rows: etudid (inscrits au semestre, avec DEM et DEF)
|
||||
@ -22,12 +30,11 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
||||
moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted]
|
||||
etudids = [inscr.etudid for inscr in formsemestre.inscriptions]
|
||||
df = pd.DataFrame(index=etudids, dtype=int)
|
||||
with db.engine.begin() as connection:
|
||||
for moduleimpl_id in moduleimpl_ids:
|
||||
ins_df = pd.read_sql_query(
|
||||
"""SELECT etudid, 1 AS "%(moduleimpl_id)s"
|
||||
FROM notes_moduleimpl_inscription
|
||||
WHERE moduleimpl_id=%(moduleimpl_id)s""",
|
||||
db.engine,
|
||||
_load_modimpl_inscr_q,
|
||||
connection,
|
||||
params={"moduleimpl_id": moduleimpl_id},
|
||||
index_col="etudid",
|
||||
dtype=int,
|
||||
|
@ -7,6 +7,7 @@
|
||||
"""Stockage des décisions de jury
|
||||
"""
|
||||
import pandas as pd
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||
@ -132,7 +133,8 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
# Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne
|
||||
# and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' )
|
||||
|
||||
query = """
|
||||
query = sa.text(
|
||||
"""
|
||||
SELECT DISTINCT SFV.*, ue.ue_code
|
||||
FROM
|
||||
notes_ue ue,
|
||||
@ -144,21 +146,22 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
|
||||
WHERE ue.formation_id = nf.id
|
||||
and nf.formation_code = nf2.formation_code
|
||||
and nf2.id=%(formation_id)s
|
||||
and nf2.id=:formation_id
|
||||
and ins.etudid = SFV.etudid
|
||||
and ins.formsemestre_id = %(formsemestre_id)s
|
||||
and ins.formsemestre_id = :formsemestre_id
|
||||
|
||||
and SFV.ue_id = ue.id
|
||||
and SFV.code = 'ADM'
|
||||
|
||||
and ( (sem.id = SFV.formsemestre_id
|
||||
and sem.date_debut < %(date_debut)s
|
||||
and sem.semestre_id = %(semestre_id)s )
|
||||
and sem.date_debut < :date_debut
|
||||
and sem.semestre_id = :semestre_id )
|
||||
or (
|
||||
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
|
||||
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
|
||||
AND (SFV.semestre_id is NULL OR SFV.semestre_id=:semestre_id)
|
||||
) )
|
||||
"""
|
||||
)
|
||||
params = {
|
||||
"formation_id": formsemestre.formation.id,
|
||||
"formsemestre_id": formsemestre.id,
|
||||
@ -166,5 +169,6 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
"date_debut": formsemestre.date_debut,
|
||||
}
|
||||
|
||||
df = pd.read_sql_query(query, db.engine, params=params, index_col="etudid")
|
||||
with db.engine.begin() as connection:
|
||||
df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
|
||||
return df
|
||||
|
@ -38,6 +38,7 @@ from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import sqlalchemy as sa
|
||||
|
||||
import app
|
||||
from app import db
|
||||
@ -192,18 +193,23 @@ class ModuleImplResults:
|
||||
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
|
||||
self.evals_notes = evals_notes
|
||||
|
||||
_load_evaluation_notes_q = sa.text(
|
||||
"""SELECT n.etudid, n.value AS ":evaluation_id"
|
||||
FROM notes_notes n, notes_moduleimpl_inscription i
|
||||
WHERE evaluation_id=:evaluation_id
|
||||
AND n.etudid = i.etudid
|
||||
AND i.moduleimpl_id = :moduleimpl_id
|
||||
"""
|
||||
)
|
||||
|
||||
def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame:
|
||||
"""Charge les notes de l'évaluation
|
||||
Resultat: dataframe, index: etudid ayant une note, valeur: note brute.
|
||||
"""
|
||||
with db.engine.begin() as connection:
|
||||
eval_df = pd.read_sql_query(
|
||||
"""SELECT n.etudid, n.value AS "%(evaluation_id)s"
|
||||
FROM notes_notes n, notes_moduleimpl_inscription i
|
||||
WHERE evaluation_id=%(evaluation_id)s
|
||||
AND n.etudid = i.etudid
|
||||
AND i.moduleimpl_id = %(moduleimpl_id)s
|
||||
""",
|
||||
db.engine,
|
||||
self._load_evaluation_notes_q,
|
||||
connection,
|
||||
params={
|
||||
"evaluation_id": evaluation.id,
|
||||
"moduleimpl_id": evaluation.moduleimpl.id,
|
||||
@ -409,7 +415,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||
"""
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
|
||||
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
||||
ue_ids = [ue.id for ue in ues]
|
||||
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
||||
|
@ -121,7 +121,7 @@ def df_load_modimpl_coefs(
|
||||
DataFrame rows = UEs (sans bonus), columns = modimpl, value = coef.
|
||||
"""
|
||||
if ues is None:
|
||||
ues = formsemestre.query_ues().all()
|
||||
ues = formsemestre.get_ues()
|
||||
ue_ids = [x.id for x in ues]
|
||||
if modimpls is None:
|
||||
modimpls = formsemestre.modimpls_sorted
|
||||
|
@ -16,6 +16,7 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.but_refcomp import ApcParcours, ApcNiveau
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.scodoc import sco_preferences
|
||||
@ -41,6 +42,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"""ndarray (etuds x modimpl x ue)"""
|
||||
self.etuds_parcour_id = None
|
||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||
self.ues_ids_by_parcour: dict[set[int]] = {}
|
||||
"""{ parcour_id : set }, ue_id de chaque parcours"""
|
||||
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
@ -227,7 +230,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
}
|
||||
self.etuds_parcour_id = etuds_parcour_id
|
||||
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||
|
||||
ue_ids_set = set(ue_ids)
|
||||
if self.formsemestre.formation.referentiel_competence is None:
|
||||
return pd.DataFrame(
|
||||
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||
@ -237,16 +240,20 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
np.nan, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||
)
|
||||
# Construit pour chaque parcours du référentiel l'ensemble de ses UE
|
||||
# (considère aussi le cas des semestres sans parcours: None)
|
||||
# - considère aussi le cas des semestres sans parcours (clé parcour None)
|
||||
# - retire les UEs qui ont un parcours mais qui ne sont pas dans l'un des
|
||||
# parcours du semestre
|
||||
|
||||
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
||||
for (
|
||||
parcour
|
||||
) in self.formsemestre.formation.referentiel_competence.parcours.all() + [None]:
|
||||
ue_by_parcours[None if parcour is None else parcour.id] = {
|
||||
ue.id: 1.0
|
||||
for ue in self.formsemestre.formation.query_ues_parcour(
|
||||
parcour
|
||||
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
||||
for ue in self.formsemestre.formation.query_ues_parcour(parcour).filter(
|
||||
UniteEns.semestre_idx == self.formsemestre.semestre_id
|
||||
)
|
||||
if ue.id in ue_ids_set
|
||||
}
|
||||
#
|
||||
for etudid in etuds_parcour_id:
|
||||
@ -259,10 +266,46 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
def etud_ues_ids(self, etudid: int) -> list[int]:
|
||||
"""Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus).
|
||||
(surchargée ici pour prendre en compte les parcours)
|
||||
Ne prend pas en compte les éventuelles DispenseUE (pour le moment ?)
|
||||
"""
|
||||
s = self.ues_inscr_parcours_df.loc[etudid]
|
||||
return s.index[s.notna()]
|
||||
|
||||
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
|
||||
"""Ensemble les id des UEs que l'étudiant doit valider dans ce semestre compte tenu
|
||||
du parcours dans lequel il est inscrit.
|
||||
Se base sur le parcours dans ce semestre, et le référentiel de compétences.
|
||||
Note: il n'est pas nécessairement inscrit à toutes ces UEs.
|
||||
Ensemble vide si pas de référentiel.
|
||||
Si l'étudiant n'est pas inscrit dans un parcours, toutes les UEs du semestre.
|
||||
La requête est longue, les ue_ids par parcour sont donc cachés.
|
||||
"""
|
||||
parcour_id = self.etuds_parcour_id[etudid]
|
||||
if parcour_id in self.ues_ids_by_parcour: # cache
|
||||
return self.ues_ids_by_parcour[parcour_id]
|
||||
# Hors cache:
|
||||
ref_comp = self.formsemestre.formation.referentiel_competence
|
||||
if ref_comp is None:
|
||||
return set()
|
||||
if parcour_id is None:
|
||||
ues_ids = {ue.id for ue in self.ues}
|
||||
else:
|
||||
parcour: ApcParcours = ApcParcours.query.get(parcour_id)
|
||||
annee = (self.formsemestre.semestre_id + 1) // 2
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
||||
# Les UEs du formsemestre associées à ces niveaux:
|
||||
ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour)
|
||||
ues_ids = set()
|
||||
for niveau in niveaux:
|
||||
ue = ues_parcour.filter(UniteEns.niveau_competence == niveau).first()
|
||||
if ue:
|
||||
ues_ids.add(ue.id)
|
||||
|
||||
# memoize
|
||||
self.ues_ids_by_parcour[parcour_id] = ues_ids
|
||||
|
||||
return ues_ids
|
||||
|
||||
def etud_has_decision(self, etudid):
|
||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||
prend aussi en compte les autorisations de passage.
|
||||
|
@ -10,6 +10,8 @@
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Generator
|
||||
from functools import cached_property
|
||||
from operator import attrgetter
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
@ -87,6 +89,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
self.autorisations_inscription = None
|
||||
self.moyennes_matieres = {}
|
||||
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
|
||||
# self._ues_by_id_cache: dict[int, UniteEns] = {} # per-instance cache
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(formsemestre='{self.formsemestre}')>"
|
||||
@ -124,6 +127,13 @@ class ResultatsSemestre(ResultatsCache):
|
||||
# car tous les étudiants sont inscrits à toutes les UE
|
||||
return [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||
|
||||
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
|
||||
"""Ensemble des UEs que l'étudiant "doit" valider.
|
||||
En formations classiques, c'est la même chose (en set) que etud_ues_ids.
|
||||
Surchargée en BUT pour donner les UEs du parcours de l'étudiant.
|
||||
"""
|
||||
return {ue.id for ue in self.ues if ue.type != UE_SPORT}
|
||||
|
||||
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
||||
"""Liste des UE auxquelles l'étudiant est inscrit
|
||||
(sans bonus, en BUT prend en compte le parcours de l'étudiant)."""
|
||||
@ -154,7 +164,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
(indices des DataFrames).
|
||||
Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs.
|
||||
"""
|
||||
return self.formsemestre.query_ues(with_sport=True).all()
|
||||
return self.formsemestre.get_ues(with_sport=True)
|
||||
|
||||
@cached_property
|
||||
def ressources(self):
|
||||
@ -225,7 +235,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
if self.modimpl_inscr_df[modimpl.id][etudid]
|
||||
}
|
||||
ues = sorted(list(ues), key=lambda x: x.numero or 0)
|
||||
ues = sorted(list(ues), key=attrgetter("numero"))
|
||||
return ues
|
||||
|
||||
def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
|
||||
@ -275,7 +285,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
# Quand il y a une capitalisation, vérifie toutes les UEs
|
||||
sum_notes_ue = 0.0
|
||||
sum_coefs_ue = 0.0
|
||||
for ue in self.formsemestre.query_ues():
|
||||
for ue in self.formsemestre.get_ues():
|
||||
ue_cap = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_cap is None:
|
||||
continue
|
||||
@ -341,7 +351,9 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"""L'état de l'UE pour cet étudiant.
|
||||
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
||||
"""
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue: UniteEns = UniteEns.query.get(ue_id)
|
||||
ue_dict = ue.to_dict()
|
||||
|
||||
if ue.type == UE_SPORT:
|
||||
return {
|
||||
"is_capitalized": False,
|
||||
@ -351,7 +363,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"cur_moy_ue": 0.0,
|
||||
"moy": 0.0,
|
||||
"event_date": None,
|
||||
"ue": ue.to_dict(),
|
||||
"ue": ue_dict,
|
||||
"formsemestre_id": None,
|
||||
"capitalized_ue_id": None,
|
||||
"ects_pot": 0.0,
|
||||
@ -420,7 +432,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"cur_moy_ue": cur_moy_ue,
|
||||
"moy": moy_ue,
|
||||
"event_date": ue_cap["event_date"] if is_capitalized else None,
|
||||
"ue": ue.to_dict(),
|
||||
"ue": ue_dict,
|
||||
"formsemestre_id": ue_cap["formsemestre_id"] if is_capitalized else None,
|
||||
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ from app.models import Identite, FormSemestre, ModuleImpl, ScolarAutorisationIns
|
||||
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||||
class NotesTableCompat(ResultatsSemestre):
|
||||
"""Implementation partielle de NotesTable
|
||||
@ -108,7 +109,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
Si filter_sport, retire les UE de type SPORT.
|
||||
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
||||
"""
|
||||
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
|
||||
ues = self.formsemestre.get_ues(with_sport=not filter_sport)
|
||||
ues_dict = []
|
||||
for ue in ues:
|
||||
d = ue.to_dict()
|
||||
@ -178,7 +179,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
self.etud_moy_gen_ranks,
|
||||
self.etud_moy_gen_ranks_int,
|
||||
) = moy_sem.comp_ranks_series(etud_moy_gen_dem_zero)
|
||||
ues = self.formsemestre.query_ues()
|
||||
ues = self.formsemestre.get_ues()
|
||||
for ue in ues:
|
||||
moy_ue = self.etud_moy_ue[ue.id]
|
||||
self.ue_rangs[ue.id] = (
|
||||
@ -260,22 +261,27 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
Return: True|False, message explicatif
|
||||
"""
|
||||
ue_status_list = []
|
||||
for ue in self.formsemestre.query_ues():
|
||||
for ue in self.formsemestre.get_ues():
|
||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_status:
|
||||
ue_status_list.append(ue_status)
|
||||
return self.parcours.check_barre_ues(ue_status_list)
|
||||
|
||||
def all_etuds_have_sem_decisions(self):
|
||||
"""True si tous les étudiants du semestre ont une décision de jury.
|
||||
Ne regarde pas les décisions d'UE.
|
||||
def etudids_without_decisions(self) -> list[int]:
|
||||
"""Liste des id d'étudiants du semestre non démissionnaires
|
||||
n'ayant pas de décision de jury.
|
||||
- En classic: ne regarde pas que la décision de semestre (pas les décisions d'UE).
|
||||
- en BUT: utilise etud_has_decision
|
||||
"""
|
||||
for ins in self.formsemestre.inscriptions:
|
||||
if ins.etat != scu.INSCRIT:
|
||||
continue # skip démissionnaires
|
||||
if self.get_etud_decision_sem(ins.etudid) is None:
|
||||
return False
|
||||
return True
|
||||
check_func = (
|
||||
self.etud_has_decision if self.is_apc else self.get_etud_decision_sem
|
||||
)
|
||||
etudids = [
|
||||
ins.etudid
|
||||
for ins in self.formsemestre.inscriptions
|
||||
if (ins.etat == scu.INSCRIT) and (not check_func(ins.etudid))
|
||||
]
|
||||
return etudids
|
||||
|
||||
def etud_has_decision(self, etudid):
|
||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||
@ -316,7 +322,8 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||
"""Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
|
||||
Si état défaillant, force le code a DEF
|
||||
Si état défaillant, force le code a DEF.
|
||||
Toujours None en BUT.
|
||||
"""
|
||||
if self.get_etud_etat(etudid) == DEF:
|
||||
return {
|
||||
@ -477,7 +484,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
"""
|
||||
table_moyennes = []
|
||||
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
||||
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
||||
ues = self.formsemestre.get_ues(with_sport=True) # avec bonus
|
||||
for etudid in etuds_inscriptions:
|
||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||
if moy_gen is False:
|
||||
|
@ -318,7 +318,7 @@ class OffreCreationForm(FlaskForm):
|
||||
duree = _build_string_field("Durée (*)")
|
||||
depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int)
|
||||
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||
correspondant = SelectField("Correspondant à contacté", validators=[Optional()])
|
||||
correspondant = SelectField("Correspondant à contacter", validators=[Optional()])
|
||||
fichier = FileField(
|
||||
"Fichier",
|
||||
validators=[
|
||||
@ -373,7 +373,7 @@ class OffreModificationForm(FlaskForm):
|
||||
duree = _build_string_field("Durée (*)")
|
||||
depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int)
|
||||
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||
correspondant = SelectField("Correspondant à contacté", validators=[Optional()])
|
||||
correspondant = SelectField("Correspondant à contacter", validators=[Optional()])
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import os
|
||||
from config import Config
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
from flask import render_template, redirect, url_for, request, flash, send_file, abort
|
||||
from flask.json import jsonify
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import text, sql
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.decorators import permission_required
|
||||
|
||||
@ -58,8 +59,7 @@ from app.scodoc import sco_etud, sco_excel
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
from app import db
|
||||
from sqlalchemy import text, sql
|
||||
from werkzeug.utils import secure_filename
|
||||
from config import Config
|
||||
|
||||
|
||||
@bp.route("/", methods=["GET", "POST"])
|
||||
@ -1698,6 +1698,7 @@ def envoyer_offre(entreprise_id, offre_id):
|
||||
|
||||
@bp.route("/etudiants")
|
||||
@permission_required(Permission.RelationsEntreprisesChange)
|
||||
@as_json
|
||||
def json_etudiants():
|
||||
"""
|
||||
Permet de récuperer un JSON avec tous les étudiants
|
||||
@ -1723,7 +1724,7 @@ def json_etudiants():
|
||||
"info": f"Département {are.get_dept_acronym_by_id(etudiant.dept_id)}",
|
||||
}
|
||||
list.append(content)
|
||||
return jsonify(results=list)
|
||||
return list
|
||||
|
||||
|
||||
@bp.route("/responsables")
|
||||
@ -1749,7 +1750,7 @@ def json_responsables():
|
||||
value = f"{responsable.get_nomplogin()}"
|
||||
content = {"id": f"{responsable.id}", "value": value}
|
||||
list.append(content)
|
||||
return jsonify(results=list)
|
||||
return list
|
||||
|
||||
|
||||
@bp.route("/export_donnees")
|
||||
@ -1843,7 +1844,7 @@ def import_donnees():
|
||||
db.session.add(correspondant)
|
||||
correspondants.append(correspondant)
|
||||
db.session.commit()
|
||||
flash(f"Importation réussie")
|
||||
flash("Importation réussie")
|
||||
return render_template(
|
||||
"entreprises/import_donnees.j2",
|
||||
title="Importation données",
|
||||
|
1
app/forms/formation/__init__.py
Normal file
1
app/forms/formation/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# empty but required for pylint
|
35
app/forms/formation/ue_parcours_ects.py
Normal file
35
app/forms/formation/ue_parcours_ects.py
Normal file
@ -0,0 +1,35 @@
|
||||
from flask import g, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import FieldList, Form, DecimalField, validators
|
||||
|
||||
from app.models import ApcParcours, ApcReferentielCompetences, UniteEns
|
||||
|
||||
|
||||
class _UEParcoursECTSForm(FlaskForm):
|
||||
"Formulaire association ECTS par parcours à une UE"
|
||||
# construit dynamiquement ci-dessous
|
||||
|
||||
|
||||
def UEParcoursECTSForm(ue: UniteEns) -> FlaskForm:
|
||||
"Génère formulaire association ECTS par parcours à une UE"
|
||||
|
||||
class F(_UEParcoursECTSForm):
|
||||
pass
|
||||
|
||||
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
|
||||
# Initialise un champs de saisie par parcours
|
||||
for parcour in parcours:
|
||||
ects = ue.get_ects(parcour, only_parcours=True)
|
||||
setattr(
|
||||
F,
|
||||
f"ects_parcour_{parcour.id}",
|
||||
DecimalField(
|
||||
f"Parcours {parcour.code}",
|
||||
validators=[
|
||||
validators.Optional(),
|
||||
validators.NumberRange(min=0, max=30),
|
||||
],
|
||||
default=ects,
|
||||
),
|
||||
)
|
||||
return F()
|
@ -29,14 +29,14 @@ Formulaire changement formation
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import RadioField, SubmitField, validators
|
||||
from wtforms import RadioField, SubmitField
|
||||
|
||||
from app.models import Formation
|
||||
|
||||
|
||||
class FormSemestreChangeFormationForm(FlaskForm):
|
||||
"Formulaire changement formation d'un formsemestre"
|
||||
# consrtuit dynamiquement ci-dessous
|
||||
# construit dynamiquement ci-dessous
|
||||
|
||||
|
||||
def gen_formsemestre_change_formation_form(
|
||||
|
@ -21,8 +21,6 @@ convention = {
|
||||
|
||||
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
||||
|
||||
from app.models.raw_sql_init import create_database_functions
|
||||
|
||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||
from app.models.departements import Departement
|
||||
from app.models.etudiants import (
|
||||
|
@ -6,8 +6,10 @@
|
||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
"""
|
||||
from datetime import datetime
|
||||
import functools
|
||||
from operator import attrgetter
|
||||
|
||||
import flask_sqlalchemy
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm import class_mapper
|
||||
import sqlalchemy
|
||||
|
||||
@ -84,6 +86,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
backref="referentiel",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="ApcParcours.numero, ApcParcours.code",
|
||||
)
|
||||
formations = db.relationship(
|
||||
"Formation",
|
||||
@ -129,11 +132,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
}
|
||||
|
||||
def get_niveaux_by_parcours(
|
||||
self, annee: int, parcour: "ApcParcours" = None
|
||||
self, annee: int, parcours: list["ApcParcours"] = None
|
||||
) -> tuple[list["ApcParcours"], dict]:
|
||||
"""
|
||||
Construit la liste des niveaux de compétences pour chaque parcours
|
||||
de ce référentiel, ou seulement pour le parcours donné.
|
||||
de ce référentiel, ou seulement pour les parcours donnés.
|
||||
|
||||
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
|
||||
|
||||
@ -150,10 +153,8 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
)
|
||||
"""
|
||||
parcours_ref = self.parcours.order_by(ApcParcours.numero).all()
|
||||
if parcour is None:
|
||||
if parcours is None:
|
||||
parcours = parcours_ref
|
||||
else:
|
||||
parcours = [parcour]
|
||||
niveaux_by_parcours = {
|
||||
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
||||
for parcour in parcours_ref
|
||||
@ -205,9 +206,27 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
for competence in parcours[0].query_competences()
|
||||
if competence.id in ids
|
||||
],
|
||||
key=lambda c: c.numero or 0,
|
||||
key=attrgetter("numero"),
|
||||
)
|
||||
|
||||
def table_niveaux_parcours(self) -> dict:
|
||||
"""Une table avec les parcours:années BUT et les niveaux
|
||||
{ parcour_id : { 1 : { competence_id : ordre }}}
|
||||
"""
|
||||
parcours_info = {}
|
||||
for parcour in self.parcours:
|
||||
descr_parcour = {}
|
||||
parcours_info[parcour.id] = descr_parcour
|
||||
for annee in (1, 2, 3):
|
||||
descr_parcour[annee] = {
|
||||
niveau.competence.id: niveau.ordre
|
||||
for niveau in ApcNiveau.niveaux_annee_de_parcours(
|
||||
parcour, annee, self
|
||||
)
|
||||
}
|
||||
|
||||
return parcours_info
|
||||
|
||||
|
||||
class ApcCompetence(db.Model, XMLModel):
|
||||
"Compétence"
|
||||
@ -223,7 +242,7 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
titre = db.Column(db.Text(), nullable=False, index=True)
|
||||
titre_long = db.Column(db.Text())
|
||||
couleur = db.Column(db.Text())
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
_xml_attribs = { # xml_attrib : attribute
|
||||
"id": "id_orebut",
|
||||
"nom_court": "titre", # was name
|
||||
@ -289,6 +308,7 @@ class ApcSituationPro(db.Model, XMLModel):
|
||||
nullable=False,
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
|
||||
# aucun attribut (le text devient le libellé)
|
||||
def to_dict(self):
|
||||
return {"libelle": self.libelle}
|
||||
@ -358,15 +378,39 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
"competence": self.competence.to_dict_bul(),
|
||||
}
|
||||
|
||||
@functools.cached_property
|
||||
def parcours(self) -> list["ApcParcours"]:
|
||||
"""Les parcours passant par ce niveau.
|
||||
Les associations Parcours/Niveaux/compétences ne sont jamais
|
||||
changées par ScoDoc, la valeur est donc cachée.
|
||||
"""
|
||||
annee = int(self.annee[-1])
|
||||
return (
|
||||
ApcParcours.query.join(ApcAnneeParcours)
|
||||
.filter_by(ordre=annee)
|
||||
.join(ApcParcoursNiveauCompetence, ApcCompetence, ApcNiveau)
|
||||
.filter_by(id=self.id)
|
||||
.order_by(ApcParcours.numero, ApcParcours.code)
|
||||
.all()
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def is_tronc_commun(self) -> bool:
|
||||
"""Vrai si ce niveau fait partie du Tronc Commun"""
|
||||
return len(self.parcours) == self.competence.referentiel.parcours.count()
|
||||
|
||||
@classmethod
|
||||
def niveaux_annee_de_parcours(
|
||||
cls,
|
||||
parcour: "ApcParcours",
|
||||
annee: int,
|
||||
referentiel_competence: ApcReferentielCompetences = None,
|
||||
competence: ApcCompetence = None,
|
||||
) -> list["ApcNiveau"]:
|
||||
"""Les niveaux de l'année du parcours
|
||||
Si le parcour est None, tous les niveaux de l'année
|
||||
(dans ce cas, spécifier referentiel_competence)
|
||||
Si competence est indiquée, filtre les niveaux de cette compétence.
|
||||
"""
|
||||
if annee not in {1, 2, 3}:
|
||||
raise ValueError("annee invalide pour un parcours BUT")
|
||||
@ -377,15 +421,20 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
raise ScoNoReferentielCompetences()
|
||||
if not parcour:
|
||||
annee_formation = f"BUT{annee}"
|
||||
return ApcNiveau.query.filter(
|
||||
query = ApcNiveau.query.filter(
|
||||
ApcNiveau.annee == annee_formation,
|
||||
ApcCompetence.id == ApcNiveau.competence_id,
|
||||
ApcCompetence.referentiel_id == referentiel_competence.id,
|
||||
)
|
||||
annee_parcour = parcour.annees.filter_by(ordre=annee).first()
|
||||
if competence is not None:
|
||||
query = query.filter(ApcCompetence.id == competence.id)
|
||||
return query.all()
|
||||
|
||||
annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first()
|
||||
if not annee_parcour:
|
||||
return []
|
||||
|
||||
if competence is None:
|
||||
parcour_niveaux: list[
|
||||
ApcParcoursNiveauCompetence
|
||||
] = annee_parcour.niveaux_competences
|
||||
@ -393,6 +442,10 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
||||
for pn in parcour_niveaux
|
||||
]
|
||||
else:
|
||||
niveaux: list[ApcNiveau] = competence.niveaux.filter_by(
|
||||
annee=f"BUT{int(annee)}"
|
||||
).all()
|
||||
return niveaux
|
||||
|
||||
|
||||
@ -433,7 +486,7 @@ class ApcAppCritique(db.Model, XMLModel):
|
||||
ref_comp: ApcReferentielCompetences,
|
||||
annee: str,
|
||||
competence: ApcCompetence = None,
|
||||
) -> flask_sqlalchemy.BaseQuery:
|
||||
) -> Query:
|
||||
"Liste les AC de tous les parcours de ref_comp pour l'année indiquée"
|
||||
assert annee in {"BUT1", "BUT2", "BUT3"}
|
||||
query = cls.query.filter(
|
||||
@ -505,7 +558,7 @@ class ApcParcours(db.Model, XMLModel):
|
||||
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
annees = db.relationship(
|
||||
@ -514,7 +567,6 @@ class ApcParcours(db.Model, XMLModel):
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
ues = db.relationship("UniteEns", back_populates="parcour")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
|
||||
@ -532,7 +584,7 @@ class ApcParcours(db.Model, XMLModel):
|
||||
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
|
||||
return d
|
||||
|
||||
def query_competences(self) -> flask_sqlalchemy.BaseQuery:
|
||||
def query_competences(self) -> Query:
|
||||
"Les compétences associées à ce parcours"
|
||||
return (
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
@ -540,6 +592,16 @@ class ApcParcours(db.Model, XMLModel):
|
||||
.order_by(ApcCompetence.numero)
|
||||
)
|
||||
|
||||
def get_competence_by_titre(self, titre: str) -> ApcCompetence:
|
||||
"La compétence de titre donné dans ce parcours, ou None"
|
||||
return (
|
||||
ApcCompetence.query.filter_by(titre=titre)
|
||||
.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class ApcAnneeParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -550,7 +612,8 @@ class ApcAnneeParcours(db.Model, XMLModel):
|
||||
"numéro de l'année: 1, 2, 3"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} ordre={self.ordre!r} parcours={self.parcours.code!r}>"
|
||||
return f"""<{self.__class__.__name__} {
|
||||
self.id} ordre={self.ordre!r} parcours={self.parcours.code!r}>"""
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
@ -4,7 +4,7 @@
|
||||
"""
|
||||
from typing import Union
|
||||
|
||||
import flask_sqlalchemy
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN
|
||||
@ -177,7 +177,7 @@ class RegroupementCoherentUE:
|
||||
|
||||
def query_validations(
|
||||
self,
|
||||
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE]
|
||||
) -> Query: # list[ApcValidationRCUE]
|
||||
"""Les validations de jury enregistrées pour ce RCUE"""
|
||||
niveau = self.ue_2.niveau_competence
|
||||
|
||||
|
@ -18,7 +18,7 @@ from app import models
|
||||
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc.sco_bac import Baccalaureat
|
||||
from app.scodoc.sco_exceptions import ScoInvalidParamError
|
||||
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ class Identite(db.Model):
|
||||
db.UniqueConstraint("dept_id", "code_nip"),
|
||||
db.UniqueConstraint("dept_id", "code_ine"),
|
||||
db.CheckConstraint("civilite IN ('M', 'F', 'X')"),
|
||||
db.CheckConstraint("civilite_etat_civil IN ('M', 'F', 'X')"),
|
||||
)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -41,6 +42,12 @@ class Identite(db.Model):
|
||||
nom_usuel = db.Column(db.Text())
|
||||
"optionnel (si present, affiché à la place du nom)"
|
||||
civilite = db.Column(db.String(1), nullable=False)
|
||||
|
||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents officiels (bulletins, PV)
|
||||
# cf nomprenom_etat_civil()
|
||||
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
|
||||
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
|
||||
|
||||
date_naissance = db.Column(db.Date)
|
||||
lieu_naissance = db.Column(db.Text())
|
||||
dept_naissance = db.Column(db.Text())
|
||||
@ -97,7 +104,7 @@ class Identite(db.Model):
|
||||
def create_etud(cls, **args):
|
||||
"Crée un étudiant, avec admission et adresse vides."
|
||||
etud: Identite = cls(**args)
|
||||
etud.adresses.append(Adresse())
|
||||
etud.adresses.append(Adresse(typeadresse="domicile"))
|
||||
etud.admission.append(Admission())
|
||||
return etud
|
||||
|
||||
@ -108,6 +115,13 @@ class Identite(db.Model):
|
||||
"""
|
||||
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
|
||||
|
||||
@property
|
||||
def civilite_etat_civil_str(self):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
personnes ne souhaitant pas d'affichage).
|
||||
"""
|
||||
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil]
|
||||
|
||||
def sex_nom(self, no_accents=False) -> str:
|
||||
"'M. DUPONTÉ', ou si no_accents, 'M. DUPONTE'"
|
||||
s = f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()}"
|
||||
@ -154,6 +168,14 @@ class Identite(db.Model):
|
||||
r.append("-".join([x.lower().capitalize() for x in fields]))
|
||||
return " ".join(r)
|
||||
|
||||
@property
|
||||
def etat_civil(self):
|
||||
if self.prenom_etat_civil:
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil]
|
||||
return f"{civ} {self.prenom_etat_civil} {self.nom}"
|
||||
else:
|
||||
return self.nomprenom
|
||||
|
||||
@property
|
||||
def nom_short(self):
|
||||
"Nom et début du prénom pour table recap: 'DUPONT Pi.'"
|
||||
@ -183,6 +205,50 @@ class Identite(db.Model):
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"Convert fields in the given dict. No other side effect"
|
||||
fs_uppercase = {"nom", "prenom", "prenom_etat_civil"}
|
||||
fs_empty_stored_as_nulls = {
|
||||
"nom",
|
||||
"prenom",
|
||||
"nom_usuel",
|
||||
"date_naissance",
|
||||
"lieu_naissance",
|
||||
"dept_naissance",
|
||||
"nationalite",
|
||||
"statut",
|
||||
"photo_filename",
|
||||
"code_nip",
|
||||
"code_ine",
|
||||
}
|
||||
args_dict = {}
|
||||
for key, value in args.items():
|
||||
if hasattr(cls, key):
|
||||
# compat scodoc7 (mauvaise idée de l'époque)
|
||||
if key in fs_empty_stored_as_nulls and value == "":
|
||||
value = None
|
||||
if key in fs_uppercase and value:
|
||||
value = value.upper()
|
||||
if key == "civilite" or key == "civilite_etat_civil":
|
||||
value = input_civilite(value)
|
||||
elif key == "boursier":
|
||||
value = bool(value)
|
||||
elif key == "date_naissance":
|
||||
value = ndb.DateDMYtoISO(value)
|
||||
args_dict[key] = value
|
||||
return args_dict
|
||||
|
||||
def from_dict(self, args: dict):
|
||||
"update fields given in dict. Add to session but don't commit."
|
||||
args_dict = Identite.convert_dict_fields(args)
|
||||
args_dict.pop("id", None)
|
||||
args_dict.pop("etudid", None)
|
||||
for key, value in args_dict.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
db.session.add(self)
|
||||
|
||||
def to_dict_short(self) -> dict:
|
||||
"""Les champs essentiels"""
|
||||
return {
|
||||
@ -195,6 +261,8 @@ class Identite(db.Model):
|
||||
"nom_usuel": self.nom_usuel,
|
||||
"prenom": self.prenom,
|
||||
"sort_key": self.sort_key,
|
||||
"civilite_etat_civil": self.civilite_etat_civil,
|
||||
"prenom_etat_civil": self.prenom_etat_civil,
|
||||
}
|
||||
|
||||
def to_dict_scodoc7(self) -> dict:
|
||||
@ -238,6 +306,8 @@ class Identite(db.Model):
|
||||
"dept_naissance": self.dept_naissance or "",
|
||||
"nationalite": self.nationalite or "",
|
||||
"boursier": self.boursier or "",
|
||||
"civilite_etat_civil": self.civilite_etat_civil,
|
||||
"prenom_etat_civil": self.prenom_etat_civil,
|
||||
}
|
||||
if include_urls and has_request_context():
|
||||
# test request context so we can use this func in tests under the flask shell
|
||||
@ -454,10 +524,10 @@ class Identite(db.Model):
|
||||
M. Pierre Dupont
|
||||
"""
|
||||
if with_paragraph:
|
||||
return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
||||
return f"""{self.etat_civil}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{
|
||||
line_sep}à {self.lieu_naissance or ""}"""
|
||||
return self.nomprenom
|
||||
return self.etat_civil
|
||||
|
||||
def photo_html(self, title=None, size="small") -> str:
|
||||
"""HTML img tag for the photo, either in small size (h90)
|
||||
@ -521,6 +591,37 @@ def make_etud_args(
|
||||
return args
|
||||
|
||||
|
||||
def input_civilite(s):
|
||||
"""Converts external representation of civilite to internal:
|
||||
'M', 'F', or 'X' (and nothing else).
|
||||
Raises ScoValueError if conversion fails.
|
||||
"""
|
||||
s = s.upper().strip()
|
||||
if s in ("M", "M.", "MR", "H"):
|
||||
return "M"
|
||||
elif s in ("F", "MLLE", "MLLE.", "MELLE", "MME"):
|
||||
return "F"
|
||||
elif s == "X" or not s:
|
||||
return "X"
|
||||
raise ScoValueError(f"valeur invalide pour la civilité: {s}")
|
||||
|
||||
|
||||
PIVOT_YEAR = 70
|
||||
|
||||
|
||||
def pivot_year(y) -> int:
|
||||
"converti et calcule l'année si saisie à deux chiffres"
|
||||
if y == "" or y is None:
|
||||
return None
|
||||
y = int(round(float(y)))
|
||||
if y >= 0 and y < 100:
|
||||
if y < PIVOT_YEAR:
|
||||
y = y + 2000
|
||||
else:
|
||||
y = y + 1900
|
||||
return y
|
||||
|
||||
|
||||
class Adresse(db.Model):
|
||||
"""Adresse d'un étudiant
|
||||
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
||||
@ -614,19 +715,51 @@ class Admission(db.Model):
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
if no_nulls:
|
||||
for k in d.keys():
|
||||
if d[k] is None:
|
||||
for key, value in d.items():
|
||||
if value is None:
|
||||
col_type = getattr(
|
||||
sqlalchemy.inspect(models.Admission).columns, "apb_groupe"
|
||||
sqlalchemy.inspect(models.Admission).columns, key
|
||||
).expression.type
|
||||
if isinstance(col_type, sqlalchemy.Text):
|
||||
d[k] = ""
|
||||
d[key] = ""
|
||||
elif isinstance(col_type, sqlalchemy.Integer):
|
||||
d[k] = 0
|
||||
d[key] = 0
|
||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||
d[k] = False
|
||||
d[key] = False
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"Convert fields in the given dict. No other side effect"
|
||||
fs_uppercase = {"bac", "specialite"}
|
||||
args_dict = {}
|
||||
for key, value in args.items():
|
||||
if hasattr(cls, key):
|
||||
if (
|
||||
value == ""
|
||||
): # les chaines vides donne des NULLS (scodoc7 convention)
|
||||
value = None
|
||||
if key in fs_uppercase and value:
|
||||
value = value.upper()
|
||||
if key == "civilite" or key == "civilite_etat_civil":
|
||||
value = input_civilite(value)
|
||||
elif key == "annee" or key == "annee_bac":
|
||||
value = pivot_year(value)
|
||||
elif key == "classement" or key == "apb_classement_gr":
|
||||
value = ndb.int_null_is_null(value)
|
||||
args_dict[key] = value
|
||||
return args_dict
|
||||
|
||||
def from_dict(self, args: dict): # TODO à refactoriser dans une super-classe
|
||||
"update fields given in dict. Add to session but don't commit."
|
||||
args_dict = Admission.convert_dict_fields(args)
|
||||
args_dict.pop("adm_id", None)
|
||||
args_dict.pop("id", None)
|
||||
for key, value in args_dict.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
db.session.add(self)
|
||||
|
||||
|
||||
# Suivi scolarité / débouchés
|
||||
class ItemSuivi(db.Model):
|
||||
|
@ -3,6 +3,7 @@
|
||||
"""ScoDoc models: evaluations
|
||||
"""
|
||||
import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from app import db
|
||||
from app.models.etudiants import Identite
|
||||
@ -44,7 +45,7 @@ class Evaluation(db.Model):
|
||||
)
|
||||
# ordre de presentation (par défaut, le plus petit numero
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer)
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
def __repr__(self):
|
||||
@ -151,7 +152,7 @@ class Evaluation(db.Model):
|
||||
Return True if (uncommited) modification, False otherwise.
|
||||
"""
|
||||
ue_coef_dict = self.moduleimpl.module.get_ue_coef_dict()
|
||||
sem_ues = self.moduleimpl.formsemestre.query_ues(with_sport=False).all()
|
||||
sem_ues = self.moduleimpl.formsemestre.get_ues(with_sport=False)
|
||||
modified = False
|
||||
for ue in sem_ues:
|
||||
existing_poids = EvaluationUEPoids.query.filter_by(
|
||||
@ -196,7 +197,7 @@ class Evaluation(db.Model):
|
||||
return {
|
||||
p.ue.id: p.poids
|
||||
for p in sorted(
|
||||
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
||||
self.ue_poids, key=attrgetter("ue.numero", "ue.acronyme")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""ScoDoc 9 models : Formations
|
||||
"""
|
||||
import flask_sqlalchemy
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
@ -9,13 +9,12 @@ from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
)
|
||||
from app.models.modules import Module
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.ues import UniteEns, UEParcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
@ -52,7 +51,9 @@ class Formation(db.Model):
|
||||
)
|
||||
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
|
||||
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
||||
ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")
|
||||
ues = db.relationship(
|
||||
"UniteEns", lazy="dynamic", backref="formation", order_by="UniteEns.numero"
|
||||
)
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
||||
|
||||
def __repr__(self):
|
||||
@ -213,27 +214,38 @@ class Formation(db.Model):
|
||||
if change:
|
||||
app.clear_scodoc_cache()
|
||||
|
||||
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
|
||||
"""Les UEs d'un parcours de la formation.
|
||||
def query_ues_parcour(
|
||||
self, parcour: ApcParcours, with_sport: bool = False
|
||||
) -> Query:
|
||||
"""Les UEs (sans bonus, sauf si with_sport) d'un parcours de la formation
|
||||
(déclarée comme faisant partie du parcours ou du tronc commun, sans aucun parcours)
|
||||
Si parcour est None, les UE sans parcours.
|
||||
Exemple: pour avoir les UE du semestre 3, faire
|
||||
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
|
||||
`formation.query_ues_parcour(parcour).filter(UniteEns.semestre_idx == 3)`
|
||||
"""
|
||||
if with_sport:
|
||||
query_f = UniteEns.query.filter_by(formation=self)
|
||||
else:
|
||||
query_f = UniteEns.query.filter_by(formation=self, type=UE_STANDARD)
|
||||
# Les UE sans parcours:
|
||||
query_no_parcours = query_f.outerjoin(UEParcours).filter(
|
||||
UEParcours.parcours_id == None
|
||||
)
|
||||
if parcour is None:
|
||||
return UniteEns.query.filter_by(
|
||||
formation=self, type=UE_STANDARD, parcour_id=None
|
||||
)
|
||||
return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
|
||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||
(UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
|
||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||
ApcAnneeParcours.parcours_id == parcour.id,
|
||||
)
|
||||
return query_no_parcours.order_by(UniteEns.numero)
|
||||
# Ajoute les UE du parcours sélectionné:
|
||||
return query_no_parcours.union(
|
||||
query_f.join(UEParcours).filter_by(parcours_id=parcour.id)
|
||||
).order_by(UniteEns.numero)
|
||||
# return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
|
||||
# UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||
# (UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
|
||||
# ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||
# ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||
# ApcAnneeParcours.parcours_id == parcour.id,
|
||||
# )
|
||||
|
||||
def query_competences_parcour(
|
||||
self, parcour: ApcParcours
|
||||
) -> flask_sqlalchemy.BaseQuery:
|
||||
def query_competences_parcour(self, parcour: ApcParcours) -> Query:
|
||||
"""Les ApcCompetences d'un parcours de la formation.
|
||||
None si pas de référentiel de compétences.
|
||||
"""
|
||||
@ -281,7 +293,7 @@ class Matiere(db.Model):
|
||||
matiere_id = db.synonym("id")
|
||||
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"))
|
||||
titre = db.Column(db.Text())
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||
|
||||
|
@ -12,11 +12,11 @@
|
||||
"""
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from operator import attrgetter
|
||||
|
||||
from flask_login import current_user
|
||||
import flask_sqlalchemy
|
||||
|
||||
from flask import flash, g
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -24,10 +24,7 @@ from app import db, log
|
||||
from app.auth.models import User
|
||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
ApcReferentielCompetences,
|
||||
parcours_formsemestre,
|
||||
)
|
||||
@ -45,6 +42,8 @@ from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
|
||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||
|
||||
|
||||
class FormSemestre(db.Model):
|
||||
"""Mise en oeuvre d'un semestre de formation"""
|
||||
@ -111,6 +110,10 @@ class FormSemestre(db.Model):
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||
|
||||
# Data pour groups_auto_assignment
|
||||
# (ce champ est utilisé uniquement via l'API par le front js)
|
||||
groups_auto_assignment_data = db.Column(db.LargeBinary(), nullable=True)
|
||||
|
||||
# Relations:
|
||||
etapes = db.relationship(
|
||||
"FormSemestreEtape", cascade="all,delete", backref="formsemestre"
|
||||
@ -149,6 +152,7 @@ class FormSemestre(db.Model):
|
||||
secondary=parcours_formsemestre,
|
||||
lazy="subquery",
|
||||
backref=db.backref("formsemestres", lazy=True),
|
||||
order_by=(ApcParcours.numero, ApcParcours.code),
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -195,11 +199,14 @@ class FormSemestre(db.Model):
|
||||
d["date_fin"] = d["date_fin_iso"] = ""
|
||||
d["responsables"] = [u.id for u in self.responsables]
|
||||
d["titre_formation"] = self.titre_formation()
|
||||
if convert_objects:
|
||||
if convert_objects: # pour API
|
||||
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
|
||||
d["departement"] = self.departement.to_dict()
|
||||
d["formation"] = self.formation.to_dict()
|
||||
d["etape_apo"] = self.etapes_apo_str()
|
||||
else:
|
||||
# Converti les étapes Apogee sous forme d'ApoEtapeVDI (compat scodoc7)
|
||||
d["etapes"] = [e.as_apovdi() for e in self.etapes]
|
||||
return d
|
||||
|
||||
def to_dict_api(self):
|
||||
@ -281,26 +288,36 @@ class FormSemestre(db.Model):
|
||||
)
|
||||
return r or []
|
||||
|
||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||
def get_ues(self, with_sport=False) -> list[UniteEns]:
|
||||
"""UE des modules de ce semestre, triées par numéro.
|
||||
- Formations classiques: les UEs auxquelles appartiennent
|
||||
les modules mis en place dans ce semestre.
|
||||
- Formations APC / BUT: les UEs de la formation qui
|
||||
- ont le même numéro de semestre que ce formsemestre
|
||||
- sont associées à l'un des parcours de ce formsemestre (ou à aucun)
|
||||
|
||||
- ont le même numéro de semestre que ce formsemestre;
|
||||
- et sont associées à l'un des parcours de ce formsemestre
|
||||
(ou à aucun, donc tronc commun).
|
||||
"""
|
||||
if self.formation.get_cursus().APC_SAE:
|
||||
sem_ues = UniteEns.query.filter_by(
|
||||
formation=self.formation, semestre_idx=self.semestre_id
|
||||
formation: Formation = self.formation
|
||||
if formation.is_apc():
|
||||
# UEs de tronc commun (sans parcours indiqué)
|
||||
sem_ues = {
|
||||
ue.id: ue
|
||||
for ue in formation.query_ues_parcour(
|
||||
None, with_sport=with_sport
|
||||
).filter(UniteEns.semestre_idx == self.semestre_id)
|
||||
}
|
||||
# Ajoute les UE de parcours
|
||||
for parcour in self.parcours:
|
||||
sem_ues.update(
|
||||
{
|
||||
ue.id: ue
|
||||
for ue in formation.query_ues_parcour(
|
||||
parcour, with_sport=with_sport
|
||||
).filter(UniteEns.semestre_idx == self.semestre_id)
|
||||
}
|
||||
)
|
||||
if self.parcours:
|
||||
# Prend toutes les UE de l'un des parcours du sem., ou déclarées sans parcours
|
||||
sem_ues = sem_ues.filter(
|
||||
(UniteEns.parcour == None)
|
||||
| (UniteEns.parcour_id.in_([p.id for p in self.parcours]))
|
||||
)
|
||||
# si le sem. ne coche aucun parcours, prend toutes les UE
|
||||
ues = sem_ues.values()
|
||||
return sorted(ues, key=attrgetter("numero"))
|
||||
else:
|
||||
sem_ues = db.session.query(UniteEns).filter(
|
||||
ModuleImpl.formsemestre_id == self.id,
|
||||
@ -309,32 +326,7 @@ class FormSemestre(db.Model):
|
||||
)
|
||||
if not with_sport:
|
||||
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||
return sem_ues.order_by(UniteEns.numero)
|
||||
|
||||
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
|
||||
"""XXX inutilisé à part pour un test unitaire => supprimer ?
|
||||
UEs que suit l'étudiant dans ce semestre BUT
|
||||
en fonction du parcours dans lequel il est inscrit.
|
||||
Si l'étudiant n'est inscrit à aucun parcours,
|
||||
renvoie uniquement les UEs de tronc commun (sans parcours).
|
||||
|
||||
Si voulez les UE d'un parcours, il est plus efficace de passer par
|
||||
`formation.query_ues_parcour(parcour)`.
|
||||
"""
|
||||
return self.query_ues().filter(
|
||||
FormSemestreInscription.etudid == etudid,
|
||||
FormSemestreInscription.formsemestre == self,
|
||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||
or_(
|
||||
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
|
||||
and_(
|
||||
FormSemestreInscription.parcour_id.is_(None),
|
||||
UniteEns.parcour_id.is_(None),
|
||||
),
|
||||
),
|
||||
)
|
||||
return sem_ues.order_by(UniteEns.numero).all()
|
||||
|
||||
@cached_property
|
||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||
@ -937,7 +929,7 @@ class FormSemestreEtape(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<Etape {self.id} apo={self.etape_apo!r}>"
|
||||
|
||||
def as_apovdi(self):
|
||||
def as_apovdi(self) -> ApoEtapeVDI:
|
||||
return ApoEtapeVDI(self.etape_apo)
|
||||
|
||||
|
||||
@ -960,7 +952,7 @@ class FormationModalite(db.Model):
|
||||
) # code
|
||||
titre = db.Column(db.Text()) # texte explicatif
|
||||
# numero = ordre de presentation)
|
||||
numero = db.Column(db.Integer)
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
@staticmethod
|
||||
def insert_modalites():
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
"""ScoDoc models: Groups & partitions
|
||||
"""
|
||||
from operator import attrgetter
|
||||
|
||||
from app import db
|
||||
from app.models import SHORT_STR_LEN
|
||||
@ -29,7 +30,7 @@ class Partition(db.Model):
|
||||
# "TD", "TP", ... (NULL for 'all')
|
||||
partition_name = db.Column(db.String(SHORT_STR_LEN))
|
||||
# Numero = ordre de presentation)
|
||||
numero = db.Column(db.Integer)
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
# Calculer le rang ?
|
||||
bul_show_rank = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
@ -84,21 +85,38 @@ class Partition(db.Model):
|
||||
"Vrai s'il s'agit de la partition de parcours"
|
||||
return self.partition_name == scu.PARTITION_PARCOURS
|
||||
|
||||
def to_dict(self, with_groups=False) -> dict:
|
||||
"""as a dict, with or without groups"""
|
||||
def to_dict(self, with_groups=False, str_keys: bool = False) -> dict:
|
||||
"""as a dict, with or without groups.
|
||||
If str_keys, convert integer dict keys to strings (useful for JSON)
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d["partition_id"] = self.id
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formsemestre", None)
|
||||
|
||||
if with_groups:
|
||||
groups = sorted(self.groups, key=lambda g: (g.numero or 0, g.group_name))
|
||||
groups = sorted(self.groups, key=attrgetter("numero", "group_name"))
|
||||
# un dict et non plus une liste, pour JSON
|
||||
if str_keys:
|
||||
d["groups"] = {
|
||||
str(group.id): group.to_dict(with_partition=False)
|
||||
for group in groups
|
||||
}
|
||||
else:
|
||||
d["groups"] = {
|
||||
group.id: group.to_dict(with_partition=False) for group in groups
|
||||
}
|
||||
return d
|
||||
|
||||
def get_etud_group(self, etudid: int) -> "GroupDescr":
|
||||
"Le groupe de l'étudiant dans cette partition, ou None si pas présent"
|
||||
return (
|
||||
GroupDescr.query.filter_by(partition_id=self.id)
|
||||
.join(group_membership)
|
||||
.filter_by(etudid=etudid)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class GroupDescr(db.Model):
|
||||
"""Description d'un groupe d'une partition"""
|
||||
@ -112,7 +130,7 @@ class GroupDescr(db.Model):
|
||||
# "A", "C2", ... (NULL for 'all'):
|
||||
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
|
||||
# Numero = ordre de presentation
|
||||
numero = db.Column(db.Integer)
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""ScoDoc models: moduleimpls
|
||||
"""
|
||||
import pandas as pd
|
||||
import flask_sqlalchemy
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
@ -179,7 +179,7 @@ class ModuleImplInscription(db.Model):
|
||||
@classmethod
|
||||
def etud_modimpls_in_ue(
|
||||
cls, formsemestre_id: int, etudid: int, ue_id: int
|
||||
) -> flask_sqlalchemy.BaseQuery:
|
||||
) -> Query:
|
||||
"""moduleimpls de l'UE auxquels l'étudiant est inscrit.
|
||||
(Attention: inutile en APC, il faut considérer les coefficients)
|
||||
"""
|
||||
|
@ -33,7 +33,7 @@ class Module(db.Model):
|
||||
# pas un id mais le numéro du semestre: 1, 2, ...
|
||||
# note: en APC, le semestre qui fait autorité est celui de l'UE
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||
|
@ -3,6 +3,7 @@
|
||||
"""Notes, décisions de jury, évènements scolaires
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from app import db
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
@ -86,6 +87,7 @@ def etud_has_notes_attente(etudid, formsemestre_id):
|
||||
(ne compte que les notes en attente dans des évaluations avec coef. non nul).
|
||||
"""
|
||||
cursor = db.session.execute(
|
||||
sa.text(
|
||||
"""SELECT COUNT(*)
|
||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
|
||||
notes_moduleimpl_inscription i
|
||||
@ -97,7 +99,8 @@ def etud_has_notes_attente(etudid, formsemestre_id):
|
||||
and e.coefficient != 0
|
||||
and m.id = i.moduleimpl_id
|
||||
and i.etudid = :etudid
|
||||
""",
|
||||
"""
|
||||
),
|
||||
{
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
|
@ -1,57 +0,0 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""
|
||||
Create some Postgresql sequences and functions used by ScoDoc
|
||||
using raw SQL
|
||||
"""
|
||||
|
||||
from app import db
|
||||
|
||||
|
||||
def create_database_functions(): # XXX obsolete
|
||||
"""Create specific SQL functions and sequences
|
||||
|
||||
XXX Obsolete: cette fonction est dans la première migration 9.0.3
|
||||
Flask-Migrate fait maintenant (dans les versions >= 9.0.4) ce travail.
|
||||
"""
|
||||
# Important: toujours utiliser IF NOT EXISTS
|
||||
# car cette fonction peut être appelée plusieurs fois sur la même db
|
||||
db.session.execute(
|
||||
"""
|
||||
CREATE SEQUENCE IF NOT EXISTS notes_idgen_fcod;
|
||||
CREATE OR REPLACE FUNCTION notes_newid_fcod() RETURNS TEXT
|
||||
AS $$ SELECT 'FCOD' || to_char(nextval('notes_idgen_fcod'), 'FM999999999'); $$
|
||||
LANGUAGE SQL;
|
||||
CREATE OR REPLACE FUNCTION notes_newid_ucod() RETURNS TEXT
|
||||
AS $$ SELECT 'UCOD' || to_char(nextval('notes_idgen_fcod'), 'FM999999999'); $$
|
||||
LANGUAGE SQL;
|
||||
|
||||
CREATE OR REPLACE FUNCTION truncate_tables(username IN VARCHAR) RETURNS void AS $$
|
||||
DECLARE
|
||||
statements CURSOR FOR
|
||||
SELECT tablename FROM pg_tables
|
||||
WHERE tableowner = username AND schemaname = 'public'
|
||||
AND tablename <> 'notes_semestres'
|
||||
AND tablename <> 'notes_form_modalites'
|
||||
AND tablename <> 'alembic_version';
|
||||
BEGIN
|
||||
FOR stmt IN statements LOOP
|
||||
EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;';
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Fonction pour anonymisation:
|
||||
-- inspirée par https://www.simononsoftware.com/random-string-in-postgresql/
|
||||
CREATE OR REPLACE FUNCTION random_text_md5( integer ) returns text
|
||||
LANGUAGE SQL
|
||||
AS $$
|
||||
select upper( substring( (SELECT string_agg(md5(random()::TEXT), '')
|
||||
FROM generate_series(
|
||||
1,
|
||||
CEIL($1 / 32.)::integer)
|
||||
), 1, $1) );
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
db.session.commit()
|
@ -21,7 +21,7 @@ class UniteEns(db.Model):
|
||||
ue_id = db.synonym("id")
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
acronyme = db.Column(db.Text(), nullable=False)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
titre = db.Column(db.Text())
|
||||
# Le semestre_idx n'est pas un id mais le numéro du semestre: 1, 2, ...
|
||||
# En ScoDoc7 et pour les formations classiques, il est NULL
|
||||
@ -38,7 +38,7 @@ class UniteEns(db.Model):
|
||||
server_default=db.text("notes_newid_ucod()"),
|
||||
nullable=False,
|
||||
)
|
||||
ects = db.Column(db.Float) # nombre de credits ECTS
|
||||
ects = db.Column(db.Float) # nombre de credits ECTS (sauf si parcours spécifié)
|
||||
is_external = db.Column(db.Boolean(), default=False, server_default="false")
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
@ -56,11 +56,10 @@ class UniteEns(db.Model):
|
||||
)
|
||||
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
|
||||
|
||||
# Une ue appartient soit à tous les parcours (tronc commun), soit à un seul:
|
||||
parcour_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="SET NULL"), index=True
|
||||
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
|
||||
parcours = db.relationship(
|
||||
ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
|
||||
)
|
||||
parcour = db.relationship("ApcParcours", back_populates="ues")
|
||||
|
||||
# relations
|
||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||
@ -101,10 +100,9 @@ class UniteEns(db.Model):
|
||||
return ue
|
||||
|
||||
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
||||
"""as a dict, with the same conversions as in ScoDoc7
|
||||
(except ECTS: keep None)
|
||||
"""as a dict, with the same conversions as in ScoDoc7.
|
||||
If convert_objects, convert all attributes to native types
|
||||
(suitable jor json encoding).
|
||||
(suitable for json encoding).
|
||||
"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
@ -112,10 +110,19 @@ class UniteEns(db.Model):
|
||||
# ScoDoc7 output_formators
|
||||
e["ue_id"] = self.id
|
||||
e["numero"] = e["numero"] if e["numero"] else 0
|
||||
e["ects"] = e["ects"]
|
||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
||||
e["parcour"] = self.parcour.to_dict() if self.parcour else None
|
||||
e["ects_by_parcours"] = {
|
||||
parcour.code: self.get_ects(parcour) for parcour in self.parcours
|
||||
}
|
||||
e["parcours"] = []
|
||||
for parcour in self.parcours:
|
||||
p_dict = parcour.to_dict(with_annees=False)
|
||||
ects = self.get_ects(parcour, only_parcours=True)
|
||||
if ects is not None:
|
||||
p_dict["ects"] = ects
|
||||
e["parcours"].append(p_dict)
|
||||
|
||||
if with_module_ue_coefs:
|
||||
if convert_objects:
|
||||
e["module_ue_coefs"] = [
|
||||
@ -163,6 +170,44 @@ class UniteEns(db.Model):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float:
|
||||
"""Crédits ECTS associés à cette UE.
|
||||
En BUT, cela peut quelquefois dépendre du parcours.
|
||||
Si only_parcours, renvoie None si pas de valeur spéciquement définie dans
|
||||
le parcours indiqué.
|
||||
"""
|
||||
if parcour is not None:
|
||||
ue_parcour = UEParcours.query.filter_by(
|
||||
ue_id=self.id, parcours_id=parcour.id
|
||||
).first()
|
||||
if ue_parcour is not None and ue_parcour.ects is not None:
|
||||
return ue_parcour.ects
|
||||
if only_parcours:
|
||||
return None
|
||||
return self.ects
|
||||
|
||||
def set_ects(self, ects: float, parcour: ApcParcours = None):
|
||||
"""Fixe les crédits. Do not commit.
|
||||
Si le parcours n'est pas spécifié, affecte les ECTS par défaut de l'UE.
|
||||
Si ects est None et parcours indiqué, efface l'association.
|
||||
"""
|
||||
if parcour is not None:
|
||||
ue_parcour = UEParcours.query.filter_by(
|
||||
ue_id=self.id, parcours_id=parcour.id
|
||||
).first()
|
||||
if ects is None:
|
||||
if ue_parcour:
|
||||
db.session.delete(ue_parcour)
|
||||
else:
|
||||
if ue_parcour is None:
|
||||
ue_parcour = UEParcours(parcours_id=parcour.id, ue_id=self.id)
|
||||
ue_parcour.ects = float(ects)
|
||||
db.session.add(ue_parcour)
|
||||
else:
|
||||
self.ects = ects
|
||||
log(f"ue.set_ects( ue_id={self.id}, acronyme={self.acronyme}, ects={ects} )")
|
||||
db.session.add(self)
|
||||
|
||||
def get_ressources(self):
|
||||
"Liste des modules ressources rattachés à cette UE"
|
||||
return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all()
|
||||
@ -184,84 +229,203 @@ class UniteEns(db.Model):
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def _check_apc_conflict(self, new_niveau_id: int, new_parcour_id: int):
|
||||
"raises ScoFormationConflict si (niveau, parcours) pas unique dans ce semestre"
|
||||
# Les UE du même semestre que nous:
|
||||
ues_sem = self.formation.ues.filter_by(semestre_idx=self.semestre_idx)
|
||||
if (new_niveau_id, new_parcour_id) in (
|
||||
(oue.niveau_competence_id, oue.parcour_id)
|
||||
for oue in ues_sem
|
||||
if oue.id != self.id
|
||||
):
|
||||
log(
|
||||
f"set_ue_niveau_competence: {self}: ({new_niveau_id}, {new_parcour_id}) déjà associé"
|
||||
def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]:
|
||||
"""set des ids de niveaux communs à tous les parcours listés"""
|
||||
return set.intersection(
|
||||
*[
|
||||
{
|
||||
n.id
|
||||
for n in self.niveau_competence.niveaux_annee_de_parcours(
|
||||
parcour, self.annee(), self.formation.referentiel_competence
|
||||
)
|
||||
}
|
||||
for parcour in parcours
|
||||
]
|
||||
)
|
||||
raise ScoFormationConflict()
|
||||
|
||||
def set_niveau_competence(self, niveau: ApcNiveau):
|
||||
def check_niveau_unique_dans_parcours(
|
||||
self, niveau: ApcNiveau, parcours=list[ApcParcours]
|
||||
) -> tuple[bool, str]:
|
||||
"""Vérifie que
|
||||
- le niveau est dans au moins l'un des parcours listés;
|
||||
- et que l'un des parcours associé à cette UE ne contient pas
|
||||
déjà une UE associée au niveau donné dans une autre année.
|
||||
Renvoie: (True, "") si ok, sinon (False, message).
|
||||
"""
|
||||
# Le niveau est-il dans l'un des parcours listés ?
|
||||
if parcours:
|
||||
if niveau.id not in self._parcours_niveaux_ids(parcours):
|
||||
log(
|
||||
f"Le niveau {niveau} ne fait pas partie des parcours de l'UE {self}."
|
||||
)
|
||||
return (
|
||||
False,
|
||||
f"""Le niveau {
|
||||
niveau.libelle} ne fait pas partie des parcours de l'UE {self.acronyme}.""",
|
||||
)
|
||||
|
||||
for parcour in parcours or [None]:
|
||||
if parcour is None:
|
||||
code_parcour = "TC"
|
||||
ues_meme_niveau = [
|
||||
ue
|
||||
for ue in self.formation.query_ues_parcour(None).filter(
|
||||
UniteEns.niveau_competence == niveau
|
||||
)
|
||||
]
|
||||
else:
|
||||
code_parcour = parcour.code
|
||||
ues_meme_niveau = [
|
||||
ue
|
||||
for ue in parcour.ues
|
||||
if ue.id != self.id
|
||||
and ue.formation_id == self.formation_id
|
||||
and ue.niveau_competence_id == niveau.id
|
||||
]
|
||||
if ues_meme_niveau:
|
||||
msg_parc = f"parcours {code_parcour}" if parcour else "tronc commun"
|
||||
if len(ues_meme_niveau) > 1: # deja 2 UE sur ce niveau
|
||||
msg = f"""Niveau "{
|
||||
niveau.libelle}" déjà associé à deux UE du {msg_parc}"""
|
||||
log(
|
||||
f"check_niveau_unique_dans_parcours(niveau_id={niveau.id}): "
|
||||
+ msg
|
||||
)
|
||||
return False, msg
|
||||
# s'il y a déjà une UE associée à ce niveau, elle doit être dans l'autre semestre
|
||||
# de la même année scolaire
|
||||
other_semestre_idx = self.semestre_idx + (
|
||||
2 * (self.semestre_idx % 2) - 1
|
||||
)
|
||||
if ues_meme_niveau[0].semestre_idx != other_semestre_idx:
|
||||
msg = f"""Erreur: niveau "{
|
||||
niveau.libelle}" déjà associé à une autre UE du semestre S{
|
||||
ues_meme_niveau[0].semestre_idx} du {msg_parc}"""
|
||||
log(
|
||||
f"check_niveau_unique_dans_parcours(niveau_id={niveau.id}): "
|
||||
+ msg
|
||||
)
|
||||
return False, msg
|
||||
|
||||
return True, ""
|
||||
|
||||
def set_niveau_competence(self, niveau: ApcNiveau) -> tuple[bool, str]:
|
||||
"""Associe cette UE au niveau de compétence indiqué.
|
||||
Le niveau doit être dans le parcours de l'UE, s'il y en a un.
|
||||
Le niveau doit être dans l'un des parcours de l'UE (si elle n'est pas
|
||||
de tronc commun).
|
||||
Assure que ce soit la seule dans son parcours.
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
Returns True if (de)association done, False on error.
|
||||
"""
|
||||
# Sanity checks
|
||||
if not self.formation.referentiel_competence:
|
||||
return (
|
||||
False,
|
||||
"La formation n'est pas associée à un référentiel de compétences",
|
||||
)
|
||||
if niveau is not None:
|
||||
self._check_apc_conflict(niveau.id, self.parcour_id)
|
||||
# Le niveau est-il dans le parcours ? Sinon, erreur
|
||||
if self.parcour and niveau.id not in (
|
||||
n.id
|
||||
for n in niveau.niveaux_annee_de_parcours(
|
||||
self.parcour, self.annee(), self.formation.referentiel_competence
|
||||
if self.niveau_competence_id is not None:
|
||||
return (
|
||||
False,
|
||||
f"""{self.acronyme} déjà associée à un niveau de compétences ({
|
||||
self.id}, {self.niveau_competence_id})""",
|
||||
)
|
||||
if (
|
||||
niveau.competence.referentiel.id
|
||||
!= self.formation.referentiel_competence.id
|
||||
):
|
||||
log(
|
||||
f"set_niveau_competence: niveau {niveau} hors parcours {self.parcour}"
|
||||
return (
|
||||
False,
|
||||
"Le niveau n'appartient pas au référentiel de la formation",
|
||||
)
|
||||
return
|
||||
|
||||
if niveau.id == self.niveau_competence_id:
|
||||
return True, "" # nothing to do
|
||||
if self.niveau_competence_id is not None:
|
||||
ok, error_message = self.check_niveau_unique_dans_parcours(
|
||||
niveau, self.parcours
|
||||
)
|
||||
if not ok:
|
||||
return ok, error_message
|
||||
elif self.niveau_competence_id is None:
|
||||
return True, "" # nothing to do
|
||||
self.niveau_competence = niveau
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
||||
return True, ""
|
||||
|
||||
def set_parcour(self, parcour: ApcParcours):
|
||||
"""Associe cette UE au parcours indiqué.
|
||||
Assure que ce soit la seule dans son parcours.
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
def set_parcours(self, parcours: list[ApcParcours]) -> tuple[bool, str]:
|
||||
"""Associe cette UE aux parcours indiqués.
|
||||
Si un niveau est déjà associé, vérifie sa cohérence.
|
||||
Renvoie (True, "") si ok, sinon (False, error_message)
|
||||
"""
|
||||
if (parcour is not None) and self.niveau_competence is not None:
|
||||
self._check_apc_conflict(self.niveau_competence.id, parcour.id)
|
||||
self.parcour = parcour
|
||||
# Le niveau est-il dans ce parcours ? Sinon, l'enlève
|
||||
msg = ""
|
||||
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
|
||||
prev_niveau = self.niveau_competence
|
||||
if (
|
||||
parcour
|
||||
parcours
|
||||
and self.niveau_competence
|
||||
and self.niveau_competence.id
|
||||
not in (
|
||||
n.id
|
||||
for n in self.niveau_competence.niveaux_annee_de_parcours(
|
||||
parcour, self.annee(), self.formation.referentiel_competence
|
||||
)
|
||||
)
|
||||
and self.niveau_competence.id not in self._parcours_niveaux_ids(parcours)
|
||||
):
|
||||
self.niveau_competence = None
|
||||
msg = " (niveau compétence désassocié !)"
|
||||
|
||||
if parcours and self.niveau_competence:
|
||||
ok, error_message = self.check_niveau_unique_dans_parcours(
|
||||
self.niveau_competence, parcours
|
||||
)
|
||||
if not ok:
|
||||
self.niveau_competence = prev_niveau # restore
|
||||
return False, error_message
|
||||
|
||||
self.parcours = parcours
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_parcour( {self}, {parcour} )")
|
||||
log(f"ue.set_parcours( {self}, {parcours} )")
|
||||
return True, "parcours enregistrés" + msg
|
||||
|
||||
def add_parcour(self, parcour: ApcParcours) -> tuple[bool, str]:
|
||||
"""Ajoute ce parcours à ceux de l'UE"""
|
||||
if parcour.id in {p.id for p in self.parcours}:
|
||||
return True, "" # déjà présent
|
||||
if parcour.referentiel.id != self.formation.referentiel_competence.id:
|
||||
return False, "Le parcours n'appartient pas au référentiel de la formation"
|
||||
|
||||
return self.set_parcours(self.parcours + [parcour])
|
||||
|
||||
|
||||
class UEParcours(db.Model):
|
||||
"""Association ue <-> parcours, indiquant les ECTS"""
|
||||
|
||||
__tablename__ = "ue_parcours"
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
parcours_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_parcours.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
ects = db.Column(db.Float, nullable=True) # si NULL, on prendra les ECTS de l'UE
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UEParcours( ue_id={self.ue_id}, parcours_id={self.parcours_id}, ects={self.ects})>"
|
||||
|
||||
|
||||
class DispenseUE(db.Model):
|
||||
"""Dispense d'UE
|
||||
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
|
||||
qu'ils ne refont pas.
|
||||
Utilisé en APC (BUT) pour indiquer
|
||||
- les étudiants redoublants avec une UE capitalisée qu'ils ne refont pas.
|
||||
- les étudiants "non inscrit" à une UE car elle ne fait pas partie de leur Parcours.
|
||||
|
||||
La dispense d'UE n'est PAS une validation:
|
||||
- elle n'est pas affectée par les décisions de jury (pas effacée)
|
||||
- elle est associée à un formsemestre
|
||||
|
@ -275,6 +275,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
|
||||
|
||||
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
||||
|
||||
|
||||
# Règles gestion cursus
|
||||
class DUTRule(object):
|
||||
def __init__(self, rule_id, premise, conclusion):
|
||||
@ -298,7 +299,7 @@ class DUTRule(object):
|
||||
|
||||
|
||||
# Types de cursus
|
||||
DEFAULT_TYPE_CURSUS = 100 # pour le menu de creation nouvelle formation
|
||||
DEFAULT_TYPE_CURSUS = 700 # (BUT) pour le menu de creation nouvelle formation
|
||||
|
||||
|
||||
class TypeCursus:
|
||||
|
@ -40,7 +40,6 @@ Par exemple, la clé '_css_row_class' spécifie le style CSS de la ligne.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import random
|
||||
from collections import OrderedDict
|
||||
from xml.etree import ElementTree
|
||||
@ -60,7 +59,7 @@ from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_xml
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
||||
from app.scodoc.sco_pdf import SU
|
||||
from app import log
|
||||
from app import log, ScoDocJSONEncoder
|
||||
|
||||
|
||||
def mark_paras(L, tags) -> list[str]:
|
||||
@ -647,7 +646,7 @@ class GenTable(object):
|
||||
# v = str(v)
|
||||
r[cid] = v
|
||||
d.append(r)
|
||||
return json.dumps(d, cls=scu.ScoDocJSONEncoder)
|
||||
return json.dumps(d, cls=ScoDocJSONEncoder)
|
||||
|
||||
def make_page(
|
||||
self,
|
||||
|
@ -186,7 +186,7 @@ def DBSelectArgs(
|
||||
cond = ""
|
||||
i = 1
|
||||
cl = []
|
||||
for (_, aux_id) in aux_tables:
|
||||
for _, aux_id in aux_tables:
|
||||
cl.append("T0.%s = T%d.%s" % (id_name, i, aux_id))
|
||||
i = i + 1
|
||||
cond += " and ".join(cl)
|
||||
@ -403,7 +403,7 @@ class EditableTable(object):
|
||||
|
||||
def format_output(self, r, disable_formatting=False):
|
||||
"Format dict using provided output_formators"
|
||||
for (k, v) in r.items():
|
||||
for k, v in r.items():
|
||||
if v is None and self.convert_null_outputs_to_empty:
|
||||
v = ""
|
||||
# format value
|
||||
|
@ -29,16 +29,14 @@
|
||||
"""
|
||||
|
||||
from flask import g, url_for
|
||||
import flask_sqlalchemy
|
||||
from flask_sqlalchemy.query import Query
|
||||
from app.models.absences import BilletAbsence
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
def query_billets_etud(
|
||||
etudid: int = None, etat: bool = None
|
||||
) -> flask_sqlalchemy.BaseQuery:
|
||||
def query_billets_etud(etudid: int = None, etat: bool = None) -> Query:
|
||||
"""Billets d'absences pour un étudiant, ou tous si etudid is None.
|
||||
Si etat, filtre par état.
|
||||
Si dans un département et que la gestion des billets n'a pas été activée
|
||||
|
@ -46,13 +46,14 @@ Pour chaque étudiant commun:
|
||||
from flask import g, url_for
|
||||
|
||||
from app import log
|
||||
from app.scodoc import sco_apogee_csv
|
||||
from app.scodoc import sco_apogee_csv, sco_apogee_reader
|
||||
from app.scodoc.sco_apogee_csv import ApoData
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
_help_txt = """
|
||||
_HELP_TXT = """
|
||||
<div class="help">
|
||||
<p>Outil de comparaison de fichiers (maquettes CSV) Apogée.
|
||||
</p>
|
||||
@ -69,7 +70,7 @@ def apo_compare_csv_form():
|
||||
"""<h2>Comparaison de fichiers Apogée</h2>
|
||||
<form id="apo_csv_add" action="apo_compare_csv" method="post" enctype="multipart/form-data">
|
||||
""",
|
||||
_help_txt,
|
||||
_HELP_TXT,
|
||||
"""
|
||||
<div class="apo_compare_csv_form_but">
|
||||
Fichier Apogée A:
|
||||
@ -109,14 +110,14 @@ def apo_compare_csv(file_a, file_b, autodetect=True):
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Erreur: l'encodage de l'un des fichiers est incorrect.
|
||||
Vérifiez qu'il est bien en {sco_apogee_csv.APO_INPUT_ENCODING}
|
||||
Vérifiez qu'il est bien en {sco_apogee_reader.APO_INPUT_ENCODING}
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
) from exc
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
||||
"<h2>Comparaison de fichiers Apogée</h2>",
|
||||
_help_txt,
|
||||
_HELP_TXT,
|
||||
'<div class="apo_compare_csv">',
|
||||
_apo_compare_csv(apo_data_a, apo_data_b),
|
||||
"</div>",
|
||||
@ -130,17 +131,17 @@ def _load_apo_data(csvfile, autodetect=True):
|
||||
"Read data from request variable and build ApoData"
|
||||
data_b = csvfile.read()
|
||||
if autodetect:
|
||||
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
|
||||
data_b, message = sco_apogee_reader.fix_data_encoding(data_b)
|
||||
if message:
|
||||
log(f"apo_compare_csv: {message}")
|
||||
if not data_b:
|
||||
raise ScoValueError("fichier vide ? (apo_compare_csv: no data)")
|
||||
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
|
||||
data = data_b.decode(sco_apogee_reader.APO_INPUT_ENCODING)
|
||||
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
|
||||
return apo_data
|
||||
|
||||
|
||||
def _apo_compare_csv(A, B):
|
||||
def _apo_compare_csv(apo_a: ApoData, apo_b: ApoData):
|
||||
"""Generate html report comparing A and B, two instances of ApoData
|
||||
representing Apogee CSV maquettes.
|
||||
"""
|
||||
@ -148,74 +149,75 @@ def _apo_compare_csv(A, B):
|
||||
# 1-- Check etape and codes
|
||||
L.append('<div class="section"><div class="tit">En-tête</div>')
|
||||
L.append('<div><span class="key">Nom fichier A:</span><span class="val_ok">')
|
||||
L.append(A.orig_filename)
|
||||
L.append(apo_a.orig_filename)
|
||||
L.append("</span></div>")
|
||||
L.append('<div><span class="key">Nom fichier B:</span><span class="val_ok">')
|
||||
L.append(B.orig_filename)
|
||||
L.append(apo_b.orig_filename)
|
||||
L.append("</span></div>")
|
||||
L.append('<div><span class="key">Étape Apogée:</span>')
|
||||
if A.etape_apogee != B.etape_apogee:
|
||||
if apo_a.etape_apogee != apo_b.etape_apogee:
|
||||
L.append(
|
||||
'<span class="val_dif">%s != %s</span>' % (A.etape_apogee, B.etape_apogee)
|
||||
f"""<span class="val_dif">{apo_a.etape_apogee} != {apo_b.etape_apogee}</span>"""
|
||||
)
|
||||
else:
|
||||
L.append('<span class="val_ok">%s</span>' % (A.etape_apogee,))
|
||||
L.append(f"""<span class="val_ok">{apo_a.etape_apogee}</span>""")
|
||||
L.append("</div>")
|
||||
|
||||
L.append('<div><span class="key">VDI Apogée:</span>')
|
||||
if A.vdi_apogee != B.vdi_apogee:
|
||||
L.append('<span class="val_dif">%s != %s</span>' % (A.vdi_apogee, B.vdi_apogee))
|
||||
if apo_a.vdi_apogee != apo_b.vdi_apogee:
|
||||
L.append(
|
||||
f"""<span class="val_dif">{apo_a.vdi_apogee} != {apo_b.vdi_apogee}</span>"""
|
||||
)
|
||||
else:
|
||||
L.append('<span class="val_ok">%s</span>' % (A.vdi_apogee,))
|
||||
L.append(f"""<span class="val_ok">{apo_a.vdi_apogee}</span>""")
|
||||
L.append("</div>")
|
||||
|
||||
L.append('<div><span class="key">Code diplôme :</span>')
|
||||
if A.cod_dip_apogee != B.cod_dip_apogee:
|
||||
if apo_a.cod_dip_apogee != apo_b.cod_dip_apogee:
|
||||
L.append(
|
||||
'<span class="val_dif">%s != %s</span>'
|
||||
% (A.cod_dip_apogee, B.cod_dip_apogee)
|
||||
f"""<span class="val_dif">{apo_a.cod_dip_apogee} != {apo_b.cod_dip_apogee}</span>"""
|
||||
)
|
||||
else:
|
||||
L.append('<span class="val_ok">%s</span>' % (A.cod_dip_apogee,))
|
||||
L.append(f"""<span class="val_ok">{apo_a.cod_dip_apogee}</span>""")
|
||||
L.append("</div>")
|
||||
|
||||
L.append('<div><span class="key">Année scolaire :</span>')
|
||||
if A.annee_scolaire != B.annee_scolaire:
|
||||
if apo_a.annee_scolaire != apo_b.annee_scolaire:
|
||||
L.append(
|
||||
'<span class="val_dif">%s != %s</span>'
|
||||
% (A.annee_scolaire, B.annee_scolaire)
|
||||
% (apo_a.annee_scolaire, apo_b.annee_scolaire)
|
||||
)
|
||||
else:
|
||||
L.append('<span class="val_ok">%s</span>' % (A.annee_scolaire,))
|
||||
L.append('<span class="val_ok">%s</span>' % (apo_a.annee_scolaire,))
|
||||
L.append("</div>")
|
||||
|
||||
# Colonnes:
|
||||
A_elts = set(A.apo_elts.keys())
|
||||
B_elts = set(B.apo_elts.keys())
|
||||
a_elts = set(apo_a.apo_csv.apo_elts.keys())
|
||||
b_elts = set(apo_b.apo_csv.apo_elts.keys())
|
||||
L.append('<div><span class="key">Éléments Apogée :</span>')
|
||||
if A_elts == B_elts:
|
||||
L.append('<span class="val_ok">%d</span>' % len(A_elts))
|
||||
if a_elts == b_elts:
|
||||
L.append(f"""<span class="val_ok">{len(a_elts)}</span>""")
|
||||
else:
|
||||
elts_communs = A_elts.intersection(B_elts)
|
||||
elts_only_A = A_elts - A_elts.intersection(B_elts)
|
||||
elts_only_B = B_elts - A_elts.intersection(B_elts)
|
||||
elts_communs = a_elts.intersection(b_elts)
|
||||
elts_only_a = a_elts - a_elts.intersection(b_elts)
|
||||
elts_only_b = b_elts - a_elts.intersection(b_elts)
|
||||
L.append(
|
||||
'<span class="val_dif">différents (%d en commun, %d seulement dans A, %d seulement dans B)</span>'
|
||||
% (
|
||||
len(elts_communs),
|
||||
len(elts_only_A),
|
||||
len(elts_only_B),
|
||||
len(elts_only_a),
|
||||
len(elts_only_b),
|
||||
)
|
||||
)
|
||||
if elts_only_A:
|
||||
if elts_only_a:
|
||||
L.append(
|
||||
'<div span class="key">Éléments seulement dans A : </span><span class="val_dif">%s</span></div>'
|
||||
% ", ".join(sorted(elts_only_A))
|
||||
% ", ".join(sorted(elts_only_a))
|
||||
)
|
||||
if elts_only_B:
|
||||
if elts_only_b:
|
||||
L.append(
|
||||
'<div span class="key">Éléments seulement dans B : </span><span class="val_dif">%s</span></div>'
|
||||
% ", ".join(sorted(elts_only_B))
|
||||
% ", ".join(sorted(elts_only_b))
|
||||
)
|
||||
L.append("</div>")
|
||||
L.append("</div>") # /section
|
||||
@ -223,22 +225,21 @@ def _apo_compare_csv(A, B):
|
||||
# 2--
|
||||
L.append('<div class="section"><div class="tit">Étudiants</div>')
|
||||
|
||||
A_nips = set(A.etud_by_nip)
|
||||
B_nips = set(B.etud_by_nip)
|
||||
nb_etuds_communs = len(A_nips.intersection(B_nips))
|
||||
nb_etuds_dif = len(A_nips.union(B_nips) - A_nips.intersection(B_nips))
|
||||
a_nips = set(apo_a.etud_by_nip)
|
||||
b_nips = set(apo_b.etud_by_nip)
|
||||
nb_etuds_communs = len(a_nips.intersection(b_nips))
|
||||
nb_etuds_dif = len(a_nips.union(b_nips) - a_nips.intersection(b_nips))
|
||||
L.append("""<div><span class="key">Liste d'étudiants :</span>""")
|
||||
if A_nips == B_nips:
|
||||
if a_nips == b_nips:
|
||||
L.append(
|
||||
"""<span class="s_ok">
|
||||
%d étudiants (tous présents dans chaque fichier)</span>
|
||||
f"""<span class="s_ok">
|
||||
{len(a_nips)} étudiants (tous présents dans chaque fichier)</span>
|
||||
"""
|
||||
% len(A_nips)
|
||||
)
|
||||
else:
|
||||
L.append(
|
||||
'<span class="val_dif">différents (%d en commun, %d différents)</span>'
|
||||
% (nb_etuds_communs, nb_etuds_dif)
|
||||
f"""<span class="val_dif">différents ({nb_etuds_communs} en commun, {
|
||||
nb_etuds_dif} différents)</span>"""
|
||||
)
|
||||
L.append("</div>")
|
||||
L.append("</div>") # /section
|
||||
@ -247,19 +248,22 @@ def _apo_compare_csv(A, B):
|
||||
if nb_etuds_communs > 0:
|
||||
L.append(
|
||||
"""<div class="section sec_table">
|
||||
<div class="tit">Différences de résultats des étudiants présents dans les deux fichiers</div>
|
||||
<div class="tit">Différences de résultats des étudiants présents dans les deux fichiers
|
||||
</div>
|
||||
<p>
|
||||
"""
|
||||
)
|
||||
T = apo_table_compare_etud_results(A, B)
|
||||
T = apo_table_compare_etud_results(apo_a, apo_b)
|
||||
if T.get_nb_rows() > 0:
|
||||
L.append(T.html())
|
||||
else:
|
||||
L.append(
|
||||
"""<p class="p_ok">aucune différence de résultats
|
||||
sur les %d étudiants communs (<em>les éléments Apogée n'apparaissant pas dans les deux fichiers sont omis</em>)</p>
|
||||
f"""<p class="p_ok">aucune différence de résultats
|
||||
sur les {nb_etuds_communs} étudiants communs
|
||||
(<em>les éléments Apogée n'apparaissant pas dans les deux
|
||||
fichiers sont omis</em>)
|
||||
</p>
|
||||
"""
|
||||
% nb_etuds_communs
|
||||
)
|
||||
L.append("</div>") # /section
|
||||
|
||||
@ -290,19 +294,17 @@ def apo_table_compare_etud_results(A, B):
|
||||
|
||||
def _build_etud_res(e, apo_data):
|
||||
r = {}
|
||||
for elt_code in apo_data.apo_elts:
|
||||
elt = apo_data.apo_elts[elt_code]
|
||||
for elt_code in apo_data.apo_csv.apo_elts:
|
||||
elt = apo_data.apo_csv.apo_elts[elt_code]
|
||||
try:
|
||||
# les colonnes de cet élément
|
||||
col_ids_type = [
|
||||
(ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
|
||||
]
|
||||
col_ids_type = [(ec["apoL_a01_code"], ec["Type Rés."]) for ec in elt.cols]
|
||||
except KeyError as exc:
|
||||
raise ScoValueError(
|
||||
"Erreur: un élément sans 'Type R\xc3\xa9s.'. Vérifiez l'encodage de vos fichiers."
|
||||
"Erreur: un élément sans 'Type Rés.'. Vérifiez l'encodage de vos fichiers."
|
||||
) from exc
|
||||
r[elt_code] = {}
|
||||
for (col_id, type_res) in col_ids_type:
|
||||
for col_id, type_res in col_ids_type:
|
||||
r[elt_code][type_res] = e.cols[col_id]
|
||||
return r
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
487
app/scodoc/sco_apogee_reader.py
Normal file
487
app/scodoc/sco_apogee_reader.py
Normal file
@ -0,0 +1,487 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Lecture du fichier "maquette" Apogée
|
||||
|
||||
Le fichier CSV, champs séparés par des tabulations, a la structure suivante:
|
||||
|
||||
<pre>
|
||||
XX-APO_TITRES-XX
|
||||
apoC_annee 2007/2008
|
||||
apoC_cod_dip VDTCJ
|
||||
apoC_Cod_Exp 1
|
||||
apoC_cod_vdi 111
|
||||
apoC_Fichier_Exp VDTCJ_V1CJ.txt
|
||||
apoC_lib_dip DUT CJ
|
||||
apoC_Titre1 Export Apogée du 13/06/2008 à 14:29
|
||||
apoC_Titre2
|
||||
|
||||
XX-APO_TYP_RES-XX
|
||||
...section optionnelle au contenu quelconque...
|
||||
|
||||
XX-APO_COLONNES-XX
|
||||
apoL_a01_code Type Objet Code Version Année Session Admission/Admissibilité Type Rés. Etudiant Numéro
|
||||
apoL_a02_nom 1 Nom
|
||||
apoL_a03_prenom 1 Prénom
|
||||
apoL_a04_naissance Session Admissibilité Naissance
|
||||
APO_COL_VAL_DEB
|
||||
apoL_c0001 VET V1CJ 111 2007 0 1 N V1CJ - DUT CJ an1 0 1 Note
|
||||
apoL_c0002 VET V1CJ 111 2007 0 1 B 0 1 Barème
|
||||
apoL_c0003 VET V1CJ 111 2007 0 1 R 0 1 Résultat
|
||||
APO_COL_VAL_FIN
|
||||
apoL_c0030 APO_COL_VAL_FIN
|
||||
|
||||
XX-APO_VALEURS-XX
|
||||
apoL_a01_code apoL_a02_nom apoL_a03_prenom apoL_a04_naissance apoL_c0001 apoL_c0002 apoL_c0003 apoL_c0004 apoL_c0005 apoL_c0006 apoL_c0007 apoL_c0008 apoL_c0009 apoL_c0010 apoL_c0011 apoL_c0012 apoL_c0013 apoL_c0014 apoL_c0015 apoL_c0016 apoL_c0017 apoL_c0018 apoL_c0019 apoL_c0020 apoL_c0021 apoL_c0022 apoL_c0023 apoL_c0024 apoL_c0025 apoL_c0026 apoL_c0027 apoL_c0028 apoL_c0029
|
||||
10601232 AARIF MALIKA 22/09/1986 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM 18 20 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM
|
||||
</pre>
|
||||
|
||||
|
||||
On récupère nos éléments pédagogiques dans la section XX-APO-COLONNES-XX et
|
||||
notre liste d'étudiants dans la section XX-APO_VALEURS-XX. Les champs de la
|
||||
section XX-APO_VALEURS-XX sont décrits par les lignes successives de la
|
||||
section XX-APO_COLONNES-XX.
|
||||
|
||||
Le fichier CSV correspond à une étape, qui est récupérée sur la ligne
|
||||
<pre>
|
||||
apoL_c0001 VET V1CJ ...
|
||||
</pre>
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import io
|
||||
import pprint
|
||||
import re
|
||||
|
||||
# Pour la détection auto de l'encodage des fichiers Apogée:
|
||||
from chardet import detect as chardet_detect
|
||||
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoFormatError
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
APO_PORTAL_ENCODING = (
|
||||
"utf8" # encodage du fichier CSV Apogée (était 'ISO-8859-1' avant jul. 2016)
|
||||
)
|
||||
APO_INPUT_ENCODING = "ISO-8859-1" #
|
||||
APO_OUTPUT_ENCODING = APO_INPUT_ENCODING # encodage des fichiers Apogee générés
|
||||
APO_DECIMAL_SEP = "," # separateur décimal: virgule
|
||||
APO_SEP = "\t"
|
||||
APO_NEWLINE = "\r\n"
|
||||
|
||||
ApoEtudTuple = namedtuple("ApoEtudTuple", ("nip", "nom", "prenom", "naissance", "cols"))
|
||||
|
||||
|
||||
class DictCol(dict):
|
||||
"A dict, where we can add attributes"
|
||||
|
||||
|
||||
class StringIOWithLineNumber(io.StringIO):
|
||||
"simple wrapper to use a string as a file with line numbers"
|
||||
|
||||
def __init__(self, data: str):
|
||||
super().__init__(data)
|
||||
self.lineno = 0
|
||||
|
||||
def readline(self):
|
||||
self.lineno += 1
|
||||
return super().readline()
|
||||
|
||||
|
||||
class ApoCSVReadWrite:
|
||||
"Gestion lecture/écriture de fichiers csv Apogée"
|
||||
|
||||
def __init__(self, data: str):
|
||||
if not data:
|
||||
raise ScoFormatError("Fichier Apogée vide !")
|
||||
self.data = data
|
||||
self._file = StringIOWithLineNumber(data) # pour traiter comme un fichier
|
||||
self.apo_elts: dict = None
|
||||
self.cols: dict[str, dict[str, str]] = None
|
||||
self.column_titles: str = None
|
||||
self.col_ids: list[str] = None
|
||||
self.csv_etuds: list[ApoEtudTuple] = []
|
||||
# section_str: utilisé pour ré-écrire les headers sans aucune altération
|
||||
self.sections_str: dict[str, str] = {}
|
||||
"contenu initial de chaque section"
|
||||
# self.header: str = ""
|
||||
# "début du fichier Apogée jusqu'à XX-APO_TYP_RES-XX non inclu (sera ré-écrit non modifié)"
|
||||
self.header_apo_typ_res: str = ""
|
||||
"section XX-APO_TYP_RES-XX (qui peut en option ne pas être ré-écrite)"
|
||||
self.titles: dict[str, str] = {}
|
||||
"titres Apogée (section XX-APO_TITRES-XX)"
|
||||
|
||||
self.read_sections()
|
||||
|
||||
# Check that we have collected all requested infos:
|
||||
if not self.header_apo_typ_res:
|
||||
# on pourrait rendre XX-APO_TYP_RES-XX optionnelle mais mieux vaut vérifier:
|
||||
raise ScoFormatError(
|
||||
"format incorrect: pas de XX-APO_TYP_RES-XX",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
if self.cols is None:
|
||||
raise ScoFormatError(
|
||||
"format incorrect: pas de XX-APO_COLONNES-XX",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
if self.column_titles is None:
|
||||
raise ScoFormatError(
|
||||
"format incorrect: pas de XX-APO_VALEURS-XX",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
|
||||
def read_sections(self):
|
||||
"""Lit une à une les sections du fichier Apogée"""
|
||||
# sanity check: we are at the begining of Apogee CSV
|
||||
start_pos = self._file.tell()
|
||||
section = self._file.readline().strip()
|
||||
if section != "XX-APO_TITRES-XX":
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
|
||||
|
||||
while True:
|
||||
self.read_section(section)
|
||||
line, end_pos = _apo_next_non_blank_line(self._file)
|
||||
self.sections_str[section] = self.data[start_pos:end_pos]
|
||||
if not line:
|
||||
break
|
||||
section = line
|
||||
start_pos = end_pos
|
||||
|
||||
def read_section(self, section_name: str):
|
||||
"""Read a section: _file is on the first line after section title"""
|
||||
if section_name == "XX-APO_TITRES-XX":
|
||||
# Titres:
|
||||
# on va y chercher apoC_Fichier_Exp qui donnera le nom du fichier
|
||||
# ainsi que l'année scolaire et le code diplôme.
|
||||
self.titles = self._apo_read_titres(self._file)
|
||||
elif section_name == "XX-APO_TYP_RES-XX":
|
||||
self.header_apo_typ_res = _apo_read_typ_res(self._file)
|
||||
elif section_name == "XX-APO_COLONNES-XX":
|
||||
self.cols = self.apo_read_cols()
|
||||
self.apo_elts = self.group_elt_cols(self.cols)
|
||||
elif section_name == "XX-APO_VALEURS-XX":
|
||||
# les étudiants
|
||||
self.apo_read_section_valeurs()
|
||||
else:
|
||||
raise ScoFormatError(
|
||||
f"format incorrect: section inconnue: {section_name}",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
|
||||
def apo_read_cols(self):
|
||||
"""Lecture colonnes apo :
|
||||
Démarre après la balise XX-APO_COLONNES-XX
|
||||
et s'arrête après la ligne suivant la balise APO_COL_VAL_FIN
|
||||
|
||||
Colonne Apogee: les champs sont données par la ligne
|
||||
apoL_a01_code de la section XX-APO_COLONNES-XX
|
||||
col_id est apoL_c0001, apoL_c0002, ...
|
||||
|
||||
:return: { col_id : { title : value } }
|
||||
Example: { 'apoL_c0001' : { 'Type Objet' : 'VET', 'Code' : 'V1IN', ... }, ... }
|
||||
"""
|
||||
line = self._file.readline().strip(" " + APO_NEWLINE)
|
||||
fields = line.split(APO_SEP)
|
||||
if fields[0] != "apoL_a01_code":
|
||||
raise ScoFormatError(
|
||||
f"invalid line: {line} (expecting apoL_a01_code)",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
col_keys = fields
|
||||
|
||||
while True: # skip premiere partie (apoL_a02_nom, ...)
|
||||
line = self._file.readline().strip(" " + APO_NEWLINE)
|
||||
if line == "APO_COL_VAL_DEB":
|
||||
break
|
||||
# après APO_COL_VAL_DEB
|
||||
cols = {}
|
||||
i = 0
|
||||
while True:
|
||||
line = self._file.readline().strip(" " + APO_NEWLINE)
|
||||
if line == "APO_COL_VAL_FIN":
|
||||
break
|
||||
i += 1
|
||||
fields = line.split(APO_SEP)
|
||||
# sanity check
|
||||
col_id = fields[0] # apoL_c0001, ...
|
||||
if col_id in cols:
|
||||
raise ScoFormatError(
|
||||
f"duplicate column definition: {col_id}",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
m = re.match(r"^apoL_c([0-9]{4})$", col_id)
|
||||
if not m:
|
||||
raise ScoFormatError(
|
||||
f"invalid column id: {line} (expecting apoL_c{col_id})",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
if int(m.group(1)) != i:
|
||||
raise ScoFormatError(
|
||||
f"invalid column id: {col_id} for index {i}",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
|
||||
cols[col_id] = DictCol(list(zip(col_keys, fields)))
|
||||
cols[col_id].lineno = self._file.lineno # for debuging purpose
|
||||
|
||||
self._file.readline() # skip next line
|
||||
|
||||
return cols
|
||||
|
||||
def group_elt_cols(self, cols) -> dict:
|
||||
"""Return (ordered) dict of ApoElt from list of ApoCols.
|
||||
Clé: id apogée, eg 'V1RT', 'V1GE2201', ...
|
||||
Valeur: ApoElt, avec les attributs code, type_objet
|
||||
|
||||
Si les id Apogée ne sont pas uniques (ce n'est pas garanti), garde le premier
|
||||
"""
|
||||
elts = {}
|
||||
for col_id in sorted(list(cols.keys()), reverse=True):
|
||||
col = cols[col_id]
|
||||
if col["Code"] in elts:
|
||||
elts[col["Code"]].append(col)
|
||||
else:
|
||||
elts[col["Code"]] = ApoElt([col])
|
||||
return elts # { code apo : ApoElt }
|
||||
|
||||
def apo_read_section_valeurs(self):
|
||||
"traitement de la section XX-APO_VALEURS-XX"
|
||||
self.column_titles = self._file.readline()
|
||||
self.col_ids = self.column_titles.strip().split()
|
||||
self.csv_etuds = self.apo_read_etuds()
|
||||
|
||||
def apo_read_etuds(self) -> list[ApoEtudTuple]:
|
||||
"""Lecture des étudiants (et résultats) du fichier CSV Apogée.
|
||||
Les lignes "étudiant" commencent toujours par
|
||||
`12345678 NOM PRENOM 15/05/2003`
|
||||
le premier code étant le NIP.
|
||||
"""
|
||||
etud_tuples = []
|
||||
while True:
|
||||
line = self._file.readline()
|
||||
# cette section est impérativement la dernière du fichier
|
||||
# donc on arrête ici:
|
||||
if not line:
|
||||
break
|
||||
if not line.strip():
|
||||
continue # silently ignore blank lines
|
||||
line = line.strip(APO_NEWLINE)
|
||||
fields = line.split(APO_SEP)
|
||||
if len(fields) < 4:
|
||||
raise ScoFormatError(
|
||||
"""Ligne étudiant invalide
|
||||
(doit commencer par 'NIP NOM PRENOM dd/mm/yyyy')""",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
cols = {} # { col_id : value }
|
||||
for i, field in enumerate(fields):
|
||||
cols[self.col_ids[i]] = field
|
||||
etud_tuples.append(
|
||||
ApoEtudTuple(
|
||||
nip=fields[0], # id etudiant
|
||||
nom=fields[1],
|
||||
prenom=fields[2],
|
||||
naissance=fields[3],
|
||||
cols=cols,
|
||||
)
|
||||
# XXX à remettre dans apogee_csv.py
|
||||
# export_res_etape=self.export_res_etape,
|
||||
# export_res_sem=self.export_res_sem,
|
||||
# export_res_ues=self.export_res_ues,
|
||||
# export_res_modules=self.export_res_modules,
|
||||
# export_res_sdj=self.export_res_sdj,
|
||||
# export_res_rat=self.export_res_rat,
|
||||
# )
|
||||
)
|
||||
|
||||
return etud_tuples
|
||||
|
||||
def _apo_read_titres(self, f) -> dict:
|
||||
"Lecture section TITRES du fichier Apogée, renvoie dict"
|
||||
d = {}
|
||||
while True:
|
||||
line = f.readline().strip(
|
||||
" " + APO_NEWLINE
|
||||
) # ne retire pas le \t (pour les clés vides)
|
||||
if not line.strip(): # stoppe sur ligne pleines de \t
|
||||
break
|
||||
|
||||
fields = line.split(APO_SEP)
|
||||
if len(fields) == 2:
|
||||
k, v = fields
|
||||
else:
|
||||
log(f"Error read CSV: \nline={line}\nfields={fields}")
|
||||
log(dir(f))
|
||||
raise ScoFormatError(
|
||||
f"Fichier Apogee incorrect (section titres, {len(fields)} champs au lieu de 2)",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
d[k] = v
|
||||
#
|
||||
if not d.get("apoC_Fichier_Exp", None):
|
||||
raise ScoFormatError(
|
||||
"Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp",
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
# keep only basename: may be a windows or unix pathname
|
||||
s = d["apoC_Fichier_Exp"].split("/")[-1]
|
||||
s = s.split("\\")[-1] # for DOS paths, eg C:\TEMP\VL4RT_V3ASR.TXT
|
||||
d["apoC_Fichier_Exp"] = s
|
||||
return d
|
||||
|
||||
def get_filename(self) -> str:
|
||||
"""Le nom du fichier APogée, tel qu'indiqué dans le fichier
|
||||
ou vide."""
|
||||
if self.titles:
|
||||
return self.titles.get("apoC_Fichier_Exp", "")
|
||||
return ""
|
||||
|
||||
def write(self, apo_etuds: list["ApoEtud"]) -> bytes:
|
||||
"""Renvoie le contenu actualisé du fichier Apogée"""
|
||||
f = io.StringIO()
|
||||
self._write_header(f)
|
||||
self._write_etuds(f, apo_etuds)
|
||||
return f.getvalue().encode(APO_OUTPUT_ENCODING)
|
||||
|
||||
def _write_etuds(self, f, apo_etuds: list["ApoEtud"]):
|
||||
"""write apo CSV etuds on f"""
|
||||
for apo_etud in apo_etuds:
|
||||
fields = [] # e['nip'], e['nom'], e['prenom'], e['naissance'] ]
|
||||
for col_id in self.col_ids:
|
||||
try:
|
||||
fields.append(str(apo_etud.new_cols[col_id]))
|
||||
except KeyError:
|
||||
log(
|
||||
f"""Error: {apo_etud["nip"]} {apo_etud["nom"]} missing column key {col_id}
|
||||
Details:\napo_etud = {pprint.pformat(apo_etud)}
|
||||
col_ids={pprint.pformat(self.col_ids)}
|
||||
étudiant ignoré.
|
||||
"""
|
||||
)
|
||||
f.write(APO_SEP.join(fields) + APO_NEWLINE)
|
||||
|
||||
def _write_header(self, f):
|
||||
"""write apo CSV header on f
|
||||
(beginning of CSV until columns titles just after XX-APO_VALEURS-XX line)
|
||||
"""
|
||||
remove_typ_res = sco_preferences.get_preference("export_res_remove_typ_res")
|
||||
for section, data in self.sections_str.items():
|
||||
# ne recopie pas la section résultats, et en option supprime APO_TYP_RES
|
||||
if (section != "XX-APO_VALEURS-XX") and (
|
||||
section != "XX-APO_TYP_RES-XX" or not remove_typ_res
|
||||
):
|
||||
f.write(data)
|
||||
|
||||
f.write("XX-APO_VALEURS-XX" + APO_NEWLINE)
|
||||
f.write(self.column_titles)
|
||||
|
||||
|
||||
class ApoElt:
|
||||
"""Définition d'un Element Apogée
|
||||
sur plusieurs colonnes du fichier CSV
|
||||
"""
|
||||
|
||||
def __init__(self, cols):
|
||||
assert len(cols) > 0
|
||||
assert len(set([c["Code"] for c in cols])) == 1 # colonnes de meme code
|
||||
assert len(set([c["Type Objet"] for c in cols])) == 1 # colonnes de meme type
|
||||
self.cols = cols
|
||||
self.code = cols[0]["Code"]
|
||||
self.version = cols[0]["Version"]
|
||||
self.type_objet = cols[0]["Type Objet"]
|
||||
|
||||
def append(self, col):
|
||||
"""ajoute une "colonne" à l'élément"""
|
||||
assert col["Code"] == self.code
|
||||
if col["Type Objet"] != self.type_objet:
|
||||
log(
|
||||
f"""Warning: ApoElt: duplicate id {
|
||||
self.code} ({self.type_objet} and {col["Type Objet"]})"""
|
||||
)
|
||||
self.type_objet = col["Type Objet"]
|
||||
self.cols.append(col)
|
||||
|
||||
def __repr__(self):
|
||||
return f"ApoElt(code='{self.code}', cols={pprint.pformat(self.cols)})"
|
||||
|
||||
|
||||
def guess_data_encoding(text: bytes, threshold=0.6):
|
||||
"""Guess string encoding, using chardet heuristics.
|
||||
Returns encoding, or None if detection failed (confidence below threshold)
|
||||
"""
|
||||
r = chardet_detect(text)
|
||||
if r["confidence"] < threshold:
|
||||
return None
|
||||
else:
|
||||
return r["encoding"]
|
||||
|
||||
|
||||
def fix_data_encoding(
|
||||
text: bytes,
|
||||
default_source_encoding=APO_INPUT_ENCODING,
|
||||
dest_encoding=APO_INPUT_ENCODING,
|
||||
) -> tuple[bytes, str]:
|
||||
"""Try to ensure that text is using dest_encoding
|
||||
returns converted text, and a message describing the conversion.
|
||||
|
||||
Raises UnicodeEncodeError en cas de problème, en général liée à
|
||||
une auto-détection errornée.
|
||||
"""
|
||||
message = ""
|
||||
detected_encoding = guess_data_encoding(text)
|
||||
if not detected_encoding:
|
||||
if default_source_encoding != dest_encoding:
|
||||
message = f"converting from {default_source_encoding} to {dest_encoding}"
|
||||
text = text.decode(default_source_encoding).encode(dest_encoding)
|
||||
else:
|
||||
if detected_encoding != dest_encoding:
|
||||
message = (
|
||||
f"converting from detected {default_source_encoding} to {dest_encoding}"
|
||||
)
|
||||
text = text.decode(detected_encoding).encode(dest_encoding)
|
||||
return text, message
|
||||
|
||||
|
||||
def _apo_read_typ_res(f) -> str:
|
||||
"Lit la section XX-APO_TYP_RES-XX"
|
||||
text = "XX-APO_TYP_RES-XX" + APO_NEWLINE
|
||||
while True:
|
||||
line = f.readline()
|
||||
stripped_line = line.strip()
|
||||
if not stripped_line:
|
||||
break
|
||||
text += line
|
||||
return text
|
||||
|
||||
|
||||
def _apo_next_non_blank_line(f: StringIOWithLineNumber) -> tuple[str, int]:
|
||||
"Ramène prochaine ligne non blanche, stripped, et l'indice de son début"
|
||||
while True:
|
||||
pos = f.tell()
|
||||
line = f.readline()
|
||||
if not line:
|
||||
return "", -1
|
||||
stripped_line = line.strip()
|
||||
if stripped_line:
|
||||
return stripped_line, pos
|
@ -64,7 +64,7 @@ from flask import flash, g, request, url_for
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from config import Config
|
||||
from app import log
|
||||
from app import log, ScoDocJSONEncoder
|
||||
from app.but import jury_but_pv
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
@ -365,7 +365,7 @@ def do_formsemestre_archive(
|
||||
|
||||
# Bulletins en JSON
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
||||
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
|
||||
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
|
||||
if data:
|
||||
PVArchive.store(archive_id, "Bulletins.json", data_js)
|
||||
# Décisions de jury, en XLS
|
||||
|
@ -33,8 +33,9 @@ import email
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from flask import g, request
|
||||
from flask import flash, jsonify, render_template, url_for
|
||||
from flask import g, request, Response
|
||||
from flask import flash, render_template, url_for
|
||||
from flask_json import json_response
|
||||
from flask_login import current_user
|
||||
|
||||
from app import email
|
||||
@ -79,14 +80,14 @@ def get_formsemestre_bulletin_etud_json(
|
||||
etud: Identite,
|
||||
force_publishing=False,
|
||||
version="long",
|
||||
) -> str:
|
||||
) -> Response:
|
||||
"""Le JSON du bulletin d'un étudiant, quel que soit le type de formation."""
|
||||
if formsemestre.formation.is_apc():
|
||||
bulletins_sem = bulletin_but.BulletinBUT(formsemestre)
|
||||
if not etud.id in bulletins_sem.res.identdict:
|
||||
return json_error(404, "get_formsemestre_bulletin_etud_json: invalid etud")
|
||||
return jsonify(
|
||||
bulletins_sem.bulletin_etud(
|
||||
return json_response(
|
||||
data_=bulletins_sem.bulletin_etud(
|
||||
etud,
|
||||
formsemestre,
|
||||
force_publishing=force_publishing,
|
||||
@ -143,7 +144,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
"""
|
||||
from app.scodoc import sco_abs
|
||||
|
||||
if not version in scu.BULLETINS_VERSIONS:
|
||||
if version not in scu.BULLETINS_VERSIONS:
|
||||
raise ValueError("invalid version code !")
|
||||
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
|
@ -167,8 +167,9 @@ class BulletinGenerator:
|
||||
|
||||
formsemestre_id = self.bul_dict["formsemestre_id"]
|
||||
nomprenom = self.bul_dict["etud"]["nomprenom"]
|
||||
etat_civil = self.bul_dict["etud"]["etat_civil"]
|
||||
marque_debut_bulletin = sco_pdf.DebutBulletin(
|
||||
nomprenom,
|
||||
self.bul_dict["etat_civil"],
|
||||
filigranne=self.bul_dict["filigranne"],
|
||||
footer_content=f"""ScoDoc - Bulletin de {nomprenom} - {time.strftime("%d/%m/%Y %H:%M")}""",
|
||||
)
|
||||
@ -211,7 +212,7 @@ class BulletinGenerator:
|
||||
document,
|
||||
author="%s %s (E. Viennet) [%s]"
|
||||
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
|
||||
title=f"""Bulletin {sem["titremois"]} de {nomprenom}""",
|
||||
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
|
||||
subject="Bulletin de note",
|
||||
margins=self.margins,
|
||||
server_name=self.server_name,
|
||||
|
@ -33,6 +33,7 @@ import json
|
||||
|
||||
from flask import abort
|
||||
|
||||
from app import ScoDocJSONEncoder
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import but_validations
|
||||
@ -74,7 +75,7 @@ def make_json_formsemestre_bulletinetud(
|
||||
version=version,
|
||||
)
|
||||
|
||||
return json.dumps(d, cls=scu.ScoDocJSONEncoder)
|
||||
return json.dumps(d, cls=ScoDocJSONEncoder)
|
||||
|
||||
|
||||
# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
|
||||
@ -387,10 +388,10 @@ def _list_modimpls(
|
||||
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
|
||||
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
|
||||
if prefs["bul_show_minmax_eval"]:
|
||||
eval_dict["min"] = scu.fmt_note(etat["mini"])
|
||||
eval_dict["max"] = scu.fmt_note(etat["maxi"])
|
||||
eval_dict["min"] = etat["mini"] # chaine, sur 20
|
||||
eval_dict["max"] = etat["maxi"]
|
||||
if prefs["bul_show_moypromo"]:
|
||||
eval_dict["moy"] = scu.fmt_note(etat["moy"])
|
||||
eval_dict["moy"] = etat["moy"]
|
||||
|
||||
mod_dict["evaluation"].append(eval_dict)
|
||||
|
||||
|
@ -69,6 +69,7 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import gen_tables
|
||||
|
||||
|
||||
# Important: Le nom de la classe ne doit pas changer (bien le choisir),
|
||||
# car il sera stocké en base de données (dans les préférences)
|
||||
class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
@ -685,10 +686,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
|
||||
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
|
||||
if prefs["bul_show_minmax_eval"]:
|
||||
t["min"] = scu.fmt_note(etat["mini"])
|
||||
t["max"] = scu.fmt_note(etat["maxi"])
|
||||
t["min"] = etat["mini"]
|
||||
t["max"] = etat["maxi"]
|
||||
if prefs["bul_show_moypromo"]:
|
||||
t["moy"] = scu.fmt_note(etat["moy"])
|
||||
t["moy"] = etat["moy"]
|
||||
P.append(t)
|
||||
nbeval += 1
|
||||
return nbeval
|
||||
|
@ -31,7 +31,7 @@ from app import db
|
||||
from app.but import apc_edit_ue
|
||||
from app.models import UniteEns, Matiere, Module, FormSemestre, ModuleImpl
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc import codes_cursus
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
@ -74,7 +74,11 @@ def html_edit_formation_apc(
|
||||
ues_by_sem[semestre_idx] = formation.ues.filter_by(
|
||||
semestre_idx=semestre_idx
|
||||
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
||||
ects = [ue.ects for ue in ues_by_sem[semestre_idx] if ue.type != UE_SPORT]
|
||||
ects = [
|
||||
ue.ects
|
||||
for ue in ues_by_sem[semestre_idx]
|
||||
if ue.type != codes_cursus.UE_SPORT
|
||||
]
|
||||
if None in ects:
|
||||
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
||||
else:
|
||||
@ -107,7 +111,8 @@ def html_edit_formation_apc(
|
||||
icons=icons,
|
||||
ues_by_sem=ues_by_sem,
|
||||
ects_by_sem=ects_by_sem,
|
||||
form_ue_choix_niveau=apc_edit_ue.form_ue_choix_niveau,
|
||||
scu=scu,
|
||||
codes_cursus=codes_cursus,
|
||||
),
|
||||
]
|
||||
for semestre_idx in semestre_ids:
|
||||
@ -118,7 +123,7 @@ def html_edit_formation_apc(
|
||||
Matiere.ue_id == UniteEns.id,
|
||||
UniteEns.formation_id == formation.id,
|
||||
UniteEns.semestre_idx == semestre_idx,
|
||||
UniteEns.type != UE_SPORT,
|
||||
UniteEns.type != codes_cursus.UE_SPORT,
|
||||
).first()
|
||||
H += [
|
||||
render_template(
|
||||
|
@ -30,6 +30,7 @@
|
||||
"""
|
||||
import re
|
||||
|
||||
import sqlalchemy as sa
|
||||
import flask
|
||||
from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
@ -127,7 +128,7 @@ def do_ue_create(args):
|
||||
):
|
||||
# évite les conflits de code
|
||||
while True:
|
||||
cursor = db.session.execute("select notes_newid_ucod();")
|
||||
cursor = db.session.execute(sa.text("select notes_newid_ucod();"))
|
||||
code = cursor.fetchone()[0]
|
||||
if UniteEns.query.filter_by(ue_code=code).count() == 0:
|
||||
break
|
||||
@ -368,7 +369,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"min_value": 0,
|
||||
"max_value": 1000,
|
||||
"title": "ECTS",
|
||||
"explanation": "nombre de crédits ECTS (indiquer 0 si UE bonus)",
|
||||
"explanation": "nombre de crédits ECTS (indiquer 0 si UE bonus)"
|
||||
+ (
|
||||
". (si les ECTS dépendent du parcours, voir plus bas.)"
|
||||
if is_apc
|
||||
else ""
|
||||
),
|
||||
"allow_null": not is_apc, # ects requis en APC
|
||||
},
|
||||
),
|
||||
@ -470,9 +476,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
cancelbutton="Revenir à la formation",
|
||||
)
|
||||
if tf[0] == 0:
|
||||
niveau_competence_div = ""
|
||||
ue_parcours_div = ""
|
||||
if ue and is_apc:
|
||||
niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(ue)
|
||||
ue_parcours_div = apc_edit_ue.form_ue_choix_parcours(ue)
|
||||
if ue and ue.modules.count() and ue.semestre_idx is not None:
|
||||
modules_div = f"""<div id="ue_list_modules">
|
||||
<div><b>{ue.modules.count()} modules sont rattachés
|
||||
@ -502,7 +508,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"\n".join(H)
|
||||
+ tf[1]
|
||||
+ clone_form
|
||||
+ niveau_competence_div
|
||||
+ ue_parcours_div
|
||||
+ modules_div
|
||||
+ bonus_div
|
||||
+ ue_div
|
||||
@ -737,8 +743,10 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
)
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||
javascripts=[
|
||||
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
||||
+ [
|
||||
"libjs/jinplace-1.2.1.min.js",
|
||||
"js/ue_list.js",
|
||||
"js/edit_ue.js",
|
||||
@ -822,7 +830,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
<a href="{url_for('notes.refcomp_show',
|
||||
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
|
||||
class="stdlink">
|
||||
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
|
||||
{formation.referentiel_competence.type_titre}
|
||||
{formation.referentiel_competence.specialite_long}
|
||||
</a> """
|
||||
msg_refcomp = "changer"
|
||||
H.append(f"""<ul><li>{descr_refcomp}""")
|
||||
@ -838,9 +847,23 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
)
|
||||
elif formation.referentiel_competence is not None:
|
||||
H.append("""(non modifiable car utilisé par des semestres)""")
|
||||
H.append("</li>")
|
||||
if formation.referentiel_competence is not None:
|
||||
H.append(
|
||||
"""<li>Parcours, compétences et UEs :
|
||||
<div class="formation_parcs">
|
||||
"""
|
||||
)
|
||||
for parc in formation.referentiel_competence.parcours:
|
||||
H.append(
|
||||
f"""<div><a href="{url_for("notes.parcour_formation",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation.id, parcour_id=parc.id )
|
||||
}">{parc.code}</a></div>"""
|
||||
)
|
||||
H.append("""</div></li>""")
|
||||
|
||||
H.append(
|
||||
f"""</li>
|
||||
f"""
|
||||
<li> <a class="stdlink" href="{
|
||||
url_for('notes.edit_modules_ue_coefs',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
|
||||
@ -855,7 +878,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
<div class="formation_ue_list">
|
||||
<div class="ue_list_tit">Programme pédagogique:</div>
|
||||
<form>
|
||||
<input type="checkbox" class="sco_tag_checkbox">montrer les tags</input>
|
||||
<input type="checkbox" class="sco_tag_checkbox">montrer les tags des modules</input>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
@ -1428,7 +1451,7 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
||||
if ues and ues[0]["ue_id"] != ue_id:
|
||||
raise ScoValueError(
|
||||
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
||||
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
||||
(chaque UE doit avoir un acronyme unique dans la formation.)"""
|
||||
)
|
||||
# On ne peut pas supprimer le code UE:
|
||||
if "ue_code" in args and not args["ue_code"]:
|
||||
|
@ -76,7 +76,7 @@ import re
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_archives
|
||||
from app.scodoc import sco_apogee_csv
|
||||
from app.scodoc import sco_apogee_csv, sco_apogee_reader
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
|
||||
# sanity check
|
||||
filesize = len(csv_data)
|
||||
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
|
||||
raise ScoValueError("Fichier csv de taille invalide ! (%d)" % filesize)
|
||||
raise ScoValueError(f"Fichier csv de taille invalide ! ({filesize})")
|
||||
|
||||
if not annee_scolaire:
|
||||
raise ScoValueError("Impossible de déterminer l'année scolaire !")
|
||||
@ -121,13 +121,13 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
|
||||
|
||||
if str(apo_data.etape) in apo_csv_list_stored_etapes(annee_scolaire, sem_id=sem_id):
|
||||
raise ScoValueError(
|
||||
"Etape %s déjà stockée pour cette année scolaire !" % apo_data.etape
|
||||
f"Etape {apo_data.etape} déjà stockée pour cette année scolaire !"
|
||||
)
|
||||
|
||||
oid = "%d-%d" % (annee_scolaire, sem_id)
|
||||
description = "%s;%s;%s" % (str(apo_data.etape), annee_scolaire, sem_id)
|
||||
oid = f"{annee_scolaire}-{sem_id}"
|
||||
description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}"""
|
||||
archive_id = ApoCSVArchive.create_obj_archive(oid, description)
|
||||
csv_data_bytes = csv_data.encode(sco_apogee_csv.APO_OUTPUT_ENCODING)
|
||||
csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING)
|
||||
ApoCSVArchive.store(archive_id, filename, csv_data_bytes)
|
||||
|
||||
return apo_data.etape
|
||||
@ -212,7 +212,7 @@ def apo_csv_get(etape_apo="", annee_scolaire="", sem_id="") -> str:
|
||||
data = ApoCSVArchive.get(archive_id, etape_apo + ".csv")
|
||||
# ce fichier a été archivé donc généré par ScoDoc
|
||||
# son encodage est donc APO_OUTPUT_ENCODING
|
||||
return data.decode(sco_apogee_csv.APO_OUTPUT_ENCODING)
|
||||
return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
|
@ -32,13 +32,13 @@ import io
|
||||
from zipfile import ZipFile
|
||||
|
||||
import flask
|
||||
from flask import flash, g, request, send_file, url_for
|
||||
from flask import flash, g, request, Response, send_file, url_for
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Formation
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_apogee_csv
|
||||
from app.scodoc import sco_apogee_csv, sco_apogee_reader
|
||||
from app.scodoc import sco_etape_apogee
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_portal_apogee
|
||||
@ -46,7 +46,7 @@ from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_semset
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_apogee_csv import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
|
||||
from app.scodoc.sco_apogee_reader import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
@ -240,7 +240,11 @@ def apo_semset_maq_status(
|
||||
if semset["jury_ok"]:
|
||||
H.append("""<li>Décisions de jury saisies</li>""")
|
||||
else:
|
||||
H.append("""<li>Il manque des décisions de jury !</li>""")
|
||||
H.append(
|
||||
f"""<li>Il manque de {semset["jury_nb_missing"]}
|
||||
décision{"s" if semset["jury_nb_missing"] > 1 else ""}
|
||||
de jury !</li>"""
|
||||
)
|
||||
|
||||
if ok_for_export:
|
||||
H.append("""<li>%d étudiants, prêt pour l'export.</li>""" % len(nips_ok))
|
||||
@ -275,11 +279,10 @@ def apo_semset_maq_status(
|
||||
|
||||
if semset and ok_for_export:
|
||||
H.append(
|
||||
"""<form class="form_apo_export" action="apo_csv_export_results" method="get">
|
||||
f"""<form class="form_apo_export" action="apo_csv_export_results" method="get">
|
||||
<input type="submit" value="Export vers Apogée">
|
||||
<input type="hidden" name="semset_id" value="%s"/>
|
||||
<input type="hidden" name="semset_id" value="{semset_id}"/>
|
||||
"""
|
||||
% (semset_id,)
|
||||
)
|
||||
H.append('<div id="param_export_res">')
|
||||
|
||||
@ -372,7 +375,7 @@ def apo_semset_maq_status(
|
||||
H.append("</div>")
|
||||
# Aide:
|
||||
H.append(
|
||||
"""
|
||||
f"""
|
||||
<p><a class="stdlink" href="semset_page">Retour aux ensembles de semestres</a></p>
|
||||
|
||||
<div class="pas_help">
|
||||
@ -381,10 +384,12 @@ def apo_semset_maq_status(
|
||||
l'export des résultats après les jurys, puis de remplir et exporter ces fichiers.
|
||||
</p>
|
||||
<p>
|
||||
Les fichiers ("maquettes") Apogée sont de type CSV, du texte codé en %s.
|
||||
Les fichiers ("maquettes") Apogée sont de type CSV, du texte codé en {APO_INPUT_ENCODING}.
|
||||
</p>
|
||||
<p>On a un fichier par étape Apogée. Pour les obtenir, soit on peut les télécharger
|
||||
directement (si votre ScoDoc est interfacé avec Apogée), soit se débrouiller pour
|
||||
exporter le fichier texte depuis Apogée. Son contenu ressemble à cela:
|
||||
</p>
|
||||
<p>On a un fichier par étape Apogée. Pour les obtenir, soit on peut les télécharger directement (si votre ScoDoc est interfacé avec Apogée), soit se débrouiller pour exporter le fichier
|
||||
texte depuis Apogée. Son contenu ressemble à cela:</p>
|
||||
<pre class="small_pre_acc">
|
||||
XX-APO_TITRES-XX
|
||||
apoC_annee 2007/2008
|
||||
@ -427,7 +432,6 @@ def apo_semset_maq_status(
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
% (APO_INPUT_ENCODING,)
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
@ -446,21 +450,25 @@ def table_apo_csv_list(semset):
|
||||
# Ajoute qq infos pour affichage:
|
||||
csv_data = sco_etape_apogee.apo_csv_get(t["etape_apo"], annee_scolaire, sem_id)
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
|
||||
t["filename"] = apo_data.titles["apoC_Fichier_Exp"]
|
||||
t["filename"] = apo_data.apo_csv.titles["apoC_Fichier_Exp"]
|
||||
t["nb_etuds"] = len(apo_data.etuds)
|
||||
t["date_str"] = t["date"].strftime("%d/%m/%Y à %H:%M")
|
||||
view_link = "view_apo_csv?etape_apo=%s&semset_id=%s" % (
|
||||
t["etape_apo"],
|
||||
semset["semset_id"],
|
||||
view_link = url_for(
|
||||
"notes.view_apo_csv",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etape_apo=t["etape_apo"],
|
||||
semset_id=semset["semset_id"],
|
||||
)
|
||||
t["_filename_target"] = view_link
|
||||
t["_etape_apo_target"] = view_link
|
||||
t["suppress"] = scu.icontag(
|
||||
"delete_small_img", border="0", alt="supprimer", title="Supprimer"
|
||||
)
|
||||
t["_suppress_target"] = "view_apo_csv_delete?etape_apo=%s&semset_id=%s" % (
|
||||
t["etape_apo"],
|
||||
semset["semset_id"],
|
||||
t["_suppress_target"] = url_for(
|
||||
"notes.view_apo_csv_delete",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etape_apo=t["etape_apo"],
|
||||
semset_id=semset["semset_id"],
|
||||
)
|
||||
|
||||
columns_ids = ["filename", "etape_apo", "date_str", "nb_etuds"]
|
||||
@ -504,13 +512,16 @@ def view_apo_etuds(semset_id, title="", nip_list="", format="html"):
|
||||
for etud in etuds.values():
|
||||
etud_sco = sco_etud.get_etud_info(code_nip=etud["nip"], filled=True)
|
||||
if etud_sco:
|
||||
e = etud_sco[0]
|
||||
etud["inscriptions_scodoc"] = ", ".join(
|
||||
[
|
||||
'<a href="formsemestre_bulletinetud?formsemestre_id={s[formsemestre_id]}&etudid={e[etudid]}">{s[etapes_apo_str]} (S{s[semestre_id]})</a>'.format(
|
||||
s=sem, e=e
|
||||
)
|
||||
for sem in e["sems"]
|
||||
f"""<a href="{
|
||||
url_for('notes.formsemestre_bulletinetud',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem["formsemestre_id"],
|
||||
etudid=etud_sco[0]["etudid"])
|
||||
}">{sem["etapes_apo_str"]} (S{sem["semestre_id"]})</a>
|
||||
"""
|
||||
for sem in etud_sco[0]["sems"]
|
||||
]
|
||||
)
|
||||
|
||||
@ -534,8 +545,8 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
|
||||
tgt = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"])
|
||||
e["_nom_target"] = tgt
|
||||
e["_prenom_target"] = tgt
|
||||
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_nom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
|
||||
e["_prenom_td_attrs"] = f"""id="pre-{e['etudid']}" class="etudinfo" """
|
||||
|
||||
return _view_etuds_page(
|
||||
semset_id,
|
||||
@ -546,20 +557,14 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
|
||||
)
|
||||
|
||||
|
||||
def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
|
||||
def _view_etuds_page(
|
||||
semset_id: int, title="", etuds: list = None, keys=(), format="html"
|
||||
) -> str:
|
||||
"Affiche les étudiants indiqués"
|
||||
# Tri les étudiants par nom:
|
||||
if etuds:
|
||||
if etuds: # XXX TODO modifier pour utiliser clé de tri
|
||||
etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title=title,
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
),
|
||||
"<h2>%s</h2>" % title,
|
||||
]
|
||||
|
||||
tab = GenTable(
|
||||
titles={
|
||||
"nip": "Code NIP",
|
||||
@ -579,14 +584,23 @@ def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
|
||||
if format != "html":
|
||||
return tab.make_page(format=format)
|
||||
|
||||
H.append(tab.html())
|
||||
return f"""
|
||||
{html_sco_header.sco_header(
|
||||
page_title=title,
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
)}
|
||||
<h2>{title}</h2>
|
||||
|
||||
H.append(
|
||||
"""<p><a href="apo_semset_maq_status?semset_id=%s">Retour à la page d'export Apogée</a>"""
|
||||
% semset_id
|
||||
)
|
||||
{tab.html()}
|
||||
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
<p><a href="{
|
||||
url_for("notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept, semset_id=semset_id)
|
||||
}">Retour à la page d'export Apogée</a>
|
||||
</p>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
|
||||
|
||||
def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False):
|
||||
@ -603,7 +617,7 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=
|
||||
if autodetect:
|
||||
# check encoding (although documentation states that users SHOULD upload LATIN1)
|
||||
|
||||
data, message = sco_apogee_csv.fix_data_encoding(data)
|
||||
data, message = sco_apogee_reader.fix_data_encoding(data)
|
||||
if message:
|
||||
log(f"view_apo_csv_store: {message}")
|
||||
else:
|
||||
@ -623,7 +637,7 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=
|
||||
f"""
|
||||
Erreur: l'encodage du fichier est mal détecté.
|
||||
Essayez sans auto-détection, ou vérifiez le codage et le contenu
|
||||
du fichier (qui doit être en {sco_apogee_csv.APO_INPUT_ENCODING}).
|
||||
du fichier (qui doit être en {sco_apogee_reader.APO_INPUT_ENCODING}).
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
) from exc
|
||||
@ -631,7 +645,7 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Erreur: l'encodage du fichier est incorrect.
|
||||
Vérifiez qu'il est bien en {sco_apogee_csv.APO_INPUT_ENCODING}
|
||||
Vérifiez qu'il est bien en {sco_apogee_reader.APO_INPUT_ENCODING}
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
) from exc
|
||||
@ -640,21 +654,21 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=
|
||||
apo_data = sco_apogee_csv.ApoData(
|
||||
data_str, periode=semset["sem_id"]
|
||||
) # parse le fichier -> exceptions
|
||||
if apo_data.etape not in semset["etapes"]:
|
||||
raise ScoValueError(
|
||||
"Le code étape de ce fichier ne correspond pas à ceux de cet ensemble"
|
||||
)
|
||||
|
||||
sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"])
|
||||
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
dest_url = url_for(
|
||||
"notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
semset_id=semset_id,
|
||||
)
|
||||
if apo_data.etape not in semset["etapes"]:
|
||||
raise ScoValueError(
|
||||
"Le code étape de ce fichier ne correspond pas à ceux de cet ensemble",
|
||||
dest_url=dest_url,
|
||||
)
|
||||
|
||||
sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"])
|
||||
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
|
||||
"""Download maquette and store it"""
|
||||
@ -679,9 +693,8 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
|
||||
dest_url = f"apo_semset_maq_status?semset_id={semset_id}"
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
|
||||
<p>La suppression sera définitive.</p>"""
|
||||
% (etape_apo,),
|
||||
f"""<h2>Confirmer la suppression du fichier étape <tt>{etape_apo}</tt>?</h2>
|
||||
<p>La suppression sera définitive.</p>""",
|
||||
dest_url="",
|
||||
cancel_url=dest_url,
|
||||
parameters={"semset_id": semset_id, "etape_apo": etape_apo},
|
||||
@ -727,24 +740,24 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Maquette Apogée enregistrée pour %s" % etape_apo,
|
||||
page_title=f"""Maquette Apogée enregistrée pour {etape_apo}""",
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
),
|
||||
"""<h2>Etudiants dans la maquette Apogée %s</h2>""" % etape_apo,
|
||||
"""<p>Pour l'ensemble <a class="stdlink" href="apo_semset_maq_status?semset_id=%(semset_id)s">%(title)s</a> (indice semestre: %(sem_id)s)</p>"""
|
||||
% semset,
|
||||
]
|
||||
# Infos générales
|
||||
H.append(
|
||||
"""
|
||||
f"""<h2>Étudiants dans la maquette Apogée {etape_apo}</h2>
|
||||
<p>Pour l'ensemble <a class="stdlink" href="{
|
||||
url_for("notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept, semset_id=semset["semset_id"])
|
||||
}">{semset['title']}</a> (indice semestre: {semset['sem_id']})
|
||||
</p>
|
||||
<div class="apo_csv_infos">
|
||||
<div class="apo_csv_etape"><span>Code étape:</span><span>{0.etape_apogee} VDI {0.vdi_apogee} (année {0.annee_scolaire})</span></div>
|
||||
<div class="apo_csv_etape"><span>Code étape:</span><span>{
|
||||
apo_data.etape_apogee} VDI {apo_data.vdi_apogee} (année {apo_data.annee_scolaire
|
||||
})</span>
|
||||
</div>
|
||||
""".format(
|
||||
apo_data
|
||||
)
|
||||
)
|
||||
</div>
|
||||
""",
|
||||
]
|
||||
|
||||
# Liste des étudiants (sans les résultats pour le moment): TODO
|
||||
etuds = apo_data.etuds
|
||||
@ -789,12 +802,21 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
|
||||
return tab.make_page(format=format)
|
||||
|
||||
H += [
|
||||
tab.html(),
|
||||
"""<p><a class="stdlink" href="view_apo_csv?etape_apo=%s&semset_id=%s&format=raw">fichier maquette CSV brut (non rempli par ScoDoc)</a></p>"""
|
||||
% (etape_apo, semset_id),
|
||||
"""<div><a class="stdlink" href="apo_semset_maq_status?semset_id=%s">Retour</a>
|
||||
</div>"""
|
||||
% semset_id,
|
||||
f"""
|
||||
{tab.html()}
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("notes.view_apo_csv",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etape_apo=etape_apo, semset_id=semset_id, format="raw")
|
||||
}">fichier maquette CSV brut (non rempli par ScoDoc)</a>
|
||||
</p>
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept, semset_id=semset_id)
|
||||
}">Retour</a>
|
||||
</div>
|
||||
""",
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
|
||||
@ -809,7 +831,7 @@ def apo_csv_export_results(
|
||||
block_export_res_ues=False,
|
||||
block_export_res_modules=False,
|
||||
block_export_res_sdj=False,
|
||||
):
|
||||
) -> Response:
|
||||
"""Remplit les fichiers CSV archivés
|
||||
et donne un ZIP avec tous les résultats.
|
||||
"""
|
||||
@ -833,8 +855,7 @@ def apo_csv_export_results(
|
||||
periode = semset["sem_id"]
|
||||
|
||||
data = io.BytesIO()
|
||||
dest_zip = ZipFile(data, "w")
|
||||
|
||||
with ZipFile(data, "w") as dest_zip:
|
||||
etapes_apo = sco_etape_apogee.apo_csv_list_stored_etapes(
|
||||
annee_scolaire, periode, etapes=semset.list_etapes()
|
||||
)
|
||||
@ -852,12 +873,10 @@ def apo_csv_export_results(
|
||||
dest_zip=dest_zip,
|
||||
)
|
||||
|
||||
dest_zip.close()
|
||||
data.seek(0)
|
||||
basename = (
|
||||
sco_preferences.get_preference("DeptName")
|
||||
+ str(annee_scolaire)
|
||||
+ "-%s-" % periode
|
||||
+ f"{annee_scolaire}-{periode}-"
|
||||
+ "-".join(etapes_apo)
|
||||
)
|
||||
basename = scu.unescape_html(basename)
|
||||
|
@ -174,7 +174,7 @@ class DataEtudiant(object):
|
||||
return self.data_apogee["nom"] + self.data_apogee["prenom"]
|
||||
|
||||
|
||||
def help():
|
||||
def _help() -> str:
|
||||
return """
|
||||
<div id="export_help" class="pas_help"> <span>Explications sur les tableaux des effectifs et liste des
|
||||
étudiants</span>
|
||||
@ -501,7 +501,7 @@ class EtapeBilan:
|
||||
entete_liste_etudiant(),
|
||||
self.table_effectifs(),
|
||||
"""</details>""",
|
||||
help(),
|
||||
_help(),
|
||||
]
|
||||
|
||||
return "\n".join(H)
|
||||
|
@ -35,10 +35,10 @@ from operator import itemgetter
|
||||
|
||||
from flask import url_for, g
|
||||
|
||||
from app import email
|
||||
from app import db, email
|
||||
from app import log
|
||||
from app.models import Admission
|
||||
from app.models.etudiants import make_etud_args
|
||||
from app.models import Admission, Identite
|
||||
from app.models.etudiants import input_civilite, make_etud_args, pivot_year
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
@ -57,7 +57,12 @@ def format_etud_ident(etud):
|
||||
else:
|
||||
etud["nom_usuel"] = ""
|
||||
etud["prenom"] = format_prenom(etud["prenom"])
|
||||
if "prenom_etat_civil" in etud:
|
||||
etud["prenom_etat_civil"] = format_prenom(etud["prenom_etat_civil"])
|
||||
else:
|
||||
etud["prenom_etat_civil"] = ""
|
||||
etud["civilite_str"] = format_civilite(etud["civilite"])
|
||||
etud["civilite_etat_civil_str"] = format_civilite(etud["civilite_etat_civil"])
|
||||
# Nom à afficher:
|
||||
if etud["nom_usuel"]:
|
||||
etud["nom_disp"] = etud["nom_usuel"]
|
||||
@ -67,6 +72,7 @@ def format_etud_ident(etud):
|
||||
etud["nom_disp"] = etud["nom"]
|
||||
|
||||
etud["nomprenom"] = format_nomprenom(etud) # M. Pierre DUPONT
|
||||
etud["etat_civil"] = format_etat_civil(etud)
|
||||
if etud["civilite"] == "M":
|
||||
etud["ne"] = ""
|
||||
elif etud["civilite"] == "F":
|
||||
@ -122,21 +128,6 @@ def format_nom(s, uppercase=True):
|
||||
return format_prenom(s)
|
||||
|
||||
|
||||
def input_civilite(s):
|
||||
"""Converts external representation of civilite to internal:
|
||||
'M', 'F', or 'X' (and nothing else).
|
||||
Raises ScoValueError if conversion fails.
|
||||
"""
|
||||
s = s.upper().strip()
|
||||
if s in ("M", "M.", "MR", "H"):
|
||||
return "M"
|
||||
elif s in ("F", "MLLE", "MLLE.", "MELLE", "MME"):
|
||||
return "F"
|
||||
elif s == "X" or not s:
|
||||
return "X"
|
||||
raise ScoValueError("valeur invalide pour la civilité: %s" % s)
|
||||
|
||||
|
||||
def format_civilite(civilite):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
personne ne souhaitant pas d'affichage).
|
||||
@ -152,6 +143,14 @@ def format_civilite(civilite):
|
||||
raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)
|
||||
|
||||
|
||||
def format_etat_civil(etud: dict):
|
||||
if etud["prenom_etat_civil"]:
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[etud["civilite_etat_civil"]]
|
||||
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
|
||||
else:
|
||||
return etud["nomprenom"]
|
||||
|
||||
|
||||
def format_lycee(nomlycee):
|
||||
nomlycee = nomlycee.strip()
|
||||
s = nomlycee.lower()
|
||||
@ -190,21 +189,6 @@ def format_pays(s):
|
||||
return ""
|
||||
|
||||
|
||||
PIVOT_YEAR = 70
|
||||
|
||||
|
||||
def pivot_year(y):
|
||||
if y == "" or y is None:
|
||||
return None
|
||||
y = int(round(float(y)))
|
||||
if y >= 0 and y < 100:
|
||||
if y < PIVOT_YEAR:
|
||||
y = y + 2000
|
||||
else:
|
||||
y = y + 1900
|
||||
return y
|
||||
|
||||
|
||||
def etud_sort_key(etud: dict) -> tuple:
|
||||
"""Clé de tri pour les étudiants représentés par des dict (anciens codes).
|
||||
Equivalent moderne: identite.sort_key
|
||||
@ -225,7 +209,12 @@ _identiteEditor = ndb.EditableTable(
|
||||
"nom",
|
||||
"nom_usuel",
|
||||
"prenom",
|
||||
"prenom_etat_civil",
|
||||
"cas_id",
|
||||
"cas_allow_login",
|
||||
"cas_allow_scodoc_login",
|
||||
"civilite", # 'M", "F", or "X"
|
||||
"civilite_etat_civil",
|
||||
"date_naissance",
|
||||
"lieu_naissance",
|
||||
"dept_naissance",
|
||||
@ -242,7 +231,9 @@ _identiteEditor = ndb.EditableTable(
|
||||
input_formators={
|
||||
"nom": force_uppercase,
|
||||
"prenom": force_uppercase,
|
||||
"prenom_etat_civil": force_uppercase,
|
||||
"civilite": input_civilite,
|
||||
"civilite_etat_civil": input_civilite,
|
||||
"date_naissance": ndb.DateDMYtoISO,
|
||||
"boursier": bool,
|
||||
},
|
||||
@ -263,12 +254,15 @@ def identite_list(cnx, *a, **kw):
|
||||
else:
|
||||
o["annee_naissance"] = o["date_naissance"]
|
||||
o["civilite_str"] = format_civilite(o["civilite"])
|
||||
o["civilite_etat_civil_str"] = format_civilite(o["civilite_etat_civil"])
|
||||
return objs
|
||||
|
||||
|
||||
def identite_edit_nocheck(cnx, args):
|
||||
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
|
||||
_identiteEditor.edit(cnx, args)
|
||||
etud = Identite.query.get(args["etudid"])
|
||||
etud.from_dict(args)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
|
||||
@ -559,6 +553,7 @@ admission_delete = _admissionEditor.delete
|
||||
admission_list = _admissionEditor.list
|
||||
admission_edit = _admissionEditor.edit
|
||||
|
||||
|
||||
# Edition simultanee de identite et admission
|
||||
class EtudIdentEditor(object):
|
||||
def create(self, cnx, args):
|
||||
@ -602,7 +597,6 @@ class EtudIdentEditor(object):
|
||||
_etudidentEditor = EtudIdentEditor()
|
||||
etudident_list = _etudidentEditor.list
|
||||
etudident_edit = _etudidentEditor.edit
|
||||
etudident_create = _etudidentEditor.create
|
||||
|
||||
|
||||
def log_unknown_etud():
|
||||
@ -628,21 +622,8 @@ def get_etud_info(etudid=False, code_nip=False, filled=False) -> list[dict]:
|
||||
return etud
|
||||
|
||||
|
||||
# Optim par cache local, utilité non prouvée mais
|
||||
# on s'oriente vers un cahce persistent dans Redis ou bien l'utilisation de NT
|
||||
# def get_etud_info_filled_by_etudid(etudid, cnx=None) -> dict:
|
||||
# """Infos sur un étudiant, avec cache local à la requête"""
|
||||
# if etudid in g.stored_etud_info:
|
||||
# return g.stored_etud_info[etudid]
|
||||
# cnx = cnx or ndb.GetDBConnexion()
|
||||
# etud = etudident_list(cnx, args={"etudid": etudid})
|
||||
# fill_etuds_info(etud)
|
||||
# g.stored_etud_info[etudid] = etud[0]
|
||||
# return etud[0]
|
||||
|
||||
|
||||
def create_etud(cnx, args={}):
|
||||
"""Creation d'un étudiant. génère aussi évenement et "news".
|
||||
def create_etud(cnx, args: dict = None):
|
||||
"""Création d'un étudiant. Génère aussi évenement et "news".
|
||||
|
||||
Args:
|
||||
args: dict avec les attributs de l'étudiant
|
||||
@ -653,16 +634,16 @@ def create_etud(cnx, args={}):
|
||||
from app.models import ScolarNews
|
||||
|
||||
# creation d'un etudiant
|
||||
etudid = etudident_create(cnx, args)
|
||||
# crée une adresse vide (chaque etudiant doit etre dans la table "adresse" !)
|
||||
_ = adresse_create(
|
||||
cnx,
|
||||
{
|
||||
"etudid": etudid,
|
||||
"typeadresse": "domicile",
|
||||
"description": "(creation individuelle)",
|
||||
},
|
||||
)
|
||||
args_dict = Identite.convert_dict_fields(args)
|
||||
args_dict["dept_id"] = g.scodoc_dept_id
|
||||
etud = Identite.create_etud(**args_dict)
|
||||
db.session.add(etud)
|
||||
db.session.commit()
|
||||
admission = etud.admission.first()
|
||||
admission.from_dict(args)
|
||||
db.session.add(admission)
|
||||
db.session.commit()
|
||||
etudid = etud.id
|
||||
|
||||
# event
|
||||
scolar_events_create(
|
||||
|
@ -79,7 +79,7 @@ def evaluation_create_form(
|
||||
mod = modimpl_o["module"]
|
||||
formsemestre_id = modimpl_o["formsemestre_id"]
|
||||
formsemestre = modimpl.formsemestre
|
||||
sem_ues = formsemestre.query_ues(with_sport=False).all()
|
||||
sem_ues = formsemestre.get_ues(with_sport=False)
|
||||
is_malus = mod["module_type"] == ModuleType.MALUS
|
||||
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
preferences = sco_preferences.SemPreferences(formsemestre.id)
|
||||
|
@ -97,11 +97,22 @@ def ListMedian(L):
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=False):
|
||||
"""donne infos sur l'état de l'évaluation
|
||||
{ nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att,
|
||||
moyenne, mediane, mini, maxi,
|
||||
date_last_modif, gr_complets, gr_incomplets, evalcomplete }
|
||||
def do_evaluation_etat(
|
||||
evaluation_id: int, partition_id: int = None, select_first_partition=False
|
||||
) -> dict:
|
||||
"""Donne infos sur l'état de l'évaluation.
|
||||
Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul.
|
||||
{
|
||||
nb_inscrits : inscrits au module
|
||||
nb_notes
|
||||
nb_abs,
|
||||
nb_neutre,
|
||||
nb_att,
|
||||
moy, median, mini, maxi : # notes, en chaine, sur 20
|
||||
last_modif: datetime,
|
||||
gr_complets, gr_incomplets,
|
||||
evalcomplete
|
||||
}
|
||||
evalcomplete est vrai si l'eval est complete (tous les inscrits
|
||||
à ce module ont des notes)
|
||||
evalattente est vrai s'il ne manque que des notes en attente
|
||||
@ -137,7 +148,7 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
|
||||
insmod = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=E["moduleimpl_id"]
|
||||
)
|
||||
insmodset = set([x["etudid"] for x in insmod])
|
||||
insmodset = {x["etudid"] for x in insmod}
|
||||
# retire de insem ceux qui ne sont pas inscrits au module
|
||||
ins = [i for i in insem if i["etudid"] in insmodset]
|
||||
|
||||
@ -155,14 +166,13 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
|
||||
moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes)
|
||||
if moy_num is None:
|
||||
median, moy = "", ""
|
||||
median_num, moy_num = None, None
|
||||
mini, maxi = "", ""
|
||||
mini_num, maxi_num = None, None
|
||||
maxi_num = None
|
||||
else:
|
||||
median = scu.fmt_note(median_num)
|
||||
moy = scu.fmt_note(moy_num)
|
||||
mini = scu.fmt_note(mini_num)
|
||||
maxi = scu.fmt_note(maxi_num)
|
||||
moy = scu.fmt_note(moy_num, E["note_max"])
|
||||
mini = scu.fmt_note(mini_num, E["note_max"])
|
||||
maxi = scu.fmt_note(maxi_num, E["note_max"])
|
||||
# cherche date derniere modif note
|
||||
if len(etuds_notes_dict):
|
||||
t = [x["date"] for x in etuds_notes_dict.values()]
|
||||
@ -226,28 +236,22 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
|
||||
|
||||
# Calcul moyenne dans chaque groupe de TD
|
||||
gr_moyennes = [] # group : {moy,median, nb_notes}
|
||||
for group_id in GrNotes.keys():
|
||||
notes = GrNotes[group_id]
|
||||
for group_id, notes in GrNotes.items():
|
||||
gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes)
|
||||
gr_moyennes.append(
|
||||
{
|
||||
"group_id": group_id,
|
||||
"group_name": groups[group_id]["group_name"],
|
||||
"gr_moy_num": gr_moy,
|
||||
"gr_moy": scu.fmt_note(gr_moy),
|
||||
"gr_median_num": gr_median,
|
||||
"gr_median": scu.fmt_note(gr_median),
|
||||
"gr_mini": scu.fmt_note(gr_mini),
|
||||
"gr_maxi": scu.fmt_note(gr_maxi),
|
||||
"gr_mini_num": gr_mini,
|
||||
"gr_maxi_num": gr_maxi,
|
||||
"gr_moy": scu.fmt_note(gr_moy, E["note_max"]),
|
||||
"gr_median": scu.fmt_note(gr_median, E["note_max"]),
|
||||
"gr_mini": scu.fmt_note(gr_mini, E["note_max"]),
|
||||
"gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]),
|
||||
"gr_nb_notes": len(notes),
|
||||
"gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]),
|
||||
}
|
||||
)
|
||||
gr_moyennes.sort(key=operator.itemgetter("group_name"))
|
||||
|
||||
# retourne mapping
|
||||
return {
|
||||
"evaluation_id": evaluation_id,
|
||||
"nb_inscrits": nb_inscrits,
|
||||
@ -256,14 +260,11 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
|
||||
"nb_abs": nb_abs,
|
||||
"nb_neutre": nb_neutre,
|
||||
"nb_att": nb_att,
|
||||
"moy": moy,
|
||||
"moy_num": moy_num,
|
||||
"moy": moy, # chaine formattée, sur 20
|
||||
"median": median,
|
||||
"mini": mini,
|
||||
"mini_num": mini_num,
|
||||
"maxi": maxi,
|
||||
"maxi_num": maxi_num,
|
||||
"median_num": median_num,
|
||||
"maxi_num": maxi_num, # note maximale, en nombre
|
||||
"last_modif": last_modif,
|
||||
"gr_incomplets": gr_incomplets,
|
||||
"gr_moyennes": gr_moyennes,
|
||||
@ -283,18 +284,19 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
||||
[ {
|
||||
'coefficient': 1.0,
|
||||
'description': 'QCM et cas pratiques',
|
||||
'etat': {'evalattente': False,
|
||||
'etat': {
|
||||
'evalattente': False,
|
||||
'evalcomplete': True,
|
||||
'evaluation_id': 'GEAEVAL82883',
|
||||
'gr_incomplets': [],
|
||||
'gr_moyennes': [{'gr_median': '12.00',
|
||||
'gr_median_num' : 12.,
|
||||
'gr_moyennes': [{
|
||||
'gr_median': '12.00', # sur 20
|
||||
'gr_moy': '11.88',
|
||||
'gr_moy_num' : 11.88,
|
||||
'gr_nb_att': 0,
|
||||
'gr_nb_notes': 166,
|
||||
'group_id': 'GEAG266762',
|
||||
'group_name': None}],
|
||||
'group_name': None
|
||||
}],
|
||||
'groups': {'GEAG266762': {'etudid': 'GEAEID80603',
|
||||
'group_id': 'GEAG266762',
|
||||
'group_name': None,
|
||||
@ -362,7 +364,7 @@ def _eval_etat(evals):
|
||||
if last_modif is not None:
|
||||
dates.append(e["etat"]["last_modif"])
|
||||
|
||||
if len(dates):
|
||||
if dates:
|
||||
dates = scu.sort_dates(dates)
|
||||
last_modif = dates[-1] # date de derniere modif d'une note dans un module
|
||||
else:
|
||||
|
@ -29,6 +29,7 @@
|
||||
"""
|
||||
from flask_login import current_user
|
||||
|
||||
|
||||
# --- Exceptions
|
||||
class ScoException(Exception):
|
||||
"super classe de toutes les exceptions ScoDoc."
|
||||
@ -44,6 +45,7 @@ class ScoInvalidCSRF(ScoException):
|
||||
|
||||
class ScoValueError(ScoException):
|
||||
"Exception avec page d'erreur utilisateur, et qui stoque dest_url"
|
||||
|
||||
# mal nommée: super classe de toutes les exceptions avec page
|
||||
# d'erreur gentille.
|
||||
def __init__(self, msg, dest_url=None):
|
||||
@ -75,7 +77,11 @@ class InvalidEtudId(NoteProcessError):
|
||||
|
||||
|
||||
class ScoFormatError(ScoValueError):
|
||||
pass
|
||||
"Erreur lecture d'un fichier fourni par l'utilisateur"
|
||||
|
||||
def __init__(self, msg, filename="", dest_url=None):
|
||||
super().__init__(msg, dest_url=dest_url)
|
||||
self.filename = filename
|
||||
|
||||
|
||||
class ScoInvalidParamError(ScoValueError):
|
||||
|
@ -127,9 +127,7 @@ def formation_export_dict(
|
||||
ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle
|
||||
ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
|
||||
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
|
||||
# Et le parcour:
|
||||
if ue.parcour:
|
||||
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
|
||||
|
||||
# pour les coefficients:
|
||||
ue_dict["reference"] = ue.id if ue_reference_style == "id" else ue.acronyme
|
||||
if not export_ids:
|
||||
@ -266,8 +264,8 @@ def _formation_retreive_refcomp(f_dict: dict) -> int:
|
||||
def _formation_retreive_apc_niveau(
|
||||
referentiel_competence_id: int, ue_dict: dict
|
||||
) -> int:
|
||||
"""Recherche dans le ref. de comp. un niveau pour cette UE
|
||||
utilise comme clé (libelle, annee, ordre)
|
||||
"""Recherche dans le ref. de comp. un niveau pour cette UE.
|
||||
Utilise (libelle, annee, ordre) comme clé.
|
||||
"""
|
||||
libelle = ue_dict.get("apc_niveau_libelle")
|
||||
annee = ue_dict.get("apc_niveau_annee")
|
||||
@ -365,13 +363,15 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
assert ue
|
||||
if xml_ue_id:
|
||||
ues_old2new[xml_ue_id] = ue_id
|
||||
|
||||
# élément optionnel présent dans les exports BUT:
|
||||
ue_reference = ue_info[1].get("reference")
|
||||
if ue_reference:
|
||||
ue_reference_to_id[int(ue_reference)] = ue_id
|
||||
|
||||
# -- create matieres
|
||||
# -- Create matieres
|
||||
for mat_info in ue_info[2]:
|
||||
# Backward compat: un seul parcours par UE (ScoDoc < 9.4.71)
|
||||
if mat_info[0] == "parcour":
|
||||
# Parcours (BUT)
|
||||
code_parcours = mat_info[1]["code"]
|
||||
@ -380,11 +380,30 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
referentiel_id=referentiel_competence_id,
|
||||
).first()
|
||||
if parcour:
|
||||
ue.parcour = parcour
|
||||
ue.parcours = [parcour]
|
||||
db.session.add(ue)
|
||||
else:
|
||||
flash(f"Attention: parcours {code_parcours} inexistant !")
|
||||
log(f"Warning: parcours {code_parcours} inexistant !")
|
||||
continue
|
||||
elif mat_info[0] == "parcours":
|
||||
# Parcours (BUT), liste (ScoDoc > 9.4.70), avec ECTS en option
|
||||
code_parcours = mat_info[1]["code"]
|
||||
ue_parcour_ects = mat_info[1].get("ects")
|
||||
parcour = ApcParcours.query.filter_by(
|
||||
code=code_parcours,
|
||||
referentiel_id=referentiel_competence_id,
|
||||
).first()
|
||||
if parcour:
|
||||
ue.parcours.append(parcour)
|
||||
else:
|
||||
flash(f"Attention: parcours {code_parcours} inexistant !")
|
||||
log(f"Warning: parcours {code_parcours} inexistant !")
|
||||
if ue_parcour_ects is not None:
|
||||
ue.set_ects(ue_parcour_ects, parcour)
|
||||
db.session.add(ue)
|
||||
continue
|
||||
|
||||
assert mat_info[0] == "matiere"
|
||||
mat_info[1]["ue_id"] = ue_id
|
||||
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
|
||||
|
@ -37,6 +37,7 @@ from flask import flash, redirect, render_template, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app.but.cursus_but import formsemestre_warning_apc_setup
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
@ -604,7 +605,7 @@ def formsemestre_description_table(
|
||||
columns_ids += ["Coef."]
|
||||
ues = [] # liste des UE, seulement en APC pour les coefs
|
||||
else:
|
||||
ues = formsemestre.query_ues().all()
|
||||
ues = formsemestre.get_ues()
|
||||
columns_ids += [f"ue_{ue.id}" for ue in ues]
|
||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
||||
columns_ids += ["ects"]
|
||||
@ -645,16 +646,23 @@ def formsemestre_description_table(
|
||||
ects_str = ue.ects
|
||||
ue_info = {
|
||||
"UE": ue.acronyme,
|
||||
"Code": "",
|
||||
"ects": ects_str,
|
||||
"Module": ue.titre,
|
||||
"_css_row_class": "table_row_ue",
|
||||
"_UE_td_attrs": f'style="background-color: {ue.color} !important;"'
|
||||
if ue.color
|
||||
else "",
|
||||
}
|
||||
if use_ue_coefs:
|
||||
ue_info["Coef."] = ue.coefficient
|
||||
ue_info["Coef._class"] = "ue_coef"
|
||||
if ue.color:
|
||||
for k in list(ue_info.keys()):
|
||||
if not k.startswith("_"):
|
||||
ue_info[
|
||||
f"_{k}_td_attrs"
|
||||
] = f'style="background-color: {ue.color} !important;"'
|
||||
if not formsemestre.formation.is_apc():
|
||||
# n'affiche la ligne UE qu'en formation classique
|
||||
# car l'UE de rattachement n'a pas d'intérêt en BUT
|
||||
rows.append(ue_info)
|
||||
|
||||
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
@ -662,9 +670,9 @@ def formsemestre_description_table(
|
||||
)
|
||||
enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants)
|
||||
|
||||
l = {
|
||||
row = {
|
||||
"UE": modimpl.module.ue.acronyme,
|
||||
"_UE_td_attrs": ue_info["_UE_td_attrs"],
|
||||
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
|
||||
"Code": modimpl.module.code or "",
|
||||
"Module": modimpl.module.abbrev or modimpl.module.titre,
|
||||
"_Module_class": "scotext",
|
||||
@ -691,26 +699,32 @@ def formsemestre_description_table(
|
||||
sum_coef += modimpl.module.coefficient
|
||||
coef_dict = modimpl.module.get_ue_coef_dict()
|
||||
for ue in ues:
|
||||
l[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
|
||||
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
|
||||
if with_parcours:
|
||||
l["parcours"] = ", ".join(
|
||||
row["parcours"] = ", ".join(
|
||||
sorted([pa.code for pa in modimpl.module.parcours])
|
||||
)
|
||||
|
||||
rows.append(l)
|
||||
rows.append(row)
|
||||
|
||||
if with_evals:
|
||||
# Ajoute lignes pour evaluations
|
||||
evals = nt.get_mod_evaluation_etat_list(modimpl.id)
|
||||
evals.reverse() # ordre chronologique
|
||||
# Ajoute etat:
|
||||
eval_rows = []
|
||||
for eval_dict in evals:
|
||||
e = eval_dict.copy()
|
||||
e["_description_target"] = url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e["evaluation_id"],
|
||||
)
|
||||
e["_jour_order"] = e["jour"].isoformat()
|
||||
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
||||
e["UE"] = l["UE"]
|
||||
e["_UE_td_attrs"] = l["_UE_td_attrs"]
|
||||
e["Code"] = l["Code"]
|
||||
e["UE"] = row["UE"]
|
||||
e["_UE_td_attrs"] = row["_UE_td_attrs"]
|
||||
e["Code"] = row["Code"]
|
||||
e["_css_row_class"] = "evaluation"
|
||||
e["Module"] = "éval."
|
||||
# Cosmetic: conversions pour affichage
|
||||
@ -733,8 +747,9 @@ def formsemestre_description_table(
|
||||
e[f"ue_{ue_id}"] = poids or ""
|
||||
e[f"_ue_{ue_id}_class"] = "poids"
|
||||
e[f"_ue_{ue_id}_help"] = "poids vers l'UE"
|
||||
eval_rows.append(e)
|
||||
|
||||
rows += evals
|
||||
rows += eval_rows
|
||||
|
||||
sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
|
||||
rows.append(sums)
|
||||
@ -1057,6 +1072,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
formsemestre_status_head(
|
||||
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
|
||||
),
|
||||
formsemestre_warning_apc_setup(formsemestre, nt),
|
||||
formsemestre_warning_etuds_sans_note(formsemestre, nt)
|
||||
if can_change_all_notes
|
||||
else "",
|
||||
@ -1282,7 +1298,7 @@ def formsemestre_tableau_modules(
|
||||
"""
|
||||
)
|
||||
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
|
||||
coefs = mod.ue_coefs_list(ues=formsemestre.query_ues().all())
|
||||
coefs = mod.ue_coefs_list(ues=formsemestre.get_ues())
|
||||
H.append(f'<a class="invisible_link" href="#" title="{mod_descr}">')
|
||||
for coef in coefs:
|
||||
if coef[1] > 0:
|
||||
|
@ -606,7 +606,9 @@ def formsemestre_recap_parcours_table(
|
||||
else:
|
||||
# si l'étudiant n'est pas inscrit à un parcours mais que le semestre a plus d'UE
|
||||
# signale un éventuel problème:
|
||||
if nt.formsemestre.query_ues().count() > len(nt.etud_ues_ids(etudid)):
|
||||
if len(nt.formsemestre.get_ues()) > len(
|
||||
nt.etud_ues_ids(etudid)
|
||||
): # XXX sans dispenses
|
||||
parcours_name = f"""
|
||||
<span class="code_parcours no_parcours">{scu.EMO_WARNING} pas de parcours
|
||||
</span>"""
|
||||
|
@ -40,7 +40,7 @@ import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.models import ScolarNews, GroupDescr
|
||||
|
||||
from app.models.etudiants import input_civilite
|
||||
from app.scodoc.sco_excel import COLORS
|
||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||
do_formsemestre_inscription_with_modules,
|
||||
@ -71,6 +71,8 @@ FORMAT_FILE = "format_import_etudiants.txt"
|
||||
ADMISSION_MODIFIABLE_FIELDS = (
|
||||
"code_nip",
|
||||
"code_ine",
|
||||
"prenom_etat_civil",
|
||||
"civilite_etat_civil",
|
||||
"date_naissance",
|
||||
"lieu_naissance",
|
||||
"bac",
|
||||
@ -368,7 +370,7 @@ def scolars_import_excel_file(
|
||||
# xxx Ad-hoc checks (should be in format description)
|
||||
if titleslist[i].lower() == "sexe":
|
||||
try:
|
||||
val = sco_etud.input_civilite(val)
|
||||
val = input_civilite(val)
|
||||
except:
|
||||
raise ScoValueError(
|
||||
"valeur invalide pour 'SEXE' (doit etre 'M', 'F', ou 'MME', 'H', 'X' ou vide, mais pas '%s') ligne %d, colonne %s"
|
||||
|
@ -36,7 +36,13 @@ from flask_login import current_user
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
Identite,
|
||||
Partition,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
|
||||
from app import log
|
||||
from app.tables import list_etuds
|
||||
@ -517,11 +523,23 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
(UniteEns.query.get(ue_id) for ue_id in ue_ids),
|
||||
key=lambda u: (u.numero or 0, u.acronyme),
|
||||
)
|
||||
H.append("""<table><tr><th></th>""")
|
||||
H.append(
|
||||
"""<table id="but_ue_inscriptions" class="stripe compact">
|
||||
<thead>
|
||||
<tr><th>Nom</th><th>Parcours</th>
|
||||
"""
|
||||
)
|
||||
for ue in ues:
|
||||
H.append(f"""<th title="{ue.titre or ''}">{ue.acronyme}</th>""")
|
||||
H.append("""</tr>""")
|
||||
|
||||
H.append(
|
||||
"""</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
)
|
||||
partition_parcours: Partition = Partition.query.filter_by(
|
||||
formsemestre=res.formsemestre, partition_name=scu.PARTITION_PARCOURS
|
||||
).first()
|
||||
etuds = list_etuds.etuds_sorted_from_ids(table_inscr.keys())
|
||||
for etud in etuds:
|
||||
ues_etud = table_inscr[etud.id]
|
||||
@ -534,6 +552,11 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
)}"
|
||||
>{etud.nomprenom}</a></td>"""
|
||||
)
|
||||
# Parcours:
|
||||
group = partition_parcours.get_etud_group(etud.id)
|
||||
parcours_name = group.group_name if group else ""
|
||||
H.append(f"""<td class="parcours">{parcours_name}</td>""")
|
||||
# UEs:
|
||||
for ue in ues:
|
||||
td_class = ""
|
||||
est_inscr = ues_etud.get(ue.id) # None si pas concerné
|
||||
@ -568,31 +591,38 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
content = f"""<input type="checkbox"
|
||||
{'checked' if est_inscr else ''}
|
||||
{'disabled' if read_only else ''}
|
||||
title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}",
|
||||
title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}"
|
||||
onchange="change_ue_inscr(this);"
|
||||
data-url_inscr={
|
||||
data-url_inscr="{
|
||||
url_for("notes.etud_inscrit_ue",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud.id,
|
||||
formsemestre_id=res.formsemestre.id, ue_id=ue.id)
|
||||
}
|
||||
data-url_desinscr={
|
||||
}"
|
||||
data-url_desinscr="{
|
||||
url_for("notes.etud_desinscrit_ue",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud.id,
|
||||
formsemestre_id=res.formsemestre.id, ue_id=ue.id)
|
||||
}
|
||||
}"
|
||||
/>
|
||||
"""
|
||||
|
||||
H.append(f"""<td{td_class}>{content}</td>""")
|
||||
H.append(
|
||||
"""</table>
|
||||
"""
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<div class="help">
|
||||
L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules
|
||||
<p>L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules
|
||||
mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.
|
||||
Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'autres cas particuliers.
|
||||
La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre)
|
||||
</p>
|
||||
<p>Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
|
||||
présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres
|
||||
cas particuliers.
|
||||
</p>
|
||||
<p>La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre)
|
||||
et n'affecte pas les notes saisies.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
@ -176,6 +176,18 @@ def ficheEtud(etudid=None):
|
||||
sco_etud.fill_etuds_info([etud_])
|
||||
#
|
||||
info = etud_
|
||||
if etud.prenom_etat_civil:
|
||||
info["etat_civil"] = (
|
||||
"<h3>Etat-civil: "
|
||||
+ etud.civilite_etat_civil_str
|
||||
+ " "
|
||||
+ etud.prenom_etat_civil
|
||||
+ " "
|
||||
+ etud.nom
|
||||
+ "</h3>"
|
||||
)
|
||||
else:
|
||||
info["etat_civil"] = ""
|
||||
info["ScoURL"] = scu.ScoURL()
|
||||
info["authuser"] = authuser
|
||||
info["info_naissance"] = info["date_naissance"]
|
||||
@ -325,9 +337,9 @@ def ficheEtud(etudid=None):
|
||||
if not sco_permissions_check.can_suppress_annotation(a["id"]):
|
||||
a["dellink"] = ""
|
||||
else:
|
||||
a["dellink"] = (
|
||||
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
|
||||
% (
|
||||
a[
|
||||
"dellink"
|
||||
] = '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>' % (
|
||||
etudid,
|
||||
a["id"],
|
||||
scu.icontag(
|
||||
@ -337,7 +349,6 @@ def ficheEtud(etudid=None):
|
||||
title="Supprimer cette annotation",
|
||||
),
|
||||
)
|
||||
)
|
||||
author = sco_users.user_info(a["author"])
|
||||
alist.append(
|
||||
f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} :
|
||||
@ -473,7 +484,7 @@ def ficheEtud(etudid=None):
|
||||
<div class="ficheEtud" id="ficheEtud"><table>
|
||||
<tr><td>
|
||||
<h2>%(nomprenom)s (%(inscription)s)</h2>
|
||||
|
||||
%(etat_civil)s
|
||||
<span>%(emaillink)s</span>
|
||||
</td><td class="photocell">
|
||||
<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a>
|
||||
|
@ -111,9 +111,7 @@ get_base_preferences(formsemestre_id)
|
||||
|
||||
"""
|
||||
import flask
|
||||
from flask import flash, g, request
|
||||
|
||||
# from flask_login import current_user
|
||||
from flask import current_app, flash, g, request, url_for
|
||||
|
||||
from app.models import Departement
|
||||
from app.scodoc import sco_cache
|
||||
@ -208,6 +206,7 @@ PREF_CATEGORIES = (
|
||||
("abs", {"title": "Suivi des absences", "related": ("bul",)}),
|
||||
("assi", {"title": "Gestion de l'assiduité"}),
|
||||
("portal", {"title": "Liaison avec portail (Apogée, etc)"}),
|
||||
("apogee", {"title": "Exports Apogée"}),
|
||||
(
|
||||
"pdf",
|
||||
{
|
||||
@ -235,7 +234,9 @@ PREF_CATEGORIES = (
|
||||
"bul_margins",
|
||||
{
|
||||
"title": "Marges additionnelles des bulletins, en millimètres",
|
||||
"subtitle": "Le bulletin de notes notes est toujours redimensionné pour occuper l'espace disponible entre les marges.",
|
||||
"subtitle": """Le bulletin de notes notes est toujours redimensionné
|
||||
pour occuper l'espace disponible entre les marges.
|
||||
""",
|
||||
"related": ("bul", "bul_mail", "pdf"),
|
||||
},
|
||||
),
|
||||
@ -321,7 +322,9 @@ class BasePreferences(object):
|
||||
{
|
||||
"initvalue": "",
|
||||
"title": "Nom de l'Institut",
|
||||
"explanation": 'exemple "IUT de Villetaneuse". Peut être utilisé sur les bulletins.',
|
||||
"explanation": """exemple "IUT de Villetaneuse".
|
||||
Peut être utilisé sur les bulletins.
|
||||
""",
|
||||
"size": 40,
|
||||
"category": "general",
|
||||
"only_global": True,
|
||||
@ -355,7 +358,9 @@ class BasePreferences(object):
|
||||
"initvalue": "",
|
||||
"title": "e-mails à qui notifier les opérations",
|
||||
"size": 70,
|
||||
"explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc).",
|
||||
"explanation": """adresses séparées par des virgules; notifie les opérations
|
||||
(saisies de notes, etc).
|
||||
""",
|
||||
"category": "general",
|
||||
"only_global": False, # peut être spécifique à un semestre
|
||||
},
|
||||
@ -367,9 +372,14 @@ class BasePreferences(object):
|
||||
"initvalue": "",
|
||||
"title": "Adresse mail origine",
|
||||
"size": 40,
|
||||
"explanation": """adresse expéditeur pour tous les envois par mails (bulletins,
|
||||
comptes, etc.).
|
||||
Si vide, utilise la config globale.""",
|
||||
"explanation": f"""adresse expéditeur pour tous les envois par mail
|
||||
(bulletins, notifications, etc.). Si vide, utilise la config globale.
|
||||
Pour les comptes (mot de passe), voir la config globale accessible
|
||||
en tant qu'administrateur depuis la <a class="stdlink" href="{
|
||||
url_for("scodoc.index")
|
||||
}">page d'accueil</a>.
|
||||
|
||||
""",
|
||||
"category": "misc",
|
||||
"only_global": True,
|
||||
},
|
||||
@ -778,7 +788,7 @@ class BasePreferences(object):
|
||||
"explanation": "remplissage maquettes export Apogée",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "portal",
|
||||
"category": "apogee",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
@ -790,7 +800,7 @@ class BasePreferences(object):
|
||||
"explanation": "remplissage maquettes export Apogée",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "portal",
|
||||
"category": "apogee",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
@ -802,7 +812,7 @@ class BasePreferences(object):
|
||||
"explanation": "remplissage maquettes export Apogée",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "portal",
|
||||
"category": "apogee",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
@ -814,7 +824,7 @@ class BasePreferences(object):
|
||||
"explanation": "remplissage maquettes export Apogée",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "portal",
|
||||
"category": "apogee",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
@ -826,7 +836,7 @@ class BasePreferences(object):
|
||||
"explanation": "si coché, exporte exporte étudiants même si pas décision de jury saisie (sinon laisse vide)",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "portal",
|
||||
"category": "apogee",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
@ -838,7 +848,19 @@ class BasePreferences(object):
|
||||
"explanation": "si coché, exporte exporte étudiants en attente de ratrapage comme ATT (sinon laisse vide)",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "portal",
|
||||
"category": "apogee",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
"export_res_remove_typ_res",
|
||||
{
|
||||
"initvalue": 0,
|
||||
"title": "Ne pas recopier la section APO_TYP_RES",
|
||||
"explanation": "si coché, ne réécrit pas la section APO_TYP_RES (rarement utile, utiliser avec précaution)",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "apogee",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
@ -1989,6 +2011,7 @@ class BasePreferences(object):
|
||||
value = _get_pref_default_value_from_config(name, pref[1])
|
||||
self.default[name] = value
|
||||
self.prefs[None][name] = value
|
||||
if not current_app.testing:
|
||||
log(f"creating missing preference for {name}={value}")
|
||||
# add to db table
|
||||
self._editor.create(
|
||||
@ -2266,7 +2289,6 @@ class SemPreferences:
|
||||
raise ScoValueError(
|
||||
"sem_preferences.edit doit etre appele sur un semestre !"
|
||||
) # a bug !
|
||||
sem = sco_formsemestre.get_formsemestre(self.formsemestre_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Préférences du semestre",
|
||||
|
@ -242,7 +242,19 @@ def formsemestre_recapcomplet(
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
# Légende
|
||||
H.append(
|
||||
"""
|
||||
<div class="table_recap_caption">
|
||||
<div class="title">Codes utilisés dans cette table:</div>
|
||||
<div class="captions">
|
||||
<div><tt>~</tt></div><div>valeur manquante</div>
|
||||
<div><tt>=</tt></div><div>UE dispensée</div>
|
||||
<div><tt>nan</tt></div><div>valeur non disponible</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
# HTML or binary data ?
|
||||
if len(H) > 1:
|
||||
|
@ -152,7 +152,7 @@ def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
|
||||
absents = [] # etudid absents
|
||||
tosuppress = [] # etudids avec ancienne note à supprimer
|
||||
|
||||
for (etudid, note) in notes:
|
||||
for etudid, note in notes:
|
||||
note = str(note).strip().upper()
|
||||
try:
|
||||
etudid = int(etudid) #
|
||||
@ -536,7 +536,7 @@ def notes_add(
|
||||
evaluation_id, getallstudents=True, include_demdef=True
|
||||
)
|
||||
}
|
||||
for (etudid, value) in notes:
|
||||
for etudid, value in notes:
|
||||
if check_inscription and (etudid not in inscrits):
|
||||
raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
|
||||
if (value is not None) and not isinstance(value, float):
|
||||
@ -556,7 +556,7 @@ def notes_add(
|
||||
[]
|
||||
) # etudids pour lesquels il y a une decision de jury et que la note change
|
||||
try:
|
||||
for (etudid, value) in notes:
|
||||
for etudid, value in notes:
|
||||
changed = False
|
||||
if etudid not in notes_db:
|
||||
# nouvelle note
|
||||
@ -657,6 +657,7 @@ def notes_add(
|
||||
formsemestre_id=M["formsemestre_id"]
|
||||
) # > modif notes (exception)
|
||||
sco_cache.EvaluationCache.delete(evaluation_id)
|
||||
raise # XXX
|
||||
raise ScoGenError("Erreur enregistrement note: merci de ré-essayer") from exc
|
||||
if do_it:
|
||||
cnx.commit()
|
||||
|
@ -84,15 +84,17 @@ class SemSet(dict):
|
||||
self.semset_id = semset_id
|
||||
self["semset_id"] = semset_id
|
||||
self.sems = []
|
||||
self.formsemestre_ids = []
|
||||
self.formsemestres = [] # modernisation en cours...
|
||||
self.is_apc = False
|
||||
self.formsemestre_ids = set()
|
||||
cnx = ndb.GetDBConnexion()
|
||||
if semset_id: # read existing set
|
||||
L = semset_list(cnx, args={"semset_id": semset_id})
|
||||
if not L:
|
||||
semsets = semset_list(cnx, args={"semset_id": semset_id})
|
||||
if not semsets:
|
||||
raise ScoValueError(f"Ensemble inexistant ! (semset {semset_id})")
|
||||
self["title"] = L[0]["title"]
|
||||
self["annee_scolaire"] = L[0]["annee_scolaire"]
|
||||
self["sem_id"] = L[0]["sem_id"]
|
||||
self["title"] = semsets[0]["title"]
|
||||
self["annee_scolaire"] = semsets[0]["annee_scolaire"]
|
||||
self["sem_id"] = semsets[0]["sem_id"]
|
||||
r = ndb.SimpleDictFetch(
|
||||
"SELECT formsemestre_id FROM notes_semset_formsemestre WHERE semset_id = %(semset_id)s",
|
||||
{"semset_id": semset_id},
|
||||
@ -123,8 +125,13 @@ class SemSet(dict):
|
||||
def load_sems(self):
|
||||
"""Load formsemestres"""
|
||||
self.sems = []
|
||||
self.formsemestres = []
|
||||
for formsemestre_id in self.formsemestre_ids:
|
||||
self.sems.append(sco_formsemestre.get_formsemestre(formsemestre_id))
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
self.formsemestres.append(formsemestre)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
self.sems.append(sem)
|
||||
self["is_apc"] = formsemestre.formation.is_apc()
|
||||
|
||||
if self.sems:
|
||||
self["date_debut"] = min([sem["date_debut_iso"] for sem in self.sems])
|
||||
@ -137,8 +144,15 @@ class SemSet(dict):
|
||||
self["semtitles"] = [sem["titre_num"] for sem in self.sems]
|
||||
|
||||
# Construction du ou des lien(s) vers le semestre
|
||||
pattern = '<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>'
|
||||
self["semlinks"] = [(pattern % sem) for sem in self.sems]
|
||||
self["semlinks"] = [
|
||||
f"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)
|
||||
}">{formsemestre.titre_annee()}</a>
|
||||
"""
|
||||
for formsemestre in self.formsemestres
|
||||
]
|
||||
|
||||
self["semtitles_str"] = "<br>".join(self["semlinks"])
|
||||
|
||||
def fill_formsemestres(self):
|
||||
@ -149,6 +163,8 @@ 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)
|
||||
# check
|
||||
if formsemestre_id in self.formsemestre_ids:
|
||||
return # already there
|
||||
@ -159,6 +175,17 @@ class SemSet(dict):
|
||||
f"can't add {formsemestre_id} to set {self.semset_id}: incompatible sem_id"
|
||||
)
|
||||
|
||||
if self.formsemestre_ids and formsemestre.formation.is_apc() != self["is_apc"]:
|
||||
raise ScoValueError(
|
||||
"""On ne peut pas mélanger des semestres BUT/APC
|
||||
avec des semestres ordinaires dans le même export.""",
|
||||
dest_url=url_for(
|
||||
"notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
semset_id=self.semset_id,
|
||||
),
|
||||
)
|
||||
|
||||
ndb.SimpleQuery(
|
||||
"""INSERT INTO notes_semset_formsemestre
|
||||
(formsemestre_id, semset_id)
|
||||
@ -242,17 +269,28 @@ class SemSet(dict):
|
||||
def load_etuds(self):
|
||||
self["etuds_without_nip"] = set() # etudids
|
||||
self["jury_ok"] = True
|
||||
self["jury_nb_missing"] = 0
|
||||
is_apc = None
|
||||
for sem in self.sems:
|
||||
formsemestre = FormSemestre.query.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(
|
||||
"Incohérence: semestre APC (BUT) et ordinaires mélangés !"
|
||||
)
|
||||
else:
|
||||
is_apc = nt.is_apc
|
||||
sem["etuds"] = list(nt.identdict.values())
|
||||
sem["nips"] = {e["code_nip"] for e in sem["etuds"] if e["code_nip"]}
|
||||
sem["etuds_without_nip"] = {
|
||||
e["etudid"] for e in sem["etuds"] if not e["code_nip"]
|
||||
}
|
||||
self["etuds_without_nip"] |= sem["etuds_without_nip"]
|
||||
sem["jury_ok"] = nt.all_etuds_have_sem_decisions()
|
||||
sem["etudids_no_jury"] = nt.etudids_without_decisions()
|
||||
sem["jury_ok"] = not sem["etudids_no_jury"]
|
||||
self["jury_ok"] &= sem["jury_ok"]
|
||||
self["jury_nb_missing"] += len(sem["etudids_no_jury"])
|
||||
self["is_apc"] = bool(is_apc)
|
||||
|
||||
def html_descr(self):
|
||||
"""Short HTML description"""
|
||||
@ -272,36 +310,21 @@ class SemSet(dict):
|
||||
)
|
||||
H.append("</p>")
|
||||
|
||||
H.append(
|
||||
f"""<p>Période: <select name="periode" onchange="set_periode(this);">
|
||||
<option value="1" {"selected" if self["sem_id"] == 1 else ""}>1re période (S1, S3)</option>
|
||||
<option value="2" {"selected" if self["sem_id"] == 2 else ""}>2de période (S2, S4)</option>
|
||||
<option value="0" {"selected" if self["sem_id"] == 0 else ""}>non semestrialisée (LP, ...)</option>
|
||||
</select>
|
||||
</p>
|
||||
<script>
|
||||
function set_periode(elt) {{
|
||||
fetch(
|
||||
"{ url_for("apiweb.semset_set_periode", scodoc_dept=g.scodoc_dept,
|
||||
semset_id=self.semset_id )
|
||||
}",
|
||||
{{
|
||||
method: "POST",
|
||||
headers: {{
|
||||
'Content-Type': 'application/json'
|
||||
}},
|
||||
body: JSON.stringify( elt.value )
|
||||
}},
|
||||
).then(sco_message("période modifiée"));
|
||||
}};
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
if self["sem_id"] == 1:
|
||||
periode = "1re période (S1, S3)"
|
||||
elif self["sem_id"] == 2:
|
||||
periode = "2de période (S2, S4)"
|
||||
else:
|
||||
periode = "non semestrialisée (LP, ...). Incompatible avec BUT."
|
||||
|
||||
H.append(
|
||||
f"<p>Etapes: <tt>{sco_formsemestre.etapes_apo_str(self.list_etapes())}</tt></p>"
|
||||
f"""
|
||||
<p>Période: <b>{periode}</b></p>
|
||||
<p>Etapes: <tt>{sco_formsemestre.etapes_apo_str(self.list_etapes())}</tt></p>
|
||||
|
||||
<h4>Semestres de l'ensemble:</h4><ul class="semset_listsems">
|
||||
"""
|
||||
)
|
||||
H.append("""<h4>Semestres de l'ensemble:</h4><ul class="semset_listsems">""")
|
||||
|
||||
for sem in self.sems:
|
||||
H.append(
|
||||
@ -364,7 +387,6 @@ class SemSet(dict):
|
||||
"""
|
||||
if sco_portal_apogee.has_portal():
|
||||
return self.bilan.html_diagnostic()
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
@ -423,13 +445,15 @@ def do_semset_add_sem(semset_id, formsemestre_id):
|
||||
raise ScoValueError("empty semset_id")
|
||||
if formsemestre_id == "":
|
||||
raise ScoValueError("pas de semestre choisi !")
|
||||
s = SemSet(semset_id=semset_id)
|
||||
# check for valid formsemestre_id
|
||||
_ = sco_formsemestre.get_formsemestre(formsemestre_id) # raise exc
|
||||
|
||||
s.add(formsemestre_id)
|
||||
|
||||
return flask.redirect("apo_semset_maq_status?semset_id=%s" % semset_id)
|
||||
semset = SemSet(semset_id=semset_id)
|
||||
semset.add(formsemestre_id)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
semset_id=semset_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def do_semset_remove_sem(semset_id, formsemestre_id):
|
||||
@ -535,7 +559,7 @@ def semset_page(format="html"):
|
||||
<select name="sem_id">
|
||||
<option value="1">1re période (S1, S3)</option>
|
||||
<option value="2">2de période (S2, S4)</option>
|
||||
<option value="0">non semestrialisée (LP, ...)</option>
|
||||
<option value="0">non semestrialisée (LP, ... mais pas pour le BUT !)</option>
|
||||
</select>
|
||||
<input type="text" name="title" size="32"/>
|
||||
<input type="submit" value="Créer"/>
|
||||
|
@ -351,7 +351,7 @@ def check_modif_user(
|
||||
|
||||
# Unicité du cas_id
|
||||
if cas_id:
|
||||
cas_users = User.query.filter_by(cas_id=cas_id).all()
|
||||
cas_users = User.query.filter_by(cas_id=str(cas_id)).all()
|
||||
if edit:
|
||||
if cas_users and (
|
||||
len(cas_users) > 1 or cas_users[0].user_name != user_name
|
||||
|
@ -56,13 +56,14 @@ from pytz import timezone
|
||||
import dateutil.parser as dtparser
|
||||
|
||||
import flask
|
||||
from flask import g, request
|
||||
from flask import flash, url_for, make_response, jsonify
|
||||
from flask import g, request, Response
|
||||
from flask import flash, url_for, make_response
|
||||
from flask_json import json_response
|
||||
from werkzeug.http import HTTP_STATUS_CODES
|
||||
|
||||
from config import Config
|
||||
from app import log
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app import log, ScoDocJSONEncoder
|
||||
|
||||
from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
|
||||
from app.scodoc import sco_xml
|
||||
import sco_version
|
||||
@ -855,16 +856,6 @@ def sendPDFFile(data, filename): # DEPRECATED utiliser send_file
|
||||
return send_file(data, filename=filename, mime=PDF_MIMETYPE, attached=True)
|
||||
|
||||
|
||||
class ScoDocJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o): # pylint: disable=E0202
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return o.isoformat()
|
||||
elif isinstance(o, ApoEtapeVDI):
|
||||
return str(o)
|
||||
else:
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
def sendJSON(data, attached=False, filename=None):
|
||||
js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
|
||||
return send_file(
|
||||
@ -976,24 +967,26 @@ def get_request_args():
|
||||
return vals
|
||||
|
||||
|
||||
def json_error(status_code, message=None):
|
||||
"""Simple JSON response, for errors"""
|
||||
def json_error(status_code, message=None) -> Response:
|
||||
"""Simple JSON for errors.
|
||||
If as-response, returns Flask's Response. Otherwise returns a dict.
|
||||
"""
|
||||
payload = {
|
||||
"error": HTTP_STATUS_CODES.get(status_code, "Unknown error"),
|
||||
"status": status_code,
|
||||
}
|
||||
if message:
|
||||
payload["message"] = message
|
||||
response = jsonify(payload)
|
||||
response = json_response(status_=status_code, data_=payload)
|
||||
response.status_code = status_code
|
||||
log(f"Error: {response}")
|
||||
return response
|
||||
|
||||
|
||||
def json_ok_response(status_code=200, payload=None):
|
||||
def json_ok_response(status_code=200, payload=None) -> Response:
|
||||
"""Simple JSON respons for "success" """
|
||||
payload = payload or {"OK": True}
|
||||
response = jsonify(payload)
|
||||
response = json_response(status_=status_code, data_=payload)
|
||||
response.status_code = status_code
|
||||
return response
|
||||
|
||||
@ -1157,8 +1150,8 @@ def icontag(name, file_format="png", no_size=False, **attrs):
|
||||
file_format,
|
||||
),
|
||||
)
|
||||
im = PILImage.open(img_file)
|
||||
width, height = im.size[0], im.size[1]
|
||||
with PILImage.open(img_file) as image:
|
||||
width, height = image.size[0], image.size[1]
|
||||
ICONSIZES[name] = (width, height) # cache
|
||||
else:
|
||||
width, height = ICONSIZES[name]
|
||||
|
@ -33,7 +33,7 @@ from app.scodoc.sco_exceptions import ScoValueError
|
||||
class ApoEtapeVDI(object):
|
||||
_ETAPE_VDI_SEP = "!"
|
||||
|
||||
def __init__(self, etape_vdi=None, etape="", vdi=""):
|
||||
def __init__(self, etape_vdi: str = None, etape: str = "", vdi: str = ""):
|
||||
"""Build from string representation, e.g. 'V1RT!111'"""
|
||||
if etape_vdi:
|
||||
self.etape_vdi = etape_vdi
|
||||
@ -52,6 +52,10 @@ class ApoEtapeVDI(object):
|
||||
def __str__(self):
|
||||
return self.etape_vdi
|
||||
|
||||
def __json__(self) -> str:
|
||||
"json repr for flask_json"
|
||||
return str(self)
|
||||
|
||||
def _cmp(self, other):
|
||||
"""Test égalité de deux codes étapes.
|
||||
Si le VDI des deux est spécifié, on l'utilise. Sinon, seul le code étape est pris en compte.
|
||||
|
156
app/static/css/parcour_formation.css
Normal file
156
app/static/css/parcour_formation.css
Normal file
@ -0,0 +1,156 @@
|
||||
div.les_parcours {
|
||||
display: flex;
|
||||
margin-left: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
div.les_parcours>div {
|
||||
font-size: 130%;
|
||||
margin-top: 12px;
|
||||
margin-left: 8px;
|
||||
background-color: #09c;
|
||||
opacity: 0.7;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
div.les_parcours>div.focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.les_parcours>div.link {
|
||||
background-color: var(--sco-color-background);
|
||||
color: navy;
|
||||
}
|
||||
|
||||
|
||||
div.les_parcours>div.parc>a:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
div.les_parcours>div.parc>a,
|
||||
div.les_parcours>div.parc>a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.parcour_formation {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
min-width: 1200px;
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
.titre_parcours {
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
div.competence {
|
||||
/* display: grid; */
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.titre_competence {
|
||||
/* grid-column-start: 1;
|
||||
grid-column-end: span -1;
|
||||
grid-row-start: 1;
|
||||
grid-row-start: 2; */
|
||||
border-bottom: 6px solid white;
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.niveaux {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
--arrow-width: 24px;
|
||||
}
|
||||
|
||||
/* Flèches vers la droite */
|
||||
.niveaux>div {
|
||||
padding: 8px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.niveaux>div:not(:first-child) {
|
||||
padding-left: calc(var(--arrow-width) + 8px);
|
||||
}
|
||||
|
||||
.niveaux>div:not(:last-child)::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(100% - 1px);
|
||||
bottom: 0;
|
||||
width: var(--arrow-width);
|
||||
background: var(--color);
|
||||
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.niveau {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.niveau>div {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.titre_niveau {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1 / 2;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
span.parcs {
|
||||
margin-left: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
span.parc {
|
||||
font-size: 75%;
|
||||
font-weight: bold;
|
||||
/* color: rgb(92, 87, 255); */
|
||||
color: white;
|
||||
margin-right: 8px;
|
||||
padding: 4px;
|
||||
background-color: #09c;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.ue {
|
||||
grid-row-start: 2;
|
||||
/* border: 1px dashed blue; */
|
||||
}
|
||||
|
||||
div.ue.impair {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
|
||||
div.ue.pair {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
.ue select {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* ne fonctionne pas
|
||||
option.non_associe {
|
||||
background-color: yellow;
|
||||
color: red;
|
||||
} */
|
||||
|
||||
.links {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
@ -1,17 +1,23 @@
|
||||
:host {
|
||||
font-family: Verdana;
|
||||
background: rgb(14, 5, 73);
|
||||
display: block;
|
||||
padding: 12px 32px;
|
||||
padding: 6px 32px;
|
||||
color: #FFF;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
margin-left: 12px;
|
||||
margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
div.titre {
|
||||
color: black;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/**********************/
|
||||
/* Zone parcours */
|
||||
/**********************/
|
||||
@ -60,27 +66,29 @@ h1 {
|
||||
}
|
||||
|
||||
.comp1 {
|
||||
background: #a44
|
||||
background: var(--col-c1-3);
|
||||
}
|
||||
|
||||
.comp2 {
|
||||
background: #84a
|
||||
background: var(--col-c2-3);
|
||||
}
|
||||
|
||||
.comp3 {
|
||||
background: #a84
|
||||
background: var(--col-c3-3);
|
||||
}
|
||||
|
||||
.comp4 {
|
||||
background: #8a4
|
||||
background: var(--col-c4-3);
|
||||
}
|
||||
|
||||
.comp5 {
|
||||
background: #4a8
|
||||
background: var(--col-c5-3);
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.comp6 {
|
||||
background: #48a
|
||||
background: var(--col-c6-3);
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.competences>.focus {
|
||||
|
180
app/static/css/refcomp_parcours_niveaux.css
Normal file
180
app/static/css/refcomp_parcours_niveaux.css
Normal file
@ -0,0 +1,180 @@
|
||||
:root {
|
||||
--col-c1-1: rgb(224, 201, 201);
|
||||
--col-c1-2: rgb(231, 127, 130);
|
||||
--col-c1-3: rgb(167, 0, 9);
|
||||
--col-c2-1: rgb(240, 218, 198);
|
||||
--col-c2-2: rgb(231, 142, 95);
|
||||
--col-c2-3: rgb(231, 119, 64);
|
||||
--col-c3-1: rgb(241, 227, 167);
|
||||
--col-c3-2: rgb(238, 208, 86);
|
||||
--col-c3-3: rgb(233, 174, 17);
|
||||
--col-c4-1: rgb(218, 225, 205);
|
||||
--col-c4-2: rgb(159, 207, 111);
|
||||
--col-c4-3: rgb(124, 192, 64);
|
||||
--col-c5-1: rgb(191, 206, 230);
|
||||
--col-c5-2: rgb(119, 156, 208);
|
||||
--col-c5-3: rgb(10, 22, 75);
|
||||
--col-c6-1: rgb(203, 199, 176);
|
||||
--col-c6-2: rgb(152, 143, 97);
|
||||
--col-c6-3: rgb(13, 13, 13);
|
||||
}
|
||||
|
||||
div.refcomp_show {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
div.refcomp_show>div {
|
||||
background: rgb(210, 210, 210);
|
||||
border-radius: 8px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
div.table_niveaux_parcours {
|
||||
margin-top: 12px;
|
||||
color: #111;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
|
||||
div.liens {
|
||||
margin-top: 3ex;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div.table_niveaux_parcours .titre {
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
|
||||
table.table_niveaux_parcours tr th:first-child {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours th {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours tr.parcours_but {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours tr.parcours_but td {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours tr.parcours_but td b {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours tr.annee_but {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours tr td:not(:first-child) {
|
||||
width: 120px;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours tr.annee_but td:first-child {
|
||||
width: 92px;
|
||||
font-weight: bold;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
table.table_niveaux_parcours tr.annee_but td.empty {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Les couleurs des niveaux de compétences du BO */
|
||||
.comp-c1-1 {
|
||||
background: var(--col-c1-1);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.comp-c1-2 {
|
||||
background: var(--col-c1-2);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.comp-c1-3,
|
||||
.comp-c1 {
|
||||
background: var(--col-c1-3);
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.comp-c2-1 {
|
||||
background: var(--col-c2-1);
|
||||
}
|
||||
|
||||
.comp-c2-2 {
|
||||
background: var(--col-c2-2);
|
||||
}
|
||||
|
||||
.comp-c2-3,
|
||||
.comp-c2 {
|
||||
background: var(--col-c2-3);
|
||||
}
|
||||
|
||||
.comp-c3-1 {
|
||||
background: var(--col-c3-1);
|
||||
}
|
||||
|
||||
.comp-c3-2 {
|
||||
background: var(--col-c3-2);
|
||||
}
|
||||
|
||||
.comp-c3-3,
|
||||
.comp-c3 {
|
||||
background: var(--col-c3-3);
|
||||
}
|
||||
|
||||
.comp-c4-1 {
|
||||
background: var(--col-c4-1);
|
||||
}
|
||||
|
||||
.comp-c4-2 {
|
||||
background: var(--col-c4-2);
|
||||
}
|
||||
|
||||
.comp-c4-3,
|
||||
.comp-c4 {
|
||||
background: var(--col-c4-3);
|
||||
}
|
||||
|
||||
.comp-c5-1 {
|
||||
background: var(--col-c5-1);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.comp-c5-2 {
|
||||
background: var(--col-c5-2);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.comp-c5-3,
|
||||
.comp-c5 {
|
||||
background: var(--col-c5-3);
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.comp-c6-1 {
|
||||
background: var(--col-c6-1);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.comp-c6-2 {
|
||||
background: var(--col-c6-2);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.comp-c6-3,
|
||||
.comp-c6 {
|
||||
background: var(--col-c6-3);
|
||||
color: #eee;
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
--sco-content-min-width: 600px;
|
||||
--sco-content-max-width: 1024px;
|
||||
--sco-color-explication: rgb(10, 58, 140);
|
||||
--sco-color-background: rgb(242, 242, 238);
|
||||
}
|
||||
|
||||
html,
|
||||
@ -12,7 +13,7 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
background-color: rgb(242, 242, 238);
|
||||
background-color: var(--sco-color-background);
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
}
|
||||
@ -63,6 +64,37 @@ div#gtrcontent {
|
||||
display: None;
|
||||
}
|
||||
|
||||
div.flashes {
|
||||
transition: opacity 0.5s ease;
|
||||
margin-top: 8px;
|
||||
left: 50%;
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
transform: translateX(-50%);
|
||||
width: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
div.alert {
|
||||
/*
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px; */
|
||||
}
|
||||
|
||||
div.alert-info {
|
||||
color: #0019d7;
|
||||
background-color: #68f36d;
|
||||
border-color: #0a8d0c;
|
||||
}
|
||||
|
||||
div.alert-error {
|
||||
color: #ef0020;
|
||||
background-color: #ffff00;
|
||||
border-color: #8d0a17;
|
||||
}
|
||||
|
||||
|
||||
div.tab-content {
|
||||
margin-top: 10px;
|
||||
margin-left: 15px;
|
||||
@ -191,7 +223,7 @@ div.head_message {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.message_curtom {
|
||||
.message_custom {
|
||||
position: fixed;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
@ -205,6 +237,18 @@ div.head_message {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
div.message_error {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
padding: 20px;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
background: rgb(212, 0, 0);
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
div.passwd_warn {
|
||||
font-weight: bold;
|
||||
@ -231,9 +275,6 @@ p.footer {
|
||||
border-top: 1px solid rgb(60, 60, 60);
|
||||
}
|
||||
|
||||
div.part2 {
|
||||
margin-top: 3ex;
|
||||
}
|
||||
|
||||
/* ---- (left) SIDEBAR ----- */
|
||||
|
||||
@ -2017,6 +2058,7 @@ span.eval_coef_ue_titre {}
|
||||
div.list_but_ue_inscriptions {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 8px;
|
||||
padding-left: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-radius: 16px;
|
||||
@ -2066,6 +2108,17 @@ form.list_but_ue_inscriptions td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
table#but_ue_inscriptions {
|
||||
margin-left: 16px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
div#but_ue_inscriptions_filter {
|
||||
margin-left: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Formulaire edition des partitions */
|
||||
form#editpart table {
|
||||
border: 1px solid gray;
|
||||
@ -2179,16 +2232,23 @@ span.explication {
|
||||
|
||||
div.formation_ue_list {
|
||||
border: 1px solid black;
|
||||
background-color: rgb(232, 249, 255);
|
||||
margin-top: 5px;
|
||||
margin-right: 12px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
div.formation_list_ues_titre {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
border-top-right-radius: 18px;
|
||||
border-top-left-radius: 18px;
|
||||
background-color: #0051a9;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
div.formation_list_modules,
|
||||
@ -2205,6 +2265,8 @@ div.formation_list_ues {
|
||||
margin-top: 20px
|
||||
}
|
||||
|
||||
div.formation_list_ues_content {}
|
||||
|
||||
div.formation_list_modules {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@ -2266,6 +2328,41 @@ span.notes_module_list_buts {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
div.formation_parcs {
|
||||
display: inline-flex;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
div.formation_parcs>div {
|
||||
font-size: 100%;
|
||||
color: white;
|
||||
background-color: #09c;
|
||||
opacity: 0.7;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
div.formation_parcs>div.focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.formation_parcs>div>a:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
div.formation_parcs>div>a,
|
||||
div.formation_parcs>div>a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.ue_choix_niveau>div.formation_parcs>div {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
|
||||
div.ue_list_tit {
|
||||
font-weight: bold;
|
||||
margin-top: 8px;
|
||||
@ -2476,6 +2573,19 @@ div.cont_ue_choix_niveau select.select_niveau_ue {
|
||||
width: 490px;
|
||||
}
|
||||
|
||||
div.ue_advanced {
|
||||
background-color: rgb(244, 253, 255);
|
||||
border: 1px solid blue;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
div.ue_advanced h3 {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
div#ue_list_modules {
|
||||
background-color: rgb(251, 225, 165);
|
||||
border: 1px solid blue;
|
||||
@ -2661,6 +2771,30 @@ table.notes_recapcomplet a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.table_recap_caption {
|
||||
width: fit-content;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(202, 255, 180);
|
||||
}
|
||||
|
||||
div.table_recap_caption div.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.table_recap_caption div.captions {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 200px;
|
||||
}
|
||||
|
||||
div.table_recap_caption div.captions div:nth-child(odd) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.table_recap_caption div.captions div:nth-child(even) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* bulletin */
|
||||
div.notes_bulletin {
|
||||
margin-right: 5px;
|
||||
|
@ -11,7 +11,6 @@ $().ready(function () {
|
||||
});
|
||||
update_bonus_description();
|
||||
}
|
||||
update_menus_niveau_competence();
|
||||
});
|
||||
|
||||
function update_bonus_description() {
|
||||
@ -37,69 +36,28 @@ function update_ue_list() {
|
||||
});
|
||||
}
|
||||
|
||||
function set_ue_parcour(elem) {
|
||||
let ue_id = elem.dataset.ue_id;
|
||||
let parcour_id = elem.value;
|
||||
let set_ue_parcour_url = elem.dataset.setter;
|
||||
$.post(set_ue_parcour_url,
|
||||
{
|
||||
ue_id: ue_id,
|
||||
parcour_id: parcour_id,
|
||||
function set_ue_parcour(checkbox) {
|
||||
let url = checkbox.dataset.setter;
|
||||
const checkboxes = document.querySelectorAll('#choix_parcours input[type="checkbox"]:checked');
|
||||
const parcours_ids = [];
|
||||
checkboxes.forEach(function (checkbox) {
|
||||
parcours_ids.push(checkbox.value);
|
||||
});
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
function (result) {
|
||||
sco_message("UE associée au parcours");
|
||||
update_menus_niveau_competence();
|
||||
body: JSON.stringify(parcours_ids)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status == 404) {
|
||||
sco_error_message(data.message);
|
||||
} else {
|
||||
sco_message(data.message);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function set_ue_niveau_competence(elem) {
|
||||
let ue_id = elem.dataset.ue_id;
|
||||
let niveau_id = elem.value;
|
||||
let set_ue_niveau_competence_url = elem.dataset.setter;
|
||||
$.post(set_ue_niveau_competence_url,
|
||||
{
|
||||
ue_id: ue_id,
|
||||
niveau_id: niveau_id,
|
||||
},
|
||||
function (result) {
|
||||
sco_message("niveau de compétence enregistré");
|
||||
|
||||
update_menus_niveau_competence();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Met à jour les niveaux utilisés (disabled) ou non affectés
|
||||
// dans les menus d'association UE <-> niveau
|
||||
function update_menus_niveau_competence() {
|
||||
// let selected_niveaux = [];
|
||||
// document.querySelectorAll("form.form_ue_choix_niveau select").forEach(
|
||||
// elem => { selected_niveaux.push(elem.value); }
|
||||
// );
|
||||
|
||||
// document.querySelectorAll("form.form_ue_choix_niveau select").forEach(
|
||||
// elem => {
|
||||
// for (let i = 0; i < elem.options.length; i++) {
|
||||
// elem.options[i].disabled = (i != elem.options.selectedIndex)
|
||||
// && (selected_niveaux.indexOf(elem.options[i].value) != -1)
|
||||
// && (elem.options[i].value != "");
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// nouveau:
|
||||
document.querySelectorAll("select.select_niveau_ue").forEach(
|
||||
elem => {
|
||||
let ue_id = elem.dataset.ue_id;
|
||||
$.get("get_ue_niveaux_options_html",
|
||||
{
|
||||
ue_id: ue_id,
|
||||
},
|
||||
function (result) {
|
||||
elem.innerHTML = result;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
@ -15,3 +15,23 @@ function change_ue_inscr(elt) {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$("table#but_ue_inscriptions").DataTable(
|
||||
{
|
||||
paging: false,
|
||||
searching: true,
|
||||
info: false,
|
||||
autoWidth: false,
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
footer: false
|
||||
},
|
||||
orderCellsTop: true, // cellules ligne 1 pour tri
|
||||
aaSorting: [], // Prevent initial sorting
|
||||
"oLanguage": {
|
||||
"sSearch": "Chercher :"
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
class ref_competences extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
@ -5,6 +6,7 @@ class ref_competences extends HTMLElement {
|
||||
|
||||
/* Template de base */
|
||||
this.shadow.innerHTML = `
|
||||
<div class=titre>Cliquer sur un parcours pour afficher ses niveaux de compétences</div>
|
||||
<div class=parcours></div>
|
||||
<div class=competences></div>
|
||||
<div class=ACs></div>
|
||||
@ -13,11 +15,7 @@ class ref_competences extends HTMLElement {
|
||||
/* Style du module */
|
||||
const styles = document.createElement('link');
|
||||
styles.setAttribute('rel', 'stylesheet');
|
||||
if (location.href.split("/")[3] == "ScoDoc") {
|
||||
styles.setAttribute('href', '/ScoDoc/static/css/ref-competences.css');
|
||||
} else {
|
||||
styles.setAttribute('href', 'ref-competences.css');
|
||||
}
|
||||
styles.setAttribute('href', removeLastTwoComponents(getCurrentScriptPath()) + '/css/ref-competences.css');
|
||||
|
||||
this.shadow.appendChild(styles);
|
||||
}
|
||||
@ -31,7 +29,7 @@ class ref_competences extends HTMLElement {
|
||||
let parcoursDIV = this.shadow.querySelector(".parcours");
|
||||
Object.entries(this.data.parcours).forEach(([cle, parcours]) => {
|
||||
let div = document.createElement("div");
|
||||
div.innerText = parcours.libelle;
|
||||
div.innerHTML = `<a title="${parcours.libelle}">${parcours.code}</a>`;
|
||||
div.addEventListener("click", (event) => { this.competences(event, cle) })
|
||||
parcoursDIV.appendChild(div);
|
||||
})
|
||||
|
@ -15,8 +15,8 @@ class releveBUT extends HTMLElement {
|
||||
/* Style du module */
|
||||
const styles = document.createElement('link');
|
||||
styles.setAttribute('rel', 'stylesheet');
|
||||
if (location.href.split("/")[3] == "ScoDoc") {
|
||||
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css'); // Scodoc
|
||||
if (location.href.includes("ScoDoc")) {
|
||||
styles.setAttribute('href', removeLastTwoComponents(getCurrentScriptPath()) + '/css/releve-but.css'); // Scodoc
|
||||
} else {
|
||||
styles.setAttribute('href', '/assets/styles/releve-but.css'); // Passerelle
|
||||
}
|
||||
|
@ -67,17 +67,22 @@ $(function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Affiche un message transitoire
|
||||
function sco_message(msg) {
|
||||
// Affiche un message transitoire (duration milliseconds, 0 means infinity)
|
||||
function sco_message(msg, className = "message_custom", duration = 0) {
|
||||
var div = document.createElement("div");
|
||||
div.className = "message_curtom";
|
||||
div.className = className;
|
||||
div.innerHTML = msg;
|
||||
document.querySelector("body").appendChild(div);
|
||||
if (duration) {
|
||||
setTimeout(() => {
|
||||
div.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function sco_error_message(msg) {
|
||||
sco_message(msg, className = "message_error", duration = 0);
|
||||
}
|
||||
|
||||
|
||||
function get_query_args() {
|
||||
@ -256,3 +261,27 @@ class ScoFieldEditor {
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentScriptPath() {
|
||||
// Get all the script elements on the page
|
||||
var scripts = document.getElementsByTagName('script');
|
||||
|
||||
// Find the last script element (which is the currently executing script)
|
||||
var currentScript = scripts[scripts.length - 1];
|
||||
|
||||
// Retrieve the src attribute of the script element
|
||||
var scriptPath = currentScript.src;
|
||||
|
||||
return scriptPath;
|
||||
}
|
||||
function removeLastTwoComponents(path) {
|
||||
// Split the path into individual components
|
||||
var components = path.split('/');
|
||||
|
||||
// Remove the last two components (filename and enclosing directory)
|
||||
components.splice(-2);
|
||||
|
||||
// Join the remaining components back into a path
|
||||
var newPath = components.join('/');
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ class TableJury(TableRecap):
|
||||
niveau: ApcNiveau = validation_rcue.niveau()
|
||||
titre = f"C{niveau.competence.numero}" # à voir (nommer les compétences...)
|
||||
row.add_cell(
|
||||
f"c_{competence_id}_annee",
|
||||
f"c_{competence_id}_{annee}",
|
||||
titre,
|
||||
validation_rcue.code,
|
||||
group="cursus_" + annee,
|
||||
|
@ -74,7 +74,7 @@ class TableRecap(tb.Table):
|
||||
# couples (modimpl, ue) effectivement présents dans la table:
|
||||
self.modimpl_ue_ids = set()
|
||||
|
||||
ues = res.formsemestre.query_ues(with_sport=True) # avec bonus
|
||||
ues = res.formsemestre.get_ues(with_sport=True) # avec bonus
|
||||
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
|
||||
|
||||
if res.formsemestre.etuds_inscriptions: # table non vide
|
||||
@ -285,9 +285,9 @@ class TableRecap(tb.Table):
|
||||
notes = res.modimpl_notes(modimpl.id, ue.id)
|
||||
if np.isnan(notes).all():
|
||||
# aucune note valide
|
||||
row_min.add_cell(col_id, None, np.nan)
|
||||
row_max.add_cell(col_id, None, np.nan)
|
||||
moy = np.nan
|
||||
row_min.add_cell(col_id, None, "")
|
||||
row_max.add_cell(col_id, None, "")
|
||||
moy = ""
|
||||
else:
|
||||
row_min.add_cell(col_id, None, self.fmt_note(np.nanmin(notes)))
|
||||
row_max.add_cell(col_id, None, self.fmt_note(np.nanmax(notes)))
|
||||
@ -297,7 +297,7 @@ class TableRecap(tb.Table):
|
||||
None,
|
||||
self.fmt_note(moy),
|
||||
# aucune note dans ce module ?
|
||||
classes=["col_empty" if np.isnan(moy) else ""],
|
||||
classes=["col_empty" if (moy == "" or np.isnan(moy)) else ""],
|
||||
)
|
||||
row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "")
|
||||
|
||||
@ -618,7 +618,7 @@ class RowRecap(tb.Row):
|
||||
):
|
||||
"""Ajoute cols moy_gen moy_ue et tous les modules..."""
|
||||
etud = self.etud
|
||||
table = self.table
|
||||
table: TableRecap = self.table
|
||||
res = table.res
|
||||
# --- Si DEM ou DEF, ne montre aucun résultat d'UE ni moy. gen.
|
||||
if res.get_etud_etat(etud.id) != scu.INSCRIT:
|
||||
@ -676,7 +676,7 @@ class RowRecap(tb.Row):
|
||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
||||
self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"])
|
||||
|
||||
self.nb_ues_etud_parcours = len(res.etud_ues_ids(etud.id))
|
||||
self.nb_ues_etud_parcours = len(res.etud_parcours_ues_ids(etud.id))
|
||||
ue_valid_txt = (
|
||||
ue_valid_txt_html
|
||||
) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
|
||||
@ -701,13 +701,17 @@ class RowRecap(tb.Row):
|
||||
def add_ue_cols(self, ue: UniteEns, ue_status: dict, col_group: str = None):
|
||||
"Ajoute résultat UE au row (colonne col_ue)"
|
||||
# sous-classé par JuryRow pour ajouter les codes
|
||||
table = self.table
|
||||
table: TableRecap = self.table
|
||||
formsemestre: FormSemestre = table.res.formsemestre
|
||||
table.group_titles[
|
||||
"col_ue"
|
||||
] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
val = ue_status["moy"]
|
||||
val = (
|
||||
ue_status["moy"]
|
||||
if (self.etud.id, ue.id) not in table.res.dispense_ues
|
||||
else "="
|
||||
)
|
||||
note_classes = []
|
||||
if isinstance(val, float):
|
||||
if val < table.barre_moy:
|
||||
|
@ -62,16 +62,23 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="container flashes">
|
||||
{% include "flashed_messages.j2" %}
|
||||
</div>
|
||||
|
||||
{# application content needs to be provided in the app_content block #}
|
||||
<div class="container">
|
||||
{% block app_content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var flashes = document.getElementsByClassName("flashes")[0];
|
||||
if (flashes) {
|
||||
flashes.style.display = "none";
|
||||
}
|
||||
}, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -10,10 +10,16 @@
|
||||
{% include 'bul_head.j2' %}
|
||||
|
||||
<releve-but></releve-but>
|
||||
<script src="{{sco.scu.STATIC_DIR}}/js/releve-but.js"></script>
|
||||
|
||||
{% include 'bul_foot.j2' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
|
||||
<script src="{{scu.STATIC_DIR}}/js/releve-but.js"></script>
|
||||
|
||||
<script>
|
||||
let dataSrc = "{{bul_url|safe}}";
|
||||
fetch(dataSrc)
|
||||
@ -46,4 +52,5 @@
|
||||
// });
|
||||
document.querySelector("html").style.scrollBehavior = "smooth";
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
189
app/templates/but/parcour_formation.j2
Normal file
189
app/templates/but/parcour_formation.j2
Normal file
@ -0,0 +1,189 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link href="{{scu.STATIC_DIR}}/css/refcomp_parcours_niveaux.css" rel="stylesheet" type="text/css" />
|
||||
<link href="{{scu.STATIC_DIR}}/css/parcour_formation.css" rel="stylesheet" type="text/css" />
|
||||
{% endblock %}
|
||||
|
||||
{% macro menu_ue(niv, sem="pair", sem_idx=0) -%}
|
||||
{% if niv['niveau'] %}
|
||||
{% if current_user.has_permission(sco.Permission.ScoChangeFormation) %}
|
||||
<select name="ue_niv_{{niv['niveau'].id}}" id="ue_niv_{{niv['niveau'].id}}"
|
||||
onchange="assoc_ue_niveau(event,
|
||||
{{niv['niveau'].id}}, {{parcour.id}}
|
||||
);"
|
||||
{% if niv['ue_'+sem] %}
|
||||
data-ue_id="{{niv['ue_'+sem].id}}"
|
||||
{% else %}
|
||||
data-ue_id=""
|
||||
{% endif %}
|
||||
>
|
||||
{%- if not niv['ue_'+sem] -%}
|
||||
<option value="" class="non_associe">UE de S{{sem_idx}} ?</option>
|
||||
{%-else-%}
|
||||
<option value="">Désassocier</option>
|
||||
{%-endif-%}
|
||||
{% for ue in niv['ues_'+sem] %}
|
||||
<option value="{{ue.id}}"
|
||||
{% if niv['ue_'+sem] and niv['ue_'+sem].id == ue.id -%}
|
||||
selected
|
||||
{%- endif %}
|
||||
>{{ue.acronyme}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
{# Vue en lecture seule #}
|
||||
{% if niv['ue_'+sem] %}
|
||||
{{ niv['ue_'+sem].acronyme }}
|
||||
{% else %}
|
||||
<span class="fontred">{{scu.EMO_WARNING|safe}} non associé</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2>{{formation.to_html()}}</h2>
|
||||
|
||||
{# Liens vers les différents parcours #}
|
||||
<div class="les_parcours">
|
||||
{% for parc in formation.referentiel_competence.parcours %}
|
||||
<div class="parc {{'focus' if parcour and parc.id == parcour.id else ''}}">
|
||||
<a href="{{
|
||||
url_for('notes.parcour_formation', scodoc_dept=g.scodoc_dept,
|
||||
parcour_id=parc.id, formation_id=formation.id )
|
||||
}}">{{parc.code}}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="link">
|
||||
<a class="stdlink" target="_blank" href="{{
|
||||
url_for('notes.refcomp_show',
|
||||
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id )
|
||||
}}">référentiel de compétences</a>
|
||||
</div>
|
||||
<div class="link"><a class="stdlink" href="{{
|
||||
url_for('notes.ue_table',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation.id )
|
||||
}}">formation</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Description d'un parcours #}
|
||||
{% if parcour %}
|
||||
<div class="parcour_formation">
|
||||
|
||||
<div class="titre_parcours">Parcours {{parcour.code}} « {{parcour.libelle}} »</div>
|
||||
|
||||
{% for comp in competences_parcour %}
|
||||
{% set color_idx = 1 + loop.index0 % 6 %}
|
||||
<div class="competence comp-c{{color_idx}}">
|
||||
<div class="titre_competence tc">
|
||||
Compétence {{comp['competence'].numero}} : {{comp['competence'].titre}}
|
||||
</div>
|
||||
<div class="niveaux">
|
||||
{% for annee, niv in comp['niveaux'].items() %}
|
||||
<div class="niveau comp-c{{color_idx}}-{{annee}}"
|
||||
style="--color: var(--col-c{{color_idx}}-{{annee}});">
|
||||
<div class="titre_niveau n{{annee}}">
|
||||
<span class="parcs">
|
||||
{% if niv['niveau'].is_tronc_commun %}
|
||||
<span class="parc">TC</span>
|
||||
{% elif niv['niveau'].parcours|length > 1 %}
|
||||
<span class="parc">
|
||||
{% set virg = joiner(", ") %}
|
||||
{% for p in niv['niveau'].parcours %}
|
||||
{{ virg() }}{{p.code}}
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{{niv['niveau'].libelle if niv['niveau'] else ''}}
|
||||
</div>
|
||||
<div class="ue impair u{{annee}}1">
|
||||
{{ menu_ue(niv, "impair", 2*annee-1) }}
|
||||
</div>
|
||||
<div class="ue pair u{{annee}}1">
|
||||
{{ menu_ue(niv, "pair", 2*annee) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
Choisissez un parcours...
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if parcour %}
|
||||
<div class="help">
|
||||
|
||||
<p> Cette page représente le parcours <span class="parc">{{parcour.code}}</span>
|
||||
du référentiel de compétence {{formation.referentiel_competence.specialite}}, et permet
|
||||
d'associer à chaque semestre d'un niveau de compétence une UE de la formation
|
||||
<a class="stdlink"
|
||||
href="{{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
|
||||
}}">{{formation.to_html()}}
|
||||
</a>.</p>
|
||||
|
||||
<p>Le symbole <span class="parc">TC</span> désigne un niveau du tronc commun
|
||||
(c'est à dire présent dans tous les parcours de la spécialité). </p>
|
||||
|
||||
<p>Ce formulaire ne vérifie pas si l'UE est bien conçue pour ce parcours.</p>
|
||||
|
||||
<p>Les modifications sont enregistrées au fur et à mesure.</p>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function assoc_ue_niveau(event, niveau_id) {
|
||||
let ue_id = event.target.value;
|
||||
let url = "";
|
||||
let must_reload = false;
|
||||
if (ue_id == "") {
|
||||
/* Dé-associe */
|
||||
ue_id = event.target.dataset.ue_id;
|
||||
const desassoc_url = '{{
|
||||
url_for(
|
||||
"apiweb.desassoc_ue_niveau",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
ue_id=11111
|
||||
)
|
||||
}}';
|
||||
url = desassoc_url.replace('11111', ue_id);
|
||||
must_reload=true;
|
||||
} else {
|
||||
const assoc_url = '{{
|
||||
url_for(
|
||||
"apiweb.assoc_ue_niveau",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
ue_id=11111, niveau_id=22222
|
||||
)
|
||||
}}';
|
||||
url = assoc_url.replace('11111', ue_id).replace('22222', niveau_id);
|
||||
}
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
/* body: JSON.stringify( {} ) */
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status) {
|
||||
/* sco_message(data.message); */
|
||||
/* revert menu to initial state */
|
||||
event.target.value = event.target.dataset.ue_id;
|
||||
}
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
28
app/templates/but/refcomp_parcours_niveaux.j2
Normal file
28
app/templates/but/refcomp_parcours_niveaux.j2
Normal file
@ -0,0 +1,28 @@
|
||||
<div class="table_niveaux_parcours">
|
||||
<div class="titre">Niveaux de compétences des parcours</div>
|
||||
<table class="table_niveaux_parcours">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for comp in ref.competences %}
|
||||
<th class="comp-c{{1 + loop.index0 % 6}}">{{comp.titre}}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set nb_cols = ref.competences.count() + 1 %}
|
||||
{% for parcour in ref.parcours %}
|
||||
<tr class="parcours_but"><td colspan="{{nb_cols}}">Parcours <b>{{parcour.code}}</b> <i>{{parcour.libelle}}</i></td></tr>
|
||||
{% for annee in [ 1, 2, 3] %}
|
||||
<tr class="annee_but">
|
||||
<td>BUT{{annee}}</td>
|
||||
{% for comp in ref.competences %}
|
||||
{% set niveau = table_niveaux_parcours[parcour.id][annee][comp.id] %}
|
||||
<td class="comp-c{{1 + loop.index0 % 6}} {% if not niveau %}empty{% endif %}">{{ niveau }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -2,23 +2,27 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link href="{{scu.STATIC_DIR}}/css/refcomp_parcours_niveaux.css" rel="stylesheet" type="text/css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2>Référentiel de compétences {{ref.type_titre}} {{ref.specialite_long}}</h2>
|
||||
|
||||
|
||||
<ref-competences></ref-competences>
|
||||
|
||||
<script src="{{sco.scu.STATIC_DIR}}/js/ref_competences.js"></script>
|
||||
|
||||
<div class="help">
|
||||
Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à
|
||||
partir du fichier <tt>{{ref.scodoc_orig_filename or "(inconnu)"}}</tt>.
|
||||
</div>
|
||||
|
||||
<div class="refcomp_show">
|
||||
|
||||
<div class="part2">
|
||||
<div>
|
||||
<ref-competences></ref-competences>
|
||||
</div>
|
||||
|
||||
{% include "but/refcomp_parcours_niveaux.j2" %}
|
||||
|
||||
|
||||
<div class="liens">
|
||||
<ul>
|
||||
<li>Formations se référant à ce référentiel:
|
||||
<ul>
|
||||
@ -26,12 +30,15 @@
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
|
||||
}}">{{ formation.get_titre_version() }}</a></li>
|
||||
{% else %}
|
||||
<li><em>aucune</em></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{{url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept)}}">Liste des référentiels</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -40,6 +47,8 @@
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
|
||||
<script src="{{scu.STATIC_DIR}}/js/ref_competences.js"></script>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
let data_url = "{{data_source}}";
|
||||
|
@ -1,9 +1,18 @@
|
||||
{# Message flask : utilisé uniquement par les anciennes pages ScoDoc #}
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
<div class="head_message_container">
|
||||
{# Messages flask (flash) #}
|
||||
|
||||
<div class="container flashes">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="head_message alert-info alert-{{ category }}" role="alert">{{ message }}</div>
|
||||
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var flashes = document.getElementsByClassName("flashes")[0];
|
||||
if (flashes) {
|
||||
flashes.style.display = "none";
|
||||
}
|
||||
}, 5000);
|
||||
</script>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user