Compare commits

..

112 Commits
master ... main

Author SHA1 Message Date
Iziram
b0ae231725 remove bootstrap : add styles WIP 2024-08-26 17:39:49 +02:00
Iziram
74de287ad6 remove bootstrap : remove files and links 2024-08-26 16:37:39 +02:00
0509f1c923 version 2024-08-26 14:00:46 +02:00
659c790af2 prerm mode 2024-08-26 14:00:46 +02:00
88d9356209 Install/Upgrade: améliore script, mail si échec 2024-08-26 14:00:46 +02:00
84580a8a6f Templatification des vues (WIP) 2024-08-26 14:00:46 +02:00
bf53dfa93a Install/Upgrade: améliore script, mail si échec (WIP) 2024-08-26 14:00:46 +02:00
c1d8a34e34 Fix typo (PV jury pdf) 2024-08-26 14:00:46 +02:00
dc53fe8395 Fix: clone formsemestre 2024-08-26 14:00:46 +02:00
464d678299 Cosmetic / templatification 2024-08-26 14:00:46 +02:00
Iziram
aa9d85f4bd fix multi-select "formData" 2024-08-25 08:05:40 +02:00
6e6f76dc26 Templatification. Attention: reste bug sélection groupes multiselect en GET 2024-08-23 23:16:49 +02:00
b9cf1e9efa Refix formulaire_feuille_appel avec multiselect corrigé 2024-08-23 19:13:15 +02:00
Iziram
871e85ae26 fixes multi-select (form + groups) 2024-08-24 18:24:46 +02:00
ef44a71e39 templification saisie_notes_tableur 2024-08-23 19:02:33 +02:00
867ed6dde8 marge droite sur tous les contenus 2024-08-23 19:01:49 +02:00
c1e6b67f20 Améliore page affichage ScoValueError 2024-08-23 19:01:10 +02:00
f2f616d643 Fix formsemestre menubar anciennes pages (sco_header) 2024-08-23 18:25:15 +02:00
ce80b9f765 Fix formulaire_feuille_appel. A vérifier/simplifier ? 2024-08-23 17:59:55 +02:00
Iziram
bdc6c90bfc update sco_formsemestre_status : hide tableau-modules on mobile 2024-08-23 17:30:23 +02:00
6ab027dffe merge el 2024-08-23 16:35:01 +02:00
d8f6fe35e9 Liste groupe: affichage optionnel de la date d'inscription 2024-08-23 16:33:35 +02:00
Iziram
8fd1fa5a25 fix lien mobileNav sidebar (scodoc -> scodoc.index au lieu de scolar.index_html 2024-08-23 16:24:23 +02:00
Iziram
47f17157f1 update etud_info : func attach_etud_info 2024-08-23 16:09:46 +02:00
Iziram
10b7b876ea fix mass_selection signal_assiduites_group 2024-08-23 10:25:36 +02:00
Iziram
597f7ef85c remove "Calendrier" signal_assiduites_group 2024-08-23 10:08:56 +02:00
d4e875a7bd Fix typo (check_group_apogee) 2024-08-23 09:13:09 +02:00
430b378723 merge master 2024-08-23 09:10:21 +02:00
37c7e82994 9.7.5 2024-08-23 08:53:39 +02:00
ca11132503 Merge branch 'revamp' of https://scodoc.org/git/viennet/ScoDoc 2024-08-23 08:51:28 +02:00
478bf80c6b Cosmetic: dernières opérations, sidebar, ... 2024-08-23 08:39:53 +02:00
a904db9eee Fix: synchro etud, unicité codes 2024-08-23 04:30:53 +02:00
26dcc0db3b Pages groupes: 3 pages séparées au lieu des tabs 2024-08-22 22:02:34 +02:00
b448e32f8a Merge branch 'frontend' of https://scodoc.org/git/iziram/ScoDoc into revamp 2024-08-22 20:10:05 +02:00
20bb9cc9ed FakePortal (tests): ajout INE 2024-08-22 19:03:29 +02:00
8a6b167f8c typo 2024-08-22 19:03:04 +02:00
17d9b8daa9 BUT: Ajout warning si niveau comp. associé à plusieurs UEs 2024-08-22 18:07:38 +02:00
6a48d5bbcf Préf. pour envoi d’une notification mail à chaque (de)inscription 2024-08-22 16:42:38 +02:00
Iziram
4ab7142488 fix visuels signal_assiduites_group 2024-08-21 11:26:10 +02:00
Iziram
07f09ddead signal_assiduites_group : rendu mobile 2024-08-20 17:56:59 +02:00
Iziram
4f40713787 fix pagination tableaux 2024-08-20 15:20:14 +02:00
Iziram
71f88dfa97 bilan_etud : responsive in progress 2024-08-20 15:08:40 +02:00
Iziram
9caa6bf75d fix taille champs desc / raison et modimpl form assiduite 2024-08-20 15:04:09 +02:00
Iziram
3d74979237 scodoc9_mobile.css in progress 2024-08-20 15:03:41 +02:00
Iziram
e1a5ea31cd fix responsive calendriers 2024-08-20 14:50:36 +02:00
Iziram
5624637f30 fix blocage défilement 2024-08-19 18:22:40 +02:00
Iziram
11c9ab332f changement logo rectangle sco_page mobile nav 2024-08-19 17:05:02 +02:00
Iziram
514e9e4c83 ajout no_sidebar dans template sco_page fix #973 2024-08-19 12:28:34 +02:00
f136b80c84 Templatification: progress 2024-08-19 12:15:31 +02:00
405533798e Templatification: remplacement html_sem_header 2024-08-19 12:15:31 +02:00
99b0f23bca minor css fixes 2024-08-19 12:15:31 +02:00
1437c1bafa Templates: partition_editor, ue_table, evaluation_listenotes 2024-08-19 12:15:31 +02:00
Iziram
ebceb70f05 frontend: fix ligne semestre scolar/index.j2 2024-08-19 09:55:07 +02:00
814d458beb Templatification: progress 2024-08-18 18:40:11 +02:00
47e40a9634 Templatification: remplacement html_sem_header 2024-08-16 01:11:06 +02:00
19a8e9650b minor css fixes 2024-08-15 17:06:07 +02:00
376fa570f6 Merge branch 'iziram-frontend' into revamp 2024-08-15 08:21:37 +02:00
b16d519362 Templates: partition_editor, ue_table, evaluation_listenotes 2024-08-15 00:04:35 +02:00
4bfd0858a8 API FormSemestreDescription: images: upload, tests. 2024-08-14 15:39:57 +02:00
Iziram
24de3ac1b5 helper ordi -> desktop 2024-08-14 14:49:59 +02:00
Iziram
fb6b63bf0b MobileNav 2024-08-14 14:44:19 +02:00
Iziram
0eb407f5e3 fix bug menu liens + debug fix bug scroll 2024-08-14 11:14:14 +02:00
6b8667522b Fix API unit tests (APIError) 2024-08-14 10:55:13 +02:00
37dcdca65b Fix regression: inscription 2024-08-14 10:55:13 +02:00
7c7d128b57 Fixed bug (migration to Numpy 2.0) and warnings. All tests OK. 2024-08-14 10:55:13 +02:00
646c30bb21 Fix: regression (inscriptions groupes) 2024-08-14 10:55:13 +02:00
055839757b remove all Model.query.get() [tests not OK] WIP 2024-08-14 10:55:13 +02:00
6eb3921774 API: modimpl and formsemestre inscription/desinscription 2024-08-14 10:55:13 +02:00
b2443a2c69 API: code http 403 et non 401 si permission non accordée. 2024-08-14 10:55:13 +02:00
0df77b20fb Modifie test_api.sh pour générer les exemples de la documentation (samples). 2024-08-14 10:55:13 +02:00
6d922adb7e Fix: moduleimpl_status sans évaluations 2024-08-14 10:55:13 +02:00
bf6cad9d77 Upgrade to NumPy 2.0 + other pip packages. 2024-08-14 10:55:13 +02:00
5a751cb6e7 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into el 2024-08-14 10:50:09 +02:00
cb0c9d8f53 Fix API unit tests (APIError) 2024-08-14 10:47:48 +02:00
e6f86d655b API FormSemestreDescription + test 2024-08-14 10:28:26 +02:00
Iziram
af7b5b01fb fix effet bord menu formsemestre 2024-08-13 17:11:31 +02:00
Iziram
330036c1be fix esthétique menu formsemestre 2024-08-13 17:04:57 +02:00
513fb3d46d FormSemestreDescription: champs pour spécifs EL. Formulaire saisie. 2024-08-13 16:47:55 +02:00
Iziram
ae84ed26c3 Rework : Menu formsemestre 2024-08-13 16:01:31 +02:00
3971145abd Capacité d'accueil: vérif. édition semestre, messages. 2024-08-12 14:40:35 +02:00
5edd7a8ba3 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into el 2024-08-12 12:41:12 +02:00
07c2f00d0d Fix regression: inscription 2024-08-12 12:40:01 +02:00
3d34f9330d Capacité d'accueil des formsemestre 2024-08-11 23:24:40 +02:00
525d0446cc FormSemestreDescription: informations pour applications tierces: Modèle, API, éditeur. 2024-08-11 21:39:43 +02:00
fc036705e8 Fixed bug (migration to Numpy 2.0) and warnings. All tests OK. 2024-08-07 21:04:47 +02:00
bee6d10c90 Fix: regression (inscriptions groupes) 2024-08-07 12:48:35 +02:00
74401da853 remove all Model.query.get() [tests not OK] WIP 2024-08-06 23:18:17 +02:00
450d503c39 Merge branch 'el' of https://scodoc.org/git/viennet/ScoDoc 2024-08-06 22:40:33 +02:00
d7f5c3e5e1 Fix: moduleimpl_status sans évaluations 2024-08-06 22:37:06 +02:00
03ba057a87 API: modimpl and formsemestre inscription/desinscription 2024-08-06 22:30:30 +02:00
0533ad59fd API: code http 403 et non 401 si permission non accordée. 2024-08-06 09:23:53 +02:00
9302a173aa Modifie test_api.sh pour générer les exemples de la documentation (samples). 2024-08-06 09:22:27 +02:00
Iziram
5b68adaf87 multiselect.py + fix bug event + changement icône + unfixed height 2024-07-31 16:08:21 +02:00
933968c99b Upgrade to NumPy 2.0 + other pip packages. 2024-07-31 15:12:32 +02:00
Iziram
00eb37e8ac change multi-select 2024-07-31 13:44:04 +02:00
Iziram
7f08f84934 suite upgrade bootstrap 2024-07-31 13:44:00 +02:00
Iziram
d77bf8f700 remove bootstrap 3.7.3 2024-07-30 11:46:32 +02:00
Iziram
5cb7ade189 update tabs to bootstrap 5.3.3 2024-07-30 11:09:55 +02:00
Iziram
5e066d13f0 update index navbar 2024-07-30 10:59:01 +02:00
Iziram
d9d8d8d7fe upgrade bootstrap to 5.3.3 2024-07-30 10:52:03 +02:00
3cecfbb697 Fix détails sur tables recap avec nouvelle version de datatables. 2024-07-29 21:43:34 +02:00
ad8bb5aace WIP: upgrade datatable. Ajustements en cours 2024-07-29 14:44:41 +02:00
77adda5c90 Upgrade jQuery to 3.7.1. (#943) 2024-07-28 19:15:51 +02:00
f6897a2c12 Upgrade jQuery to 3.7.1. (#943) 2024-07-28 19:13:21 +02:00
7a77b5a81a Nettoyage legacy sco_header 2024-07-28 17:40:40 +02:00
7ff0fd39fb Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into revamp 2024-07-27 15:38:04 +02:00
64038687b7 Fixes for unit tests 2024-07-27 14:34:20 +02:00
75c10a4917 API: ajout samples + amélioration des tests 2024-07-27 13:30:02 +02:00
4824b33358 Ameliore generation doc API 2024-07-27 13:28:55 +02:00
f87ed3bb68 Script test interactif API 2024-07-27 13:28:06 +02:00
0a7bb35604 Merge pull request 'APIDoc : Samples + fix query + légères modifs' (#970) from iziram/ScoDoc:master into master
Reviewed-on: ScoDoc/ScoDoc#970
2024-07-25 15:12:58 +02:00
c9ca97df6e WIP: revamp html/css: sidebar, hamburger, templates 2024-07-22 16:53:50 +02:00
573 changed files with 107498 additions and 175301 deletions

View File

@ -28,6 +28,9 @@ from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from jinja2 import select_autoescape
import numpy as np
import psycopg2
from psycopg2.extensions import AsIs as psycopg2_AsIs
import sqlalchemy as sa
import werkzeug.debug
from wtforms.fields import HiddenField
@ -68,6 +71,19 @@ cache = Cache(
)
# NumPy & Psycopg2 (necessary with Numpy 2.0)
# probablement à changer quand on passera à psycopg3.2
def adapt_numpy_scalar(numpy_scalar):
"""Adapt numeric types for psycopg2"""
return psycopg2_AsIs(numpy_scalar if not np.isnan(numpy_scalar) else "'NaN'")
psycopg2.extensions.register_adapter(np.float32, adapt_numpy_scalar)
psycopg2.extensions.register_adapter(np.float64, adapt_numpy_scalar)
psycopg2.extensions.register_adapter(np.int32, adapt_numpy_scalar)
psycopg2.extensions.register_adapter(np.int64, adapt_numpy_scalar)
def handle_sco_value_error(exc):
"page d'erreur avec message"
return render_template("sco_value_error.j2", exc=exc), 404
@ -337,8 +353,7 @@ def create_app(config_class=DevConfig):
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
app.jinja_env.globals["is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)

View File

@ -7,7 +7,7 @@ from flask_json import as_json
from flask import Blueprint
from flask import current_app, g, request
from flask_login import current_user
from app import db
from app import db, log
from app.decorators import permission_required
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoException
@ -47,6 +47,7 @@ def api_permission_required(permission):
@api_bp.errorhandler(404)
def api_error_handler(e):
"erreurs API => json"
log(f"api_error_handler: {e}")
return scu.json_error(404, message=str(e))

View File

@ -25,6 +25,7 @@ from app.models import BilletAbsence
from app.models.etudiants import Identite
from app.scodoc import sco_abs_billets
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
@bp.route("/billets_absence/etudiant/<int:etudid>")
@ -59,13 +60,17 @@ def billets_absence_create():
"justified" : bool
}
```
SAMPLES
-------
/billets_absence/create;{""etudid"":""1"",""abs_begin"":""2023-10-27T10:00"",""abs_end"":""2023-10-28T10:00"",""description"":""grave malade"",""justified"":""1""}
"""
data = request.get_json(force=True) # may raise 400 Bad Request
etudid = data.get("etudid")
abs_begin = data.get("abs_begin")
abs_end = data.get("abs_end")
description = data.get("description", "")
justified = data.get("justified", False)
justified = scu.to_bool(data.get("justified", False))
if None in (etudid, abs_begin, abs_end):
return json_error(
404, message="Paramètre manquant: etudid, abs_begin, abs_end requis"

View File

@ -38,7 +38,13 @@ from app.scodoc.sco_utils import json_error
@permission_required(Permission.ScoView)
@as_json
def departements_list():
"""Liste tous les départements."""
"""Liste tous les départements.
SAMPLES
-------
/departements;
"""
return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
@ -48,7 +54,13 @@ def departements_list():
@permission_required(Permission.ScoView)
@as_json
def departements_ids():
"""Liste des ids de tous les départements."""
"""Liste des ids de tous les départements.
SAMPLES
-------
/departements_ids;
"""
return [dept.id for dept in Departement.query]
@ -61,17 +73,10 @@ def departement_by_acronym(acronym: str):
"""
Info sur un département. Accès par acronyme.
Exemple de résultat :
```json
{
"id": 1,
"acronym": "TAPI",
"dept_name" : "TEST",
"description": null,
"visible": true,
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
}
```
SAMPLES
-------
/departement/TAPI;
"""
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return dept.to_dict(with_dept_name=True)
@ -82,9 +87,14 @@ def departement_by_acronym(acronym: str):
@scodoc
@permission_required(Permission.ScoView)
@as_json
def departement_by_id(dept_id: int):
def departement_get(dept_id: int):
"""
Info sur un département. Accès par id.
SAMPLES
-------
/departement/id/1;
"""
dept = Departement.query.get_or_404(dept_id)
return dept.to_dict()
@ -107,6 +117,10 @@ def departement_create():
"visible": bool,
}
```
SAMPLES
-------
/departement/create;{""acronym"":""MYDEPT"",""visible"":""1""}
"""
data = request.get_json(force=True) # may raise 400 Bad Request
acronym = str(data.get("acronym", ""))
@ -180,23 +194,10 @@ def departement_etudiants(acronym: str):
------
acronym : l'acronyme d'un département
Exemple de résultat :
```json
[
{
"civilite": "M",
"code_ine": "7899X61616",
"code_nip": "F6777H88",
"date_naissance": null,
"email": "toto@toto.fr",
"emailperso": null,
"etudid": 18,
"nom": "MOREL",
"prenom": "JACQUES"
},
...
]
```
SAMPLES
-------
/departement/TAPI/etudiants;
"""
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [etud.to_dict_short() for etud in dept.etudiants]
@ -221,7 +222,13 @@ def departement_etudiants_by_id(dept_id: int):
@permission_required(Permission.ScoView)
@as_json
def departement_formsemestres_ids(acronym: str):
"""Liste des ids de tous les formsemestres du département."""
"""Liste des ids de tous les formsemestres du département.
SAMPLES
-------
/departement/TAPI/formsemestres_ids;
"""
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [formsemestre.id for formsemestre in dept.formsemestres]
@ -232,7 +239,13 @@ def departement_formsemestres_ids(acronym: str):
@permission_required(Permission.ScoView)
@as_json
def departement_formsemestres_ids_by_id(dept_id: int):
"""Liste des ids de tous les formsemestres du département."""
"""Liste des ids de tous les formsemestres du département.
SAMPLES
-------
/departement/id/1/formsemestres_ids;
"""
dept = Departement.query.get_or_404(dept_id)
return [formsemestre.id for formsemestre in dept.formsemestres]
@ -253,6 +266,9 @@ def departement_formsemestres_courants(acronym: str = "", dept_id: int | None =
-----
date_courante:<string:date_courante>
SAMPLES
-------
/departement/id/1/formsemestres_courants?date_courante=2022-01-01
"""
dept = (
Departement.query.filter_by(acronym=acronym).first_or_404()

View File

@ -101,27 +101,16 @@ def etudiants_courants(long: bool = False):
et les formsemestres contenant la date courante,
ou à défaut celle indiquée en argument (au format ISO).
En format "long": voir l'exemple.
QUERY
-----
date_courante:<string:date_courante>
Exemple de résultat :
```json
[
{
"id": 1234,
"code_nip": "12345678",
"code_ine": null,
"nom": "JOHN",
"nom_usuel": None,
"prenom": "DEUF",
"civilite": "M",
}
...
]
```
En format "long": voir documentation.
SAMPLES
-------
/etudiants/courants?date_courante=2022-05-01;
/etudiants/courants/long?date_courante=2022-05-01;
"""
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
@ -436,6 +425,10 @@ def bulletin(
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
pdf : si spécifié, bulletin au format PDF (et non JSON).
SAMPLES
-------
/etudiant/etudid/1/formsemestre/1/bulletin
"""
if version == "pdf":
version = "long"
@ -494,33 +487,9 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant
Exemple de résultat :
```json
[
{
"partition_id": 1,
"id": 1,
"formsemestre_id": 1,
"partition_name": null,
"numero": 0,
"bul_show_rank": false,
"show_in_lists": true,
"group_id": 1,
"group_name": null
},
{
"partition_id": 2,
"id": 2,
"formsemestre_id": 1,
"partition_name": "TD",
"numero": 1,
"bul_show_rank": false,
"show_in_lists": true,
"group_id": 2,
"group_name": "A"
}
]
```
SAMPLES
-------
/etudiant/etudid/1/formsemestre/1/groups
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
@ -627,6 +596,10 @@ def etudiant_edit(
------
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
`code`: la valeur du code
SAMPLES
-------
/etudiant/ine/INE1/edit;{""prenom"":""Nouveau Prénom"", ""adresses"":[{""email"":""nouvelle@adresse.fr""}]}
"""
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
if not ok:
@ -682,6 +655,10 @@ def etudiant_annotation(
"comment" : string
}
```
SAMPLES
-------
/etudiant/etudid/1/annotation;{""comment"":""une annotation sur l'étudiant""}
"""
if not current_user.has_permission(Permission.ViewEtudData):
return json_error(403, "non autorisé (manque ViewEtudData)")

View File

@ -84,7 +84,9 @@ def moduleimpl_evaluations(moduleimpl_id: int):
------
moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat : voir `/evaluation`.
SAMPLES
-------
/moduleimpl/1/evaluations
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
@ -104,30 +106,9 @@ def evaluation_notes(evaluation_id: int):
------
evaluation_id : l'id de l'évaluation
Exemple de résultat :
```json
{
"11": {
"etudid": 11,
"evaluation_id": 1,
"value": 15.0,
"note_max" : 20.0,
"comment": "",
"date": "2024-07-19T19:08:44+02:00",
"uid": 2
},
"12": {
"etudid": 12,
"evaluation_id": 1,
"value": "ABS",
"note_max" : 20.0,
"comment": "",
"date": "2024-07-19T19:08:44+02:00",
"uid": 2
},
...
}
```
SAMPLES
-------
/evaluation/2/notes;
"""
query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
@ -173,10 +154,14 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
Résultat:
- nb_changed: nombre de notes changées
- nb_suppress: nombre de notes effacées
- etudids_changed: étudiants dont la note est modifiée
- etudids_with_decision: liste des etudiants dont la note a changé
alors qu'ils ont une décision de jury enregistrée.
- history_menu: un fragment de HTML expliquant l'historique de la note de chaque étudiant modifié.
SAMPLES
-------
/evaluation/1/notes/set;{""notes"": [[1, 17], [2, ""SUPR""]], ""comment"" : ""sample test""}
"""
query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
@ -224,6 +209,11 @@ def evaluation_create(moduleimpl_id: int):
}
Résultat: l'évaluation créée.
SAMPLES
-------
/moduleimpl/1/evaluation/create;{""description"":""Exemple éval.""}
"""
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
if not moduleimpl.can_edit_evaluation(current_user):

View File

@ -44,6 +44,11 @@ def formations():
"""
Retourne la liste de toutes les formations (tous départements,
sauf si route départementale).
SAMPLES
-------
/formations;
"""
query = Formation.query
if g.scodoc_dept:
@ -64,6 +69,11 @@ def formations_ids():
(tous départements, ou du département indiqué dans la route)
Exemple de résultat : `[ 17, 99, 32 ]`.
SAMPLES
-------
/formations_ids;
"""
query = Formation.query
if g.scodoc_dept:
@ -77,28 +87,14 @@ def formations_ids():
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formation_by_id(formation_id: int):
def formation_get(formation_id: int):
"""
La formation d'id donné.
SAMPLES
-------
/formation/1;
Exemple de résultat :
```json
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;T",
"version": 1,
"type_parcours": 700,
"referentiel_competence_id": null,
"formation_id": 1
}
```
"""
query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept:
@ -135,97 +131,9 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
formation_id : l'id d'une formation
export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation.
Exemple de résultat :
```json
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;T",
"version": 1,
"type_parcours": 700,
"referentiel_competence_id": null,
"formation_id": 1,
"ue": [
{
"acronyme": "RT1.1",
"numero": 1,
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"type": 0,
"ue_code": "UCOD11",
"ects": 12.0,
"is_external": false,
"code_apogee": "",
"coefficient": 0.0,
"semestre_idx": 1,
"color": "#B80004",
"reference": 1,
"matiere": [
{
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"numero": 1,
"module": [
{
"titre": "Initiation aux r\u00e9seaux informatiques",
"abbrev": "Init aux r\u00e9seaux informatiques",
"code": "R101",
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"coefficient": 1.0,
"ects": "",
"semestre_id": 1,
"numero": 10,
"code_apogee": "",
"module_type": 2,
"coefficients": [
{
"ue_reference": "1",
"coef": "12.0"
},
{
"ue_reference": "2",
"coef": "4.0"
},
{
"ue_reference": "3",
"coef": "4.0"
}
]
},
{
"titre": "Se sensibiliser \u00e0 l&apos;hygi\u00e8ne informatique...",
"abbrev": "Hygi\u00e8ne informatique",
"code": "SAE11",
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"coefficient": 1.0,
"ects": "",
"semestre_id": 1,
"numero": 10,
"code_apogee": "",
"module_type": 3,
"coefficients": [
{
"ue_reference": "1",
"coef": "16.0"
}
]
},
...
]
},
...
]
},
]
}
```
SAMPLES
-------
/formation/1/export
"""
query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept:
@ -250,6 +158,11 @@ def referentiel_competences(formation_id: int):
"""
Retourne le référentiel de compétences de la formation
ou null si pas de référentiel associé.
SAMPLES
-------
/formation/1/referentiel_competences;
"""
query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept:
@ -360,7 +273,12 @@ def ue_desassoc_niveau(ue_id: int):
@scodoc
@permission_required(Permission.ScoView)
def get_ue(ue_id: int):
"""Renvoie l'UE."""
"""Renvoie l'UE.
SAMPLES
-------
/formation/ue/1;
"""
query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
@ -374,7 +292,12 @@ def get_ue(ue_id: int):
@scodoc
@permission_required(Permission.ScoView)
def formation_module_get(module_id: int):
"""Renvoie le module."""
"""Renvoie le module.
SAMPLES
-------
/formation/module/1;
"""
query = Module.query.filter_by(id=module_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)

View File

@ -13,11 +13,14 @@
FormSemestre
"""
import base64
import io
from operator import attrgetter, itemgetter
from flask import g, make_response, request
from flask_json import as_json
from flask_login import current_user, login_required
import PIL
import sqlalchemy as sa
import app
from app import db, log
@ -32,6 +35,7 @@ from app.models import (
Departement,
Evaluation,
FormSemestre,
FormSemestreDescription,
FormSemestreEtape,
FormSemestreInscription,
Identite,
@ -41,6 +45,10 @@ from app.models import (
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_edt_cal
from app.scodoc.sco_formsemestre_inscriptions import (
do_formsemestre_inscription_with_modules,
do_formsemestre_desinscription,
)
from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import ModuleType
@ -54,49 +62,17 @@ from app.tables.recap import TableRecap, RowRecap
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_infos(formsemestre_id: int):
def formsemestre_get(formsemestre_id: int):
"""
Information sur le formsemestre indiqué.
formsemestre_id : l'id du formsemestre
Exemple de résultat :
```json
{
"block_moyennes": false,
"bul_bgcolor": "white",
"bul_hide_xml": false,
"date_debut_iso": "2021-09-01",
"date_debut": "01/09/2021",
"date_fin_iso": "2022-08-31",
"date_fin": "31/08/2022",
"dept_id": 1,
"elt_annee_apo": null,
"elt_passage_apo" : null,
"elt_sem_apo": null,
"ens_can_edit_eval": false,
"etat": true,
"formation_id": 1,
"formsemestre_id": 1,
"gestion_compensation": false,
"gestion_semestrielle": false,
"id": 1,
"modalite": "FI",
"resp_can_change_ens": true,
"resp_can_edit": false,
"responsables": [1, 99], // uids
"scodoc7_id": null,
"semestre_id": 1,
"titre_formation" : "BUT GEA",
"titre_num": "BUT GEA semestre 1",
"titre": "BUT GEA",
}
```
SAMPLES
-------
/formsemestre/1
"""
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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
return formsemestre.to_dict_api()
@ -270,7 +246,7 @@ def formsemestre_set_apo_etapes():
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
Ce changement peut être fait sur un semestre verrouillé
Ce changement peut être fait sur un semestre verrouillé.
DATA
----
@ -378,7 +354,7 @@ def formsemestre_set_elt_annee_apo():
@scodoc
@permission_required(Permission.EditApogee)
def formsemestre_set_elt_passage_apo():
"""Change les codes apogée de passage du semestre indiqué (par le champ oid).
"""Change les codes Apogée de passage du semestre indiqué (par le champ oid).
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
@ -425,14 +401,11 @@ def bulletins(formsemestre_id: int, version: str = "long"):
formsemestre_id : int
version : string ("long", "short", "selectedevals")
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
SAMPLES
-------
/formsemestre/1/bulletins
"""
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()
if formsemestre is None:
return json_error(404, "formsemestre non trouve")
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
data = []
@ -455,72 +428,11 @@ def formsemestre_programme(formsemestre_id: int):
"""
Retourne la liste des UEs, ressources et SAEs d'un semestre
Exemple de résultat :
```json
{
"ues": [
{
"type": 0,
"formation_id": 1,
"ue_code": "UCOD11",
"id": 1,
"ects": 12.0,
"acronyme": "RT1.1",
"is_external": false,
"numero": 1,
"code_apogee": "",
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"coefficient": 0.0,
"semestre_idx": 1,
"color": "#B80004",
"ue_id": 1
},
...
],
"ressources": [
{
"ens": [ 10, 18 ],
"formsemestre_id": 1,
"id": 15,
"module": {
"abbrev": "Programmer",
"code": "SAE15",
"code_apogee": "V7GOP",
"coefficient": 1.0,
"formation_id": 1,
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"id": 15,
"matiere_id": 3,
"module_id": 15,
"module_type": 3,
"numero": 50,
"semestre_id": 1,
"titre": "Programmer en Python",
"ue_id": 3
},
"module_id": 15,
"moduleimpl_id": 15,
"responsable_id": 2
},
...
],
"saes": [
{
...
},
...
],
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
}
```
SAMPLES
-------
/formsemestre/1/programme
"""
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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
ues = formsemestre.get_ues()
m_list = {
ModuleType.RESSOURCE: [],
@ -588,11 +500,12 @@ def formsemestre_etudiants(
-----
etat:<string:etat>
SAMPLES
-------
/formsemestre/1/etudiants/query;
"""
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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if with_query:
etat = request.args.get("etat")
if etat is not None:
@ -624,6 +537,63 @@ def formsemestre_etudiants(
return sorted(etuds, key=itemgetter("sort_key"))
@bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/inscrit")
@api_web_bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/inscrit")
@login_required
@scodoc
@permission_required(Permission.EtudInscrit)
@as_json
def formsemestre_etud_inscrit(formsemestre_id: int, etudid: int):
"""Inscrit l'étudiant à ce formsemestre et TOUS ses modules STANDARDS
(donc sauf les modules bonus sport).
DATA
----
```json
{
"dept_id" : int, # le département
"etape" : string, # optionnel: l'étape Apogée d'inscription
"group_ids" : [int], # optionnel: liste des groupes où inscrire l'étudiant (doivent exister)
}
```
"""
data = request.get_json(force=True) if request.data else {}
dept_id = data.get("dept_id", g.scodoc_dept_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
app.set_sco_dept(formsemestre.departement.acronym)
etud = Identite.get_etud(etudid)
group_ids = data.get("group_ids", [])
etape = data.get("etape", None)
do_formsemestre_inscription_with_modules(
formsemestre.id, etud.id, dept_id=dept_id, etape=etape, group_ids=group_ids
)
app.log(f"formsemestre_etud_inscrit: {etud} inscrit à {formsemestre}")
return (
FormSemestreInscription.query.filter_by(
formsemestre_id=formsemestre.id, etudid=etud.id
)
.first()
.to_dict()
)
@bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/desinscrit")
@api_web_bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/desinscrit")
@login_required
@scodoc
@permission_required(Permission.EtudInscrit)
@as_json
def formsemestre_etud_desinscrit(formsemestre_id: int, etudid: int):
"""Désinscrit l'étudiant de ce formsemestre et TOUS ses modules"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
etud = Identite.get_etud(etudid)
do_formsemestre_desinscription(etud.id, formsemestre.id)
app.log(f"formsemestre_etud_desinscrit: {etud} désinscrit de {formsemestre}")
return {"status": "ok"}
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@login_required
@ -634,37 +604,9 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
"""
Informations sur l'état des évaluations d'un formsemestre.
Exemple de résultat :
```json
[
{
"id": 1, // moduleimpl_id
"titre": "Initiation aux réseaux informatiques",
"evaluations": [
{
"id": 1,
"description": null,
"datetime_epreuve": null,
"heure_fin": "09:00:00",
"coefficient": "02.00"
"is_complete": true,
"nb_inscrits": 16,
"nb_manquantes": 0,
"ABS": 0,
"ATT": 0,
"EXC": 0,
"saisie_notes": {
"datetime_debut": "2021-09-11T00:00:00+02:00",
"datetime_fin": "2022-08-25T00:00:00+02:00",
"datetime_mediane": "2022-03-19T00:00:00+01:00"
}
},
...
]
},
]
```
SAMPLES
-------
/formsemestre/1/etat_evals
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
@ -749,16 +691,16 @@ def formsemestre_resultat(formsemestre_id: int):
-----
format:<string:format>
SAMPLES
-------
/formsemestre/1/resultats;
"""
format_spec = request.args.get("format", None)
if format_spec is not None and format_spec != "raw":
return json_error(API_CLIENT_ERROR, "invalid format specification")
convert_values = format_spec != "raw"
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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# Ajoute le groupe de chaque partition,
@ -796,10 +738,7 @@ def formsemestre_resultat(formsemestre_id: int):
@as_json
def groups_get_auto_assignment(formsemestre_id: int):
"""Rend les données stockées par `groups_save_auto_assignment`."""
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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
response = make_response(formsemestre.groups_auto_assignment_data or b"")
response.headers["Content-Type"] = scu.JSON_MIMETYPE
return response
@ -819,11 +758,7 @@ def groups_save_auto_assignment(formsemestre_id: int):
"""Enregistre les données, associées à ce formsemestre.
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
"""
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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_change_groups():
return json_error(403, "non autorisé (can_change_groups)")
@ -832,6 +767,7 @@ def groups_save_auto_assignment(formsemestre_id: int):
formsemestre.groups_auto_assignment_data = request.data
db.session.add(formsemestre)
db.session.commit()
return {"status": "ok"}
@bp.route("/formsemestre/<int:formsemestre_id>/edt")
@ -852,12 +788,101 @@ def formsemestre_edt(formsemestre_id: int):
group_ids : string (optionnel) filtre sur les groupes ScoDoc.
show_modules_titles: show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
"""
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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
group_ids = request.args.getlist("group_ids", int)
show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
return sco_edt_cal.formsemestre_edt_dict(
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
)
@bp.route("/formsemestre/<int:formsemestre_id>/description")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/description")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_get_description(formsemestre_id: int):
"""Description externe du formsemestre. Peut être vide.
formsemestre_id : l'id du formsemestre
SAMPLES
-------
/formsemestre/1/description
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
return formsemestre.description.to_dict() if formsemestre.description else {}
@bp.post("/formsemestre/<int:formsemestre_id>/description/edit")
@api_web_bp.post("/formsemestre/<int:formsemestre_id>/description/edit")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_edit_description(formsemestre_id: int):
"""Modifie description externe du formsemestre.
Les images peuvent êtres passées dans el json, encodées en base64.
formsemestre_id : l'id du formsemestre
SAMPLES
-------
/formsemestre/<int:formsemestre_id>/description/edit;{""description"":""descriptif du semestre"", ""dispositif"":1}
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
args = request.get_json(force=True) # may raise 400 Bad Request
if not formsemestre.description:
formsemestre.description = FormSemestreDescription()
# Decode images (base64)
for key in ["image", "photo_ens"]:
if key in args:
args[key] = base64.b64decode(args[key])
formsemestre.description.from_dict(args)
db.session.commit()
return formsemestre.description.to_dict()
@bp.route("/formsemestre/<int:formsemestre_id>/description/image")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/description/image")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_get_description_image(formsemestre_id: int):
"""Image de la description externe du formsemestre. Peut être vide.
formsemestre_id : l'id du formsemestre
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.description or not formsemestre.description.image:
return make_response("", 204) # 204 No Content status
return _image_response(formsemestre.description.image)
@bp.route("/formsemestre/<int:formsemestre_id>/description/photo_ens")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/description/photo_ens")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_get_photo_ens(formsemestre_id: int):
"""Photo du responsable, ou illustration du formsemestre. Peut être vide."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.description or not formsemestre.description.photo_ens:
return make_response("", 204) # 204 No Content status
return _image_response(formsemestre.description.photo_ens)
def _image_response(image_data: bytes):
# Guess the mimetype based on the image data
try:
image = PIL.Image.open(io.BytesIO(image_data))
mimetype = image.get_format_mimetype()
except PIL.UnidentifiedImageError:
# Default to binary stream if mimetype cannot be determined
mimetype = "application/octet-stream"
response = make_response(image_data)
response.headers["Content-Type"] = mimetype
return response

View File

@ -53,7 +53,12 @@ from app.scodoc.sco_utils import json_error
@permission_required(Permission.ScoView)
@as_json
def decisions_jury(formsemestre_id: int):
"""Décisions du jury des étudiants du formsemestre."""
"""Décisions du jury des étudiants du formsemestre.
SAMPLES
-------
/formsemestre/1/decisions_jury
"""
# APC, pair:
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
if formsemestre is None:

View File

@ -165,7 +165,7 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
"""
Renvoie tous les justificatifs d'un département
(en ajoutant un champ "formsemestre" si possible)
(en ajoutant un champ "`formsemestre`" si possible).
QUERY
-----
@ -195,7 +195,7 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
"""
# Récupération du département et des étudiants du département
dept: Departement = Departement.query.get(dept_id)
dept: Departement = db.session.get(Departement, dept_id)
if dept is None:
return json_error(404, "Assiduité non existante")
etuds: list[int] = [etud.id for etud in dept.etudiants]
@ -220,9 +220,9 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
def _set_sems(justi: Justificatif, restrict: bool) -> dict:
"""
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
_set_sems Ajoute le formsemestre associé au justificatif s'il existe.
Si le formsemestre n'existe pas, renvoie la simple représentation du justificatif
Si le formsemestre n'existe pas, renvoie la simple représentation du justificatif.
Args:
justi (Justificatif): Le justificatif
@ -263,7 +263,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
@as_json
@permission_required(Permission.ScoView)
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne tous les justificatifs du formsemestre
"""Retourne tous les justificatifs du formsemestre.
QUERY
-----
@ -337,7 +337,7 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
@permission_required(Permission.AbsChange)
def justif_create(etudid: int = None, nip=None, ine=None):
"""
Création d'un justificatif pour l'étudiant (etudid)
Création d'un justificatif pour l'étudiant.
DATA
----
@ -489,7 +489,7 @@ def _create_one(
@permission_required(Permission.AbsChange)
def justif_edit(justif_id: int):
"""
Edition d'un justificatif à partir de son id
Édition d'un justificatif à partir de son id.
DATA
----
@ -611,7 +611,7 @@ def justif_edit(justif_id: int):
@permission_required(Permission.AbsChange)
def justif_delete():
"""
Suppression d'un justificatif à partir de son id
Suppression d'un justificatif à partir de son id.
DATA
----
@ -699,7 +699,7 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
@permission_required(Permission.AbsChange)
def justif_import(justif_id: int = None):
"""
Importation d'un fichier (création d'archive)
Importation d'un fichier (création d'archive).
> Procédure d'importation de fichier : [importer un justificatif](FichiersJustificatifs.md#importer-un-fichier)
"""
@ -752,7 +752,8 @@ def justif_import(justif_id: int = None):
def justif_export(justif_id: int | None = None, filename: str | None = None):
"""
Retourne un fichier d'une archive d'un justificatif.
La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif)
La permission est `ScoView` + (`AbsJustifView` ou être l'auteur du justificatif).
> Procédure de téléchargement de fichier : [télécharger un justificatif](FichiersJustificatifs.md#télécharger-un-fichier)
"""
@ -764,7 +765,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
current_user.has_permission(Permission.AbsJustifView)
or justificatif_unique.user_id == current_user.id
):
return json_error(401, "non autorisé à voir ce fichier")
return json_error(403, "non autorisé à voir ce fichier")
# On récupère l'archive concernée
archive_name: str = justificatif_unique.fichier
@ -791,7 +792,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
@permission_required(Permission.AbsChange)
def justif_remove(justif_id: int = None):
"""
Supression d'un fichier ou d'une archive
Supression d'un fichier ou d'une archive.
> Procédure de suppression de fichier : [supprimer un justificatif](FichiersJustificatifs.md#supprimer-un-fichier)
@ -870,7 +871,7 @@ def justif_remove(justif_id: int = None):
@permission_required(Permission.ScoView)
def justif_list(justif_id: int = None):
"""
Liste les fichiers du justificatif
Liste les fichiers du justificatif.
SAMPLES
-------
@ -917,7 +918,7 @@ def justif_list(justif_id: int = None):
@permission_required(Permission.AbsChange)
def justif_justifies(justif_id: int = None):
"""
Liste assiduite_id justifiées par le justificatif
Liste `assiduite_id` justifiées par le justificatif.
SAMPLES
-------

View File

@ -50,7 +50,12 @@ from app.scodoc.sco_utils import json_error
@permission_required(Permission.ScoSuperAdmin)
@as_json
def logo_list_globals():
"""Liste des noms des logos définis pour le site ScoDoc."""
"""Liste des noms des logos définis pour le site ScoDoc.
SAMPLES
-------
/logos
"""
logos = list_logos()[None]
return list(logos.keys())
@ -63,6 +68,10 @@ def logo_get_global(logoname):
L'image est au format png ou jpg; le format retourné dépend du format sous lequel
l'image a été initialement enregistrée.
SAMPLES
-------
/logo/B
"""
logo = find_logo(logoname=logoname)
if logo is None:
@ -80,15 +89,19 @@ def _core_get_logos(dept_id) -> list:
return list(logos.keys())
@bp.route("/departement/<string:departement>/logos")
@bp.route("/departement/<string:dept_acronym>/logos")
@scodoc
@permission_required(Permission.ScoSuperAdmin)
@as_json
def logo_get_local_by_acronym(departement):
def departement_logos(dept_acronym: str):
"""Liste des noms des logos définis pour le département
désigné par son acronyme.
SAMPLES
-------
/departement/TAPI/logos
"""
dept_id = Departement.from_acronym(departement).id
dept_id = Departement.from_acronym(dept_acronym).id
return _core_get_logos(dept_id)
@ -96,7 +109,7 @@ def logo_get_local_by_acronym(departement):
@scodoc
@permission_required(Permission.ScoSuperAdmin)
@as_json
def logo_get_local_by_id(dept_id):
def departement_logos_by_id(dept_id):
"""Liste des noms des logos définis pour le département
désigné par son id.
"""

View File

@ -16,12 +16,15 @@ from flask_json import as_json
from flask_login import login_required
import app
from app import db
from app.api import api_bp as bp, api_web_bp
from app.api import api_permission_required as permission_required
from app.decorators import scodoc
from app.models import ModuleImpl
from app.scodoc import sco_liste_notes
from app.models import Identite, ModuleImpl, ModuleImplInscription
from app.scodoc import sco_cache, sco_liste_notes
from app.scodoc.sco_moduleimpl import do_moduleimpl_inscrit_etuds
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
@bp.route("/moduleimpl/<int:moduleimpl_id>")
@ -38,37 +41,9 @@ def moduleimpl(moduleimpl_id: int):
------
moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat :
```json
{
"id": 1,
"formsemestre_id": 1,
"module_id": 1,
"responsable_id": 2,
"moduleimpl_id": 1,
"ens": [],
"module": {
"heures_tp": 0,
"code_apogee": "",
"titre": "Initiation aux réseaux informatiques",
"coefficient": 1,
"module_type": 2,
"id": 1,
"ects": null,
"abbrev": "Init aux réseaux informatiques",
"ue_id": 1,
"code": "R101",
"formation_id": 1,
"heures_cours": 0,
"matiere_id": 1,
"heures_td": 0,
"semestre_id": 1,
"numero": 10,
"module_id": 1
}
}
```
SAMPLES
-------
/moduleimpl/1
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return modimpl.to_dict(convert_objects=True)
@ -83,23 +58,68 @@ def moduleimpl(moduleimpl_id: int):
def moduleimpl_inscriptions(moduleimpl_id: int):
"""Liste des inscriptions à ce moduleimpl.
Exemple de résultat :
```json
[
{
"id": 1,
"etudid": 666,
"moduleimpl_id": 1234,
},
...
]
```
SAMPLES
-------
/moduleimpl/1/inscriptions
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return [i.to_dict() for i in modimpl.inscriptions]
@bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/inscrit")
@api_web_bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/inscrit")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl_etud_inscrit(moduleimpl_id: int, etudid: int):
"""Inscrit l'étudiant à ce moduleimpl.
SAMPLES
-------
/moduleimpl/1/etudid/2/inscrit
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
if not modimpl.can_change_inscriptions():
return json_error(403, "opération non autorisée")
etud = Identite.get_etud(etudid)
do_moduleimpl_inscrit_etuds(modimpl.id, modimpl.formsemestre_id, [etud.id])
app.log(f"moduleimpl_etud_inscrit: {etud} inscrit à {modimpl}")
return (
ModuleImplInscription.query.filter_by(moduleimpl_id=modimpl.id, etudid=etud.id)
.first()
.to_dict()
)
@bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/desinscrit")
@api_web_bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/desinscrit")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl_etud_desinscrit(moduleimpl_id: int, etudid: int):
"""Désinscrit l'étudiant de ce moduleimpl.
SAMPLES
-------
/moduleimpl/1/etudid/2/desinscrit
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
if not modimpl.can_change_inscriptions():
return json_error(403, "opération non autorisée")
etud = Identite.get_etud(etudid)
inscription = ModuleImplInscription.query.filter_by(
etudid=etud.id, moduleimpl_id=modimpl.id
).first()
if inscription:
db.session.delete(inscription)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
app.log(f"moduleimpl_etud_desinscrit: {etud} inscrit à {modimpl}")
return {"status": "ok"}
@bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
@login_required
@ -108,24 +128,9 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
def moduleimpl_notes(moduleimpl_id: int):
"""Liste des notes dans ce moduleimpl.
Exemple de résultat :
```json
[
{
"etudid": 17776, // code de l'étudiant
"nom": "DUPONT",
"prenom": "Luz",
"38411": 16.0, // Note dans l'évaluation d'id 38411
"38410": 15.0,
"moymod": 15.5, // Moyenne INDICATIVE module
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
},
...
]
```
SAMPLES
-------
/moduleimpl/1/notes
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
app.set_sco_dept(modimpl.formsemestre.departement.acronym)

View File

@ -45,23 +45,9 @@ from app.scodoc import sco_utils as scu
def partition_info(partition_id: int):
"""Info sur une partition.
Exemple de résultat :
```json
{
'bul_show_rank': False,
'formsemestre_id': 39,
'groups': [
{'id': 268, 'name': 'A', 'partition_id': 100},
{'id': 269, 'name': 'B', 'partition_id': 100}
],
'groups_editable': True,
'id': 100,
'numero': 100,
'partition_name': 'TD',
'show_in_lists': True
}
```
SAMPLES
-------
/partition/1
"""
query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
@ -79,23 +65,9 @@ def partition_info(partition_id: int):
def formsemestre_partitions(formsemestre_id: int):
"""Liste de toutes les partitions d'un formsemestre.
Exemple de résultat :
```json
{
partition_id : {
"bul_show_rank": False,
"formsemestre_id": 1063,
"groups" :
group_id : {
"id" : 12,
"name" : "A",
"partition_id" : partition_id,
}
},
...
}
```
SAMPLES
-------
/formsemestre/1/partitions
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
@ -124,22 +96,9 @@ def group_etudiants(group_id: int):
------
group_id : l'id d'un groupe
Exemple de résultat :
```json
[
{
'civilite': 'M',
'id': 123456,
'ine': None,
'nip': '987654321',
'nom': 'MARTIN',
'nom_usuel': null,
'prenom': 'JEAN'}
},
...
]
```
SAMPLES
-------
/group/1/etudiants
"""
query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
@ -210,7 +169,7 @@ def group_set_etudiant(group_id: int, etudid: int):
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
return json_error(404, "etud non inscrit au formsemestre du groupe")
@ -243,7 +202,7 @@ def group_remove_etud(group_id: int, etudid: int):
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
group.remove_etud(etud)
@ -273,7 +232,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
db.session.execute(
sa.text(
"""DELETE FROM group_membership
@ -316,6 +275,10 @@ def group_create(partition_id: int): # partition-group-create
"group_name" : nom_du_groupe,
}
```
SAMPLES
-------
/partition/1/group/create;{""group_name"" : ""Nouveau Groupe""}
"""
query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
@ -326,7 +289,7 @@ def group_create(partition_id: int): # partition-group-create
if not partition.groups_editable:
return json_error(403, "partition non editable")
if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
args = request.get_json(force=True) # may raise 400 Bad Request
group_name = args.get("group_name")
@ -374,7 +337,7 @@ def group_delete(group_id: int):
if not group.partition.groups_editable:
return json_error(403, "partition non editable")
if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
formsemestre_id = group.partition.formsemestre_id
log(f"deleting {group}")
db.session.delete(group)
@ -391,7 +354,19 @@ def group_delete(group_id: int):
@permission_required(Permission.ScoView)
@as_json
def group_edit(group_id: int):
"""Édition d'un groupe."""
"""Édition d'un groupe.
DATA
----
```json
{
"group_name" : "A1"
}
SAMPLES
-------
/group/1/edit;{""group_name"":""A1""}
"""
query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
query = (
@ -403,7 +378,7 @@ def group_edit(group_id: int):
if not group.partition.groups_editable:
return json_error(403, "partition non editable")
if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
args = request.get_json(force=True) # may raise 400 Bad Request
if "group_name" in args:
@ -436,6 +411,10 @@ def group_set_edt_id(group_id: int, edt_id: str):
Contrairement à `/edit`, peut-être changé pour toute partition
d'un formsemestre non verrouillé.
SAMPLES
-------
/group/1/set_edt_id/EDT_GR1
"""
query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
@ -444,7 +423,7 @@ def group_set_edt_id(group_id: int, edt_id: str):
)
group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
log(f"group_set_edt_id( {group_id}, '{edt_id}' )")
group.edt_id = edt_id
db.session.add(group)
@ -482,7 +461,7 @@ def partition_create(formsemestre_id: int):
if not formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
data = request.get_json(force=True) # may raise 400 Bad Request
partition_name = data.get("partition_name")
if partition_name is None:
@ -544,7 +523,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
if not formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(partition_ids, list) and not all(
isinstance(x, int) for x in partition_ids
@ -590,7 +569,7 @@ def partition_order_groups(partition_id: int):
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
group_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(group_ids, list) and not all(
isinstance(x, int) for x in group_ids
@ -632,6 +611,10 @@ def partition_edit(partition_id: int):
"groups_editable":bool
}
```
SAMPLES
-------
/partition/1/edit;{""bul_show_rank"":1}
"""
query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
@ -640,7 +623,7 @@ def partition_edit(partition_id: int):
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
data = request.get_json(force=True) # may raise 400 Bad Request
modified = False
partition_name = data.get("partition_name")
@ -666,9 +649,8 @@ def partition_edit(partition_id: int):
for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"):
value = data.get(boolean_field)
value = scu.to_bool(value) if value is not None else None
if value is not None and value != getattr(partition, boolean_field):
if not isinstance(value, bool):
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
if boolean_field == "groups_editable" and partition.is_parcours():
return json_error(
API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}"
@ -707,7 +689,7 @@ def partition_delete(partition_id: int):
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée")
return json_error(403, "opération non autorisée")
if not partition.partition_name:
return json_error(
API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut"

View File

@ -37,6 +37,10 @@ from app.scodoc.sco_utils import json_error
def user_info(uid: int):
"""
Info sur un compte utilisateur ScoDoc.
SAMPLES
-------
/user/2
"""
user: User = db.session.get(User, uid)
if user is None:
@ -222,14 +226,19 @@ def user_edit(uid: int):
def user_password(uid: int):
"""Modification du mot de passe d'un utilisateur.
Champs modifiables:
Si le mot de passe ne convient pas, erreur 400.
DATA
----
```json
{
"password": str
}
```.
```
Si le mot de passe ne convient pas, erreur 400.
SAMPLES
-------
/user/3/password;{""password"" : ""rePlaCemeNT456averylongandcomplicated""}
"""
data = request.get_json(force=True) # may raise 400 Bad Request
user: User = User.query.get_or_404(uid)
@ -318,7 +327,12 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
@permission_required(Permission.UsersView)
@as_json
def permissions_list():
"""Liste des noms de permissions définies."""
"""Liste des noms de permissions définies.
SAMPLES
-------
/permissions
"""
return list(Permission.permission_by_name.keys())
@ -329,7 +343,12 @@ def permissions_list():
@permission_required(Permission.UsersView)
@as_json
def role_get(role_name: str):
"""Un rôle"""
"""Un rôle.
SAMPLES
-------
/role/Ens
"""
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
@ -340,7 +359,12 @@ def role_get(role_name: str):
@permission_required(Permission.UsersView)
@as_json
def roles_list():
"""Tous les rôles définis."""
"""Tous les rôles définis.
SAMPLES
-------
/roles
"""
return [role.to_dict() for role in Role.query]
@ -410,6 +434,10 @@ def role_create(role_name: str):
"permissions" : [ 'ScoView', ... ]
}
```
SAMPLES
-------
/role/create/customRole;{""permissions"": [""ScoView"", ""UsersView""]}
"""
role: Role = Role.query.filter_by(name=role_name).first()
if role:
@ -471,7 +499,12 @@ def role_edit(role_name: str):
@permission_required(Permission.ScoSuperAdmin)
@as_json
def role_delete(role_name: str):
"""Suprression d'un rôle."""
"""Suppression d'un rôle.
SAMPLES
-------
/role/customRole/delete
"""
role: Role = Role.query.filter_by(name=role_name).first_or_404()
db.session.delete(role)
db.session.commit()

View File

@ -445,9 +445,10 @@ class User(UserMixin, ScoDocModel):
def set_roles(self, roles, dept):
"set roles in the given dept"
self.user_roles = [
UserRole(user=self, role=r, dept=dept) for r in roles if isinstance(r, Role)
]
self.user_roles = []
for r in roles:
if isinstance(r, Role):
self.add_role(r, dept)
def get_roles(self):
"iterator on my roles"

View File

@ -44,7 +44,9 @@ def formation_change_referentiel(
ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id]
db.session.add(ue)
if ue.parcours:
new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours]
new_list = [
db.session.get(ApcParcours, parcours_map[p.id]) for p in ue.parcours
]
ue.parcours.clear()
ue.parcours.extend(new_list)
db.session.add(ue)
@ -52,7 +54,7 @@ def formation_change_referentiel(
for module in formation.modules:
if module.parcours:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours
db.session.get(ApcParcours, parcours_map[p.id]) for p in module.parcours
]
module.parcours.clear()
module.parcours.extend(new_list)
@ -76,7 +78,8 @@ def formation_change_referentiel(
# FormSemestre / parcours_formsemestre
for formsemestre in formation.formsemestres:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours
db.session.get(ApcParcours, parcours_map[p.id])
for p in formsemestre.parcours
]
formsemestre.parcours.clear()
formsemestre.parcours.extend(new_list)

View File

@ -632,17 +632,23 @@ def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int)
def ue_associee_au_niveau_du_parcours(
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
) -> UniteEns:
"L'UE associée à ce niveau, ou None"
) -> tuple[UniteEns, str]:
"""L'UE associée à ce niveau, ou None.
Renvoie aussi un message d'avertissement en cas d'associations multiples
(en principe un niveau ne doit être associé qu'à une seule UE)
"""
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
msg = ""
if len(ues) > 1:
msg = f"""{' et '.join(ue.acronyme for ue in ues)}
associées au niveau {niveau} / {sem_name}. Utilisez le cas échéant l'item "Désassocier"."""
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours]
if ues_pair_avec_parcours:
ues = ues_pair_avec_parcours
ues_avec_parcours = [ue for ue in ues if ue.parcours]
if ues_avec_parcours:
ues = ues_avec_parcours
if len(ues) > 1:
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}")
return ues[0] if ues else None
return ues[0] if ues else None, msg
def parcour_formation_competences(
@ -700,6 +706,7 @@ def parcour_formation_competences(
"ue_impair": None,
"ues_pair": [],
"ues_impair": [],
"warning": "",
}
# Toutes les UEs de la formation dans ce parcours ou tronc commun
ues = [
@ -715,10 +722,10 @@ def parcour_formation_competences(
ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
# UE associée au niveau dans ce parcours
ue_pair = ue_associee_au_niveau_du_parcours(
ue_pair, warning_pair = ue_associee_au_niveau_du_parcours(
ues_pair_possibles, niveau, f"S{2*annee}"
)
ue_impair = ue_associee_au_niveau_du_parcours(
ue_impair, warning_impair = ue_associee_au_niveau_du_parcours(
ues_impair_possibles, niveau, f"S{2*annee-1}"
)
@ -736,6 +743,7 @@ def parcour_formation_competences(
for ue in ues_impair_possibles
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
],
"warning": ", ".join(filter(None, [warning_pair, warning_impair])),
}
competences = [

View File

@ -1557,8 +1557,8 @@ class DecisionsProposeesUE(DecisionsProposees):
res: ResultatsSemestreBUT = (
self.rcue.res_pair if paire else self.rcue.res_impair
)
self.moy_ue = np.NaN
self.moy_ue_with_cap = np.NaN
self.moy_ue = np.nan
self.moy_ue_with_cap = np.nan
self.ue_status = {}
if self.ue.type != sco_codes.UE_STANDARD:

View File

@ -105,7 +105,7 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
},
xls_style_base=xls_style_base,
)
return tab.make_page(fmt=fmt, javascripts=["js/etud_info.js"], init_qtip=True)
return tab.make_page(fmt=fmt)
def pvjury_table_but(

View File

@ -541,17 +541,16 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in modimpl.evaluations]
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
if (
modimpl.module.module_type == ModuleType.RESSOURCE
or modimpl.module.module_type == ModuleType.SAE
):
if modimpl.module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
try:
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
except KeyError:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
if (
ue_poids.evaluation_id in evals_poids.index
and ue_poids.ue_id in evals_poids.columns
):
evals_poids.at[ue_poids.evaluation_id, ue_poids.ue_id] = ue_poids.poids
# ignore poids vers des UEs qui n'existent plus ou sont dans un autre semestre...
# Initialise poids non enregistrés:
default_poids = (
@ -564,7 +563,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
if np.isnan(evals_poids.values.flat).any():
ue_coefs = modimpl.module.get_ue_coef_dict()
for ue in ues:
evals_poids[ue.id][evals_poids[ue.id].isna()] = (
evals_poids.loc[evals_poids[ue.id].isna(), ue.id] = (
1 if ue_coefs.get(ue.id, default_poids) > 0 else 0
)

View File

@ -82,7 +82,7 @@ def compute_sem_moys_apc_using_ects(
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
except ZeroDivisionError:
# peut arriver si aucun module... on ignore
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
moy_gen = pd.Series(np.nan, index=etud_moy_ue_df.index)
except TypeError:
if None in ects:
formation = db.session.get(Formation, formation_id)
@ -93,7 +93,7 @@ def compute_sem_moys_apc_using_ects(
scodoc_dept=g.scodoc_dept, formation_id=formation_id)}">{formation.get_titre_version()}</a>)"""
)
)
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
moy_gen = pd.Series(np.nan, index=etud_moy_ue_df.index)
else:
raise
return moy_gen

View File

@ -92,7 +92,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
for mod_coef in query:
if mod_coef.module_id in module_coefs_df:
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
module_coefs_df.at[mod_coef.ue_id, mod_coef.module_id] = mod_coef.coef
# silently ignore coefs associated to other modules (ie when module_type is changed)
# Initialisation des poids non fixés:
@ -138,14 +138,16 @@ def df_load_modimpl_coefs(
)
for mod_coef in mod_coefs:
try:
modimpl_coefs_df[mod2impl[mod_coef.module_id]][
mod_coef.ue_id
] = mod_coef.coef
except IndexError:
if (
mod_coef.ue_id in modimpl_coefs_df.index
and mod2impl[mod_coef.module_id] in modimpl_coefs_df.columns
):
modimpl_coefs_df.at[mod_coef.ue_id, mod2impl[mod_coef.module_id]] = (
mod_coef.coef
)
# il peut y avoir en base des coefs sur des modules ou UE
# qui ont depuis été retirés de la formation
pass
# qui ont depuis été retirés de la formation : on ignore ces coefs
# Initialisation des poids non fixés:
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
@ -178,7 +180,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
except ValueError:
app.critical_error(
f"""notes_sem_assemble_cube: shapes {
", ".join([x.shape for x in modimpls_notes_arr])}"""
", ".join([str(x.shape) for x in modimpls_notes_arr])}"""
)
return modimpls_notes.swapaxes(0, 1)
@ -299,7 +301,11 @@ def compute_ue_moys_apc(
)
# Les "dispenses" sont très peu nombreuses et traitées en python:
for dispense_ue in dispense_ues:
etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0
if (
dispense_ue[0] in etud_moy_ue_df.columns
and dispense_ue[1] in etud_moy_ue_df.index
):
etud_moy_ue_df.at[dispense_ue[1], dispense_ue[0]] = 0.0
return etud_moy_ue_df

View File

@ -1 +1,23 @@
# empty but required for pylint
"""WTF Forms for ScoDoc
"""
from flask_wtf import FlaskForm
class ScoDocForm(FlaskForm):
"""Super class for ScoDoc forms
(inspired by @iziram)
"""
def __init__(self, *args, **kwargs):
"Init form, adding a filed for our error messages"
super().__init__(*args, **kwargs)
self.ok = True
self.error_messages: list[str] = [] # used to report our errors
def set_error(self, err_msg, field=None):
"Set error message both in form and field"
self.ok = False
self.error_messages.append(err_msg)
if field:
field.errors.append(err_msg)

View File

@ -0,0 +1,112 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Formulaire édition description formsemestre
"""
from wtforms import (
BooleanField,
FileField,
SelectField,
StringField,
TextAreaField,
SubmitField,
)
from wtforms.validators import AnyOf, Optional
from app.forms import ScoDocForm
from app.models import FORMSEMESTRE_DISPOSITIFS
from app.scodoc import sco_utils as scu
class DateDMYField(StringField):
"Champ date JJ/MM/AAAA"
def __init__(self, *args, **kwargs):
render_kw = kwargs.pop("render_kw", {})
render_kw.update({"class": "datepicker", "size": 10})
super().__init__(*args, render_kw=render_kw, **kwargs)
# note: process_formdata(self, valuelist) ne fonctionne pas
# en cas d'erreur de saisie les valeurs ne sont pas ré-affichées.
# On vérifie donc les valeurs dans le code de la vue.
def process_data(self, value):
"Process data from model to form"
if value:
self.data = value.strftime(scu.DATE_FMT)
else:
self.data = ""
class FormSemestreDescriptionForm(ScoDocForm):
"Formulaire édition description formsemestre"
description = TextAreaField(
"Description",
validators=[Optional()],
description="""texte libre : informations
sur le contenu, les objectifs, les modalités d'évaluation, etc.""",
)
horaire = StringField(
"Horaire", validators=[Optional()], description="ex: les lundis 9h-12h"
)
date_debut_inscriptions = DateDMYField(
"Date de début des inscriptions",
description="""date d'ouverture des inscriptions
(laisser vide pour autoriser tout le temps)""",
render_kw={
"id": "date_debut_inscriptions",
},
)
date_fin_inscriptions = DateDMYField(
"Date de fin des inscriptions",
render_kw={
"id": "date_fin_inscriptions",
},
)
image = FileField(
"Image", validators=[Optional()], description="Image illustrant cette formation"
)
campus = StringField(
"Campus", validators=[Optional()], description="ex: Villetaneuse"
)
salle = StringField("Salle", validators=[Optional()], description="ex: salle 123")
dispositif = SelectField(
"Dispositif",
choices=FORMSEMESTRE_DISPOSITIFS.items(),
coerce=int,
description="modalité de formation",
validators=[AnyOf(FORMSEMESTRE_DISPOSITIFS.keys())],
)
modalites_mcc = TextAreaField(
"Modalités de contrôle des connaissances",
validators=[Optional()],
description="texte libre",
)
photo_ens = FileField(
"Photo de l'enseignant(e)",
validators=[Optional()],
description="ou autre illustration",
)
public = StringField(
"Public visé", validators=[Optional()], description="ex: débutants"
)
prerequis = TextAreaField(
"Prérequis", validators=[Optional()], description="texte libre"
)
responsable = StringField(
"Responsable",
validators=[Optional()],
description="""nom de l'enseignant de la formation, ou personne
chargée de l'organisation du semestre.""",
)
wip = BooleanField(
"Travaux en cours",
description="work in progress: si coché, affichera juste le titre du semestre",
)
submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -14,12 +14,13 @@ class _EditModimplsCodesForm(FlaskForm):
# construit dynamiquement ci-dessous
# pylint: disable=invalid-name
def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
"Création d'un formulaire pour éditer les codes"
# Formulaire dynamique, on créé une classe ad-hoc
class F(_EditModimplsCodesForm):
pass
"class factory"
def _gen_mod_form(modimpl: ModuleImpl):
field = StringField(

View File

@ -39,7 +39,7 @@ from wtforms import ValidationError
from wtforms.fields.simple import StringField, HiddenField
from app.models import Departement
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_logos
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import LogoInsert
@ -47,10 +47,6 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_logos import find_logo
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
# class ItemForm(FlaskForm):
# """Unused Generic class to document common behavior for classes
# * ScoConfigurationForm
@ -369,6 +365,7 @@ def _make_data(modele):
class LogosConfigurationForm(FlaskForm):
"Panneau de configuration des logos"
depts = FieldList(FormField(DeptForm))
def __init__(self, *args, **kwargs):

118
app/forms/multiselect.py Normal file
View File

@ -0,0 +1,118 @@
"""
Simplification des multiselect HTML/JS
"""
class MultiSelect:
"""
Classe pour faciliter l'utilisation du multi-select HTML/JS
Les values sont représentées en dict {
value: "...",
label:"...",
selected: True/False (default to False),
single: True/False (default to False)
}
Args:
values (dict[str, list[dict]]): Dictionnaire des valeurs
génère des <optgroup> pour chaque clef du dictionnaire
génère des <option> pour chaque valeur du dictionnaire
name (str, optional): Nom du multi-select. Defaults to "multi-select".
html_id (str, optional): Id HTML du multi-select. Defaults to "multi-select".
classname (str, optional): Classe CSS du multi-select. Defaults to "".
label (str, optional): Label du multi-select. Defaults to "".
export (str, optional): Format du multi-select (HTML/JS). Defaults to "js".
HTML : group_ids="val1"&group_ids="val2"...
JS : ["val1","val2", ...]
**kwargs: Arguments supplémentaires (appliqué au multiselect en HTML <multi-select key="value" ...>)
"""
def __init__(
self,
values: dict[str, list[dict]],
name="multi-select",
html_id="multi-select",
label="",
classname="",
**kwargs,
) -> None:
self.values: dict[str, list[dict]] = values
self._on = ""
self.name: str = name
self.html_id: str = html_id
self.classname: str = classname
self.label: str = label or name
self.args: dict = kwargs
self.js: str = ""
self.export: str = "return values"
def html(self) -> str:
"""
Génère l'HTML correspondant au multi-select
"""
opts: list[str] = []
for key, values in self.values.items():
optgroup = f"<optgroup label='{key}'>"
for value in values:
selected = "selected" if value.get("selected", False) else ""
single = "single" if value.get("single", False) else ""
opt = f"<option value='{value.get('value')}' {selected} {single} >{value.get('label')}</option>"
optgroup += opt
optgroup += "</optgroup>"
opts.append(optgroup)
args: list[str] = [f'{key}="{value}"' for key, value in self.args.items()]
js: str = "{" + self.js + "}"
export: str = "{" + self.export + "}"
return f"""
<multi-select
label="{self.label}"
id="{self.html_id}"
name="{self.name}"
class="{self.classname}"
{" ".join(args)}
>
{"".join(opts)}
</multi-select>
<script>
window.addEventListener('load', () => {{document.getElementById("{self.html_id}").on((values)=>{js});
document.getElementById("{self.html_id}").format((values)=>{export});}} );
</script>
"""
def change_event(self, js: str) -> None:
"""
Ajoute un évènement de changement au multi-select
CallBack JS : (event) => {/*actions à effectuer*/}
Sera retranscrit dans l'HTML comme :
document.getElementById(%self.id%).on((event)=>{%self.js%})
Exemple d'utilisation :
js : "console.log(event.target.value)"
"""
self.js: str = js
def export_format(self, js: str) -> None:
"""
Met à jour le format de retour de valeur du multi-select
CallBack JS : (values) => {/*actions à effectuer*/}
Sera retranscrit dans l'HTML comme :
document.getElementById(%self.id%).format((values)=>{%self.js%})
Exemple d'utilisation :
js : "return values.map(v=> 'val:'+v)"
"""
self.export: str = js

View File

@ -53,4 +53,6 @@ class FeuilleAppelPreForm(FlaskForm):
},
)
submit = SubmitField("Télécharger la liste d'émargement")
submit = SubmitField(
"Télécharger la liste d'émargement", id="btn-submit", name="btn-submit"
)

View File

@ -38,7 +38,7 @@ class ScoDocModel(db.Model):
__abstract__ = True # declare an abstract class for SQLAlchemy
def clone(self, not_copying=()):
"""Clone, not copying the given attrs
"""Clone, not copying the given attrs, and add to session.
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
"""
d = dict(self.__dict__)
@ -111,6 +111,12 @@ class ScoDocModel(db.Model):
db.session.add(self)
return modified
def to_dict(self) -> dict:
"dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def edit_from_form(self, form) -> bool:
"""Generic edit method for updating model instance.
True if modification.
@ -182,6 +188,10 @@ from app.models.formsemestre import (
NotesSemSet,
notes_semset_formsemestre,
)
from app.models.formsemestre_descr import (
FormSemestreDescription,
FORMSEMESTRE_DISPOSITIFS,
)
from app.models.moduleimpls import (
ModuleImpl,
notes_modules_enseignants,

View File

@ -297,7 +297,7 @@ class Assiduite(ScoDocModel):
moduleimpl_id = int(moduleimpl_id)
except ValueError as exc:
raise ScoValueError("Module non reconnu") from exc
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
# ici moduleimpl est None si non spécifié
@ -352,8 +352,8 @@ class Assiduite(ScoDocModel):
"""
if self.moduleimpl_id is not None:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
mod: Module = Module.query.get(modimpl.module_id)
modimpl: ModuleImpl = db.session.get(ModuleImpl, self.moduleimpl_id)
mod: Module = db.session.get(Module, modimpl.module_id)
if traduire:
return f"{mod.code} {mod.titre}"
return mod

View File

@ -260,8 +260,8 @@ class Identite(models.ScoDocModel):
Add to session but don't commit.
True if modification.
"""
check_etud_duplicate_code(args, "code_nip")
check_etud_duplicate_code(args, "code_ine")
check_etud_duplicate_code(args, "code_nip", etudid=self.id)
check_etud_duplicate_code(args, "code_ine", etudid=self.id)
return super().from_dict(args, **kwargs)
@classmethod
@ -796,11 +796,11 @@ class Identite(models.ScoDocModel):
)
def check_etud_duplicate_code(args, code_name, edit=True):
def check_etud_duplicate_code(args, code_name, edit=True, etudid: int | None = None):
"""Vérifie que le code n'est pas dupliqué.
Raises ScoGenError si problème.
"""
etudid = args.get("etudid", None)
etudid = etudid or args.get("etudid", None)
if not args.get(code_name, None):
return
etuds = Identite.query.filter_by(

View File

@ -109,7 +109,7 @@ class ScolarNews(db.Model):
)
def __str__(self):
"'Chargement notes dans Stage (S3 FI) par Aurélie Dupont'"
"exemple: 'Notes dans Stage (S3 FI) par Aurélie Dupont'"
formsemestre = self.get_news_formsemestre()
user = User.query.filter_by(user_name=self.authenticated_user).first()
@ -271,7 +271,7 @@ class ScolarNews(db.Model):
return ""
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
H = [
f"""<div class="scobox news"><div class="scobox-title"><a href="{
f"""<div class="scobox news" desktop="true"><div class="scobox-title" desktop="true"><a href="{
dept_news_url
}">Dernières opérations</a>
</div><ul class="newslist">"""
@ -286,14 +286,18 @@ class ScolarNews(db.Model):
f"""<li class="newslist">
<span class="newstext"><a href="{dept_news_url}" class="stdlink">...</a>
</span>
</li>"""
</li>
</ul>
<ul class="newslist" mobile="true" style="margin-bottom: 0px;">
<li><a href="{dept_news_url}" class="stdlink">Dernières opérations</a></li>
</ul>
</div>
"""
)
H.append("</ul></div>")
# Informations générales
H.append(
f"""<div>
f"""<div desktop="true">
Pour en savoir plus sur ScoDoc voir
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
</div>

View File

@ -23,7 +23,7 @@ from sqlalchemy.sql import text
from sqlalchemy import func
import app.scodoc.sco_utils as scu
from app import db, log
from app import db, email, log
from app.auth.models import User
from app import models
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
@ -36,7 +36,7 @@ from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
from app.models.events import ScolarNews
from app.models.events import Scolog, ScolarNews
from app.models.formations import Formation
from app.models.groups import GroupDescr, Partition
from app.models.moduleimpls import (
@ -45,9 +45,10 @@ from app.models.moduleimpls import (
notes_modules_enseignants,
)
from app.models.modules import Module
from app.models.scolar_event import ScolarEvent
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus, sco_preferences
from app.scodoc import codes_cursus, sco_cache, sco_preferences
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
@ -69,6 +70,8 @@ class FormSemestre(models.ScoDocModel):
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
titre = db.Column(db.Text(), nullable=False)
# nb max d'inscriptions (non DEM), null si illimité:
capacite_accueil = db.Column(db.Integer, nullable=True)
date_debut = db.Column(db.Date(), nullable=False)
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
@ -143,6 +146,12 @@ class FormSemestre(models.ScoDocModel):
lazy="dynamic",
cascade="all, delete-orphan",
)
description = db.relationship(
"FormSemestreDescription",
back_populates="formsemestre",
cascade="all, delete-orphan",
uselist=False,
)
etuds = db.relationship(
"Identite",
secondary="notes_formsemestre_inscription",
@ -1013,20 +1022,129 @@ class FormSemestre(models.ScoDocModel):
codes |= {x.strip() for x in self.elt_passage_apo.split(",") if x}
return codes
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
def get_inscrits(
self, include_demdef=False, order=False, etats: set | None = None
) -> list[Identite]:
"""Liste des étudiants inscrits à ce semestre
Si include_demdef, tous les étudiants, avec les démissionnaires
et défaillants.
Si etats, seuls les étudiants dans l'un des états indiqués.
Si order, tri par clé sort_key
"""
if include_demdef:
etuds = [ins.etud for ins in self.inscriptions]
else:
elif not etats:
etuds = [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
else:
etuds = [ins.etud for ins in self.inscriptions if ins.etat in etats]
if order:
etuds.sort(key=lambda e: e.sort_key)
return etuds
def inscrit_etudiant(
self,
etud: "Identite",
etat: str = scu.INSCRIT,
etape: str | None = None,
method: str | None = None,
) -> "FormSemestreInscription":
"""Inscrit l'étudiant au semestre, ou renvoie son inscription s'il l'est déjà.
Vérifie la capacité d'accueil si indiquée (non null): si le semestre est plein,
lève une exception. Génère un évènement et un log étudiant.
method: indique origine de l'inscription pour le log étudiant.
"""
# remplace ancien do_formsemestre_inscription_create()
if not self.etat: # check lock
raise ScoValueError("inscrit_etudiant: semestre verrouille")
inscr = FormSemestreInscription.query.filter_by(
formsemestre_id=self.id, etudid=etud.id
).first()
if inscr is not None:
return inscr
if self.capacite_accueil is not None:
# tous sauf démissionnaires:
inscriptions = self.get_inscrits(etats={scu.INSCRIT, scu.DEF})
if len(inscriptions) >= self.capacite_accueil:
raise ScoValueError(
f"Semestre {self.titre} complet : {len(self.inscriptions)} inscrits",
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=self.id,
),
)
inscr = FormSemestreInscription(
formsemestre_id=self.id, etudid=etud.id, etat=etat, etape=etape
)
db.session.add(inscr)
# Évènement
event = ScolarEvent(
etudid=etud.id,
formsemestre_id=self.id,
event_type="INSCRIPTION",
)
db.session.add(event)
# Log etudiant
Scolog.logdb(
method=method,
etudid=etud.id,
msg=f"inscription en semestre {self.titre_annee()}",
commit=True,
)
log(
f"inscrit_etudiant: {etud.nomprenom} ({etud.id}) au semestre {self.titre_annee()}"
)
# Notification mail
self._notify_inscription(etud)
sco_cache.invalidate_formsemestre(formsemestre_id=self.id)
return inscr
def desinscrit_etudiant(self, etud: Identite):
"Désinscrit l'étudiant du semestre (et notifie le cas échéant)"
inscr_sem = FormSemestreInscription.query.filter_by(
etudid=etud.id, formsemestre_id=self.id
).first()
if not inscr_sem:
raise ScoValueError(
f"{etud.nomprenom} ({etud.id}) n'est pas inscrit au semestre !"
)
db.session.delete(inscr_sem)
Scolog.logdb(
method="desinscrit_etudiant",
etudid=etud.id,
msg=f"désinscription semestre {self.titre_annee()}",
commit=True,
)
log(
f"desinscrit_etudiant: {etud.nomprenom} ({etud.id}) au semestre {self.titre_annee()}"
)
self._notify_inscription(etud, action="désinscrit")
sco_cache.invalidate_formsemestre(formsemestre_id=self.id)
def _notify_inscription(self, etud: Identite, action="inscrit") -> None:
"Notifie inscription d'un étudiant: envoie un mail selon paramétrage"
destinations = (
sco_preferences.get_preference("emails_notifications_inscriptions", self.id)
or ""
)
destinations = [x.strip() for x in destinations.split(",")]
destinations = [x for x in destinations if x]
if not destinations:
return
txt = f"""{etud.nom_prenom()}
s'est {action}{etud.e}
en {self.titre_annee()}"""
subject = f"""Inscription de {etud.nom_prenom()} en {self.titre_annee()}"""
# build mail
log(f"_notify_inscription: sending notification to {destinations}")
log(f"_notify_inscription: subject: {subject}")
log(txt)
email.send_email(
"[ScoDoc] " + subject, email.get_from_addr(), destinations, txt
)
def get_partitions_list(
self, with_default=True, only_listed=False
) -> list[Partition]:
@ -1318,7 +1436,7 @@ notes_formsemestre_responsables = db.Table(
)
class FormSemestreEtape(db.Model):
class FormSemestreEtape(models.ScoDocModel):
"""Étape Apogée associée au semestre"""
__tablename__ = "notes_formsemestre_etapes"
@ -1349,7 +1467,7 @@ class FormSemestreEtape(db.Model):
return ApoEtapeVDI(self.etape_apo)
class FormationModalite(db.Model):
class FormationModalite(models.ScoDocModel):
"""Modalités de formation, utilisées pour la présentation
(grouper les semestres, générer des codes, etc.)
"""
@ -1400,7 +1518,7 @@ class FormationModalite(db.Model):
raise
class FormSemestreUECoef(db.Model):
class FormSemestreUECoef(models.ScoDocModel):
"""Coef des UE capitalisees arrivant dans ce semestre"""
__tablename__ = "notes_formsemestre_uecoef"
@ -1441,7 +1559,7 @@ class FormSemestreUEComputationExpr(db.Model):
computation_expr = db.Column(db.Text())
class FormSemestreCustomMenu(db.Model):
class FormSemestreCustomMenu(models.ScoDocModel):
"""Menu custom associe au semestre"""
__tablename__ = "notes_formsemestre_custommenu"
@ -1457,7 +1575,7 @@ class FormSemestreCustomMenu(db.Model):
idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu
class FormSemestreInscription(db.Model):
class FormSemestreInscription(models.ScoDocModel):
"""Inscription à un semestre de formation"""
__tablename__ = "notes_formsemestre_inscription"
@ -1503,7 +1621,7 @@ class FormSemestreInscription(db.Model):
} {('etape="'+self.etape+'"') if self.etape else ''}>"""
class NotesSemSet(db.Model):
class NotesSemSet(models.ScoDocModel):
"""semsets: ensemble de formsemestres pour exports Apogée"""
__tablename__ = "notes_semset"

View File

@ -0,0 +1,82 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Description d'un formsemestre pour applications tierces.
Ces informations sont éditables dans ScoDoc et publiés sur l'API
pour affichage dans l'application tierce.
"""
from app import db
from app import models
class FormSemestreDescription(models.ScoDocModel):
"""Informations décrivant un "semestre" (session) de formation
pour un apprenant.
"""
__tablename__ = "notes_formsemestre_description"
id = db.Column(db.Integer, primary_key=True)
description = db.Column(db.Text(), nullable=False, default="", server_default="")
"description du cours, html autorisé"
horaire = db.Column(db.Text(), nullable=False, default="", server_default="")
"indication sur l'horaire, texte libre"
date_debut_inscriptions = db.Column(db.DateTime(timezone=True), nullable=True)
date_fin_inscriptions = db.Column(db.DateTime(timezone=True), nullable=True)
wip = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
"work in progress: si vrai, affichera juste le titre du semestre"
# Store image data directly in the database:
image = db.Column(db.LargeBinary(), nullable=True)
campus = db.Column(db.Text(), nullable=False, default="", server_default="")
salle = db.Column(db.Text(), nullable=False, default="", server_default="")
dispositif = db.Column(db.Integer, nullable=False, default=0, server_default="0")
"0 présentiel, 1 online, 2 hybride"
modalites_mcc = db.Column(db.Text(), nullable=False, default="", server_default="")
"modalités de contrôle des connaissances"
photo_ens = db.Column(db.LargeBinary(), nullable=True)
"photo de l'enseignant(e)"
public = db.Column(db.Text(), nullable=False, default="", server_default="")
"public visé"
prerequis = db.Column(db.Text(), nullable=False, default="", server_default="")
"prérequis (texte libre, html autorisé)"
responsable = db.Column(db.Text(), nullable=False, default="", server_default="")
"responsable du cours (texte libre, html autorisé)"
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
nullable=False,
)
formsemestre = db.relationship(
"FormSemestre", back_populates="description", uselist=False
)
def __repr__(self):
return f"<FormSemestreDescription {self.id} {self.formsemestre}>"
def clone(self, not_copying=()) -> "FormSemestreDescription":
"""clone instance"""
return super().clone(not_copying=not_copying + ("formsemestre_id",))
def to_dict(self, exclude_images=True) -> dict:
"dict, tous les attributs sauf les images"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
if exclude_images:
d.pop("image", None)
d.pop("photo_ens", None)
return d
FORMSEMESTRE_DISPOSITIFS = {
0: "présentiel",
1: "en ligne",
2: "hybride",
}

View File

@ -242,6 +242,16 @@ class GroupDescr(ScoDocModel):
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
)
@classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
Exclude `partition_id` : a group cannot be moved from a partition to another.
"""
return super().filter_model_attributes(
data,
excluded=(excluded or set()) | {"partition_id"},
)
def get_nom_with_part(self, default="-") -> str:
"""Nom avec partition: 'TD A'
Si groupe par défaut (tous), utilise default ou "-"

View File

@ -325,7 +325,7 @@ notes_modules_enseignants = db.Table(
# XXX il manque probablement une relation pour gérer cela
class ModuleImplInscription(db.Model):
class ModuleImplInscription(ScoDocModel):
"""Inscription à un module (etudiants,moduleimpl)"""
__tablename__ = "notes_moduleimpl_inscription"

View File

@ -56,7 +56,7 @@ class BulAppreciations(models.ScoDocModel):
return safehtml.html_to_safe_html(self.comment or "")
class NotesNotes(db.Model):
class NotesNotes(models.ScoDocModel):
"""Une note"""
__tablename__ = "notes_notes"
@ -75,12 +75,6 @@ class NotesNotes(db.Model):
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
uid = db.Column(db.Integer, db.ForeignKey("user.id"))
def to_dict(self) -> dict:
"dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def __repr__(self):
"pour debug"
from app.models.evaluations import Evaluation

View File

@ -333,7 +333,7 @@ class SxTag(pe_tabletags.TableTag):
etud_moy = np.max(set_cube_no_nan, axis=2)
# Fix les max non calculé -1 -> NaN
etud_moy[etud_moy < 0] = np.NaN
etud_moy[etud_moy < 0] = np.nan
# Le dataFrame
etud_moy_tag_df = pd.DataFrame(

View File

@ -95,7 +95,6 @@ def TrivialFormulator(
To use text_suggest elements, one must:
- specify options in text_suggest_options (a dict)
- HTML page must load JS AutoSuggest.js and CSS autosuggest_inquisitor.css
- bodyOnLoad must call JS function init_tf_form(formid)
"""
method = method.lower()
if method == "get":
@ -776,9 +775,12 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
# => only one form with text_suggest field on a page.
R.append(
"""<script type="text/javascript">
function init_tf_form(formid) {
function init_tf_form() {
%s
}
document.addEventListener('DOMContentLoaded', function() {
init_tf_form();
});
</script>"""
% "\n".join(suggest_js)
)

View File

@ -57,7 +57,6 @@ from reportlab.lib.units import cm
from flask import render_template
from app.scodoc import html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc import sco_excel
from app.scodoc import sco_pdf
@ -686,6 +685,7 @@ class GenTable:
javascripts=(),
with_html_headers=True,
publish=True,
template="sco_page.j2",
):
"""
Build page at given format
@ -704,7 +704,7 @@ class GenTable:
H.append(self.html())
if with_html_headers:
return render_template(
"sco_page.j2",
template,
content="\n".join(H),
title=page_title,
javascripts=javascripts,

View File

@ -27,10 +27,7 @@
"""HTML Header/Footer for ScoDoc pages"""
import html
from flask import g, render_template, url_for
from flask import request
from flask_login import current_user
import app.scodoc.sco_utils as scu
@ -39,22 +36,6 @@ from app.scodoc import html_sidebar
import sco_version
# Some constants:
# Multiselect menus are used on a few pages and not loaded by default
BOOTSTRAP_MULTISELECT_JS = [
"libjs/bootstrap/js/bootstrap.min.js",
"libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js",
"libjs/purl.js",
]
BOOTSTRAP_MULTISELECT_CSS = [
"libjs/bootstrap/css/bootstrap.min.css",
"libjs/bootstrap/css/bootstrap-theme.min.css",
"libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css",
]
def standard_html_header():
"""Standard HTML header for pages outside depts"""
# not used in ZScolar, see sco_header
@ -85,7 +66,6 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
<meta http-equiv="Content-Style-Type" content="text/css" />
@ -98,21 +78,13 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css" />
<link href="{scu.STATIC_DIR}/css/gt_table.css" rel="stylesheet" type="text/css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{
if (document.getElementById('gtrcontent')) {{
enableTooltips("gtrcontent");
}}
if (document.getElementById('sidebar')) {{
enableTooltips("sidebar");
}}
}};
</script>
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="{scu.STATIC_DIR}/jQuery/jquery-migrate-3.5.2.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
@ -121,12 +93,30 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.css" />
<script src="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
<script src="{scu.STATIC_DIR}/js/etud_info.js"></script>
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<script>
document.addEventListener('DOMContentLoaded', function() {{
if (document.getElementById('gtrcontent')) {{
enableTooltips("gtrcontent");
}}
if (document.getElementById('sidebar')) {{
enableTooltips("sidebar");
}}
}});
</script>
"""
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
"""HTML header for top level pages"""
H = [
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
"""</head><body id="gtrcontent">""",
@ -139,17 +129,12 @@ def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
def sco_header(
# optional args
page_title="", # page title
no_side_bar=False, # hide sidebar
no_sidebar=False, # hide sidebar
cssstyles=(), # additionals CSS sheets
javascripts=(), # additionals JS filenames to load
scripts=(), # script to put in page header
bodyOnLoad="", # JS
init_qtip=False, # include qTip
init_google_maps=False, # Google maps
init_datatables=True,
titrebandeau="", # titre dans bandeau superieur
head_message="", # message action (petit cadre jaune en haut) DEPRECATED
user_check=True, # verifie passwords temporaires
etudid=None,
formsemestre_id=None,
):
@ -162,51 +147,21 @@ def sco_header(
g.current_etudid = etudid
scodoc_flash_status_messages()
# Get head message from http request:
if not head_message:
if request.method == "POST":
head_message = request.form.get("head_message", "")
elif request.method == "GET":
head_message = request.args.get("head_message", "")
params = {
"page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar,
"no_sidebar": no_sidebar,
"ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
"encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name,
}
if bodyOnLoad:
params["bodyOnLoad_mkup"] = """onload="%s" """ % bodyOnLoad
else:
params["bodyOnLoad_mkup"] = ""
if no_side_bar:
if no_sidebar:
params["margin_left"] = "1em"
else:
params["margin_left"] = "140px"
H = [
"""<!DOCTYPE html><html lang="fr">
<!-- ScoDoc legacy -->
<head>
<meta charset="utf-8"/>
<title>%(page_title)s</title>
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
H = [_HTML_BEGIN % params]
"""
% params
]
# jQuery UI
# can modify loaded theme here
H.append(
f"""
<link type="text/css" rel="stylesheet"
href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link type="text/css" rel="stylesheet"
href="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.css" />
"""
)
if init_google_maps:
# It may be necessary to add an API key:
H.append('<script src="https://maps.google.com/maps/api/js"></script>')
@ -219,61 +174,17 @@ def sco_header(
H.append(
f"""
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/gt_table.css" rel="stylesheet" type="text/css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{
if (document.getElementById('gtrcontent')) {{
enableTooltips("gtrcontent");
}}
if (document.getElementById('sidebar')) {{
enableTooltips("sidebar");
}}
}};
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
const SCO_TIMEZONE="{scu.TIME_ZONE}";
</script>"""
)
# jQuery
H.append(
f"""
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
"""
)
# qTip
if init_qtip:
H.append(
f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet"
href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
"""
)
H.append(
f"""<script
src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
"""
)
if init_google_maps:
if init_google_maps: # utilisé uniquement pour carte lycées
H.append(
f'<script src="{scu.STATIC_DIR}/libjs/jquery.ui.map.full.min.js"></script>'
)
if init_datatables:
H.append(
f"""<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css"/>
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>"""
)
# H.append(
# f'<link href="{scu.STATIC_DIR}/css/tooltip.css" rel="stylesheet" type="text/css" />'
# )
# JS additionels
for js in javascripts:
H.append(f"""<script src="{scu.STATIC_DIR}/{js}"></script>\n""")
@ -295,15 +206,18 @@ def sco_header(
H.append(script)
H.append("""</script>""")
H.append("</head>")
# Fin head, Body et bandeau haut:
H.append(
f"""</head>
<!-- Legacy ScoDoc header -->
<body>
{scu.CUSTOM_HTML_HEADER}
{'' if no_sidebar else html_sidebar.sidebar(etudid)}
<div id="mobileNav" mobile="true" style="display:none;"></div>
# Body et bandeau haut:
H.append("""<body %(bodyOnLoad_mkup)s>""" % params)
H.append(scu.CUSTOM_HTML_HEADER)
#
if not no_side_bar:
H.append(html_sidebar.sidebar(etudid))
H.append("""<div id="gtrcontent">""")
<div id="gtrcontent">
"""
)
# En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask
H.append(render_template("flashed_messages.j2"))
@ -311,10 +225,6 @@ def sco_header(
# Barre menu semestre:
H.append(formsemestre_page_title(formsemestre_id))
#
if head_message:
H.append('<div class="head_message">' + html.escape(head_message) + "</div>")
#
# div pour affichage messages temporaires
H.append('<div id="sco_msg" class="head_message"></div>')
#
@ -329,18 +239,3 @@ def sco_footer():
+ scu.CUSTOM_HTML_FOOTER
+ """</body></html>"""
)
def html_sem_header(
title, with_page_header=True, with_h2=True, page_title=None, **args
):
"Titre d'une page semestre avec lien vers tableau de bord"
# sem now unused and thus optional...
if with_page_header:
h = sco_header(page_title="%s" % (page_title or title), **args)
else:
h = ""
if with_h2:
return h + f"""<h2 class="formsemestre">{title}</h2>"""
else:
return h

View File

@ -41,10 +41,8 @@ from app.scodoc import sco_groups
from app.scodoc import sco_trombino
from app.scodoc import sco_archives
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
class EtudsArchiver(sco_archives.BaseArchiver):
@ -142,9 +140,6 @@ def etud_upload_file_form(etudid):
etud = Identite.get_etud(etudid)
H = [
html_sco_header.sco_header(
page_title=f"Chargement d'un document associé à {etud.nomprenom}",
),
f"""<h2>Chargement d'un document associé à {etud.nomprenom}</h2>
<p>Le fichier ne doit pas dépasser {
@ -171,8 +166,12 @@ def etud_upload_file_form(etudid):
cancelbutton="Annuler",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return render_template(
"sco_page.j2",
title=f"Chargement d'un document associé à {etud.nomprenom}",
content="\n".join(H) + tf[1],
)
if tf[0] == -1:
return flask.redirect(etud.url_fiche())
data = tf[2]["datafile"].read()
descr = tf[2]["description"]
@ -217,7 +216,6 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
head_message="annulation",
),
parameters={"etudid": etudid, "archive_name": archive_name},
)
@ -264,9 +262,6 @@ def etudarchive_generate_excel_sample(group_id=None):
def etudarchive_import_files_form(group_id):
"""Formulaire pour importation fichiers d'un groupe"""
H = [
html_sco_header.sco_header(
page_title="Import de fichiers associés aux étudiants"
),
"""<h2 class="formsemestre">Téléchargement de fichier associés aux étudiants</h2>
<p>Les fichiers associés (dossiers d'admission, certificats, ...), de
types quelconques (pdf, doc, images) sont accessibles aux utilisateurs via
@ -293,7 +288,6 @@ def etudarchive_import_files_form(group_id):
"""
% group_id,
]
F = html_sco_header.sco_footer()
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
@ -314,7 +308,11 @@ def etudarchive_import_files_form(group_id):
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F
return render_template(
"sco_page.j2",
title="Import de fichiers associés aux étudiants",
content="\n".join(H) + tf[1] + "</li></ol>",
)
# retrouve le semestre à partir du groupe:
group = sco_groups.get_group(group_id)
if tf[0] == -1:
@ -360,7 +358,7 @@ def etudarchive_import_files(
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_view",
"scolar.groups_feuilles",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),

View File

@ -27,7 +27,7 @@
import json
import flask
from flask import flash, g, request, url_for
from flask import flash, g, render_template, request, url_for
from app import ScoDocJSONEncoder
from app.but import jury_but_pv
@ -36,7 +36,6 @@ from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoPermissionDenied
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
@ -123,18 +122,18 @@ def do_formsemestre_archive(
)
if table_html:
flash(f"Moyennes archivées le {date}", category="info")
data = "\n".join(
[
html_sco_header.sco_header(
page_title=f"Moyennes archivées le {date}",
no_side_bar=True,
),
f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
"""<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }
data = render_template(
"sco_page.j2",
no_sidebar=True,
title=f"Moyennes archivées le {date}",
content="\n".join(
[
f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
"""<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }
</style>""",
table_html,
html_sco_header.sco_footer(),
]
table_html,
]
),
)
PV_ARCHIVER.store(
archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id
@ -238,12 +237,6 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
)
H = [
html_sco_header.html_sem_header(
"Archiver les PV et résultats du semestre",
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
"""<p class="help">Cette page permet de générer et d'archiver tous
les documents résultant de ce semestre: PV de jury, lettres individuelles,
tableaux récapitulatifs.</p><p class="help">Les documents archivés sont
@ -260,7 +253,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
}">Paramétrage</a>"
(accessible à l'administrateur du département).</em>
</p>""",
html_sco_header.sco_footer(),
]
descr = [
@ -312,7 +304,17 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
html_foot_markup=menu_choix_groupe,
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + "\n".join(F)
return render_template(
"sco_page.j2",
title="Archiver les PV et résultats",
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
content="<h2>Archiver les PV et résultats du semestre</h2>"
+ "\n".join(H)
+ "\n"
+ tf[1]
+ "\n".join(F),
)
elif tf[0] == -1:
msg = "Opération annulée"
else:
@ -372,7 +374,7 @@ def formsemestre_list_archives(formsemestre_id):
}
archives_descr.append(a)
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
H = []
if not archives_descr:
H.append("<p>aucune archive enregistrée</p>")
else:
@ -400,7 +402,9 @@ def formsemestre_list_archives(formsemestre_id):
H.append("</ul></li>")
H.append("</ul>")
return "\n".join(H) + html_sco_header.sco_footer()
return render_template(
"sco_page.j2", title="Archive des PV et résultats", content="\n".join(H)
)
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):

View File

@ -56,7 +56,6 @@ from app.models import (
)
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoTemporaryError
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_assiduites
from app.scodoc import sco_bulletins_generator
@ -960,7 +959,21 @@ def formsemestre_bulletinetud(
elif fmt == "pdfmail":
return ""
H = [
_formsemestre_bulletinetud_header_html(etud, formsemestre, fmt, version),
render_template(
"bul_head.j2",
etud=etud,
fmt=fmt,
formsemestre=formsemestre,
menu_autres_operations=make_menu_autres_operations(
etud=etud,
formsemestre=formsemestre,
endpoint="notes.formsemestre_bulletinetud",
version=version,
),
scu=scu,
time=time,
version=version,
),
bulletin,
render_template(
"bul_foot.j2",
@ -971,10 +984,18 @@ def formsemestre_bulletinetud(
inscription_courante=etud.inscription_courante(),
inscription_str=etud.inscription_descr()["inscription_str"],
),
html_sco_header.sco_footer(),
]
return "".join(H)
return render_template(
"sco_page.j2",
title=f"Bulletin de {etud.nomprenom}",
content="".join(H),
javascripts=[
"js/bulletin.js",
"libjs/d3.v3.min.js",
"js/radar_bulletin.js",
],
cssstyles=["css/radar_bulletin.css"],
)
def can_send_bulletin_by_mail(formsemestre_id):
@ -1312,38 +1333,3 @@ def make_menu_autres_operations(
},
]
return htmlutils.make_menu("Autres opérations", menu_items, alone=True)
def _formsemestre_bulletinetud_header_html(
etud,
formsemestre: FormSemestre,
fmt=None,
version=None,
):
H = [
html_sco_header.sco_header(
page_title=f"Bulletin de {etud.nomprenom}",
javascripts=[
"js/bulletin.js",
"libjs/d3.v3.min.js",
"js/radar_bulletin.js",
],
cssstyles=["css/radar_bulletin.css"],
),
render_template(
"bul_head.j2",
etud=etud,
fmt=fmt,
formsemestre=formsemestre,
menu_autres_operations=make_menu_autres_operations(
etud=etud,
formsemestre=formsemestre,
endpoint="notes.formsemestre_bulletinetud",
version=version,
),
scu=scu,
time=time,
version=version,
),
]
return "\n".join(H)

View File

@ -29,7 +29,7 @@
Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
"""
import http
from flask import url_for, g, request
from flask import g, render_template, request, url_for
from app import log
from app.comp import res_sem
@ -40,7 +40,6 @@ import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.gen_tables import GenTable
from app.scodoc import safehtml
from app.scodoc import html_sco_header
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module
@ -76,9 +75,9 @@ def report_debouche_date(start_year=None, fmt="html"):
tab.base_url = f"{request.base_url}?start_year={start_year}"
return tab.make_page(
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
javascripts=["js/etud_info.js"],
fmt=fmt,
with_html_headers=True,
template="sco_page_dept.j2",
)
@ -227,14 +226,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
def report_debouche_ask_date(msg: str) -> str:
"""Formulaire demande date départ"""
return f"""{html_sco_header.sco_header()}
return render_template(
"sco_page_dept.j2",
content=f"""
<h2>Table des débouchés des étudiants</h2>
<form method="GET">
{msg}
<input type="text" name="start_year" value="" size=10/>
</form>
{html_sco_header.sco_footer()}
"""
""",
)
# ----------------------------------------------------------------------------
@ -324,11 +325,11 @@ def itemsuivi_set_date(itemsuivi_id, item_date):
return ("", 204)
def itemsuivi_set_situation(object, value):
def itemsuivi_set_situation(obj, value):
"""set situation"""
if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
itemsuivi_id = object
itemsuivi_id = obj
situation = value.strip("-_ \t")
# log('itemsuivi_set_situation %s : %s' % (itemsuivi_id, situation))
cnx = ndb.GetDBConnexion()

View File

@ -29,7 +29,7 @@
(portage from DTML)
"""
import flask
from flask import flash, g, url_for, request
from flask import flash, g, url_for, render_template, request
import sqlalchemy
from app import db
@ -54,7 +54,6 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
formation: Formation = Formation.query.get_or_404(formation_id)
H = [
html_sco_header.sco_header(page_title="Suppression d'une formation"),
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
]
formsemestres = formation.formsemestres.all()
@ -85,6 +84,7 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
OK="Supprimer cette formation",
cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
parameters={"formation_id": formation_id},
template="sco_page_dept.j2",
)
else:
do_formation_delete(formation_id)
@ -95,8 +95,9 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
}">continuer</a></p>"""
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
return render_template(
"sco_page-dept.j2", content="\n".join(H), title="Suppression d'une formation"
)
def do_formation_delete(formation_id):

View File

@ -51,7 +51,6 @@ from app.scodoc.sco_exceptions import (
ScoGenError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_moduleimpl
@ -208,8 +207,8 @@ def module_delete(module_id=None):
)
H = [
html_sco_header.sco_header(page_title="Suppression d'un module"),
f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"} ({module.code})</h2>""",
f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"}
({module.code})</h2>""",
]
dest_url = url_for(
@ -227,7 +226,11 @@ def module_delete(module_id=None):
cancelbutton="Annuler",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
return render_template(
"sco_page_dept.j2",
title="Suppression d'un module",
content="\n".join(H) + tf[1],
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
@ -372,16 +375,6 @@ def module_edit(
"""
H = [
html_sco_header.sco_header(
page_title=page_title,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
"js/module_edit.js",
],
),
f"""<h2>{title}</h2>""",
render_template(
"scodoc/help/modules.j2",
@ -780,7 +773,7 @@ def module_edit(
scu.get_request_args(),
descr,
html_foot_markup=(
f"""<div class="sco_tag_module_edit"><span
f"""<div class="scobox sco_tag_module_edit"><span
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
>{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
"""
@ -793,8 +786,17 @@ def module_edit(
)
#
if tf[0] == 0:
return (
"\n".join(H)
return render_template(
"sco_page_dept.j2",
title=page_title,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
"js/module_edit.js",
],
content="\n".join(H)
+ tf[1]
+ (
f"""
@ -805,8 +807,7 @@ def module_edit(
"""
if not create
else ""
)
+ html_sco_header.sco_footer()
),
)
elif tf[0] == -1:
return flask.redirect(
@ -904,9 +905,6 @@ def module_table(formation_id):
raise ScoValueError("invalid formation !")
formation: Formation = Formation.query.get_or_404(formation_id)
H = [
html_sco_header.sco_header(
page_title=f"Liste des modules de {formation.titre}"
),
f"""<h2>Listes des modules dans la formation {formation.titre} ({formation.acronyme}</h2>
<ul class="notes_module_list">
""",
@ -926,8 +924,11 @@ def module_table(formation_id):
)
H.append("</li>")
H.append("</ul>")
H.append(html_sco_header.sco_footer())
return "\n".join(H)
return render_template(
"sco_page_dept.j2",
title=f"Liste des modules de {formation.titre}",
content="\n".join(H),
)
def module_is_locked(module_id):

View File

@ -25,9 +25,8 @@
#
##############################################################################
"""Ajout/Modification/Suppression UE
"""Ajout/Modification/Suppression UE"""
"""
import re
import sqlalchemy as sa
@ -506,8 +505,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
else:
clone_form = ""
return f"""
{html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"])}
return render_template(
"sco_page_dept.j2",
title=title,
javascripts=["js/edit_ue.js"],
content=f"""
<h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2>
<p class="help">Les UEs sont des groupes de modules dans une formation donnée,
utilisés pour la validation (on calcule des moyennes par UE et applique des
@ -530,9 +532,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
<div id="bonus_description" class="scobox"></div>
<div id="ue_list_code" class="sco_box sco_green_bg"></div>
{html_sco_header.sco_footer()}
"""
""",
)
elif tf[0] == 1:
if create:
if not tf[2]["ue_code"]:
@ -764,20 +765,6 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"delete_small_dis_img", title="Suppression impossible (module utilisé)"
)
H = [
html_sco_header.sco_header(
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/ue_table.css"],
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ [
"libjs/jinplace-1.2.1.min.js",
"js/ue_list.js",
"js/edit_ue.js",
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
],
page_title=f"Formation {formation.acronyme} v{formation.version}",
),
f"""<h2>{formation.html()} {lockicon}
</h2>
""",
@ -1068,8 +1055,20 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
warn, _ = sco_formsemestre_validation.check_formation_ues(formation)
H.append(warn)
H.append(html_sco_header.sco_footer())
return "".join(H)
return render_template(
"sco_page_dept.j2",
content="".join(H),
page_title=f"Formation {formation.acronyme} v{formation.version}",
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/ue_table.css"],
javascripts=[
"libjs/jinplace-1.2.1.min.js",
"js/ue_list.js",
"js/edit_ue.js",
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
],
)
def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
@ -1129,9 +1128,7 @@ def _ue_table_ues(
scodoc_dept=g.scodoc_dept,
ue_id=ue["ue_id"],
)
ue[
"code_apogee_str"
] = f""", Apo: <span
ue["code_apogee_str"] = f""", Apo: <span
class="{klass}" data-url="{edit_url}" id="{ue['ue_id']}"
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
ue["code_apogee"] or ""

View File

@ -597,8 +597,6 @@ def _view_etuds_page(
return f"""
{html_sco_header.sco_header(
page_title=title,
init_qtip=True,
javascripts=["js/etud_info.js"],
)}
<h2>{title}</h2>
@ -751,8 +749,6 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
H = [
html_sco_header.sco_header(
page_title=f"""Maquette Apogée enregistrée pour {etape_apo}""",
init_qtip=True,
javascripts=["js/etud_info.js"],
),
f"""<h2>Étudiants dans la maquette Apogée {etape_apo}</h2>
<p>Pour l'ensemble <a class="stdlink" href="{

View File

@ -27,16 +27,16 @@
"""Vérification des absences à une évaluation
"""
from flask import url_for, g
from flask import g, render_template, url_for
from flask_sqlalchemy.query import Query
from app import db
from app.models import Evaluation, FormSemestre, Identite, Assiduite
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_groups
from app.views import ScoData
def evaluation_check_absences(evaluation: Evaluation):
@ -109,8 +109,10 @@ def evaluation_check_absences(evaluation: Evaluation):
def evaluation_check_absences_html(
evaluation: Evaluation, with_header=True, show_ok=True
):
"""Affiche état vérification absences d'une évaluation"""
) -> str:
"""Affiche état vérification absences d'une évaluation.
Si with_header, génère page complète, sinon fragment html.
"""
(
note_but_abs, # une note alors qu'il était signalé abs
abs_non_signalee, # note ABS alors que pas signalé abs
@ -121,10 +123,6 @@ def evaluation_check_absences_html(
if with_header:
H = [
html_sco_header.html_sem_header(
"Vérification absences à l'évaluation",
formsemestre_id=evaluation.moduleimpl.formsemestre_id,
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation.id),
"""<p class="help">Vérification de la cohérence entre les notes saisies
et les absences signalées.</p>""",
@ -208,19 +206,19 @@ def evaluation_check_absences_html(
etudlist(abs_but_exc)
if with_header:
H.append(html_sco_header.sco_footer())
return render_template(
"sco_page.j2",
title="Vérification absences à l'évaluation",
content="<h2>Vérification absences à l'évaluation</h2>" + "\n".join(H),
sco=ScoData(formsemestre=evaluation.moduleimpl.formsemestre),
)
return "\n".join(H)
def formsemestre_check_absences_html(formsemestre_id):
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
formsemestre: FormSemestre = FormSemestre.query.filter_by(
dept_id=g.scodoc_dept_id, id=formsemestre_id
).first_or_404()
def formsemestre_check_absences_html(formsemestre_id: int):
"""Affiche état vérification absences pour toutes les évaluations du semestre."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre",
),
"""<p class="help">Vérification de la cohérence entre les notes saisies
et les absences signalées.
Sont listés tous les modules avec des évaluations.<br>Aucune action n'est effectuée:
@ -248,5 +246,9 @@ def formsemestre_check_absences_html(formsemestre_id):
)
H.append("</div>")
H.append(html_sco_header.sco_footer())
return "\n".join(H)
return render_template(
"sco_page.j2",
content="<h2>Vérification absences aux évaluations de ce semestre</h2>"
+ "\n".join(H),
title="Vérification absences aux évaluations de ce semestre",
)

View File

@ -10,7 +10,7 @@ avec leur état.
Sur une idée de Pascal Bouron, de Lyon.
"""
import time
from flask import g, url_for
from flask import g, render_template, url_for
from app import db
from app.models import Evaluation, FormSemestre
@ -23,7 +23,7 @@ import app.scodoc.sco_utils as scu
def evaluations_recap(formsemestre_id: int) -> str:
"""Page récap. de toutes les évaluations d'un semestre"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
rows, titles = evaluations_recap_table(formsemestre)
column_ids = titles.keys()
filename = scu.sanitize_filename(
@ -32,11 +32,14 @@ def evaluations_recap(formsemestre_id: int) -> str:
if not rows:
return '<div class="evaluations_recap"><div class="message">aucune évaluation</div></div>'
H = [
html_sco_header.sco_header(
page_title="Évaluations du semestre",
javascripts=["js/evaluations_recap.js"],
),
f"""<div class="evaluations_recap"><table class="evaluations_recap compact {
f"""<h2>Évaluations du semestre</h2>
<div>
<a class="stdlink" href="{url_for(
'notes.formsemestre_check_absences_html',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
)}">Vérifier les absences aux évaluations</a>
</div>
<div class="evaluations_recap"><table class="evaluations_recap compact {
'apc' if formsemestre.formation.is_apc() else 'classic'
}"
data-filename="{filename}">""",
@ -56,13 +59,19 @@ def evaluations_recap(formsemestre_id: int) -> str:
H.append(
"""</tbody></table></div>
<div class="help">Les étudiants démissionnaires ou défaillants ne sont pas pris en compte dans cette table.</div>
<div class="help">Les étudiants démissionnaires ou défaillants ne sont
pas pris en compte dans cette table.</div>
"""
)
H.append(
html_sco_header.sco_footer(),
)
return "".join(H)
return render_template(
"sco_page.j2",
title="Évaluations du semestre",
javascripts=["js/evaluations_recap.js"],
content="".join(H),
)
def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:

View File

@ -31,9 +31,7 @@ import collections
import datetime
import operator
from flask import url_for
from flask import g
from flask import request
from flask import g, render_template, request, url_for
from flask_login import current_user
from app import db
@ -45,8 +43,6 @@ from app.models import Evaluation, FormSemestre, ModuleImpl, Module
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
@ -470,7 +466,7 @@ class CalendrierEval(sco_gen_cal.Calendrier):
# View
def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre"""
"""Page avec calendrier de toutes les évaluations de ce semestre"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
@ -481,17 +477,17 @@ def formsemestre_evaluations_cal(formsemestre_id):
cal = CalendrierEval(year, evaluations, nt)
cal_html = cal.get_html()
return f"""
{
html_sco_header.html_sem_header(
"Evaluations du semestre",
cssstyles=["css/calabs.css"],
)
}
return render_template(
"sco_page.j2",
cssstyles=["css/calabs.css"],
title="Évaluations du semestre",
content=f"""
<h2>Évaluations du semestre</h2>
<div class="cal_evaluations">
{ cal_html }
</div>
<p>soit {nb_evals} évaluations planifiées;
<div class="scobox maxwidth">
<p>soit {nb_evals} évaluations planifiées&nbsp;:
</p>
<ul>
<li>en <span style=
@ -513,8 +509,9 @@ def formsemestre_evaluations_cal(formsemestre_id):
)
}" class="stdlink">voir les délais de correction</a>
</p>
{ html_sco_header.sco_footer() }
"""
</div>
""",
)
def evaluation_date_first_completion(evaluation_id) -> datetime.datetime:
@ -651,7 +648,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
"""HTML description of evaluation, for page headers
edit_in_place: allow in-place editing when permitted (not implemented)
"""
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
evaluation = Evaluation.get_evaluation(evaluation_id)
modimpl = evaluation.moduleimpl
responsable: User = db.session.get(User, modimpl.responsable_id)
resp_nomprenom = responsable.get_prenomnom()

View File

@ -45,6 +45,7 @@ from openpyxl.worksheet.worksheet import Worksheet
import app.scodoc.sco_utils as scu
from app import log
from app.models.scolar_event import ScolarEvent
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import notesdb, sco_preferences
@ -638,11 +639,12 @@ def excel_feuille_listeappel(
lines,
partitions=None,
with_codes=False,
with_date_inscription=False,
with_paiement=False,
server_name=None,
edt_params: dict = None,
):
"""generation feuille appel
"""Génération feuille appel.
edt_params :
- "discipline" : Discipline
@ -763,7 +765,8 @@ def excel_feuille_listeappel(
cells.append(ws.make_cell("etudid", style3))
cells.append(ws.make_cell("code_nip", style3))
cells.append(ws.make_cell("code_ine", style3))
if with_date_inscription:
cells.append(ws.make_cell("Date inscr.", style3))
# case Groupes
cells.append(ws.make_cell("Groupes", style3))
letter_int += 1
@ -805,7 +808,15 @@ def excel_feuille_listeappel(
cells.append(ws.make_cell(code_nip, style2t3))
code_ine = t.get("code_ine", "")
cells.append(ws.make_cell(code_ine, style2t3))
if with_date_inscription:
event = ScolarEvent.query.filter_by(
etudid=t["etudid"],
event_type="INSCRIPTION",
formsemestre_id=formsemestre_id,
).first()
if event:
date_inscription = event.event_date
cells.append(ws.make_cell(date_inscription, style2t3))
cells.append(ws.make_cell(style=style2t3))
ws.append_row(cells)
ws.set_row_dimension_height(row_id, 30)
@ -814,7 +825,7 @@ def excel_feuille_listeappel(
ws.append_blank_row()
# bas de page (date, serveur)
dt = time.strftime("%d/%m/%Y à %Hh%M")
dt = time.strftime(scu.DATEATIME_FMT)
if server_name:
dt += " sur " + server_name
cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i)

View File

@ -52,10 +52,12 @@ class ScoValueError(ScoException):
# mal nommée: super classe de toutes les exceptions avec page
# d'erreur gentille.
def __init__(self, msg, dest_url=None, safe=False):
def __init__(self, msg, dest_url=None, dest_label=None, safe=False):
super().__init__(msg)
# champs utilisés par template sco_value_error.j2
self.dest_url = dest_url
self.safe = safe # utilisé par template sco_value_error.j2
self.dest_label = dest_label
self.safe = safe
class ScoPermissionDenied(ScoValueError):

View File

@ -25,8 +25,8 @@
#
##############################################################################
"""Export d'une table avec les résultats de tous les étudiants
"""
"""Export d'une table avec les résultats de tous les étudiants"""
from flask import url_for, g, request
from app.comp import res_sem
@ -73,9 +73,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
formsemestre_ids_parcours = [sem["formsemestre_id"] for sem in semlist_parcours]
# Ensemble des étudiants
etuds_infos = (
{}
) # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
etuds_infos = {} # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
for formsemestre_id in formsemestre_ids_parcours:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
@ -287,10 +285,7 @@ def scodoc_table_results(
H = [
html_sco_header.sco_header(
page_title="Export résultats",
init_qtip=True,
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ ["js/etud_info.js", "js/export_results.js"],
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS,
javascripts=["js/export_results.js"],
),
# XXX
"""
@ -327,9 +322,9 @@ _DATE_FORM = """
</div>
<div>
<b>Types de parcours :</b>
<select name="types_parcours" id="parcours_sel" class="multiselect" multiple="multiple">
<multi-select name="types_parcours" id="parcours_sel" label="Choisir le(s) parcours...">
{menu_options}
</select>
</multi-select>
<input type="submit" name="" value=" charger " width=100/>
</form>

View File

@ -28,7 +28,7 @@
"""Recherche d'étudiants
"""
import flask
from flask import url_for, g, request
from flask import url_for, g, render_template, request
from flask_login import current_user
import sqlalchemy as sa
@ -36,7 +36,6 @@ from app import db
from app.models import Departement, Identite
import app.scodoc.notesdb as ndb
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
@ -52,7 +51,7 @@ def form_search_etud(
title="Rechercher un étudiant par nom&nbsp;: ",
add_headers=False, # complete page
):
"form recherche par nom"
"form recherche par nom: utilisé pour choisir un étudiant à inscrire, par exemple"
H = []
H.append(
f"""<form action="{
@ -93,10 +92,8 @@ def form_search_etud(
H.append("</form>")
if add_headers:
return (
html_sco_header.sco_header(page_title="Choix d'un étudiant")
+ "\n".join(H)
+ html_sco_header.sco_footer()
return render_template(
"sco_page.j2", title="Choix d'un étudiant", content="\n".join(H)
)
else:
return "\n".join(H)
@ -177,14 +174,7 @@ def search_etud_in_dept(expnom=""):
url_args["etudid"] = etuds[0].id
return flask.redirect(url_for(endpoint, **url_args))
H = [
html_sco_header.sco_header(
page_title="Recherche d'un étudiant",
no_side_bar=False,
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
H = []
if len(etuds) == 0 and len(etuds) <= 1:
H.append("""<h2>chercher un étudiant:</h2>""")
else:
@ -267,7 +257,9 @@ def search_etud_in_dept(expnom=""):
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP
de l'étudiant. Saisir au moins deux caractères.</p>"""
)
return "\n".join(H) + html_sco_header.sco_footer()
return render_template(
"sco_page_dept.j2", title="Recherche d'un étudiant", content="\n".join(H)
)
def search_etuds_infos(expnom=None, code_nip=None) -> list[dict]:
@ -314,7 +306,7 @@ def search_etud_by_name(term: str) -> list:
# ---------- Recherche sur plusieurs département
def search_etud_in_accessible_depts(
def search_etuds_in_accessible_depts(
expnom=None,
) -> tuple[list[list[Identite]], list[str]]:
"""
@ -335,14 +327,14 @@ def search_etud_in_accessible_depts(
return result, accessible_depts
def table_etud_in_accessible_depts(expnom=None):
def table_etuds_in_accessible_depts(expnom=None):
"""
Page avec table étudiants trouvés, dans tous les departements.
Attention: nous sommes ici au niveau de ScoDoc, pas dans un département
"""
result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
result, accessible_depts = search_etuds_in_accessible_depts(expnom=expnom)
H = [
f"""<div class="table_etud_in_accessible_depts">
f"""<div class="table_etuds_in_accessible_depts">
<h3>Recherche multi-département de "<tt>{expnom}</tt>"</h3>
""",
]
@ -367,7 +359,7 @@ def table_etud_in_accessible_depts(expnom=None):
rows=rows,
html_sortable=True,
html_class="table_leftalign",
# table_id="etud_in_accessible_depts",
table_id="etuds_in_accessible_depts",
)
H.append('<div class="table_etud_in_dept">')
@ -387,8 +379,10 @@ def table_etud_in_accessible_depts(expnom=None):
</div>
"""
)
return (
html_sco_header.scodoc_top_html_header(page_title="Choix d'un étudiant")
+ "\n".join(H)
+ html_sco_header.standard_html_footer()
return render_template(
"base.j2",
title="Choix d'un étudiant",
content="\n".join(H),
javascripts=["DataTables/datatables.min.js"],
cssstyles=["DataTables/datatables.min.css"],
)

View File

@ -174,7 +174,9 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
preferences=sco_preferences.SemPreferences(),
table_id="formation_table_recap",
)
return tab.make_page(fmt=fmt, javascripts=["js/formation_recap.js"])
return tab.make_page(
fmt=fmt, javascripts=["js/formation_recap.js"], template="sco_page_dept.j2"
)
def export_recap_formations_annee_scolaire(annee_scolaire):

View File

@ -149,6 +149,7 @@ def formsemestre_associate_new_version(
"formation_id": formation_id,
"formsemestre_id": formsemestre_id,
},
template="sco_page_dept.j2",
)
elif request.method == "POST":
if formsemestre_id is not None: # pas dans le form car checkbox disabled

View File

@ -657,7 +657,7 @@ def formation_list_table(detail: bool) -> GenTable:
"version": "Version",
"formation_code": "Code",
"sems_list_txt": "Semestres",
"referentiel": "Réf.",
"referentiel": "Réf. Comp.",
"date_fin_dernier_sem": "Fin dernier sem.",
"annee_dernier_sem": "Année dernier sem.",
"semestres_ues": "Semestres avec UEs",

View File

@ -53,6 +53,7 @@ _formsemestreEditor = ndb.EditableTable(
"semestre_id",
"formation_id",
"titre",
"capacite_accueil",
"date_debut",
"date_fin",
"gestion_compensation",

View File

@ -28,7 +28,7 @@
"""Menu "custom" (défini par l'utilisateur) dans les semestres
"""
import flask
from flask import g, url_for, request
from flask import g, url_for, render_template, request
from flask_login import current_user
from app.models.config import ScoDocSiteConfig, PersonalizedLink
@ -37,9 +37,6 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_formsemestre
from app.scodoc import sco_edt_cal
_custommenuEditor = ndb.EditableTable(
"notes_formsemestre_custommenu",
@ -101,19 +98,18 @@ def formsemestre_custommenu_html(formsemestre_id):
"args": {"formsemestre_id": formsemestre_id},
}
)
return htmlutils.make_menu("Liens", menu)
return menu
def formsemestre_custommenu_edit(formsemestre_id):
"""Dialog to edit the custom menu"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
dest_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
H = [
html_sco_header.html_sem_header("Modification du menu du semestre "),
"""<div class="help">
<p>Ce menu, spécifique à chaque semestre, peut être utilisé pour
placer des liens vers vos applications préférées.
@ -164,7 +160,14 @@ def formsemestre_custommenu_edit(formsemestre_id):
name="tf",
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + html_sco_header.sco_footer()
return render_template(
"sco_page.j2",
title="Modification du menu du semestre",
content="<h2>Modification du menu du semestre</h2>"
+ "\n".join(H)
+ "\n"
+ tf[1],
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:

View File

@ -28,8 +28,7 @@
"""Form choix modules / responsables et creation formsemestre
"""
import flask
from flask import url_for, flash, redirect
from flask import g, request
from flask import flash, g, request, redirect, render_template, url_for
from flask_login import current_user
import sqlalchemy as sa
@ -61,7 +60,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
@ -81,36 +79,26 @@ def _default_sem_title(formation):
def formsemestre_createwithmodules():
"""Page création d'un semestre"""
H = [
html_sco_header.sco_header(
page_title="Création d'un semestre",
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
),
"""<h2>Mise en place d'un semestre de formation</h2>""",
]
H = ["""<h2>Mise en place d'un semestre de formation</h2>"""]
r = do_formsemestre_createwithmodules()
if isinstance(r, str):
H.append(r)
else:
if not isinstance(r, str):
return r # response redirect
return "\n".join(H) + html_sco_header.sco_footer()
H.append(r)
return render_template(
"sco_page_dept.j2",
title="Création d'un semestre",
content="\n".join(H),
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
)
def formsemestre_editwithmodules(formsemestre_id):
def formsemestre_editwithmodules(formsemestre_id: int):
"""Page modification semestre"""
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
H = [
html_sco_header.html_sem_header(
"Modification du semestre",
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
)
]
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
H = []
if not formsemestre.etat:
H.append(
f"""<p>{scu.icontag(
@ -138,7 +126,13 @@ def formsemestre_editwithmodules(formsemestre_id):
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
return render_template(
"sco_page.j2",
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
title="Modification du semestre",
content="<h2>Modification du semestre</h2>" + "\n".join(H),
)
def can_edit_sem(formsemestre_id: int = None, sem=None):
@ -350,8 +344,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"labels": modalites_titles,
},
),
]
modform.append(
(
"semestre_id",
{
@ -367,10 +359,21 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
},
),
)
(
"capacite_accueil",
{
"title": "Capacité d'accueil",
"size": 4,
"explanation": "nombre max d'inscrits (hors démissionnaires). Laisser vide si pas de limite.",
"type": "int",
"allow_null": True,
},
),
]
etapes = sco_portal_apogee.get_etapes_apogee_dept()
# Propose les etapes renvoyées par le portail
# et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)
# et ajoute les étapes du semestre qui ne sont pas dans la liste
# (soit la liste a changé, soit l'étape a été ajoutée manuellement)
etapes_set = {et[0] for et in etapes}
if edit:
for etape_vdi in formsemestre.etapes_apo_vdi():
@ -508,6 +511,12 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Modifier les codes Apogée et emploi du temps des modules</a>
</p>
<p><a class="stdlink" href="{url_for("notes.edit_formsemestre_description",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Éditer la description externe du semestre</a>
</p>
<h3>Sélectionner les modules, leurs responsables et les étudiants
à inscrire:</h3>
"""
@ -843,6 +852,14 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
):
msg = '<ul class="tf-msg"><li class="tf-msg">Code étape Apogée manquant</li></ul>'
# check capacité accueil si indiquée
if edit and isinstance(tf[2]["capacite_accueil"], int):
new_capacite_accueil = tf[2]["capacite_accueil"]
inscriptions = formsemestre.get_inscrits(etats={scu.INSCRIT, scu.DEF})
if len(inscriptions) > new_capacite_accueil:
msg = f"""<ul class="tf-msg"><li class="tf-msg">Capacité d'accueil insuffisante
(il y a {len(inscriptions)} inscrits non démissionaires)</li></ul>"""
if tf[0] == 0 or msg:
return f"""<p>Formation <a class="discretelink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept,
@ -1164,13 +1181,9 @@ def formsemestre_clone(formsemestre_id):
}
H = [
html_sco_header.html_sem_header(
"Copie du semestre",
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
),
"""<p class="help">Cette opération duplique un semestre: on reprend les mêmes modules et responsables. Aucun étudiant n'est inscrit.</p>""",
"""<p class="help">Cette opération duplique un semestre:
on reprend les mêmes modules et responsables.
Aucun étudiant n'est inscrit.</p>""",
]
descr = [
@ -1247,7 +1260,13 @@ def formsemestre_clone(formsemestre_id):
if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>'
if tf[0] == 0 or msg:
return "".join(H) + msg + tf[1] + html_sco_header.sco_footer()
return render_template(
"sco_page.j2",
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
title="Copie du semestre",
content="".join(H) + msg + tf[1],
)
elif tf[0] == -1: # cancel
return flask.redirect(
url_for(
@ -1287,6 +1306,7 @@ def do_formsemestre_clone(
clone_partitions=False,
):
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
Clone description.
New dates, responsable_id
"""
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
@ -1375,10 +1395,15 @@ def do_formsemestre_clone(
# 5- Copie les parcours
formsemestre.parcours = formsemestre_orig.parcours
# 6- Copy description
if formsemestre_orig.description:
formsemestre.description = formsemestre_orig.description.clone()
db.session.add(formsemestre)
db.session.commit()
# 6- Copy partitions and groups
# 7- Copy partitions and groups
if clone_partitions:
sco_groups_copy.clone_partitions_and_groups(
orig_formsemestre_id, formsemestre.id
@ -1391,8 +1416,8 @@ def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:
"""Delete a formsemestre (affiche avertissements)"""
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header("Suppression du semestre"),
"""<div class="ue_warning"><span>Attention !</span>
"""<h2>Suppression du semestre</h2>
<div class="ue_warning"><span>Attention !</span>
<p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
<b>un semestre ne doit jamais être supprimé</b>
(on perd la mémoire des notes et de tous les événements liés à ce semestre !).
@ -1445,8 +1470,9 @@ Ceci n'est possible que si :
)
else:
H.append(tf[1])
return "\n".join(H) + html_sco_header.sco_footer()
return render_template(
"sco_page.j2", title="Suppression du semestre", content="\n".join(H)
)
if tf[0] == -1: # cancel
return flask.redirect(
@ -1746,7 +1772,6 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
if not ok:
return err
footer = html_sco_header.sco_footer()
help_msg = """<p class="help">
Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
</p>
@ -1769,7 +1794,6 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
</p>
"""
H = [
html_sco_header.html_sem_header("Coefficients des UE du semestre"),
help_msg,
]
#
@ -1812,7 +1836,11 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
initvalues=initvalues,
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + footer
return render_template(
"sco_page.j2",
title="Coefficients des UE du semestre",
content="<h2>Coefficients des UE du semestre</h2>" + "\n".join(H) + tf[1],
)
elif tf[0] == -1:
return redirect(
url_for(
@ -1849,11 +1877,13 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
)
if not ok:
return (
"\n".join(H)
render_template(
"sco_page.j2",
title="Coefficients des UE du semestre",
content="<h2>Coefficients des UE du semestre</h2>"
+ "\n".join(H)
+ "<p><ul><li>%s</li></ul></p>" % "</li><li>".join(msg)
+ tf[1]
+ footer
+ tf[1],
)
# apply modifications
@ -1876,20 +1906,25 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
for ue in ue_deleted:
message.append(f"<li>{ue.acronyme}</li>")
message.append("</ul>")
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > modif coef UE cap (modifs notes de _certains_ etudiants)
else:
message = ["""<h3>Aucune modification</h3>"""]
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > modif coef UE cap (modifs notes de _certains_ etudiants)
return f"""{html_sco_header.html_sem_header("Coefficients des UE du semestre")}
{" ".join(message)}
<p><a class="stdlink" href="{url_for("notes.formsemestre_status",
return render_template(
"sco_page.j2",
title="Coefficients des UE du semestre",
content=f"""
<h2>Coefficients des UE du semestre</h2>
{" ".join(message)}
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Revenir au tableau de bord</a>
</p>
{footer}
"""
""",
)
def _get_sem_ues_modimpls(

View File

@ -51,7 +51,6 @@ import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc import html_sco_header
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_validation

View File

@ -31,7 +31,7 @@ import collections
import time
import flask
from flask import flash, url_for, g, request
from flask import flash, url_for, g, render_template, request
from app import db
from app.comp import res_sem
@ -85,43 +85,6 @@ def do_formsemestre_inscription_listinscrits(formsemestre_id):
return r
def do_formsemestre_inscription_create(args, method=None):
"create a formsemestre_inscription (and sco event)"
cnx = ndb.GetDBConnexion()
log(f"do_formsemestre_inscription_create: args={args}")
sems = sco_formsemestre.do_formsemestre_list(
{"formsemestre_id": args["formsemestre_id"]}
)
if len(sems) != 1:
raise ScoValueError(f"code de semestre invalide: {args['formsemestre_id']}")
sem = sems[0]
# check lock
if not sem["etat"]:
raise ScoValueError("inscription: semestre verrouille")
#
r = _formsemestre_inscriptionEditor.create(cnx, args)
# Evenement
sco_etud.scolar_events_create(
cnx,
args={
"etudid": args["etudid"],
"event_date": time.strftime(scu.DATE_FMT),
"formsemestre_id": args["formsemestre_id"],
"event_type": "INSCRIPTION",
},
)
# Log etudiant
Scolog.logdb(
method=method,
etudid=args["etudid"],
msg=f"inscription en semestre {args['formsemestre_id']}",
commit=True,
)
#
sco_cache.invalidate_formsemestre(formsemestre_id=args["formsemestre_id"])
return r
def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
"delete formsemestre_inscription"
cnx = ndb.GetDBConnexion()
@ -196,7 +159,7 @@ def check_if_has_decision_jury(
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
for etudid in etudids:
if nt.etud_has_decision(etudid):
etud = Identite.query.get(etudid)
etud = db.session.get(Identite, etudid)
raise ScoValueError(
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
une décision de jury (la supprimer avant si nécessaire)"""
@ -219,12 +182,11 @@ def do_formsemestre_desinscription(
if check_has_dec_jury:
check_if_has_decision_jury(formsemestre, [etudid])
insem = do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
)
if not insem:
inscr_sem = FormSemestreInscription.query.filter_by(
etudid=etudid, formsemestre_id=formsemestre_id
).first()
if not inscr_sem:
raise ScoValueError(f"{etud.nomprenom} n'est pas inscrit au semestre !")
insem = insem[0]
# -- desinscription de tous les modules
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
@ -248,10 +210,8 @@ def do_formsemestre_desinscription(
Partition.formsemestre_remove_etud(formsemestre_id, etud)
# -- désincription du semestre
do_formsemestre_inscription_delete(
insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id
)
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
formsemestre.desinscrit_etudiant(etud)
# --- Semestre extérieur
if formsemestre.modalite == "EXT":
if 0 == len(formsemestre.inscriptions):
@ -263,13 +223,6 @@ def do_formsemestre_desinscription(
db.session.commit()
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
Scolog.logdb(
method="formsemestre_desinscription",
etudid=etudid,
msg=f"desinscription semestre {formsemestre_id}",
commit=True,
)
def do_formsemestre_inscription_with_modules(
formsemestre_id,
@ -283,18 +236,22 @@ def do_formsemestre_inscription_with_modules(
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
(donc sauf le sport)
Si dept_id est spécifié, utilise ce département au lieu du courant.
Vérifie la capacité d'accueil.
"""
etud = Identite.get_etud(etudid)
group_ids = group_ids or []
if isinstance(group_ids, int):
group_ids = [group_ids]
# Check that all groups exist before creating the inscription
groups = [
GroupDescr.query.get_or_404(group_id)
for group_id in group_ids
if group_id != ""
]
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
# inscription au semestre
# Inscription au semestre
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
if etat is not None:
args["etat"] = etat
if etape is not None:
args["etape"] = etape
do_formsemestre_inscription_create(args, method=method)
formsemestre.inscrit_etudiant(etud, etat=etat, etape=etape, method=method)
log(
f"""do_formsemestre_inscription_with_modules: etudid={
etudid} formsemestre_id={formsemestre_id}"""
@ -303,14 +260,13 @@ def do_formsemestre_inscription_with_modules(
# 1- inscrit au groupe 'tous'
group_id = sco_groups.get_default_group(formsemestre_id)
sco_groups.set_group(etudid, group_id)
gdone = {group_id: 1} # empeche doublons
gdone = {group_id} # empeche doublons
# 2- inscrit aux groupes
for group_id in group_ids:
if group_id and group_id not in gdone:
_ = GroupDescr.query.get_or_404(group_id)
sco_groups.set_group(etudid, group_id)
gdone[group_id] = 1
for group in groups:
if group.id not in gdone:
sco_groups.set_group(etudid, group.id)
gdone.add(group.id)
# Inscription à tous les modules de ce semestre
for modimpl in formsemestre.modimpls:
@ -423,12 +379,7 @@ def formsemestre_inscription_with_modules(
etud = Identite.get_etud(etudid)
if etud.dept_id != formsemestre.dept_id:
raise ScoValueError("l'étudiant n'est pas dans ce département")
H = [
html_sco_header.html_sem_header(
f"Inscription de {etud.nomprenom} dans ce semestre",
)
]
footer = html_sco_header.sco_footer()
H = []
# Check 1: déjà inscrit ici ?
inscr = FormSemestreInscription.query.filter_by(
etudid=etud.id, formsemestre_id=formsemestre.id
@ -451,7 +402,12 @@ def formsemestre_inscription_with_modules(
</ul>
"""
)
return "\n".join(H) + footer
return render_template(
"sco_page.j2",
title=f"Inscription de {etud.nomprenom} dans ce semestre",
content=f"<h2>Inscription de {etud.nomprenom} dans ce semestre</h2>"
+ "\n".join(H),
)
# Check 2: déjà inscrit dans un semestre recouvrant les même dates ?
# Informe et propose dé-inscriptions
others = est_inscrit_ailleurs(etudid, formsemestre_id)
@ -473,7 +429,7 @@ def formsemestre_inscription_with_modules(
H.append("<ul>")
for s in others:
H.append(
f"""<li><a href="{
f"""<li><a class="stdlink" href="{
url_for("notes.formsemestre_desinscription", scodoc_dept=g.scodoc_dept,
formsemestre_id=s["formsemestre_id"], etudid=etudid )
}" class="stdlink">désinscrire de {s["titreannee"]}
@ -481,15 +437,20 @@ def formsemestre_inscription_with_modules(
)
H.append("</ul>")
H.append(
f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
multiple_ok=1,
group_ids=group_ids )
f"""<p><a class="stdlink" href="{
url_for( "notes.formsemestre_inscription_with_modules",
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
multiple_ok=1,
group_ids=group_ids )
}">Continuer quand même l'inscription</a>
</p>"""
# was sco_groups.make_query_groups(group_ids)
)
return "\n".join(H) + footer
return render_template(
"sco_page.j2",
title=f"Inscription de {etud.nomprenom} dans ce semestre",
content=f"<h2>Inscription de {etud.nomprenom} dans ce semestre</h2>"
+ "\n".join(H),
)
#
if group_ids is not None:
# OK, inscription
@ -522,7 +483,12 @@ def formsemestre_inscription_with_modules(
</form>
"""
)
return "\n".join(H) + footer
return render_template(
"sco_page.j2",
title=f"Inscription de {etud.nomprenom} dans ce semestre",
content=f"<h2>Inscription de {etud.nomprenom} dans ce semestre</h2>"
+ "\n".join(H),
)
def formsemestre_inscription_option(etudid, formsemestre_id):
@ -850,13 +816,7 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
"""Page listant les étudiants inscrits dans un autre semestre
dont les dates recouvrent le semestre indiqué.
"""
H = [
html_sco_header.html_sem_header(
"Inscriptions multiples parmi les étudiants du semestre ",
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
H = []
insd = list_inscrits_ailleurs(formsemestre_id)
# liste ordonnée par nom
etudlist = [Identite.get_etud(etudid) for etudid, sems in insd.items() if sems]
@ -908,4 +868,9 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
)
else:
H.append("""<p>Aucun étudiant en inscription multiple (c'est normal) !</p>""")
return "\n".join(H) + html_sco_header.sco_footer()
return render_template(
"sco_page.j2",
title="Inscriptions multiples parmi les étudiants du semestre",
content="<h2>Inscriptions multiples parmi les étudiants du semestre</h2>"
+ "\n".join(H),
)

View File

@ -59,8 +59,6 @@ import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc import codes_cursus
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_archives_formsemestre
from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_bulletins
@ -335,7 +333,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
},
{
"title": "Exporter table des étudiants",
"endpoint": "scolar.groups_view",
"endpoint": "scolar.groups_lists",
"args": {
"fmt": "allxls",
"group_ids": sco_groups.get_default_group(
@ -354,12 +352,26 @@ def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
can_change_groups = formsemestre.can_change_groups()
menu_groupes = [
{
"title": "Listes, photos, feuilles...",
"endpoint": "scolar.groups_view",
"title": "Listes des groupes",
"endpoint": "scolar.groups_lists",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
"helpmsg": "Accès aux listes des groupes d'étudiants",
},
{
"title": "Trombinoscopes",
"endpoint": "scolar.groups_photos",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
"helpmsg": "Accès aux photos des groupes d'étudiants",
},
{
"title": "Assiduité, feuilles d'appel, ...",
"endpoint": "scolar.groups_feuilles",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
"helpmsg": "Accès aux feuilles d'appel des groupes d'étudiants",
},
{
"title": "Modifier groupes et partitions",
"endpoint": "scolar.partition_editor",
@ -473,18 +485,20 @@ def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
]
menu_stats = _build_menu_stats(formsemestre)
H = [
'<ul id="sco_menu">',
htmlutils.make_menu("Semestre", menu_semestre),
htmlutils.make_menu("Inscriptions", menu_inscriptions),
htmlutils.make_menu("Groupes", menu_groupes),
htmlutils.make_menu("Notes", menu_notes),
htmlutils.make_menu("Jury", menu_jury),
htmlutils.make_menu("Statistiques", menu_stats),
formsemestre_custommenu_html(formsemestre_id),
"</ul>",
]
return "\n".join(H)
menus = {
"Semestre": menu_semestre,
"Inscriptions": menu_inscriptions,
"Groupes": menu_groupes,
"Notes": menu_notes,
"Jury": menu_jury,
"Statistiques": menu_stats,
"Liens": formsemestre_custommenu_html(formsemestre_id),
}
return render_template(
"formsemestre/menu.j2", menu=menus, formsemestre=formsemestre
)
# Element HTML decrivant un semestre (barre de menu et infos)
@ -739,9 +753,7 @@ def formsemestre_description_table(
columns_ids=columns_ids,
html_caption=title,
html_class="table_leftalign formsemestre_description",
html_title=html_sco_header.html_sem_header(
"Description du semestre", with_page_header=False
),
html_title="<h2>Description du semestre</h2>",
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title=title,
pdf_title=title,
@ -790,7 +802,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
})</span></h3>"""
)
#
H.append('<div class="sem-groups-abs">')
H.append('<div class="sem-groups-abs space-before-18">')
disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
show_abs: str = "hidden" if disable_abs else ""
@ -800,6 +812,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
groups = partition.groups.all()
effectifs = {g.id: g.get_nb_inscrits() for g in groups}
partition_is_empty = sum(effectifs.values()) == 0
if partition_is_empty and (partition.is_default() or partition.is_parcours()):
continue # inutile de montrer des partitions vides non éditables
H.append(
f"""
<div class="sem-groups-partition">
@ -826,7 +840,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
<div class="sem-groups-list">
<div>
<a class="stdlink" href="{
url_for("scolar.groups_view",
url_for("scolar.groups_lists",
group_ids=group.id,
scodoc_dept=g.scodoc_dept,
)
@ -897,7 +911,9 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
)
H.append("</div>") # /sem-groups-assi
if partition_is_empty and not partition.is_default():
if partition_is_empty and not (
partition.is_default() or partition.is_parcours()
):
H.append(
'<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition'
)
@ -912,41 +928,72 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
H.append("</div>")
H.append("</div>") # /sem-groups-partition
# Boite avec liens divers
autres_liens = []
if formsemestre.can_change_groups():
H.append(
f"""<h4><a class="stdlink"
autres_liens.append(
f"""<a class="stdlink"
title="une partition est un ensemble de groupes: TD, TP, ..."
href="{url_for("scolar.partition_editor",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
edit_partition=1)
}">Ajouter une partition</a></h4>"""
}">Ajouter une partition</a>"""
)
# --- Formulaire importation Assiduité excel (si autorisé)
if current_user.has_permission(Permission.AbsChange) and not disable_abs:
H.append(
f"""<p>
autres_liens.append(
f"""
<a class="stdlink" href="{url_for('assiduites.feuille_abs_formsemestre',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}">
Importation de l'assiduité depuis un fichier excel</a>
</p>"""
"""
)
# --- Lien Traitement Justificatifs:
if (
current_user.has_permission(Permission.AbsJustifView)
and current_user.has_permission(Permission.JustifValidate)
and not disable_abs
):
H.append(
f"""<p>
autres_liens.append(
f"""
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}">
Traitement des justificatifs d'absence</a>
</p>"""
"""
)
# --- Lien pour mail aux enseignants
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(u.email for u in formsemestre.responsables)
for modimpl in formsemestre.modimpls_sorted:
mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
adrlist = list(mails_enseignants - {None, ""})
if adrlist:
autres_liens.append(
f"""
<a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
len(adrlist)} enseignants du semestre</a>
"""
)
# Met le tout en boite
if autres_liens:
H.append(
f"""
<div class="sem-groups-partition sem-groups-autres-liens">
<div class="sem-groups-none">
<ul>
<li>{'</li><li>'.join(autres_liens)}</li>
</ul>
</div>
</div>
"""
)
H.append("</div>")
@ -975,11 +1022,8 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
page_title = page_title or "Modules de "
H = [
html_sco_header.html_sem_header(
page_title, with_page_header=False, with_h2=False
),
f"""<table>
<tr><td class="fichetitre2">Formation: </td><td>
f"""<table class="formsemestre_status_head">
<tr><td class="fichetitre2">Formation&nbsp;: </td><td>
<a href="{url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)}"
class="discretelink" title="Formation {
@ -1002,21 +1046,31 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
sem_parcours = formsemestre.get_parcours_apc()
H.append(
f"""
<tr><td class="fichetitre2">Parcours: </td>
<tr><td class="fichetitre2">Parcours&nbsp;: </td>
<td style="color: blue;">{', '.join(parcours.code for parcours in sem_parcours)}</td>
</tr>
"""
)
if formsemestre.capacite_accueil is not None:
H.append(
f"""
<tr><td class="fichetitre2">Capacité d'accueil&nbsp;: </td>
<td>{formsemestre.capacite_accueil}</td>
</tr>
"""
)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
H.append(
'<tr><td class="fichetitre2">Évaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
"""<tr><td class="fichetitre2">Évaluations&nbsp;: </td>
<td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides"""
% evals
)
if evals["last_modif"]:
H.append(
" <em>(dernière note saisie le %s)</em>"
% evals["last_modif"].strftime("%d/%m/%Y à %Hh%M")
f""" <em>(dernière note saisie le {
evals["last_modif"].strftime(scu.DATEATIME_FMT)
})</em>"""
)
H.append("</td></tr>")
H.append("</table>")
@ -1055,12 +1109,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
modimpls = formsemestre.modimpls_sorted
nt = res_sem.load_formsemestre_results(formsemestre)
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(u.email for u in formsemestre.responsables)
for modimpl in modimpls:
mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
can_edit = formsemestre.can_be_edited_by(current_user)
can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) or (
current_user.id in [resp.id for resp in formsemestre.responsables]
@ -1115,6 +1163,18 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
]
H += [
f"""
<details id="tableau-modules-details" open>
<!-- script pour fermer automatiquement si mobile -->
<script>
document.addEventListener("DOMContentLoaded", () => {{
if (window.innerWidth < 769) {{
document.getElementById("tableau-modules-details").open = false;
}}
}});
</script>
<summary id="tableau-modules-summary">
<h3 title="cliquer pour afficher ou cacher le tableau">Tableau des Ressources et SAEs</h3>
</summary>
<div class="tableau_modules">
{_TABLEAU_MODULES_HEAD}
<tr class="formsemestre_status_cat">
@ -1144,7 +1204,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
autres, nt, formsemestre, can_edit=can_edit, show_ues=False
),
]
H += [_TABLEAU_MODULES_FOOT, "</div>"]
H += [_TABLEAU_MODULES_FOOT, "</div></details>"]
else:
# formations classiques: groupe par UE
# élimine les modules BUT qui aurait pu se glisser là suite à un
@ -1176,20 +1236,11 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
)
# --- LISTE DES ETUDIANTS
H += [
'<div class="formsemestre-groupes">',
'<div class="formsemestre-groupes space-before-24">',
_make_listes_sem(formsemestre),
"</div>",
]
# --- Lien mail enseignants:
adrlist = list(mails_enseignants - {None, ""})
if adrlist:
H.append(
f"""<p>
<a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
len(adrlist)} enseignants du semestre</a>
</p>"""
)
return render_template(
"sco_page.j2",
content="".join(H),

View File

@ -30,8 +30,7 @@
import time
import flask
from flask import url_for, flash, g, request
from flask.templating import render_template
from flask import flash, g, render_template, request, url_for
import sqlalchemy as sa
from app.models import Identite, Evaluation
@ -584,6 +583,11 @@ def formsemestre_recap_parcours_table(
)
is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id
num_sem += 1
url_status = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
pv = dpv["decisions"][0]
@ -643,7 +647,7 @@ def formsemestre_recap_parcours_table(
H.append(
f"""
<td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
<td class="datedebut">{formsemestre.mois_debut()}</td>
<td class="datedebut"><a href="{url_status}">{formsemestre.mois_debut()}</a></td>
<td class="rcp_titre_sem"><a class="formsemestre_status_link"
href="{
url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
@ -725,8 +729,9 @@ def formsemestre_recap_parcours_table(
f"""Autre formation: {formsemestre.formation.formation_code}"""
)
H.append(
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
% (formsemestre.mois_fin(), sem_info.get(formsemestre.id, default_sem_info))
f"""<td class="datefin"><a href="{url_status}">{formsemestre.mois_fin()}</a></td>
<td class="sem_info">{sem_info.get(formsemestre.id, default_sem_info)}</td>
"""
)
# Moy Gen (sous le code decision)
H.append(
@ -957,11 +962,13 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
# -----------
def formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre"
H = [
html_sco_header.html_sem_header("Saisie automatique des décisions du semestre"),
f"""
def formsemestre_validation_auto(formsemestre_id: int):
"Formulaire saisie automatisée des décisions d'un semestre"
return render_template(
"sco_page.j2",
title="Saisie automatique des décisions",
content=f"""
<h2>Saisie automatique des décisions du semestre</h2>
<ul>
<li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et
toutes les barres, semestre précédent validé);</li>
@ -978,9 +985,7 @@ def formsemestre_validation_auto(formsemestre_id):
<p><em>Le calcul prend quelques minutes, soyez patients !</em></p>
</form>
""",
html_sco_header.sco_footer(),
]
return "\n".join(H)
)
def do_formsemestre_validation_auto(formsemestre_id):

View File

@ -65,7 +65,7 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
)
annotations = groups_list_annotation(groups_infos.group_ids)
for annotation in annotations:
annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M")
annotation["date_str"] = annotation["date"].strftime(scu.DATEATIME_FMT)
if fmt == "xls":
columns_ids = ("etudid", "nom", "prenom", "date", "comment")
else:

View File

@ -30,16 +30,17 @@ sous forme: de liste html (table exportable), de trombinoscope (exportable en pd
"""
# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)
# Modif en 2024 (9.7/revamp, abandon des tabs bootstrap)
import datetime
from urllib.parse import parse_qs
from flask import url_for, g, request
from flask import url_for, g, render_template, request
from flask_login import current_user
from app import db
from app.models import FormSemestre, Identite
from app import db, log
from app.models import FormSemestre, Identite, ScolarEvent
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_assiduites as scass
@ -56,19 +57,13 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
from app.scodoc.sco_permissions import Permission
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
"js/etud_info.js",
"js/groups_view.js",
]
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
# view:
def groups_view(
# view
def groups_lists(
group_ids=(),
fmt="html",
with_codes=0,
with_date_inscription=0,
etat=None,
with_paiement=0,
with_archives=0,
@ -87,6 +82,7 @@ def groups_view(
formsemestre_id est utilisé si aucun groupe selectionné pour construire la liste des groupes.
"""
# version sans tabs: juste la liste des étudiants
# Informations sur les groupes à afficher:
groups_infos = DisplayedGroupsInfos(
group_ids,
@ -100,6 +96,7 @@ def groups_view(
groups_infos=groups_infos,
fmt=fmt,
with_codes=with_codes,
with_date_inscription=with_date_inscription,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,
@ -112,57 +109,59 @@ def groups_view(
# - charger tous les etudiants au debut, quels que soient les groupes selectionnés
# - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change
return f"""
{ html_sco_header.sco_header(
javascripts=JAVASCRIPTS,
cssstyles=CSSSTYLES,
init_qtip=True,
)
}
<style>
div.multiselect-container.dropdown-menu {{
min-width: 180px;
}}
span.warning_unauthorized {{
color: pink;
font-style: italic;
margin-left: 12px;
}}
</style>
<div id="group-tabs">
<!-- Menu choix groupe -->
{form_groups_choice(groups_infos, submit_on_change=True)}
<ul class="nav nav-tabs">
<li class="active"><a href="#tab-listes" data-toggle="tab">Listes</a></li>
<li><a href="#tab-photos" data-toggle="tab">Photos</a></li>
<li><a href="#tab-abs" data-toggle="tab">Absences et feuilles...</a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-pane active" id="tab-listes">
{
groups_table(
groups_infos=groups_infos,
fmt=fmt,
with_codes=with_codes,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,
with_annotations=with_annotations,
with_bourse=with_bourse,
)
}
</div>
<div class="tab-pane" id="tab-photos">
{ tab_photos_html(groups_infos, etat=etat) }
</div>
<div class="tab-pane" id="tab-abs">
{ tab_absences_html(groups_infos, etat=etat) }
</div>
</div>
{ html_sco_header.sco_footer() }
return render_template(
"formsemestre/groups_lists.j2",
form_groups_choice=form_groups_choice(groups_infos, submit_on_change=True),
groups_table=groups_table(
groups_infos=groups_infos,
fmt=fmt,
with_codes=with_codes,
with_date_inscription=with_date_inscription,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,
with_annotations=with_annotations,
with_bourse=with_bourse,
),
groups_titles=groups_infos.groups_titles,
)
# view
def groups_photos(group_ids=(), etat=None, formsemestre_id=None):
"""Affichage des photos des étudiants (trombi) des groupes indiqués
group_ids: liste de group_id
formsemestre_id est utilisé si aucun groupe selectionné pour construire la liste des groupes.
"""
groups_infos = DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
)
return render_template(
"formsemestre/groups_photos.j2",
form_groups_choice=form_groups_choice(groups_infos, submit_on_change=True),
tab_photos_html=tab_photos_html(groups_infos, etat=etat),
groups_titles=groups_infos.groups_titles,
)
def groups_feuilles(group_ids=(), etat=None, formsemestre_id=None):
"""Affichage des feuilles d'appel des groupes indiqués
group_ids: liste de group_id
formsemestre_id est utilisé si aucun groupe selectionné pour construire la liste des groupes.
"""
groups_infos = DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
)
return render_template(
"formsemestre/groups_feuilles.j2",
form_groups_choice=form_groups_choice(groups_infos, submit_on_change=True),
tab_absences_html=tab_absences_html(groups_infos, etat=etat),
groups_titles=groups_infos.groups_titles,
)
def form_groups_choice(
@ -215,47 +214,61 @@ def form_groups_choice(
def menu_groups_choice(
groups_infos, submit_on_change=False, default_deselect_others=True
groups_infos,
submit_on_change=False,
default_deselect_others=True,
):
"""menu pour selection groupes
"""Menu pour selection groupes
group_ids est la liste des groupes actuellement sélectionnés
et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
(utilisé pour retrouver le semestre et proposer la liste des autres groupes)
Si url_export :
selecteur.value = &group_ids=xxx&group_ids=yyy...
sinon :
selecteur.value = [xxx, yyy, ...]
"""
default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
n_members = len(sco_groups.get_group_members(default_group_id))
H = [
f"""<select name="group_ids" id="group_ids_sel"
class="multiselect
{'submit_on_change' if submit_on_change else ''}
{'default_deselect_others' if default_deselect_others else ''}
"
multiple="multiple">
<option class="default_group"
value="{default_group_id}"
{'selected' if default_group_id in groups_infos.group_ids else ''}
>Tous ({n_members})</option>
"""
]
values: dict = {
# Choix : Tous (tous les groupes)
"": [
{
"value": default_group_id,
"label": f"Tous ({n_members})",
"selected": default_group_id in groups_infos.group_ids,
"single": default_deselect_others,
}
]
}
for partition in groups_infos.partitions:
H.append('<optgroup label="%s">' % partition["partition_name"])
p_name: str = partition["partition_name"]
vals: list[tuple[str, str, bool]] = []
# Les groupes dans cette partition:
for g in sco_groups.get_partition_groups(partition):
if g["group_id"] in groups_infos.group_ids:
selected = "selected"
else:
selected = ""
if g["group_name"]:
n_members = len(sco_groups.get_group_members(g["group_id"]))
H.append(
'<option value="%s" %s>%s (%s)</option>'
% (g["group_id"], selected, g["group_name"], n_members)
for grp in sco_groups.get_partition_groups(partition):
selected: bool = grp["group_id"] in groups_infos.group_ids
if grp["group_name"]:
vals.append(
{
"value": grp["group_id"],
"label": f"{grp['group_name']} ({len(sco_groups.get_group_members(grp['group_id']))})",
"selected": selected,
}
)
H.append("</optgroup>")
H.append("</select> ")
return "\n".join(H)
values[p_name] = vals
multi_select: scu.MultiSelect = scu.MultiSelect(
values=values, name="group_ids", html_id="group_ids_sel"
)
if submit_on_change:
multi_select.change_event("submit_group_selector();")
return multi_select.html()
def menu_group_choice(group_id=None, formsemestre_id=None):
@ -340,6 +353,7 @@ class DisplayedGroupsInfos:
try:
group_ids = [int(g) for g in group_ids]
except ValueError as exc:
log(f"DisplayedGroupsInfos: invalid group_id '{group_ids}'")
raise ScoValueError(
"identifiant de groupe invalide (mettre à jour vos bookmarks ?)"
) from exc
@ -481,6 +495,7 @@ class DisplayedGroupsInfos:
def groups_table(
groups_infos: DisplayedGroupsInfos = None,
with_codes=0,
with_date_inscription=0,
etat=None,
fmt="html",
with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
@ -496,15 +511,16 @@ def groups_table(
can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
with_codes = int(with_codes)
with_date_inscription = int(with_date_inscription)
with_paiement = int(with_paiement) and can_view_etud_data
with_archives = int(with_archives) and can_view_etud_data
with_annotations = int(with_annotations) and can_view_etud_data
with_bourse = int(with_bourse) and can_view_etud_data
base_url_np = groups_infos.base_url + f"&with_codes={with_codes}"
base_url = (
base_url_np
+ f"""&with_paiement={with_paiement}&with_archives={
groups_infos.base_url
+ f"""&with_codes={with_codes}&with_date_inscription={
with_date_inscription}&with_paiement={with_paiement}&with_archives={
with_archives}&with_annotations={with_annotations
}&with_bourse={with_bourse}"""
)
@ -520,6 +536,7 @@ def groups_table(
"etudid": "etudid",
"code_nip": "code_nip",
"code_ine": "code_ine",
"date_inscription": "Date inscription",
"datefinalisationinscription_str": "Finalisation inscr.",
"paiementinscription_str": "Paiement",
"etudarchive": "Fichiers",
@ -553,9 +570,11 @@ def groups_table(
if with_codes:
columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
if with_date_inscription:
columns_ids += ["date_inscription"]
if with_paiement:
columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"]
if with_paiement: # or with_codes:
if with_paiement:
sco_portal_apogee.check_paiement_etuds(groups_infos.members)
if with_archives:
from app.scodoc import sco_archives_etud
@ -571,6 +590,16 @@ def groups_table(
moodle_groupenames = set()
# ajoute liens
for etud_info in groups_infos.members:
if with_date_inscription:
event = ScolarEvent.query.filter_by(
etudid=etud_info["etudid"],
event_type="INSCRIPTION",
formsemestre_id=groups_infos.formsemestre_id,
).first()
if event:
etud_info["date_inscription"] = event.event_date.strftime(scu.DATE_FMT)
etud_info["_date_inscription_xls"] = event.event_date
etud_info["_date_inscription_order"] = event.event_date.isoformat
if etud_info["email"]:
etud_info["_email_target"] = "mailto:" + etud_info["email"]
else:
@ -587,7 +616,7 @@ def groups_table(
etud_info["_prenom_target"] = fiche_url
etud_info["_nom_disp_td_attrs"] = (
'id="%s" class="etudinfo"' % (etud_info["etudid"])
f"""id="{etud_info['etudid']}" class="etudinfo" """
)
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
if etud_info["etat"] == "D":
@ -692,9 +721,9 @@ def groups_table(
"""
]
if groups_infos.members:
menu_options = []
options = {
"with_codes": "Affiche codes",
"with_date_inscription": "Date inscription",
}
if can_view_etud_data:
options.update(
@ -705,34 +734,33 @@ def groups_table(
"with_bourse": "Statut boursier",
}
)
valeurs: list[tuple[str, str]] = []
for option, label in options.items():
if locals().get(option, False):
selected = "selected"
else:
selected = ""
menu_options.append(
f"""<option value="{option}" {selected}>{label}</option>"""
selected = locals().get(option, False)
valeurs.append(
{
"value": option,
"label": label,
"selected": selected,
}
)
multi_select: scu.MultiSelect = scu.MultiSelect(
values={"": valeurs},
label="Options",
name="options",
html_id="group_list_options",
)
multi_select.change_event("change_list_options(values);")
H.extend(
# ;
[
"""<span style="margin-left: 2em;">
<select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
"\n".join(menu_options),
"""</select></span>
<script type="text/javascript">
$(document).ready(function() {
$('#group_list_options').multiselect(
{
includeSelectAllOption: false,
nonSelectedText:'Options...',
onChange: function(element, checked){
change_list_options();
}
}
);
});
</script>
f"""
<span style="margin-left: 2em;">
{multi_select.html()}
</span>
""",
(
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
@ -747,16 +775,21 @@ def groups_table(
[
tab.html(),
f"""
<ul>
<li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a>
</li>
<li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a>
</li>
<li>
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id={groups_infos.formsemestre_id}">
Fichier CSV pour Moodle (tous les groupes)</a>
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
</li>""",
<ul>
<li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel
groupe(s) {groups_infos.groups_titles}</a>
</li>
<li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour
Moodle groupe(s) {groups_infos.groups_titles}</a>
</li>
<li>
<a class="stdlink" href="{{
url_for('notes.export_groups_as_moodle_csv',
scodoc_dept=g.scodoc_dept, formsemestre_id=groups_infos.formsemestre_id)
}}">
Fichier CSV pour Moodle (tous les groupes)</a>
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
</li>""",
]
)
if amail_inst:
@ -800,6 +833,7 @@ def groups_table(
groups_infos.members,
partitions=groups_infos.partitions,
with_codes=with_codes,
with_date_inscription=with_date_inscription,
with_paiement=with_paiement,
server_name=request.url_root,
)
@ -923,11 +957,14 @@ def tab_absences_html(groups_infos, etat=None):
"""
]
url_feuille_appel: str = url_for(
"scolar.formulaire_feuille_appel",
scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id,
group_ids=group_ids,
url_feuille_appel: str = (
url_for(
"scolar.formulaire_feuille_appel",
scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id,
)
+ "?"
+ groups_infos.groups_query_args
)
H.extend(

View File

@ -743,17 +743,21 @@ def scolars_import_admission(
# Type admission: traitement particulier
if not cur_adm["type_admission"] and not args.get("type_admission"):
args["type_admission"] = type_admission
sco_etud.etudident_edit(cnx, args, disable_notify=True)
sco_etud.etudident_edit( # TODO utiliser modèle
cnx, args, disable_notify=True
)
adr = sco_etud.adresse_list(cnx, args={"etudid": etud["etudid"]})
if adr:
args["adresse_id"] = adr[0]["adresse_id"]
sco_etud.adresse_edit(
sco_etud.adresse_edit( # TODO utiliser modèle
cnx, args, disable_notify=True
) # pas de notification ici
else:
args["typeadresse"] = "domicile"
args["description"] = "(infos admission)"
adresse_id = sco_etud.adresse_create(cnx, args)
adresse_id = sco_etud.adresse_create( # TODO utiliser modèle
cnx, args
)
# log('import_adm: %s' % args )
# Change les groupes si nécessaire:
if "groupes" in args:

View File

@ -31,16 +31,13 @@
import datetime
from operator import itemgetter
from flask import url_for, g, request
from flask import url_for, g, render_template, request
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Formation, FormSemestre, GroupDescr, Identite
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_etud
@ -312,14 +309,7 @@ def formsemestre_inscr_passage(
# -- check lock
if not formsemestre.etat:
raise ScoValueError("opération impossible: semestre verrouille")
H = [
html_sco_header.sco_header(
page_title="Passage des étudiants",
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
footer = html_sco_header.sco_footer()
H = []
etuds = [] if etuds is None else etuds
if isinstance(etuds, str):
# string, vient du form de confirmation
@ -377,7 +367,7 @@ def formsemestre_inscr_passage(
if a_desinscrire:
H.append("<h3>Étudiants à désinscrire</h3><ol>")
a_desinscrire_ident = sorted(
(Identite.query.get(eid) for eid in a_desinscrire),
(db.session.get(Identite, eid) for eid in a_desinscrire),
key=lambda x: x.sort_key,
)
for etud in a_desinscrire_ident:
@ -458,8 +448,9 @@ def formsemestre_inscr_passage(
)
#
H.append(footer)
return "\n".join(H)
return render_template(
"sco_page.j2", title="Passage des étudiants", content="\n".join(H)
)
def _build_page(
@ -488,10 +479,9 @@ def _build_page(
else:
ignore_jury_checked = ""
H = [
html_sco_header.html_sem_header(
"Passages dans le semestre", with_page_header=False
),
f"""<form name="f" method="post" action="{request.base_url}">
f"""
<h2 class="formsemestre">Passages dans le semestre</h2>
<form name="f" method="post" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}"/>
@ -590,7 +580,7 @@ def formsemestre_inscr_passage_help(formsemestre: FormSemestre):
def etuds_select_boxes(
auth_etuds_by_cat,
inscrits_ailleurs={},
inscrits_ailleurs: dict = None,
sel_inscrits=True,
show_empty_boxes=False,
export_cat_xls=None,
@ -603,6 +593,7 @@ def etuds_select_boxes(
sel_inscrits=
export_cat_xls =
"""
inscrits_ailleurs = inscrits_ailleurs or {}
if export_cat_xls:
return etuds_select_box_xls(auth_etuds_by_cat[export_cat_xls])
@ -634,7 +625,7 @@ def etuds_select_boxes(
for src_cat in auth_etuds_by_cat.keys():
infos = auth_etuds_by_cat[src_cat]["infos"]
infos["comment"] = infos.get("comment", "") # commentaire dans sous-titre boite
help = infos.get("help", "")
help_txt = infos.get("help", "")
etuds = auth_etuds_by_cat[src_cat]["etuds"]
etuds.sort(key=itemgetter("nom"))
with_checkbox = (not read_only) and auth_etuds_by_cat[src_cat]["infos"].get(
@ -651,8 +642,8 @@ def etuds_select_boxes(
<div class="pas_sembox_title"><a href="%(title_target)s" """
% infos
)
if help: # bubble
H.append('title="%s"' % help)
if help_txt: # bubble
H.append('title="%s"' % help_txt)
H.append(
""">%(title)s</a></div>
<div class="pas_sembox_subtitle">(%(nbetuds)d étudiants%(comment)s)"""

View File

@ -98,8 +98,7 @@ def scodoc_table_etuds_lycees(fmt="html"):
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/map_lycees.js"],
javascripts=["js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des %d étudiants (%d semestres)</h2>"""
% (len(etuds), len(semdepts)),
@ -219,10 +218,8 @@ def formsemestre_etuds_lycees(
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS
+ ["js/etud_info.js", "js/map_lycees.js"],
javascripts=sco_groups_view.JAVASCRIPTS + ["js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des étudiants</h2>""",
"\n".join(F),

View File

@ -31,7 +31,7 @@ import collections
from operator import attrgetter
import flask
from flask import url_for, g, request
from flask import url_for, g, render_template, request
from flask_login import current_user
from app import db, log
@ -46,7 +46,6 @@ from app.models import (
UniteEns,
Scolog,
)
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
@ -82,14 +81,8 @@ def moduleimpl_inscriptions_edit(
# -- check permission (and lock)
if not modimpl.can_change_inscriptions():
return # can_change_inscriptions raises exception
header = html_sco_header.sco_header(
page_title="Inscription au module",
init_qtip=True,
javascripts=["js/etud_info.js"],
)
footer = html_sco_header.sco_footer()
H = [
header,
f"""<h2>Inscriptions au module <a class="stdlink" href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id)
@ -133,14 +126,15 @@ def moduleimpl_inscriptions_edit(
if (partitionIdx==-1) {
for (var i =nb_inputs_to_skip; i < elems.length; i++) {
elems[i].checked=check;
elems[i].checked=check;
}
} else {
for (var i =nb_inputs_to_skip; i < elems.length; i++) {
var cells = elems[i].parentNode.parentNode.getElementsByTagName("td")[partitionIdx].childNodes;
if (cells.length && cells[0].nodeValue == groupName) {
elems[i].checked=check;
}
let tds = elems[i].parentNode.parentNode.getElementsByTagName("td");
var cells = tds[partitionIdx].childNodes;
if (cells.length && cells[0].nodeValue == groupName) {
elems[i].checked=check;
}
}
}
}
@ -179,19 +173,19 @@ def moduleimpl_inscriptions_edit(
else:
checked = ""
H.append(
f"""<tr><td class="etud"><input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>"""
f"""<tr><td class="etud">
<input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>
<a class="discretelink etudinfo" href="{
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
)
}" id="{etud['etudid']}">{etud['nomprenom']}</a>
</input>
</td>
"""
)
H.append(
f"""<a class="discretelink etudinfo" href="{
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
)
}" id="{etud['etudid']}">{etud['nomprenom']}</a>"""
)
H.append("""</input></td>""")
groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre.id)
for partition in partitions:
if partition["partition_name"]:
@ -216,8 +210,9 @@ def moduleimpl_inscriptions_edit(
)
)
#
H.append(footer)
return "\n".join(H)
return render_template(
"sco_page.j2", title="Inscriptions au module", content="\n".join(H)
)
def _make_menu(partitions: list[dict], title="", check="true") -> str:
@ -301,15 +296,12 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
# Page HTML:
H = [
html_sco_header.html_sem_header(
"Inscriptions aux modules et UE du semestre",
javascripts=["js/etud_info.js", "js/moduleimpl_inscriptions_stats.js"],
init_qtip=True,
)
f"""
<h2 class="formsemestre">Inscriptions aux modules et UE du semestre</h2>
<h3>Inscrits au semestre: {len(inscrits)} étudiants</h3>
"""
]
H.append(f"<h3>Inscrits au semestre: {len(inscrits)} étudiants</h3>")
if options:
H.append("<h3>Modules auxquels tous les étudiants ne sont pas inscrits:</h3>")
H.append(
@ -496,8 +488,12 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
"""
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
return render_template(
"sco_page.j2",
title="Inscriptions aux modules et UE du semestre",
javascripts=["js/moduleimpl_inscriptions_stats.js"],
content="\n".join(H),
)
def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) -> str:

View File

@ -483,6 +483,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
}" title="Charger toutles les notes via tableur">Importer les notes</a>
"""
)
else:
bot_table_links = top_table_links
if nb_evaluations > 0:
H.append(
'<div class="moduleimpl_evaluations_top_links">'
@ -654,7 +656,7 @@ def _ligne_evaluation(
if etat["last_modif"]:
H.append(
f"""<span class="mievr_lastmodif">(dernière modif le {
etat["last_modif"].strftime("%d/%m/%Y à %Hh%M")})</span>"""
etat["last_modif"].strftime(scu.DATEATIME_FMT)})</span>"""
)
#
H.append(

View File

@ -47,7 +47,6 @@ from app.models import (
)
from app.scodoc import (
codes_cursus,
html_sco_header,
htmlutils,
sco_archives_etud,
sco_bac,
@ -255,7 +254,7 @@ def fiche_etud(etudid=None):
grlinks.append(
f"""<a class="discretelink" href="{
url_for('scolar.groups_view',
url_for('scolar.groups_lists',
scodoc_dept=g.scodoc_dept, group_ids=partition['group_id'])
}" title="Liste du groupe {gr_name}">{gr_name}</a>
"""
@ -628,8 +627,10 @@ def fiche_etud(etudid=None):
</div>
"""
)
header = html_sco_header.sco_header(
page_title=f"Fiche étudiant {etud.nomprenom}",
return render_template(
"sco_page_dept.j2",
content=tmpl % info,
title=f"Fiche étudiant {etud.nomprenom}",
cssstyles=[
"libjs/jQuery-tagEditor/jquery.tag-editor.css",
"css/jury_but.css",
@ -644,7 +645,6 @@ def fiche_etud(etudid=None):
"js/etud_debouche.js",
],
)
return header + tmpl % info + html_sco_header.sco_footer()
def _format_adresse(adresse: Adresse | None) -> dict:
@ -874,10 +874,6 @@ def etud_info_html(etudid, with_photo="1", debug=False):
H += "</div>"
if debug:
return (
html_sco_header.standard_html_header()
+ H
+ html_sco_header.standard_html_footer()
)
else:
return H
return render_template("sco_page.j2", title="debug", content=H)
return H

View File

@ -51,7 +51,7 @@ from wtforms import (
from app.models import Evaluation, ModuleImpl
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header, sco_preferences
from app.scodoc import sco_preferences
from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations
from app.scodoc import sco_excel
@ -204,14 +204,14 @@ def placement_eval_selectetuds(evaluation_id):
% runner.__dict__
)
return runner.exec_placement() # calcul et generation du fichier
htmls = [
html_sco_header.sco_header(),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"<h3>Placement et émargement des étudiants</h3>",
render_template("scodoc/forms/placement.j2", form=form),
]
footer = html_sco_header.sco_footer()
return "\n".join(htmls) + "<p>" + footer
return render_template(
"scodoc/forms/placement.j2",
evaluations_description=sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id
),
form=form,
)
class PlacementRunner:

View File

@ -233,7 +233,6 @@ def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
return tab.make_page(
title="""<h2 class="formsemestre">Poursuite d'études</h2>""",
javascripts=["js/etud_info.js"],
fmt=fmt,
with_html_headers=True,
)

View File

@ -112,7 +112,7 @@ get_base_preferences(formsemestre_id)
"""
import flask
from flask import current_app, flash, g, request, url_for
from flask import current_app, flash, g, render_template, request, url_for
from app import db, log
from app.models import Departement
@ -369,10 +369,23 @@ class BasePreferences:
"emails_notifications",
{
"initvalue": "",
"title": "e-mails à qui notifier les opérations",
"title": "e-mail(s) à qui notifier les opérations",
"size": 70,
"explanation": """adresses séparées par des virgules; notifie les opérations
(saisies de notes, etc).
"explanation": """optionnel; 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
},
),
(
"emails_notifications_inscriptions",
{
"initvalue": "",
"title": "e-mail(s) à qui notifier les inscriptions d'étudiants",
"size": 70,
"explanation": """optionnel; adresses séparées par des virgules;
notifie les inscriptions/désincriptions de chaque individu.
""",
"category": "general",
"only_global": False, # peut être spécifique à un semestre
@ -2243,14 +2256,8 @@ class BasePreferences:
def edit(self):
"""HTML dialog: edit global preferences"""
from app.scodoc import html_sco_header
self.load()
H = [
html_sco_header.sco_header(
page_title=f"Préférences {g.scodoc_dept}",
javascripts=["js/detail_summary_persistence.js"],
),
f"<h2>Préférences globales pour le département {g.scodoc_dept}</h2>",
# f"""<p><a href="{url_for("scodoc.configure_logos", scodoc_dept=g.scodoc_dept)
# }">modification des logos du département (pour documents pdf)</a></p>"""
@ -2277,7 +2284,12 @@ class BasePreferences:
)
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
return render_template(
"sco_page_dept.j2",
content="\n".join(H) + tf[1],
title=f"Préférences {g.scodoc_dept}",
javascripts=["js/detail_summary_persistence.js"],
)
if tf[0] == -1:
return flask.redirect(dest_url) # cancel
#
@ -2321,6 +2333,7 @@ class BasePreferences:
<option value="create">Spécifier valeur pour ce
semestre seulement</option>
</select>
<span class="pref-comment">{descr["comment"]}</span>
"""
descr["explanation"] = menu_global
@ -2384,18 +2397,11 @@ class SemPreferences:
# The dialog
def edit(self, categories=[]):
"""Dialog to edit semestre preferences in given categories"""
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre
if not self.formsemestre_id:
raise ScoValueError(
"sem_preferences.edit doit etre appele sur un semestre !"
) # a bug !
H = [
html_sco_header.html_sem_header(
"Préférences du semestre",
javascripts=["js/detail_summary_persistence.js"],
),
"""
<p class="help">Les paramètres définis ici ne s'appliqueront qu'à ce semestre.</p>
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
@ -2456,7 +2462,12 @@ function set_global_pref(el, pref_name) {
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
return render_template(
"sco_page.j2",
content="\n".join(H) + tf[1],
title="Préférences du semestre",
javascripts=["js/detail_summary_persistence.js"],
)
elif tf[0] == -1:
flash("Annulé")
return flask.redirect(dest_url)

View File

@ -292,7 +292,7 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]:
)
)
):
ue = UniteEns.query.get(ue_id)
ue = db.session.get(UniteEns, ue_id)
assert ue
# note modernisation code: on utilise des dict tant que get_etud_ue_status renvoie des dicts
uelist.append(ue.to_dict())

View File

@ -35,14 +35,12 @@ from reportlab.platypus import Paragraph
from reportlab.lib import styles
import flask
from flask import flash, redirect, url_for
from flask import g, request
from flask import flash, g, redirect, render_template, request, url_for
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_pv_dict
from app.scodoc import sco_etud
@ -223,15 +221,13 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
)
)
footer = html_sco_header.sco_footer()
dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True)
if not dpv:
if fmt == "html":
return (
html_sco_header.sco_header()
+ "<h2>Aucune information disponible !</h2>"
+ footer
return render_template(
"sco_page.j2",
title="PV Jury",
content="<h2>Aucune information disponible !</h2>",
)
else:
return None
@ -262,12 +258,10 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
)
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Décisions du jury pour le semestre",
init_qtip=True,
javascripts=["js/etud_info.js"],
),
"""<p>(dernière modif le %s)</p>""" % dpv["date"],
f"""
<h2 class="formsemestre">Décisions du jury pour le semestre</h2>
<p>(dernière modif le {dpv["date"]})</p>
""",
]
H.append(
@ -334,7 +328,9 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
"""
)
H.append("</div>") # /codes
return "\n".join(H) + footer
return render_template(
"sco_page.j2", title="Décisions du jury pour le semestre", content="\n".join(H)
)
# ---------------------------------------------------------------------------
@ -352,12 +348,9 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
if etudid:
# PV pour ce seul étudiant:
etud = Identite.get_etud(etudid)
etuddescr = f"""<a class="discretelink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">{etud.nomprenom}</a>"""
etudids = [etudid]
else:
etuddescr = ""
etud = None
if not group_ids:
# tous les inscrits du semestre
group_ids = [sco_groups.get_default_group(formsemestre_id)]
@ -368,12 +361,6 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
etudids = [m["etudid"] for m in groups_infos.members]
H = [
html_sco_header.html_sem_header(
f"Édition du PV de jury {etuddescr}",
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
f"""<div class="help">Utiliser cette page pour éditer des versions provisoires des PV.
<span class="fontred">Il est recommandé d'archiver les versions définitives:
<a class="stdlink" href="{url_for(
@ -386,7 +373,6 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
"""<p><em>Voir aussi si besoin les réglages sur la page "Paramétrage"
(accessible à l'administrateur du département).</em>
</p>""",
html_sco_header.sco_footer(),
]
descr = descrform_pvjury(formsemestre)
if etudid:
@ -411,7 +397,20 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
html_foot_markup=menu_choix_groupe,
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + "\n".join(F)
return render_template(
"sco_page.j2",
title=f"Édition du PV de jury {('de ' + etud.nom_prenom()) if etud else ''}",
content=f"""<h2 class="formsemestre">Édition du PV de jury
de <a class="discretelink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">{etud.nomprenom}</a></h2>"""
+ "\n".join(H)
+ "\n"
+ tf[1]
+ "\n".join(F),
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
)
elif tf[0] == -1:
return flask.redirect(
url_for(
@ -543,7 +542,7 @@ def descrform_pvjury(formsemestre: FormSemestre):
]
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=()):
"Lettres avis jury en PDF"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not group_ids:
@ -555,13 +554,9 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
etudids = [m["etudid"] for m in groups_infos.members]
H = [
html_sco_header.html_sem_header(
"Édition des lettres individuelles",
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
f"""<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
f"""
<h2 class="formsemestre">Édition des lettres individuelles</h2>
<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
<span class="fontred">Il est recommandé d'archiver les versions définitives: <a
href="{url_for(
"notes.formsemestre_archive",
@ -571,7 +566,6 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
>voir cette page</a></span></p>
""",
]
F = html_sco_header.sco_footer()
descr = descrform_lettres_individuelles()
menu_choix_groupe = (
"""<div class="group_ids_sel_menu">Groupes d'étudiants à lister: """
@ -590,7 +584,13 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
html_foot_markup=menu_choix_groupe,
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + F
return render_template(
"sco_page.j2",
title="Édition des lettres individuelles",
content="\n".join(H) + "\n" + tf[1],
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
)
elif tf[0] == -1:
return flask.redirect(
url_for(

View File

@ -32,8 +32,7 @@ import datetime
import time
from xml.etree import ElementTree
from flask import g, request
from flask import abort, url_for
from flask import abort, g, render_template, request, url_for
from app import log
from app.but import bulletin_but
@ -44,14 +43,12 @@ from app.models import FormSemestre
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_xml
from app.scodoc import sco_cache
from app.scodoc import sco_evaluations
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_preferences
from app.tables.recap import TableRecap
from app.tables.jury_recap import TableJury
@ -119,16 +116,9 @@ def formsemestre_recapcomplet(
)
H = [
html_sco_header.sco_header(
page_title=f"{formsemestre.sem_modalite()}: "
+ ("jury" if mode_jury else "moyennes"),
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/table_recap.js"],
),
sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre_id
),
# sco_formsemestre_status.formsemestre_status_head(
# formsemestre_id=formsemestre_id
# ),
]
if len(formsemestre.inscriptions) > 0:
H.append(
@ -274,10 +264,17 @@ def formsemestre_recapcomplet(
</div>
"""
)
H.append(html_sco_header.sco_footer())
# HTML or binary data ?
if len(H) > 1:
return "".join(H)
return render_template(
"sco_page.j2",
content="".join(H),
title=f"{formsemestre.sem_modalite()}: "
+ ("jury" if mode_jury else "moyennes"),
javascripts=["js/table_recap.js"],
formsemestre_id=formsemestre_id,
no_sidebar=True,
)
elif len(H) == 1:
return H[0]
else:

View File

@ -37,7 +37,7 @@ import time
import datetime
from operator import itemgetter
from flask import url_for, g, request
from flask import url_for, g, render_template, request
import pydot
from app import log
@ -50,7 +50,6 @@ from app.models.etudiants import Identite
from app.scodoc import (
codes_cursus,
html_sco_header,
sco_etud,
sco_formsemestre,
sco_formsemestre_inscriptions,
@ -411,11 +410,6 @@ def formsemestre_report_counts(
if fmt != "html":
return tableau
H = [
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=title,
),
tableau,
"\n".join(F),
"""<p class="help">Le tableau affiche le nombre d'étudiants de ce semestre dans chacun
@ -423,9 +417,14 @@ def formsemestre_report_counts(
pour les lignes et les colonnes. Le <tt>codedecision</tt> est le code de la décision
du jury.
</p>""",
html_sco_header.sco_footer(),
]
return "\n".join(H)
return render_template(
"sco_page.j2",
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
title=title,
content="\n".join(H),
)
# --------------------------------------------------------------------------
@ -813,11 +812,6 @@ def formsemestre_suivi_cohorte(
href="{burl}&percent=1">Afficher les résultats en pourcentages</a></p>"""
H = [
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=tab.page_title,
),
"""<h2 class="formsemestre">Suivi cohorte: devenir des étudiants de ce semestre</h2>""",
_gen_form_selectetuds(
formsemestre.id,
@ -853,9 +847,14 @@ def formsemestre_suivi_cohorte(
</p>
""",
expl,
html_sco_header.sco_footer(),
]
return "\n".join(H)
return render_template(
"sco_page.j2",
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
title=tab.page_title,
content="\n".join(H),
)
def _gen_form_selectetuds(
@ -1365,17 +1364,11 @@ def formsemestre_suivi_cursus(
]
H = [
html_sco_header.sco_header(
page_title=tab.page_title,
init_qtip=True,
javascripts=["js/etud_info.js"],
),
"""<h2 class="formsemestre">Cursus suivis par les étudiants de ce semestre</h2>""",
"\n".join(F),
t,
html_sco_header.sco_footer(),
]
return "\n".join(H)
return render_template("sco_page.j2", title=tab.page_title, content="\n".join(H))
# -------------
@ -1744,12 +1737,6 @@ def formsemestre_graph_cursus(
)
H = [
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title="Graphe cursus de %(titreannee)s" % sem,
no_side_bar=True,
),
"""<h2 class="formsemestre">Cursus des étudiants de ce semestre</h2>""",
doc,
f"<p>{len(etuds)} étudiants sélectionnés</p>",
@ -1773,11 +1760,12 @@ def formsemestre_graph_cursus(
),
"""<p>Origine et devenir des étudiants inscrits dans %(titreannee)s"""
% sem,
"""(<a href="%s">version pdf</a>"""
% url_for("notes.formsemestre_graph_cursus", fmt="pdf", **url_kw),
""", <a href="%s">image PNG</a>)"""
% url_for("notes.formsemestre_graph_cursus", fmt="png", **url_kw),
f"""
f"""(<a href="{
url_for("notes.formsemestre_graph_cursus", fmt="pdf", **url_kw)
}">version pdf</a>,
<a href="{
url_for("notes.formsemestre_graph_cursus", fmt="png", **url_kw)}">image PNG</a>)
</p>
<p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre
sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans
@ -1790,8 +1778,14 @@ def formsemestre_graph_cursus(
étudiants appartenant aux groupes indiqués <em>dans le semestre d'origine</em>.
</p>
""",
html_sco_header.sco_footer(),
]
return "\n".join(H)
return render_template(
"sco_page.j2",
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=f"Graphe cursus de {sem['titreannee']}",
no_sidebar=True,
content="\n".join(H),
)
else:
raise ValueError(f"invalid format: {fmt}")

View File

@ -31,21 +31,18 @@
from collections import defaultdict
from flask import request
from flask import render_template, request
from app import db
from app.but import jury_but
from app.models import FormSemestre
from app.models.formsemestre import FormSemestreInscription
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
import sco_version
from app.scodoc.gen_tables import GenTable
import sco_version
# Titres, ordonnés
@ -107,13 +104,11 @@ def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
if fmt != "html":
return t
H = [
html_sco_header.sco_header(page_title=title),
t,
"""<p class="help">
</p>""",
html_sco_header.sco_footer(),
]
return "\n".join(H)
return render_template("sco_page.j2", title=title, content="\n".join(H))
def but_indicateurs_by_bac(formsemestre: FormSemestre) -> dict[str:dict]:

View File

@ -51,7 +51,6 @@ from flask_login import current_user
from app.models import Evaluation, FormSemestre, Identite, ModuleImpl, ScolarNews
from app.scodoc.sco_excel import COLORS, ScoExcelSheet
from app.scodoc import (
html_sco_header,
sco_cache,
sco_evaluations,
sco_evaluation_db,
@ -651,7 +650,7 @@ def do_evaluations_upload_xls(
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=obj_id,
text=f"""Chargement notes dans <a href="{status_url}">{modules_str}</a>""",
text=f"""Notes dans <a href="{status_url}">{modules_str}</a>""",
url=status_url,
max_frequency=10 * 60, # 10 minutes
)
@ -969,19 +968,21 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
moduleimpl_id = evaluation.moduleimpl.id
formsemestre_id = evaluation.moduleimpl.formsemestre_id
if not evaluation.moduleimpl.can_edit_notes(current_user):
return (
html_sco_header.sco_header()
+ f"""
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)
</p>
<p><a class="stdlink" href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id)
}">Continuer</a></p>
"""
+ html_sco_header.sco_footer()
dest_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
raise ScoValueError(
f"""
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)
</p>
<p><a class="stdlink" href="{dest_url}">Continuer</a></p>
""",
safe=True,
dest_url="",
)
page_title = "Saisie des notes" + (
@ -997,12 +998,6 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
)
H = [
html_sco_header.sco_header(
page_title=page_title,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"""<span class="eval_title">Saisie des notes par fichier</span>""",
]
@ -1177,8 +1172,13 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
</div>
"""
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
return render_template(
"sco_page.j2",
content="\n".join(H),
page_title=page_title,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
)
def formsemestre_import_notes(

View File

@ -31,7 +31,7 @@ import time
import flask
from flask import g, url_for
from flask import g, render_template, url_for
from flask_login import current_user
from flask_sqlalchemy.query import Query
import psycopg2
@ -57,7 +57,6 @@ from app.scodoc.sco_exceptions import (
ScoInvalidParamError,
ScoValueError,
)
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import sco_etud
@ -247,14 +246,15 @@ def do_evaluation_set_missing(
if len(invalids) > 0:
diag = f"Valeur {value} invalide ou hors barème"
if diag:
return f"""
{html_sco_header.sco_header()}
return render_template(
"sco_page.j2",
content=f"""
<h2>{diag}</h2>
<p><a href="{ dest_url }">
Recommencer</a>
</p>
{html_sco_header.sco_footer()}
"""
""",
)
# Confirm action
if not dialog_confirmed:
plural = len(valid_notes) > 1
@ -293,23 +293,24 @@ def do_evaluation_set_missing(
url=url,
max_frequency=30 * 60,
)
return f"""
{ html_sco_header.sco_header() }
return render_template(
"sco_page.j2",
content=f"""
<h2>{len(etudids_changed)} notes changées</h2>
<ul>
<li><a class="stdlink" href="{dest_url}">
Revenir au formulaire de saisie des notes</a>
</li>
<li><a class="stdlink" href="{
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=evaluation.moduleimpl_id,
)}">Tableau de bord du module</a>
</li>
<li><a class="stdlink" href="{dest_url}">
Revenir au formulaire de saisie des notes</a>
</li>
<li><a class="stdlink" href="{
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=evaluation.moduleimpl_id,
)}">Tableau de bord du module</a>
</li>
</ul>
{ html_sco_header.sco_footer() }
"""
""",
)
def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
@ -390,7 +391,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
url=status_url,
)
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
return render_template("sco_page.j2", content="\n".join(H))
def _check_inscription(
@ -408,7 +409,7 @@ def _check_inscription(
elif etudid not in etudids_inscrits_mod:
msg_err = "non inscrit au module"
if msg_err:
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
etud = db.session.get(Identite, etudid) if isinstance(etudid, int) else None
msg = f"étudiant {etud.nomprenom if etud else etudid} {msg_err}"
log(f"notes_add: {etudid} {msg}: aborting")
raise NoteProcessError(msg)
@ -454,7 +455,7 @@ def notes_add(
if (value is not None) and not isinstance(value, float):
log(f"notes_add: {etudid} valeur de note invalide ({value}): aborting")
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
etud = db.session.get(Identite, etudid) if isinstance(etudid, int) else None
raise NoteProcessError(
f"etudiant {etud.nomprenom if etud else etudid}: valeur de note invalide ({value})"
)
@ -491,7 +492,9 @@ def notes_add(
# si change sur DEM/DEF ajoute message warning aux messages
if etudid not in etudids_actifs: # DEM ou DEF
etud = (
Identite.query.get(etudid) if isinstance(etudid, int) else None
db.session.get(Identite, etudid)
if isinstance(etudid, int)
else None
)
messages.append(
f"""étudiant {etud.nomprenom if etud else etudid
@ -638,16 +641,17 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
# Check access
# (admin, respformation, and responsable_id)
if not evaluation.moduleimpl.can_edit_notes(current_user):
return f"""
{html_sco_header.sco_header()}
return render_template(
"sco_page.j2",
content=f"""
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p>
<p><a href="{ moduleimpl_status_url }">Continuer</a>
</p>
{html_sco_header.sco_footer()}
"""
""",
)
# Informations sur les groupes à afficher:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
@ -664,12 +668,6 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
)
# HTML page:
H = [
html_sco_header.sco_header(
page_title=page_title,
javascripts=sco_groups_view.JAVASCRIPTS + ["js/saisie_notes.js"],
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, link_saisie=False
),
@ -753,9 +751,13 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
</ul>
</div>"""
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
return render_template(
"sco_page.j2",
content="\n".join(H),
title=page_title,
javascripts=sco_groups_view.JAVASCRIPTS + ["js/saisie_notes.js"],
cssstyles=sco_groups_view.CSSSTYLES,
)
def get_sorted_etuds_notes(
@ -802,7 +804,7 @@ def get_sorted_etuds_notes(
notes_db[etudid]["value"], fixed_precision_str=False
)
user = (
User.query.get(notes_db[etudid]["uid"])
db.session.get(User, notes_db[etudid]["uid"])
if notes_db[etudid]["uid"]
else None
)
@ -1047,7 +1049,7 @@ def save_notes(
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=evaluation.moduleimpl_id,
text=f"""Chargement notes dans <a href="{status_url}">{
text=f"""Notes dans <a href="{status_url}">{
evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""",
url=status_url,
max_frequency=30 * 60, # 30 minutes

View File

@ -40,17 +40,15 @@ sem_set_list()
"""
import flask
from flask import g, url_for
from flask import g, render_template, url_for
from app import db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.scodoc import html_sco_header
from app.scodoc import sco_etape_apogee
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_etape_bilan import EtapeBilan
@ -412,7 +410,7 @@ def do_semset_delete(semset_id, dialog_confirmed=False):
s = SemSet(semset_id=semset_id)
if not dialog_confirmed:
return scu.confirm_dialog(
"<h2>Suppression de l'ensemble %(title)s ?</h2>" % s,
f"<h2>Suppression de l'ensemble {s['title']} ?</h2>",
dest_url="",
parameters={"semset_id": semset_id},
cancel_url="semset_page",
@ -421,14 +419,14 @@ def do_semset_delete(semset_id, dialog_confirmed=False):
return flask.redirect("semset_page")
def edit_semset_set_title(id=None, value=None):
def edit_semset_set_title(oid=None, value=None):
"""Change title of semset"""
title = value.strip()
if not id:
if not oid:
raise ScoValueError("empty semset_id")
SemSet(semset_id=id)
SemSet(semset_id=oid)
cnx = ndb.GetDBConnexion()
semset_edit(cnx, {"semset_id": id, "title": title})
semset_edit(cnx, {"semset_id": oid, "title": title})
return title
@ -517,23 +515,18 @@ def semset_page(fmt="html"):
page_title = "Ensembles de semestres"
H = [
html_sco_header.sco_header(
page_title=page_title,
init_qtip=True,
javascripts=["libjs/jinplace-1.2.1.min.js"],
),
"""<script>$(function() {
$('.inplace_edit').jinplace();
});
</script>""",
"<h2>%s</h2>" % page_title,
f"<h2>{page_title}</h2>",
]
H.append(tab.html())
annee_courante = int(scu.annee_scolaire())
menu_annee = "\n".join(
[
'<option value="%s">%s</option>' % (i, i)
f"""<option value="{i}">{i}</option>"""
for i in range(2014, annee_courante + 1)
]
)
@ -562,8 +555,8 @@ def semset_page(fmt="html"):
H.append(
"""
<div>
<h4>Autres opérations:</h4>
<div class="scobox space-before-24">
<div class="scobox-title">Autres opérations :</div>
<ul>
<li><a class="stdlink" href="scodoc_table_results">
Table des résultats de tous les semestres
@ -576,4 +569,9 @@ def semset_page(fmt="html"):
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
return render_template(
"sco_page_dept.j2",
title=page_title,
javascripts=["libjs/jinplace-1.2.1.min.js"],
content="\n".join(H),
)

View File

@ -31,7 +31,7 @@
import time
from operator import itemgetter
from flask import g, url_for
from flask import g, render_template, url_for
from flask_login import current_user
from app import db, log
@ -39,7 +39,6 @@ from app.models import Admission, Adresse, FormSemestre, Identite, ScolarNews
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
@ -118,7 +117,6 @@ def formsemestre_synchro_etuds(
""",
safe=True,
)
footer = html_sco_header.sco_footer()
base_url = url_for(
"notes.formsemestre_synchro_etuds",
scodoc_dept=g.scodoc_dept,
@ -168,13 +166,7 @@ def formsemestre_synchro_etuds(
suffix=scu.XLSX_SUFFIX,
)
H = [
html_sco_header.sco_header(
page_title="Synchronisation étudiants",
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
H = []
if not submitted:
H += _build_page(
sem,
@ -306,8 +298,9 @@ def formsemestre_synchro_etuds(
"""
)
H.append(footer)
return "\n".join(H)
return render_template(
"sco_page.j2", title="Synchronisation des étudiants", content="\n".join(H)
)
def _build_page(

View File

@ -49,7 +49,6 @@ import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_pdf import SU
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds
from app.scodoc import sco_excel
@ -90,12 +89,7 @@ def trombino(
return _listeappel_photos_pdf(groups_infos)
elif fmt == "doc":
return sco_trombino_doc.trombino_doc(groups_infos)
else:
raise Exception("invalid format")
def _trombino_html_header():
return html_sco_header.sco_header(javascripts=["js/trombino.js"])
raise ValueError("invalid format")
def trombino_html(groups_infos):
@ -208,8 +202,7 @@ def check_local_photos_availability(groups_infos, fmt=""):
>exporter seulement les photos existantes</a>""",
dest_url="trombino",
OK="Exporter seulement les photos existantes",
cancel_url="groups_view?curtab=tab-photos&"
+ groups_infos.groups_query_args,
cancel_url="groups_photos?" + groups_infos.groups_query_args,
parameters=parameters,
),
)
@ -249,17 +242,21 @@ def trombino_copy_photos(group_ids=None, dialog_confirmed=False):
"Copy photos from portal to ScoDoc (overwriting local copy)"
group_ids = [] if group_ids is None else group_ids
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
back_url = "groups_photos?" + str(groups_infos.groups_query_args)
portal_url = sco_portal_apogee.get_portal_url()
header = html_sco_header.sco_header(page_title="Chargement des photos")
footer = html_sco_header.sco_footer()
if not portal_url:
return f"""{ header }
return render_template(
"sco_page.j2",
content=f"""
<p>portail non configuré</p>
<p><a href="{back_url}" class="stdlink">Retour au trombinoscope</a></p>
{ footer }
"""
<div>
<a class="stdlink" href="{back_url}" class="stdlink">
Retour au trombinoscope
</a>
</div>
""",
)
if not dialog_confirmed:
return scu.confirm_dialog(
f"""<h2>Copier les photos du portail vers ScoDoc ?</h2>
@ -286,14 +283,18 @@ def trombino_copy_photos(group_ids=None, dialog_confirmed=False):
msg.append(f"<b>{nok} photos correctement chargées</b>")
return f"""{ header }
return render_template(
"sco_page.j2",
content=f"""
<h2>Chargement des photos depuis le portail</h2>
<ul><li>
{ '</li><li>'.join(msg) }
</li></ul>
<p><a href="{back_url}">retour au trombinoscope</a>
{ footer }
"""
<div class="space-before-24">
<a class="stdlink" href="{back_url}">retour au trombinoscope</a>
</div>
""",
)
def _get_etud_platypus_image(t, image_width=2 * cm):
@ -504,10 +505,9 @@ def photos_import_files_form(group_ids=()):
if not group_ids:
raise ScoValueError("paramètre manquant !")
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
back_url = f"groups_view?{groups_infos.groups_query_args}&curtab=tab-photos"
back_url = f"groups_photos?{groups_infos.groups_query_args}"
H = [
html_sco_header.sco_header(page_title="Import des photos des étudiants"),
f"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
<p><b>Vous pouvez aussi charger les photos individuellement via la fiche
de chaque étudiant (menu "Étudiant" / "Changer la photo").</b>
@ -527,7 +527,6 @@ def photos_import_files_form(group_ids=()):
<li style="padding-top: 2em;">
""",
]
F = html_sco_header.sco_footer()
vals = scu.get_request_args()
vals["group_ids"] = groups_infos.group_ids
tf = TrivialFormulator(
@ -541,38 +540,40 @@ def photos_import_files_form(group_ids=()):
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F
elif tf[0] == -1:
return flask.redirect(back_url)
else:
def callback(etud: Identite, data, filename):
return sco_photos.store_photo(etud, data, filename)
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = zip_excel_import_files(
xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"],
callback=callback,
filename_title="fichier_photo",
back_url=back_url,
)
return render_template(
"scolar/photos_import_files.j2",
page_title="Téléchargement des photos des étudiants",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_view",
scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id,
curtab="tab-photos",
),
"sco_page.j2",
title="Import des photos des étudiants",
content="\n".join(H) + tf[1] + "</li></ol>",
)
if tf[0] == -1:
return flask.redirect(back_url)
def callback(etud: Identite, data, filename):
return sco_photos.store_photo(etud, data, filename)
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = zip_excel_import_files(
xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"],
callback=callback,
filename_title="fichier_photo",
back_url=back_url,
)
return render_template(
"scolar/photos_import_files.j2",
page_title="Téléchargement des photos des étudiants",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_photos",
scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id,
),
)
def _norm_zip_filename(fn, lowercase=True):

View File

@ -54,14 +54,13 @@ Solution proposée (nov 2014):
"""
import flask
from flask import flash, g, request, url_for
from flask import flash, g, request, render_template, url_for
from flask_login import current_user
from app.models.formsemestre import FormSemestre
from app import db, log
from app.models import Evaluation, Identite, ModuleImpl, UniteEns
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
@ -144,7 +143,7 @@ def external_ue_create(
),
},
)
modimpl = ModuleImpl.query.get(moduleimpl_id)
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
assert modimpl
return modimpl
@ -206,7 +205,7 @@ def get_external_moduleimpl(formsemestre_id: int, ue_id: int) -> ModuleImpl:
)
if r:
modimpl_id = r[0]["moduleimpl_id"]
modimpl = ModuleImpl.query.get(modimpl_id)
modimpl = db.session.get(ModuleImpl, modimpl_id)
assert modimpl
return modimpl
else:
@ -240,11 +239,9 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
existing_external_ue = get_existing_external_ue(formation_id)
H = [
html_sco_header.html_sem_header(
f"Ajout d'une UE externe pour {etud.nomprenom}",
javascripts=["js/sco_ue_external.js"],
),
"""<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE
f"""
<h2 class="formsemestre">Ajout d'une UE externe pour {etud.nomprenom}</h2>
<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE
dans un autre établissement et qu'elle doit être intégrée dans le semestre courant.<br>
La note (/20) obtenue par l'étudiant doit toujours être spécifiée.</br>
On peut choisir une UE externe existante (dans le menu), ou bien en créer une, qui sera
@ -252,7 +249,6 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
</p>
""",
]
html_footer = html_sco_header.sco_footer()
parcours = formsemestre.formation.get_cursus()
ue_types = [
typ for typ in parcours.ALLOWED_UE_TYPES if typ != codes_cursus.UE_SPORT
@ -349,7 +345,12 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
etudid=etudid,
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + html_footer
return render_template(
"sco_page.j2",
title=f"Ajout d'une UE externe pour {etud.nomprenom}",
javascripts=["js/sco_ue_external.js"],
content="\n".join(H) + "\n" + tf[1],
)
elif tf[0] == -1:
return flask.redirect(bull_url)
else:
@ -358,12 +359,14 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
note, 20.0, etudid=etudid, absents=[], invalids=[]
)
if invalid:
return (
"\n".join(H)
return render_template(
"sco_page.j2",
title=f"Ajout d'une UE externe pour {etud.nomprenom}",
javascripts=["js/sco_ue_external.js"],
content="\n".join(H)
+ "\n"
+ tf_error_message("valeur note invalide")
+ tf[1]
+ html_footer
+ tf[1],
)
if tf[2]["existing_ue"]:
ue_id = int(tf[2]["existing_ue"])
@ -371,12 +374,14 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
else:
acronyme = tf[2]["acronyme"].strip()
if not acronyme:
return (
"\n".join(H)
return render_template(
"sco_page.j2",
title=f"Ajout d'une UE externe pour {etud.nomprenom}",
javascripts=["js/sco_ue_external.js"],
content="\n".join(H)
+ "\n"
+ tf_error_message("spécifier acronyme d'UE")
+ tf[1]
+ html_footer
+ tf[1],
)
modimpl = external_ue_create(
formsemestre_id,

View File

@ -31,7 +31,7 @@
# Anciennement ZScoUsers.py, fonctions de gestion des données réécrites avec flask/SQLAlchemy
import re
from flask import url_for, g, request
from flask import url_for, g, render_template, request
from flask_login import current_user
@ -54,7 +54,7 @@ def index_html(
all_depts = int(all_depts)
with_inactives = int(with_inactives)
H = [html_sco_header.html_sem_header("Gestion des utilisateurs")]
H = ["<h1>Gestion des utilisateurs</h1>"]
if current_user.has_permission(Permission.UsersAdmin, g.scodoc_dept):
H.append(
@ -112,6 +112,7 @@ def index_html(
raise ScoValueError("nom de rôle invalide")
else:
having_role = None
content = list_users(
g.scodoc_dept,
all_depts=all_depts,
@ -122,10 +123,12 @@ def index_html(
)
if fmt != "html":
return content
H.append(content)
F = html_sco_header.sco_footer()
return "\n".join(H) + F
return render_template(
"sco_page_dept.j2", content="\n".join(H), title="Gestion des utilisateurs"
)
def list_users(

View File

@ -26,8 +26,8 @@
##############################################################################
""" Common definitions
"""
"""Common definitions"""
import base64
import bisect
import collections
@ -55,7 +55,7 @@ from pytz import timezone
import flask
from flask import g, request, Response
from flask import g, render_template, request, Response
from flask import flash, make_response
from flask_json import json_response
from werkzeug.http import HTTP_STATUS_CODES
@ -63,6 +63,8 @@ from werkzeug.http import HTTP_STATUS_CODES
from config import Config
from app import log, ScoDocJSONEncoder
from app.forms.multiselect import MultiSelect
from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_xml
@ -554,7 +556,7 @@ MONTH_NAMES = (
)
DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche")
TIME_FMT = "%H:%M" # affichage des heures
TIME_FMT = "%Hh%M" # affichage des heures
DATE_FMT = "%d/%m/%Y" # affichage des dates
DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
DATETIME_FMT = DATE_FMT + " " + TIME_FMT
@ -1056,6 +1058,15 @@ def flash_once(message: str):
g.sco_flashed_once.add(message)
def html_flash_message(message: str):
"""HTML for flashed messaged, for legacy codes"""
return f"""<div class="container flashes">
<div class="alert alert-info alert-message" role="alert">
{message}
</div>
</div>"""
def sendCSVFile(data, filename): # DEPRECATED utiliser send_file
"""publication fichier CSV."""
return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True)
@ -1328,7 +1339,7 @@ def format_telephone(n: str | None) -> str:
#
def timedate_human_repr():
"representation du temps courant pour utilisateur"
return time.strftime("%d/%m/%Y à %Hh%M")
return time.strftime(DATEATIME_FMT)
def annee_scolaire_repr(year, month):
@ -1514,10 +1525,9 @@ def confirm_dialog(
help_msg=None,
parameters: dict = None,
target_variable="dialog_confirmed",
template="sco_page.j2",
):
"""HTML confirmation dialog: submit (POST) to same page or dest_url if given."""
from app.scodoc import html_sco_header
parameters = parameters or {}
# dialog de confirmation simple
parameters[target_variable] = 1
@ -1556,9 +1566,7 @@ def confirm_dialog(
if help_msg:
H.append('<p class="help">' + help_msg + "</p>")
if add_headers:
return (
html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
)
return render_template(template, content="\n".join(H))
else:
return "\n".join(H)

View File

@ -1,380 +0,0 @@
@keyframes dtb-spinner {
100% {
transform: rotate(360deg);
}
}
@-o-keyframes dtb-spinner {
100% {
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-ms-keyframes dtb-spinner {
100% {
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes dtb-spinner {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes dtb-spinner {
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
div.dataTables_wrapper {
position: relative;
}
div.dt-buttons {
position: initial;
}
div.dt-button-info {
position: fixed;
top: 50%;
left: 50%;
width: 400px;
margin-top: -100px;
margin-left: -200px;
background-color: white;
border: 2px solid #111;
box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3);
border-radius: 3px;
text-align: center;
z-index: 21;
}
div.dt-button-info h2 {
padding: 0.5em;
margin: 0;
font-weight: normal;
border-bottom: 1px solid #ddd;
background-color: #f3f3f3;
}
div.dt-button-info > div {
padding: 1em;
}
div.dtb-popover-close {
position: absolute;
top: 10px;
right: 10px;
width: 22px;
height: 22px;
border: 1px solid #eaeaea;
background-color: #f9f9f9;
text-align: center;
border-radius: 3px;
cursor: pointer;
z-index: 12;
}
button.dtb-hide-drop {
display: none !important;
}
div.dt-button-collection-title {
text-align: center;
padding: 0.3em 0 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
font-size: 0.9em;
}
div.dt-button-collection-title:empty {
display: none;
}
span.dt-button-spacer {
display: inline-block;
margin: 0.5em;
white-space: nowrap;
}
span.dt-button-spacer.bar {
border-left: 1px solid rgba(0, 0, 0, 0.3);
vertical-align: middle;
padding-left: 0.5em;
}
span.dt-button-spacer.bar:empty {
height: 1em;
width: 1px;
padding-left: 0;
}
div.dt-button-collection span.dt-button-spacer {
width: 100%;
font-size: 0.9em;
text-align: center;
margin: 0.5em 0;
}
div.dt-button-collection span.dt-button-spacer:empty {
height: 0;
width: 100%;
}
div.dt-button-collection span.dt-button-spacer.bar {
border-left: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
padding-left: 0;
}
div.dt-button-collection {
position: absolute;
z-index: 2001;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
padding: 0.5rem 0;
min-width: 200px;
}
div.dt-button-collection ul.dropdown-menu {
position: relative;
display: block;
z-index: 2002;
min-width: 100%;
background-color: transparent;
border: none;
box-shadow: none;
padding: 0;
border-radius: 0;
}
div.dt-button-collection div.dt-btn-split-wrapper {
width: 100%;
display: inline-flex;
padding-left: 5px;
padding-right: 5px;
}
div.dt-button-collection button.dt-btn-split-drop-button {
width: 100%;
border: none;
border-radius: 0px;
margin-left: 0px !important;
}
div.dt-button-collection button.dt-btn-split-drop-button:focus {
border: none;
border-radius: 0px;
outline: none;
}
div.dt-button-collection.fixed {
position: fixed;
display: block;
top: 50%;
left: 50%;
margin-left: -75px;
border-radius: 5px;
background-color: white;
}
div.dt-button-collection.fixed.two-column {
margin-left: -200px;
}
div.dt-button-collection.fixed.three-column {
margin-left: -225px;
}
div.dt-button-collection.fixed.four-column {
margin-left: -300px;
}
div.dt-button-collection.fixed.columns {
margin-left: -409px;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.fixed.columns {
margin-left: -308px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.fixed.columns {
margin-left: -203px;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.fixed.columns {
margin-left: -100px;
}
}
div.dt-button-collection.fixed > :last-child {
max-height: 100vh;
overflow: auto;
}
div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child {
display: block !important;
-webkit-column-gap: 8px;
-moz-column-gap: 8px;
-ms-column-gap: 8px;
-o-column-gap: 8px;
column-gap: 8px;
}
div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * {
-webkit-column-break-inside: avoid;
break-inside: avoid;
}
div.dt-button-collection.two-column {
width: 400px;
}
div.dt-button-collection.two-column > :last-child {
padding-bottom: 1px;
column-count: 2;
}
div.dt-button-collection.three-column {
width: 450px;
}
div.dt-button-collection.three-column > :last-child {
padding-bottom: 1px;
column-count: 3;
}
div.dt-button-collection.four-column {
width: 600px;
}
div.dt-button-collection.four-column > :last-child {
padding-bottom: 1px;
column-count: 4;
}
div.dt-button-collection .dt-button {
border-radius: 0;
}
div.dt-button-collection.columns {
width: auto;
}
div.dt-button-collection.columns > :last-child {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 6px;
width: 818px;
padding-bottom: 1px;
}
div.dt-button-collection.columns > :last-child .dt-button {
min-width: 200px;
flex: 0 1;
margin: 0;
}
div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child {
justify-content: space-between;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 1 1 32%;
}
div.dt-button-collection.columns.dtb-b2 .dt-button {
flex: 1 1 48%;
}
div.dt-button-collection.columns.dtb-b1 .dt-button {
flex: 1 1 100%;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.columns > :last-child {
width: 612px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.columns > :last-child {
width: 406px;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 0 1 32%;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.columns > :last-child {
width: 200px;
}
}
div.dt-button-collection .dt-button {
min-width: 200px;
}
div.dt-button-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2001;
}
@media screen and (max-width: 767px) {
div.dt-buttons {
float: none;
width: 100%;
text-align: center;
margin-bottom: 0.5em;
}
div.dt-buttons a.btn {
float: none;
}
}
div.dt-buttons button.btn.processing,
div.dt-buttons div.btn.processing,
div.dt-buttons a.btn.processing {
color: rgba(0, 0, 0, 0.2);
}
div.dt-buttons button.btn.processing:after,
div.dt-buttons div.btn.processing:after,
div.dt-buttons a.btn.processing:after {
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
box-sizing: border-box;
display: block;
content: " ";
border: 2px solid #282828;
border-radius: 50%;
border-left-color: transparent;
border-right-color: transparent;
animation: dtb-spinner 1500ms infinite linear;
-o-animation: dtb-spinner 1500ms infinite linear;
-ms-animation: dtb-spinner 1500ms infinite linear;
-webkit-animation: dtb-spinner 1500ms infinite linear;
-moz-animation: dtb-spinner 1500ms infinite linear;
}
div.dt-btn-split-wrapper button.dt-btn-split-drop {
border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
}
div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button {
background-color: #e6e6e6;
border-color: #adadad;
}
div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop {
box-shadow: none;
background-color: #fff;
border-color: #adadad;
}
div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover {
background-color: #e6e6e6;
border-color: #adadad;
}
span.dt-down-arrow {
color: rgba(70, 70, 70, 0.9);
font-size: 10px;
padding-left: 10px;
}
div.dataTables_wrapper div.dt-buttons.btn-group button.btn:last-of-type:first-of-type {
border-radius: 4px !important;
}
span.dt-down-arrow {
display: none;
}
span.dt-button-spacer {
float: left;
}
span.dt-button-spacer.bar:empty {
height: inherit;
}
div.dt-button-collection span.dt-button-spacer {
padding-left: 1rem !important;
text-align: left;
}

File diff suppressed because one or more lines are too long

View File

@ -1,426 +0,0 @@
@keyframes dtb-spinner {
100% {
transform: rotate(360deg);
}
}
@-o-keyframes dtb-spinner {
100% {
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-ms-keyframes dtb-spinner {
100% {
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes dtb-spinner {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes dtb-spinner {
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
div.dataTables_wrapper {
position: relative;
}
div.dt-buttons {
position: initial;
}
div.dt-button-info {
position: fixed;
top: 50%;
left: 50%;
width: 400px;
margin-top: -100px;
margin-left: -200px;
background-color: white;
border: 2px solid #111;
box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3);
border-radius: 3px;
text-align: center;
z-index: 21;
}
div.dt-button-info h2 {
padding: 0.5em;
margin: 0;
font-weight: normal;
border-bottom: 1px solid #ddd;
background-color: #f3f3f3;
}
div.dt-button-info > div {
padding: 1em;
}
div.dtb-popover-close {
position: absolute;
top: 10px;
right: 10px;
width: 22px;
height: 22px;
border: 1px solid #eaeaea;
background-color: #f9f9f9;
text-align: center;
border-radius: 3px;
cursor: pointer;
z-index: 12;
}
button.dtb-hide-drop {
display: none !important;
}
div.dt-button-collection-title {
text-align: center;
padding: 0.3em 0 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
font-size: 0.9em;
}
div.dt-button-collection-title:empty {
display: none;
}
span.dt-button-spacer {
display: inline-block;
margin: 0.5em;
white-space: nowrap;
}
span.dt-button-spacer.bar {
border-left: 1px solid rgba(0, 0, 0, 0.3);
vertical-align: middle;
padding-left: 0.5em;
}
span.dt-button-spacer.bar:empty {
height: 1em;
width: 1px;
padding-left: 0;
}
div.dt-button-collection span.dt-button-spacer {
width: 100%;
font-size: 0.9em;
text-align: center;
margin: 0.5em 0;
}
div.dt-button-collection span.dt-button-spacer:empty {
height: 0;
width: 100%;
}
div.dt-button-collection span.dt-button-spacer.bar {
border-left: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
padding-left: 0;
}
div.dt-button-collection {
position: absolute;
z-index: 2001;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
padding: 0.5rem 0;
width: 200px;
}
div.dt-button-collection div.dropdown-menu {
position: relative;
display: block;
z-index: 2002;
min-width: 100%;
background-color: transparent;
border: none;
box-shadow: none;
padding: 0;
border-radius: 0;
}
div.dt-button-collection.fixed {
position: fixed;
display: block;
top: 50%;
left: 50%;
margin-left: -75px;
border-radius: 5px;
background-color: white;
}
div.dt-button-collection.fixed.two-column {
margin-left: -200px;
}
div.dt-button-collection.fixed.three-column {
margin-left: -225px;
}
div.dt-button-collection.fixed.four-column {
margin-left: -300px;
}
div.dt-button-collection.fixed.columns {
margin-left: -409px;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.fixed.columns {
margin-left: -308px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.fixed.columns {
margin-left: -203px;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.fixed.columns {
margin-left: -100px;
}
}
div.dt-button-collection.fixed > :last-child {
max-height: 100vh;
overflow: auto;
}
div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child {
display: block !important;
-webkit-column-gap: 8px;
-moz-column-gap: 8px;
-ms-column-gap: 8px;
-o-column-gap: 8px;
column-gap: 8px;
}
div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * {
-webkit-column-break-inside: avoid;
break-inside: avoid;
}
div.dt-button-collection.two-column {
width: 400px;
}
div.dt-button-collection.two-column > :last-child {
padding-bottom: 1px;
column-count: 2;
}
div.dt-button-collection.three-column {
width: 450px;
}
div.dt-button-collection.three-column > :last-child {
padding-bottom: 1px;
column-count: 3;
}
div.dt-button-collection.four-column {
width: 600px;
}
div.dt-button-collection.four-column > :last-child {
padding-bottom: 1px;
column-count: 4;
}
div.dt-button-collection .dt-button {
border-radius: 0;
}
div.dt-button-collection.columns {
width: auto;
}
div.dt-button-collection.columns > :last-child {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 6px;
width: 818px;
padding-bottom: 1px;
}
div.dt-button-collection.columns > :last-child .dt-button {
min-width: 200px;
flex: 0 1;
margin: 0;
}
div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child {
justify-content: space-between;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 1 1 32%;
}
div.dt-button-collection.columns.dtb-b2 .dt-button {
flex: 1 1 48%;
}
div.dt-button-collection.columns.dtb-b1 .dt-button {
flex: 1 1 100%;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.columns > :last-child {
width: 612px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.columns > :last-child {
width: 406px;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 0 1 32%;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.columns > :last-child {
width: 200px;
}
}
div.dt-button-collection.fixed:before, div.dt-button-collection.fixed:after {
display: none;
}
div.dt-button-collection .btn-group {
flex: 1 1 auto;
}
div.dt-button-collection .dt-button {
min-width: 200px;
}
div.dt-button-collection div.dt-btn-split-wrapper {
width: 100%;
padding-left: 5px;
padding-right: 5px;
}
div.dt-button-collection button.dt-btn-split-drop-button {
width: 100%;
color: #212529;
border: none;
background-color: white;
border-radius: 0px;
margin-left: 0px !important;
}
div.dt-button-collection button.dt-btn-split-drop-button:focus {
border: none;
border-radius: 0px;
outline: none;
}
div.dt-button-collection button.dt-btn-split-drop-button:hover {
background-color: #e9ecef;
}
div.dt-button-collection button.dt-btn-split-drop-button:active {
background-color: #007bff !important;
}
div.dt-button-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
@media screen and (max-width: 767px) {
div.dt-buttons {
float: none;
width: 100%;
text-align: center;
margin-bottom: 0.5em;
}
div.dt-buttons a.btn {
float: none;
}
}
div.dt-buttons button.btn.processing,
div.dt-buttons div.btn.processing,
div.dt-buttons a.btn.processing {
color: rgba(0, 0, 0, 0.2);
}
div.dt-buttons button.btn.processing:after,
div.dt-buttons div.btn.processing:after,
div.dt-buttons a.btn.processing:after {
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
box-sizing: border-box;
display: block;
content: " ";
border: 2px solid #282828;
border-radius: 50%;
border-left-color: transparent;
border-right-color: transparent;
animation: dtb-spinner 1500ms infinite linear;
-o-animation: dtb-spinner 1500ms infinite linear;
-ms-animation: dtb-spinner 1500ms infinite linear;
-webkit-animation: dtb-spinner 1500ms infinite linear;
-moz-animation: dtb-spinner 1500ms infinite linear;
}
div.dt-buttons div.btn-group {
position: initial;
}
div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button {
background-color: #5a6268;
border-color: #545b62;
}
div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop {
box-shadow: none;
background-color: #6c757d;
border-color: #6c757d;
}
div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover {
background-color: #5a6268;
border-color: #545b62;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group {
border-radius: 4px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child {
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child {
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child {
border-top-left-radius: 4px !important;
border-bottom-left-radius: 4px !important;
border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child {
border: 1px solid #6c757d;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper {
border: none;
}
div.dt-button-collection div.btn-group {
border-radius: 4px !important;
}
div.dt-button-collection div.btn-group button {
border-radius: 4px;
}
div.dt-button-collection div.btn-group button:last-child {
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}
div.dt-button-collection div.btn-group button:first-child {
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
div.dt-button-collection div.btn-group button:last-child:first-child {
border-top-left-radius: 4px !important;
border-bottom-left-radius: 4px !important;
border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
}
div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child {
border: 1px solid #6c757d;
}
div.dt-button-collection div.btn-group div.dt-btn-split-wrapper {
border: none;
}
span.dt-button-spacer.bar:empty {
height: inherit;
}
div.dt-button-collection span.dt-button-spacer {
padding-left: 1rem !important;
text-align: left;
}

File diff suppressed because one or more lines are too long

View File

@ -1,428 +0,0 @@
@keyframes dtb-spinner {
100% {
transform: rotate(360deg);
}
}
@-o-keyframes dtb-spinner {
100% {
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-ms-keyframes dtb-spinner {
100% {
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes dtb-spinner {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes dtb-spinner {
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
div.dataTables_wrapper {
position: relative;
}
div.dt-buttons {
position: initial;
}
div.dt-button-info {
position: fixed;
top: 50%;
left: 50%;
width: 400px;
margin-top: -100px;
margin-left: -200px;
background-color: white;
border: 2px solid #111;
box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3);
border-radius: 3px;
text-align: center;
z-index: 21;
}
div.dt-button-info h2 {
padding: 0.5em;
margin: 0;
font-weight: normal;
border-bottom: 1px solid #ddd;
background-color: #f3f3f3;
}
div.dt-button-info > div {
padding: 1em;
}
div.dtb-popover-close {
position: absolute;
top: 10px;
right: 10px;
width: 22px;
height: 22px;
border: 1px solid #eaeaea;
background-color: #f9f9f9;
text-align: center;
border-radius: 3px;
cursor: pointer;
z-index: 12;
}
button.dtb-hide-drop {
display: none !important;
}
div.dt-button-collection-title {
text-align: center;
padding: 0.3em 0 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
font-size: 0.9em;
}
div.dt-button-collection-title:empty {
display: none;
}
span.dt-button-spacer {
display: inline-block;
margin: 0.5em;
white-space: nowrap;
}
span.dt-button-spacer.bar {
border-left: 1px solid rgba(0, 0, 0, 0.3);
vertical-align: middle;
padding-left: 0.5em;
}
span.dt-button-spacer.bar:empty {
height: 1em;
width: 1px;
padding-left: 0;
}
div.dt-button-collection span.dt-button-spacer {
width: 100%;
font-size: 0.9em;
text-align: center;
margin: 0.5em 0;
}
div.dt-button-collection span.dt-button-spacer:empty {
height: 0;
width: 100%;
}
div.dt-button-collection span.dt-button-spacer.bar {
border-left: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
padding-left: 0;
}
div.dt-button-collection {
position: absolute;
z-index: 2001;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
padding: 0.5rem 0;
width: 200px;
}
div.dt-button-collection div.dropdown-menu {
position: relative;
display: block;
background-color: transparent;
border: none;
box-shadow: none;
padding: 0;
border-radius: 0;
z-index: 2002;
min-width: 100%;
}
div.dt-button-collection.fixed {
position: fixed;
display: block;
top: 50%;
left: 50%;
margin-left: -75px;
border-radius: 5px;
background-color: white;
}
div.dt-button-collection.fixed.two-column {
margin-left: -200px;
}
div.dt-button-collection.fixed.three-column {
margin-left: -225px;
}
div.dt-button-collection.fixed.four-column {
margin-left: -300px;
}
div.dt-button-collection.fixed.columns {
margin-left: -409px;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.fixed.columns {
margin-left: -308px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.fixed.columns {
margin-left: -203px;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.fixed.columns {
margin-left: -100px;
}
}
div.dt-button-collection.fixed > :last-child {
max-height: 100vh;
overflow: auto;
}
div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child {
display: block !important;
-webkit-column-gap: 8px;
-moz-column-gap: 8px;
-ms-column-gap: 8px;
-o-column-gap: 8px;
column-gap: 8px;
}
div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * {
-webkit-column-break-inside: avoid;
break-inside: avoid;
}
div.dt-button-collection.two-column {
width: 400px;
}
div.dt-button-collection.two-column > :last-child {
padding-bottom: 1px;
column-count: 2;
}
div.dt-button-collection.three-column {
width: 450px;
}
div.dt-button-collection.three-column > :last-child {
padding-bottom: 1px;
column-count: 3;
}
div.dt-button-collection.four-column {
width: 600px;
}
div.dt-button-collection.four-column > :last-child {
padding-bottom: 1px;
column-count: 4;
}
div.dt-button-collection .dt-button {
border-radius: 0;
}
div.dt-button-collection.columns {
width: auto;
}
div.dt-button-collection.columns > :last-child {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 6px;
width: 818px;
padding-bottom: 1px;
}
div.dt-button-collection.columns > :last-child .dt-button {
min-width: 200px;
flex: 0 1;
margin: 0;
}
div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child {
justify-content: space-between;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 1 1 32%;
}
div.dt-button-collection.columns.dtb-b2 .dt-button {
flex: 1 1 48%;
}
div.dt-button-collection.columns.dtb-b1 .dt-button {
flex: 1 1 100%;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.columns > :last-child {
width: 612px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.columns > :last-child {
width: 406px;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 0 1 32%;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.columns > :last-child {
width: 200px;
}
}
div.dt-button-collection.fixed:before, div.dt-button-collection.fixed:after {
display: none;
}
div.dt-button-collection .btn-group {
flex: 1 1 auto;
}
div.dt-button-collection .dt-button {
min-width: 200px;
}
div.dt-button-collection div.dt-btn-split-wrapper {
width: 100%;
}
div.dt-button-collection button.dt-btn-split-drop-button {
width: 100%;
color: #212529;
border: none;
background-color: white;
border-radius: 0px;
margin-left: 0px !important;
}
div.dt-button-collection button.dt-btn-split-drop-button:focus {
border: none;
border-radius: 0px;
outline: none;
}
div.dt-button-collection button.dt-btn-split-drop-button:hover {
background-color: #e9ecef;
}
div.dt-button-collection button.dt-btn-split-drop-button:active {
background-color: #007bff !important;
}
div.dt-button-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
@media screen and (max-width: 767px) {
div.dt-buttons {
float: none;
width: 100%;
text-align: center;
margin-bottom: 0.5em;
}
div.dt-buttons a.btn {
float: none;
}
}
div.dt-buttons button.btn.processing,
div.dt-buttons div.btn.processing,
div.dt-buttons a.btn.processing {
color: rgba(0, 0, 0, 0.2);
}
div.dt-buttons button.btn.processing:after,
div.dt-buttons div.btn.processing:after,
div.dt-buttons a.btn.processing:after {
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
box-sizing: border-box;
display: block;
content: " ";
border: 2px solid #282828;
border-radius: 50%;
border-left-color: transparent;
border-right-color: transparent;
animation: dtb-spinner 1500ms infinite linear;
-o-animation: dtb-spinner 1500ms infinite linear;
-ms-animation: dtb-spinner 1500ms infinite linear;
-webkit-animation: dtb-spinner 1500ms infinite linear;
-moz-animation: dtb-spinner 1500ms infinite linear;
}
div.dt-buttons div.btn-group {
position: initial;
}
div.dt-btn-split-wrapper button.dt-btn-split-drop {
border-top-right-radius: 0.25rem !important;
border-bottom-right-radius: 0.25rem !important;
}
div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button {
background-color: #5a6268;
border-color: #545b62;
}
div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop {
box-shadow: none;
background-color: #6c757d;
border-color: #6c757d;
}
div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover {
background-color: #5a6268;
border-color: #545b62;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group {
border-radius: 4px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child {
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child {
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child {
border-top-left-radius: 4px !important;
border-bottom-left-radius: 4px !important;
border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child {
border: 1px solid #6c757d;
}
div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper {
border: none;
}
div.dt-button-collection div.btn-group {
border-radius: 4px !important;
}
div.dt-button-collection div.btn-group button {
border-radius: 4px;
}
div.dt-button-collection div.btn-group button:last-child {
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}
div.dt-button-collection div.btn-group button:first-child {
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
div.dt-button-collection div.btn-group button:last-child:first-child {
border-top-left-radius: 4px !important;
border-bottom-left-radius: 4px !important;
border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
}
div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child {
border: 1px solid #6c757d;
}
div.dt-button-collection div.btn-group div.dt-btn-split-wrapper {
border: none;
}
span.dt-button-spacer.bar:empty {
height: inherit;
}
div.dt-button-collection span.dt-button-spacer {
padding-left: 1rem !important;
text-align: left;
}

File diff suppressed because one or more lines are too long

View File

@ -1,425 +0,0 @@
@keyframes dtb-spinner {
100% {
transform: rotate(360deg);
}
}
@-o-keyframes dtb-spinner {
100% {
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-ms-keyframes dtb-spinner {
100% {
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes dtb-spinner {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes dtb-spinner {
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
div.dataTables_wrapper {
position: relative;
}
div.dt-buttons {
position: initial;
}
div.dt-button-info {
position: fixed;
top: 50%;
left: 50%;
width: 400px;
margin-top: -100px;
margin-left: -200px;
background-color: white;
border: 2px solid #111;
box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3);
border-radius: 3px;
text-align: center;
z-index: 21;
}
div.dt-button-info h2 {
padding: 0.5em;
margin: 0;
font-weight: normal;
border-bottom: 1px solid #ddd;
background-color: #f3f3f3;
}
div.dt-button-info > div {
padding: 1em;
}
div.dtb-popover-close {
position: absolute;
top: 10px;
right: 10px;
width: 22px;
height: 22px;
border: 1px solid #eaeaea;
background-color: #f9f9f9;
text-align: center;
border-radius: 3px;
cursor: pointer;
z-index: 12;
}
button.dtb-hide-drop {
display: none !important;
}
div.dt-button-collection-title {
text-align: center;
padding: 0.3em 0 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
font-size: 0.9em;
}
div.dt-button-collection-title:empty {
display: none;
}
span.dt-button-spacer {
display: inline-block;
margin: 0.5em;
white-space: nowrap;
}
span.dt-button-spacer.bar {
border-left: 1px solid rgba(0, 0, 0, 0.3);
vertical-align: middle;
padding-left: 0.5em;
}
span.dt-button-spacer.bar:empty {
height: 1em;
width: 1px;
padding-left: 0;
}
div.dt-button-collection span.dt-button-spacer {
width: 100%;
font-size: 0.9em;
text-align: center;
margin: 0.5em 0;
}
div.dt-button-collection span.dt-button-spacer:empty {
height: 0;
width: 100%;
}
div.dt-button-collection span.dt-button-spacer.bar {
border-left: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
padding-left: 0;
}
div.dt-button-collection {
position: absolute;
z-index: 2001;
min-width: 200px;
background: white;
max-width: none;
display: block;
box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.02);
border-radius: 4;
padding-top: 0.5rem;
}
div.dt-button-collection div.dropdown-menu {
display: block;
z-index: 2002;
min-width: 100%;
}
div.dt-button-collection div.dt-btn-split-wrapper {
width: 100%;
padding-left: 5px;
padding-right: 5px;
margin-bottom: 0px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
align-items: stretch;
}
div.dt-button-collection div.dt-btn-split-wrapper button {
margin-right: 0px;
display: inline-block;
width: 0;
flex-grow: 1;
flex-shrink: 0;
flex-basis: 50px;
margin-top: 0px;
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
overflow: hidden;
text-overflow: ellipsis;
}
div.dt-button-collection div.dt-btn-split-wrapper button.dt-button {
min-width: 30px;
margin-left: -1px;
flex-grow: 0;
flex-shrink: 0;
flex-basis: 0;
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
padding: 0px;
}
div.dt-button-collection.fixed {
position: fixed;
display: block;
top: 50%;
left: 50%;
margin-left: -75px;
border-radius: 5px;
background-color: white;
}
div.dt-button-collection.fixed.two-column {
margin-left: -200px;
}
div.dt-button-collection.fixed.three-column {
margin-left: -225px;
}
div.dt-button-collection.fixed.four-column {
margin-left: -300px;
}
div.dt-button-collection.fixed.columns {
margin-left: -409px;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.fixed.columns {
margin-left: -308px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.fixed.columns {
margin-left: -203px;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.fixed.columns {
margin-left: -100px;
}
}
div.dt-button-collection.fixed > :last-child {
max-height: 100vh;
overflow: auto;
}
div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child {
display: block !important;
-webkit-column-gap: 8px;
-moz-column-gap: 8px;
-ms-column-gap: 8px;
-o-column-gap: 8px;
column-gap: 8px;
}
div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * {
-webkit-column-break-inside: avoid;
break-inside: avoid;
}
div.dt-button-collection.two-column {
width: 400px;
}
div.dt-button-collection.two-column > :last-child {
padding-bottom: 1px;
column-count: 2;
}
div.dt-button-collection.three-column {
width: 450px;
}
div.dt-button-collection.three-column > :last-child {
padding-bottom: 1px;
column-count: 3;
}
div.dt-button-collection.four-column {
width: 600px;
}
div.dt-button-collection.four-column > :last-child {
padding-bottom: 1px;
column-count: 4;
}
div.dt-button-collection .dt-button {
border-radius: 0;
}
div.dt-button-collection.columns {
width: auto;
}
div.dt-button-collection.columns > :last-child {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 6px;
width: 818px;
padding-bottom: 1px;
}
div.dt-button-collection.columns > :last-child .dt-button {
min-width: 200px;
flex: 0 1;
margin: 0;
}
div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child {
justify-content: space-between;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 1 1 32%;
}
div.dt-button-collection.columns.dtb-b2 .dt-button {
flex: 1 1 48%;
}
div.dt-button-collection.columns.dtb-b1 .dt-button {
flex: 1 1 100%;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.columns > :last-child {
width: 612px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.columns > :last-child {
width: 406px;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 0 1 32%;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.columns > :last-child {
width: 200px;
}
}
div.dt-button-collection .dropdown-content {
box-shadow: none;
padding-top: 0;
border-radius: 0;
}
div.dt-button-collection.fixed:before, div.dt-button-collection.fixed:after {
display: none;
}
div.dt-button-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
@media screen and (max-width: 767px) {
div.dt-buttons {
float: none;
width: 100%;
text-align: center;
margin-bottom: 0.5em;
}
div.dt-buttons a.btn {
float: none;
}
}
div.dt-buttons button.btn.processing,
div.dt-buttons div.btn.processing,
div.dt-buttons a.btn.processing {
color: rgba(0, 0, 0, 0.2);
}
div.dt-buttons button.btn.processing:after,
div.dt-buttons div.btn.processing:after,
div.dt-buttons a.btn.processing:after {
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
box-sizing: border-box;
display: block;
content: " ";
border: 2px solid #282828;
border-radius: 50%;
border-left-color: transparent;
border-right-color: transparent;
animation: dtb-spinner 1500ms infinite linear;
-o-animation: dtb-spinner 1500ms infinite linear;
-ms-animation: dtb-spinner 1500ms infinite linear;
-webkit-animation: dtb-spinner 1500ms infinite linear;
-moz-animation: dtb-spinner 1500ms infinite linear;
}
div.dt-buttons button.button {
margin-left: 5px;
}
div.dt-buttons button.button:first-child {
margin-left: 0px;
}
span.dt-down-arrow {
display: none;
}
span.dt-button-spacer {
display: inline-flex;
margin: 0.5em;
white-space: nowrap;
align-items: center;
font-size: 1rem;
}
span.dt-button-spacer.bar:empty {
height: inherit;
}
div.dt-button-collection span.dt-button-spacer {
text-align: left;
font-size: 0.875rem;
padding-left: 1rem !important;
}
div.dt-btn-split-wrapper {
padding-left: 5px;
padding-right: 5px;
margin-bottom: 0px;
margin-bottom: 0px !important;
}
div.dt-btn-split-wrapper button {
margin-right: 0px;
display: inline-block;
margin-top: 0px;
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
overflow: hidden;
text-overflow: ellipsis;
}
div.dt-btn-split-wrapper button.dt-button {
min-width: 30px;
margin-left: -1px;
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
padding: 0px;
}
div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button, div.dt-btn-split-wrapper.is-active:not(.disabled) button {
background-color: #eee;
border-color: transparent;
}
div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-button, div.dt-btn-split-wrapper.is-active:not(.disabled) button.dt-button {
box-shadow: none;
background-color: whitesmoke;
border-color: transparent;
}
div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover, div.dt-btn-split-wrapper.is-active:not(.disabled) button:hover {
background-color: #eee;
border-color: transparent;
}

File diff suppressed because one or more lines are too long

View File

@ -1,631 +0,0 @@
@keyframes dtb-spinner {
100% {
transform: rotate(360deg);
}
}
@-o-keyframes dtb-spinner {
100% {
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-ms-keyframes dtb-spinner {
100% {
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes dtb-spinner {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes dtb-spinner {
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
div.dataTables_wrapper {
position: relative;
}
div.dt-buttons {
position: initial;
}
div.dt-button-info {
position: fixed;
top: 50%;
left: 50%;
width: 400px;
margin-top: -100px;
margin-left: -200px;
background-color: white;
border: 2px solid #111;
box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3);
border-radius: 3px;
text-align: center;
z-index: 21;
}
div.dt-button-info h2 {
padding: 0.5em;
margin: 0;
font-weight: normal;
border-bottom: 1px solid #ddd;
background-color: #f3f3f3;
}
div.dt-button-info > div {
padding: 1em;
}
div.dtb-popover-close {
position: absolute;
top: 10px;
right: 10px;
width: 22px;
height: 22px;
border: 1px solid #eaeaea;
background-color: #f9f9f9;
text-align: center;
border-radius: 3px;
cursor: pointer;
z-index: 12;
}
button.dtb-hide-drop {
display: none !important;
}
div.dt-button-collection-title {
text-align: center;
padding: 0.3em 0 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
font-size: 0.9em;
}
div.dt-button-collection-title:empty {
display: none;
}
span.dt-button-spacer {
display: inline-block;
margin: 0.5em;
white-space: nowrap;
}
span.dt-button-spacer.bar {
border-left: 1px solid rgba(0, 0, 0, 0.3);
vertical-align: middle;
padding-left: 0.5em;
}
span.dt-button-spacer.bar:empty {
height: 1em;
width: 1px;
padding-left: 0;
}
div.dt-button-collection span.dt-button-spacer {
width: 100%;
font-size: 0.9em;
text-align: center;
margin: 0.5em 0;
}
div.dt-button-collection span.dt-button-spacer:empty {
height: 0;
width: 100%;
}
div.dt-button-collection span.dt-button-spacer.bar {
border-left: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
padding-left: 0;
}
button.dt-button,
div.dt-button,
a.dt-button,
input.dt-button {
position: relative;
display: inline-block;
box-sizing: border-box;
margin-left: 0.167em;
margin-right: 0.167em;
margin-bottom: 0.333em;
padding: 0.5em 1em;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 2px;
cursor: pointer;
font-size: 0.88em;
line-height: 1.6em;
color: black;
white-space: nowrap;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.1);
/* Fallback */
background: -webkit-linear-gradient(top, rgba(230, 230, 230, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Chrome 10+, Saf5.1+, iOS 5+ */
background: -moz-linear-gradient(top, rgba(230, 230, 230, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* FF3.6 */
background: -ms-linear-gradient(top, rgba(230, 230, 230, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* IE10 */
background: -o-linear-gradient(top, rgba(230, 230, 230, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, rgba(230, 230, 230, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(230, 230, 230, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)");
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-decoration: none;
outline: none;
text-overflow: ellipsis;
}
button.dt-button:first-child,
div.dt-button:first-child,
a.dt-button:first-child,
input.dt-button:first-child {
margin-left: 0;
}
button.dt-button.disabled,
div.dt-button.disabled,
a.dt-button.disabled,
input.dt-button.disabled {
cursor: default;
opacity: 0.4;
}
button.dt-button:active:not(.disabled), button.dt-button.active:not(.disabled),
div.dt-button:active:not(.disabled),
div.dt-button.active:not(.disabled),
a.dt-button:active:not(.disabled),
a.dt-button.active:not(.disabled),
input.dt-button:active:not(.disabled),
input.dt-button.active:not(.disabled) {
background-color: rgba(0, 0, 0, 0.1);
/* Fallback */
background: -webkit-linear-gradient(top, rgba(179, 179, 179, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Chrome 10+, Saf5.1+, iOS 5+ */
background: -moz-linear-gradient(top, rgba(179, 179, 179, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* FF3.6 */
background: -ms-linear-gradient(top, rgba(179, 179, 179, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* IE10 */
background: -o-linear-gradient(top, rgba(179, 179, 179, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, rgba(179, 179, 179, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(179, 179, 179, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)");
box-shadow: inset 1px 1px 3px #999999;
}
button.dt-button:active:not(.disabled):hover:not(.disabled), button.dt-button.active:not(.disabled):hover:not(.disabled),
div.dt-button:active:not(.disabled):hover:not(.disabled),
div.dt-button.active:not(.disabled):hover:not(.disabled),
a.dt-button:active:not(.disabled):hover:not(.disabled),
a.dt-button.active:not(.disabled):hover:not(.disabled),
input.dt-button:active:not(.disabled):hover:not(.disabled),
input.dt-button.active:not(.disabled):hover:not(.disabled) {
box-shadow: inset 1px 1px 3px #999999;
background-color: rgba(0, 0, 0, 0.1);
/* Fallback */
background: -webkit-linear-gradient(top, rgba(128, 128, 128, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Chrome 10+, Saf5.1+, iOS 5+ */
background: -moz-linear-gradient(top, rgba(128, 128, 128, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* FF3.6 */
background: -ms-linear-gradient(top, rgba(128, 128, 128, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* IE10 */
background: -o-linear-gradient(top, rgba(128, 128, 128, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, rgba(128, 128, 128, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(128, 128, 128, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)");
}
button.dt-button:hover,
div.dt-button:hover,
a.dt-button:hover,
input.dt-button:hover {
text-decoration: none;
}
button.dt-button:hover:not(.disabled),
div.dt-button:hover:not(.disabled),
a.dt-button:hover:not(.disabled),
input.dt-button:hover:not(.disabled) {
border: 1px solid #666;
background-color: rgba(0, 0, 0, 0.1);
/* Fallback */
background: -webkit-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Chrome 10+, Saf5.1+, iOS 5+ */
background: -moz-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* FF3.6 */
background: -ms-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* IE10 */
background: -o-linear-gradient(top, rgba(153, 153, 153, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, rgba(153, 153, 153, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(153, 153, 153, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)");
}
button.dt-button:focus:not(.disabled),
div.dt-button:focus:not(.disabled),
a.dt-button:focus:not(.disabled),
input.dt-button:focus:not(.disabled) {
border: 1px solid #426c9e;
text-shadow: 0 1px 0 #c4def1;
outline: none;
background-color: #79ace9;
/* Fallback */
background: -webkit-linear-gradient(top, #d1e2f7 0%, #79ace9 100%);
/* Chrome 10+, Saf5.1+, iOS 5+ */
background: -moz-linear-gradient(top, #d1e2f7 0%, #79ace9 100%);
/* FF3.6 */
background: -ms-linear-gradient(top, #d1e2f7 0%, #79ace9 100%);
/* IE10 */
background: -o-linear-gradient(top, #d1e2f7 0%, #79ace9 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #d1e2f7 0%, #79ace9 100%);
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#d1e2f7", EndColorStr="#79ace9");
}
button.dt-button span.dt-down-arrow,
div.dt-button span.dt-down-arrow,
a.dt-button span.dt-down-arrow,
input.dt-button span.dt-down-arrow {
position: relative;
top: -2px;
color: rgba(70, 70, 70, 0.75);
font-size: 8px;
padding-left: 10px;
line-height: 1em;
}
.dt-button embed {
outline: none;
}
div.dt-buttons {
float: left;
}
div.dt-buttons.buttons-right {
float: right;
}
div.dataTables_layout_cell div.dt-buttons {
float: none;
}
div.dataTables_layout_cell div.dt-buttons.buttons-right {
float: none;
}
div.dt-btn-split-wrapper {
display: inline-block;
}
div.dt-button-collection {
position: absolute;
top: 0;
left: 0;
width: 200px;
margin-top: 3px;
margin-bottom: 3px;
padding: 4px 4px 2px 4px;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.4);
background-color: white;
overflow: hidden;
z-index: 2002;
border-radius: 5px;
box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3);
box-sizing: border-box;
}
div.dt-button-collection button.dt-button,
div.dt-button-collection div.dt-button,
div.dt-button-collection a.dt-button {
position: relative;
left: 0;
right: 0;
width: 100%;
display: block;
float: none;
margin: 4px 0 2px 0;
}
div.dt-button-collection button.dt-button:active:not(.disabled), div.dt-button-collection button.dt-button.active:not(.disabled),
div.dt-button-collection div.dt-button:active:not(.disabled),
div.dt-button-collection div.dt-button.active:not(.disabled),
div.dt-button-collection a.dt-button:active:not(.disabled),
div.dt-button-collection a.dt-button.active:not(.disabled) {
background-color: #dadada;
/* Fallback */
background: -webkit-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* Chrome 10+, Saf5.1+, iOS 5+ */
background: -moz-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* FF3.6 */
background: -ms-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* IE10 */
background: -o-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%);
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#f0f0f0", EndColorStr="#dadada");
box-shadow: inset 1px 1px 3px #666;
}
div.dt-button-collection button.dt-button:first-child,
div.dt-button-collection div.dt-button:first-child,
div.dt-button-collection a.dt-button:first-child {
margin-top: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
div.dt-button-collection button.dt-button:last-child,
div.dt-button-collection div.dt-button:last-child,
div.dt-button-collection a.dt-button:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
div.dt-button-collection div.dt-btn-split-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
align-items: stretch;
margin: 4px 0 2px 0;
}
div.dt-button-collection div.dt-btn-split-wrapper button.dt-button {
margin: 0;
display: inline-block;
width: 0;
flex-grow: 1;
flex-shrink: 0;
flex-basis: 50px;
border-radius: 0;
}
div.dt-button-collection div.dt-btn-split-wrapper button.dt-btn-split-drop {
min-width: 20px;
flex-grow: 0;
flex-shrink: 0;
flex-basis: 0;
}
div.dt-button-collection div.dt-btn-split-wrapper:first-child {
margin-top: 0;
}
div.dt-button-collection div.dt-btn-split-wrapper:first-child button.dt-button {
border-top-left-radius: 3px;
}
div.dt-button-collection div.dt-btn-split-wrapper:first-child button.dt-btn-split-drop {
border-top-right-radius: 3px;
}
div.dt-button-collection div.dt-btn-split-wrapper:last-child button.dt-button {
border-bottom-left-radius: 3px;
}
div.dt-button-collection div.dt-btn-split-wrapper:last-child button.dt-btn-split-drop {
border-bottom-right-radius: 3px;
}
div.dt-button-collection div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button, div.dt-button-collection div.dt-btn-split-wrapper.active:not(.disabled) button.dt-button {
background-color: #dadada;
/* Fallback */
background: -webkit-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* Chrome 10+, Saf5.1+, iOS 5+ */
background: -moz-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* FF3.6 */
background: -ms-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* IE10 */
background: -o-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%);
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#f0f0f0", EndColorStr="#dadada");
box-shadow: inset 0px 0px 4px #666;
}
div.dt-button-collection div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-button-collection div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop {
box-shadow: none;
}
div.dt-button-collection.fixed .dt-button:first-child {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
div.dt-button-collection.fixed .dt-button:last-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
div.dt-button-collection.fixed {
position: fixed;
display: block;
top: 50%;
left: 50%;
margin-left: -75px;
border-radius: 5px;
background-color: white;
}
div.dt-button-collection.fixed.two-column {
margin-left: -200px;
}
div.dt-button-collection.fixed.three-column {
margin-left: -225px;
}
div.dt-button-collection.fixed.four-column {
margin-left: -300px;
}
div.dt-button-collection.fixed.columns {
margin-left: -409px;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.fixed.columns {
margin-left: -308px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.fixed.columns {
margin-left: -203px;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.fixed.columns {
margin-left: -100px;
}
}
div.dt-button-collection.fixed > :last-child {
max-height: 100vh;
overflow: auto;
}
div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child {
display: block !important;
-webkit-column-gap: 8px;
-moz-column-gap: 8px;
-ms-column-gap: 8px;
-o-column-gap: 8px;
column-gap: 8px;
}
div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * {
-webkit-column-break-inside: avoid;
break-inside: avoid;
}
div.dt-button-collection.two-column {
width: 400px;
}
div.dt-button-collection.two-column > :last-child {
padding-bottom: 1px;
column-count: 2;
}
div.dt-button-collection.three-column {
width: 450px;
}
div.dt-button-collection.three-column > :last-child {
padding-bottom: 1px;
column-count: 3;
}
div.dt-button-collection.four-column {
width: 600px;
}
div.dt-button-collection.four-column > :last-child {
padding-bottom: 1px;
column-count: 4;
}
div.dt-button-collection .dt-button {
border-radius: 0;
}
div.dt-button-collection.columns {
width: auto;
}
div.dt-button-collection.columns > :last-child {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 6px;
width: 818px;
padding-bottom: 1px;
}
div.dt-button-collection.columns > :last-child .dt-button {
min-width: 200px;
flex: 0 1;
margin: 0;
}
div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child {
justify-content: space-between;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 1 1 32%;
}
div.dt-button-collection.columns.dtb-b2 .dt-button {
flex: 1 1 48%;
}
div.dt-button-collection.columns.dtb-b1 .dt-button {
flex: 1 1 100%;
}
@media screen and (max-width: 1024px) {
div.dt-button-collection.columns > :last-child {
width: 612px;
}
}
@media screen and (max-width: 640px) {
div.dt-button-collection.columns > :last-child {
width: 406px;
}
div.dt-button-collection.columns.dtb-b3 .dt-button {
flex: 0 1 32%;
}
}
@media screen and (max-width: 460px) {
div.dt-button-collection.columns > :last-child {
width: 200px;
}
}
div.dt-button-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
/* Fallback */
background: -ms-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
/* IE10 Consumer Preview */
background: -moz-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
/* Firefox */
background: -o-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
/* Opera */
background: -webkit-gradient(radial, center center, 0, center center, 497, color-stop(0, rgba(0, 0, 0, 0.3)), color-stop(1, rgba(0, 0, 0, 0.7)));
/* Webkit (Safari/Chrome 10) */
background: -webkit-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
/* Webkit (Chrome 11+) */
background: radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
/* W3C Markup, IE10 Release Preview */
z-index: 2001;
}
@media screen and (max-width: 640px) {
div.dt-buttons {
float: none !important;
text-align: center;
}
}
button.dt-button.processing,
div.dt-button.processing,
a.dt-button.processing {
color: rgba(0, 0, 0, 0.2);
}
button.dt-button.processing:after,
div.dt-button.processing:after,
a.dt-button.processing:after {
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
box-sizing: border-box;
display: block;
content: " ";
border: 2px solid #282828;
border-radius: 50%;
border-left-color: transparent;
border-right-color: transparent;
animation: dtb-spinner 1500ms infinite linear;
-o-animation: dtb-spinner 1500ms infinite linear;
-ms-animation: dtb-spinner 1500ms infinite linear;
-webkit-animation: dtb-spinner 1500ms infinite linear;
-moz-animation: dtb-spinner 1500ms infinite linear;
}
button.dt-btn-split-drop {
margin-left: calc(-1px - 0.333em);
padding-bottom: calc(0.5em - 1px);
border-radius: 0px 1px 1px 0px;
color: rgba(70, 70, 70, 0.9);
border-left: none;
}
button.dt-btn-split-drop span.dt-btn-split-drop-arrow {
position: relative;
top: -1px;
left: -2px;
font-size: 8px;
}
button.dt-btn-split-drop:hover {
z-index: 2;
}
button.buttons-split {
border-right: 1px solid rgba(70, 70, 70, 0);
border-radius: 1px 0px 0px 1px;
}
button.dt-btn-split-drop-button {
background-color: white;
}
button.dt-btn-split-drop-button:hover {
background-color: white;
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More