forked from ScoDoc/ScoDoc
Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
dcb53e9c35 | |||
77f68d1c4c | |||
5e8c837fb2 | |||
3da9bb6914 | |||
369b45a8c4 | |||
4864fa5040 | |||
078e0e85e0 | |||
c3e5a5d188 | |||
2ee1a386b9 | |||
1e53aa21cb | |||
66859c53ba | |||
e943e7f283 | |||
be30cf66fa | |||
2c5e59120c | |||
e52ffb8357 | |||
d84102657f | |||
64f74800ee | |||
e61a9752d3 | |||
dc4bfe4d2e | |||
2154b60cde | |||
f1ec103b25 | |||
b76200ac87 | |||
2bfa7eb4a8 | |||
0e7857e5ca | |||
390141f145 | |||
bb589ae3ae | |||
ae752bc581 | |||
fcd34c3bdf | |||
af5b946b46 | |||
8018a0b092 | |||
cde43621f9 | |||
aec2d58dbf | |||
2bf4449dce | |||
bdc0ad488a | |||
225c97b6dc | |||
aba522a60d | |||
f89fa0bf68 | |||
5f425da6c0 | |||
19913ce89a | |||
31c40b6492 | |||
415810496f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -131,6 +131,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
envsco8/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
|
65
README.md
65
README.md
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
# SCODOC - gestion de la scolarité
|
# ScoDoc - Gestion de la scolarité
|
||||||
|
|
||||||
(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt)
|
(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt)
|
||||||
|
|
||||||
@ -8,7 +8,68 @@ Installation: voir instructions à jour sur <https://scodoc.org>
|
|||||||
|
|
||||||
Documentation utilisateur: <https://scodoc.org>
|
Documentation utilisateur: <https://scodoc.org>
|
||||||
|
|
||||||
Ce logiciel est un produit pour Zope 2.13 écrit en Python (2.4, passé à 2.7 pour ScoDoc7).
|
## Branche ScoDoc 8 expérimentale
|
||||||
|
|
||||||
|
N'utiliser que pour les développements et tests, dans le cadre de la migration de Zope vers Flask.
|
||||||
|
|
||||||
|
Basée sur **python 2.7**.
|
||||||
|
|
||||||
|
## Setup (sur Debian 10 / python2.7)
|
||||||
|
|
||||||
|
virtualenv envsco8
|
||||||
|
|
||||||
|
source envsco8/bin/activate
|
||||||
|
|
||||||
|
installation:
|
||||||
|
|
||||||
|
pip install flask
|
||||||
|
# et pas mal d'autres paquets
|
||||||
|
|
||||||
|
donc utiliser:
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
pour régénerer ce fichier:
|
||||||
|
|
||||||
|
pip freeze > requirements.txt
|
||||||
|
|
||||||
|
### Bidouilles temporaires
|
||||||
|
|
||||||
|
Installer le bon vieux `pyExcelerator` dans l'environnement:
|
||||||
|
|
||||||
|
(cd /tmp; tar xfz /opt/scodoc/Products/ScoDoc/config/softs/pyExcelerator-0.6.3a.patched.tgz )
|
||||||
|
(cd /tmp/pyExcelerator-0.6.3a.patched/; python setup.py install)
|
||||||
|
|
||||||
|
## Lancement serveur (développement, sur VM Linux)
|
||||||
|
|
||||||
|
export FLASK_APP=scodoc.py
|
||||||
|
export FLASK_ENV=development
|
||||||
|
flask run --host=0.0.0.0
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
python -m unittest tests.test_users
|
||||||
|
|
||||||
|
# Work in Progress
|
||||||
|
|
||||||
|
## Migration ZScolar
|
||||||
|
|
||||||
|
### Méthodes qui ne devraient plus être publiées:
|
||||||
|
security.declareProtected(ScoView, "get_preferences")
|
||||||
|
|
||||||
|
def get_preferences(context, formsemestre_id=None):
|
||||||
|
"Get preferences for this instance (a dict-like instance)"
|
||||||
|
return sco_preferences.sem_preferences(context, formsemestre_id)
|
||||||
|
|
||||||
|
security.declareProtected(ScoView, "get_preference")
|
||||||
|
|
||||||
|
def get_preference(context, name, formsemestre_id=None):
|
||||||
|
"""Returns value of named preference.
|
||||||
|
All preferences have a sensible default value (see sco_preferences.py),
|
||||||
|
this function always returns a usable value for all defined preferences names.
|
||||||
|
"""
|
||||||
|
return sco_preferences.get_base_preferences(context).get(formsemestre_id, name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
238
TODO
238
TODO
@ -1,238 +0,0 @@
|
|||||||
|
|
||||||
NOTES EN VRAC / Brouillon / Trucs obsoletes
|
|
||||||
|
|
||||||
|
|
||||||
#do_moduleimpl_list\(\{"([a-z_]*)"\s*:\s*(.*)\}\)
|
|
||||||
#do_moduleimpl_list( $1 = $2 )
|
|
||||||
|
|
||||||
#do_moduleimpl_list\([\s\n]*args[\s\n]*=[\s\n]*\{"([a-z_]*)"[\s\n]*:[\s\n]*(.*)[\s\n]*\}[\s\n]*\)
|
|
||||||
|
|
||||||
Upgrade JavaScript
|
|
||||||
- jquery-ui-1.12.1 introduit un problème d'affichage de la barre de menu.
|
|
||||||
Il faudrait la revoir entièrement pour upgrader.
|
|
||||||
On reste donc à jquery-ui-1.10.4.custom
|
|
||||||
Or cette version est incompatible avec jQuery 3 (messages d'erreur dans la console)
|
|
||||||
On reste donc avec jQuery 1.12.14
|
|
||||||
|
|
||||||
|
|
||||||
Suivi des requêtes utilisateurs:
|
|
||||||
table sql: id, ip, authuser, request
|
|
||||||
|
|
||||||
|
|
||||||
* Optim:
|
|
||||||
porcodeb4, avant memorisation des moy_ue:
|
|
||||||
S1 SEM14133 cold start: min 9s, max 12s, avg > 11s
|
|
||||||
inval (add note): 1.33s (pas de recalcul des autres)
|
|
||||||
inval (add abs) : min8s, max 12s (recalcule tout :-()
|
|
||||||
LP SEM14946 cold start: 0.7s - 0.86s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------- LISTE OBSOLETE (très ancienne, à trier) -----------------------
|
|
||||||
BUGS
|
|
||||||
----
|
|
||||||
|
|
||||||
- formsemestre_inscription_with_modules
|
|
||||||
si inscription 'un etud deja inscrit, IntegrityError
|
|
||||||
|
|
||||||
FEATURES REQUESTS
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
* Bulletins:
|
|
||||||
. logos IUT et Univ sur bull PDF
|
|
||||||
. nom departement: nom abbrégé (CJ) ou complet (Carrière Juridiques)
|
|
||||||
. bulletin: deplacer la barre indicateur (cf OLDGEA S2: gêne)
|
|
||||||
. bulletin: click nom titre -> ficheEtud
|
|
||||||
|
|
||||||
. formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5"
|
|
||||||
et valider correctement le form !
|
|
||||||
|
|
||||||
* Jury
|
|
||||||
. recapcomplet: revenir avec qq lignes au dessus de l'étudiant en cours
|
|
||||||
|
|
||||||
|
|
||||||
* Divers
|
|
||||||
. formsemestre_editwithmodules: confirmer suppression modules
|
|
||||||
(et pour l'instant impossible si evaluations dans le module)
|
|
||||||
|
|
||||||
* Modules et UE optionnelles:
|
|
||||||
. UE capitalisées: donc dispense possible dans semestre redoublé.
|
|
||||||
traitable en n'inscrivant pas l'etudiant au modules
|
|
||||||
de cette UE: faire interface utilisateur
|
|
||||||
|
|
||||||
. page pour inscription d'un etudiant a un module
|
|
||||||
. page pour visualiser les modules auquel un etudiant est inscrit,
|
|
||||||
et le desinscrire si besoin.
|
|
||||||
|
|
||||||
. ficheEtud indiquer si inscrit au module sport
|
|
||||||
|
|
||||||
* Absences
|
|
||||||
. EtatAbsences : verifier dates (en JS)
|
|
||||||
. Listes absences pdf et listes groupes pdf + emargements (cf mail Nathalie)
|
|
||||||
. absences par demi-journées sur EtatAbsencesDate (? à vérifier)
|
|
||||||
. formChoixSemestreGroupe: utilisé par Absences/index_html
|
|
||||||
a améliorer
|
|
||||||
|
|
||||||
|
|
||||||
* Notes et évaluations:
|
|
||||||
. Exception "Not an OLE file": generer page erreur plus explicite
|
|
||||||
. Dates evaluation: utiliser JS pour calendrier
|
|
||||||
. Saisie des notes: si une note invalide, l'indiquer dans le listing (JS ?)
|
|
||||||
. et/ou: notes invalides: afficher les noms des etudiants concernes
|
|
||||||
dans le message d'erreur.
|
|
||||||
. upload excel: message erreur peu explicite:
|
|
||||||
* Feuille "Saisie notes", 17 lignes
|
|
||||||
* Erreur: la feuille contient 1 notes invalides
|
|
||||||
* Notes invalides pour les id: ['10500494']
|
|
||||||
(pas de notes modifiées)
|
|
||||||
Notes chargées. <<< CONTRADICTOIRE !!
|
|
||||||
|
|
||||||
. recap complet semestre:
|
|
||||||
Options:
|
|
||||||
- choix groupes
|
|
||||||
- critère de tri (moy ou alphab)
|
|
||||||
- nb de chiffres a afficher
|
|
||||||
|
|
||||||
+ definir des "catégories" d'évaluations (eg "théorie","pratique")
|
|
||||||
afin de n'afficher que des moyennes "de catégorie" dans
|
|
||||||
le bulletin.
|
|
||||||
|
|
||||||
. liste des absents à une eval et croisement avec BD absences
|
|
||||||
|
|
||||||
. notes_evaluation_listenotes
|
|
||||||
- afficher groupes, moyenne, #inscrits, #absents, #manquantes dans l'en-tete.
|
|
||||||
- lien vers modif notes (selon role)
|
|
||||||
|
|
||||||
. Export excel des notes d'evaluation: indiquer date, et autres infos en haut.
|
|
||||||
. Génération PDF listes notes
|
|
||||||
. Page recap notes moyennes par groupes (choisir type de groupe?)
|
|
||||||
|
|
||||||
. (GEA) edition tableau notes avec tous les evals d'un module
|
|
||||||
(comme notes_evaluation_listenotes mais avec tt les evals)
|
|
||||||
|
|
||||||
|
|
||||||
* Non prioritaire:
|
|
||||||
. optimiser scolar_news_summary
|
|
||||||
. recapitulatif des "nouvelles"
|
|
||||||
- dernieres notes
|
|
||||||
- changement de statuts (demissions,inscriptions)
|
|
||||||
- annotations
|
|
||||||
- entreprises
|
|
||||||
|
|
||||||
. notes_table: pouvoir changer decision sans invalider tout le cache
|
|
||||||
. navigation: utiliser Session pour montrer historique pages vues ?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
A faire:
|
|
||||||
- fiche etud: code dec jury sur ligne 1
|
|
||||||
si ancien, indiquer autorisation inscription sous le parcours
|
|
||||||
|
|
||||||
- saisie notes: undo
|
|
||||||
- saisie notes: validation
|
|
||||||
- ticket #18:
|
|
||||||
UE capitalisées: donc dispense possible dans semestre redoublé. Traitable en n'inscrivant pas l'etudiant aux modules de cette UE: faire interface utilisateur.
|
|
||||||
|
|
||||||
Prévoir d'entrer une UE capitalisée avec sa note, date d'obtention et un commentaire. Coupler avec la désincription aux modules (si l'étudiant a été inscrit avec ses condisciples).
|
|
||||||
|
|
||||||
|
|
||||||
- Ticket #4: Afin d'éviter les doublons, vérifier qu'il n'existe pas d'homonyme proche lors de la création manuelle d'un étudiant. (confirmé en ScoDoc 6, vérifier aussi les imports Excel)
|
|
||||||
|
|
||||||
- Ticket #74: Il est possible d'inscrire un étudiant sans prénom par un import excel !!!
|
|
||||||
|
|
||||||
- Ticket #64: saisir les absences pour la promo entiere (et pas par groupe). Des fois, je fais signer une feuille de presence en amphi a partir de la liste de tous les etudiants. Ensuite pour reporter les absents par groupe, c'est galere.
|
|
||||||
|
|
||||||
- Ticket #62: Lors des exports Excel, le format des cellules n'est pas reconnu comme numérique sous Windows (pas de problèmes avec Macintosh et Linux).
|
|
||||||
|
|
||||||
A confirmer et corriger.
|
|
||||||
|
|
||||||
- Ticket #75: On peut modifier une décision de jury (et les autorisations de passage associées), mais pas la supprimer purement et simplement.
|
|
||||||
Ajoute ce choix dans les "décisions manuelles".
|
|
||||||
|
|
||||||
- Ticket #37: Page recap notes moyennes par groupes
|
|
||||||
Construire une page avec les moyennes dans chaque UE ou module par groupe d'étudiants.
|
|
||||||
Et aussi pourquoi pas ventiler par type de bac, sexe, parcours (nombre de semestre de parcours) ?
|
|
||||||
redemandé par CJ: à faire avant mai 2008 !
|
|
||||||
|
|
||||||
- Ticket #75: Synchro Apogée: choisir les etudiants
|
|
||||||
Sur la page de syncho Apogée (formsemestre_synchro_etuds), on peut choisir (cocher) les étudiants Apogée à importer. mais on ne peut pas le faire s'ils sont déjà dans ScoDoc: il faudrait ajouter des checkboxes dans toutes les listes.
|
|
||||||
|
|
||||||
- Ticket #9: Format des valeurs de marges des bulletins.
|
|
||||||
formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5" et valider correctement le form !
|
|
||||||
|
|
||||||
- Ticket #17: Suppression modules dans semestres
|
|
||||||
formsemestre_editwithmodules: confirmer suppression modules
|
|
||||||
|
|
||||||
- Ticket #29: changer le stoquage des photos, garder une version HD.
|
|
||||||
|
|
||||||
- bencher NotesTable sans calcul de moyennes. Etudier un cache des moyennes de modules.
|
|
||||||
- listes d'utilisateurs (modules): remplacer menus par champs texte + completions javascript
|
|
||||||
- documenter archives sur Wiki
|
|
||||||
- verifier paquet Debian pour font pdf (reportab: helvetica ... plante si font indisponible)
|
|
||||||
- chercher comment obtenir une page d'erreur correcte pour les pages POST
|
|
||||||
(eg: si le font n'existe pas, archive semestre echoue sans page d'erreur)
|
|
||||||
? je ne crois pas que le POST soit en cause. HTTP status=500
|
|
||||||
ne se produit pas avec Safari
|
|
||||||
- essayer avec IE / Win98
|
|
||||||
- faire apparaitre les diplômés sur le graphe des parcours
|
|
||||||
- démission: formulaire: vérifier que la date est bien dans le semestre
|
|
||||||
|
|
||||||
+ graphe parcours: aligner en colonnes selon les dates (de fin), placer les diplomes
|
|
||||||
dans la même colone que le semestre terminal.
|
|
||||||
|
|
||||||
- modif gestion utilisateurs (donner droits en fct du dept. d'appartenance, bug #57)
|
|
||||||
- modif form def. utilisateur (dept appartenance)
|
|
||||||
- utilisateurs: source externe
|
|
||||||
- archivage des semestres
|
|
||||||
|
|
||||||
|
|
||||||
o-------------------------------------o
|
|
||||||
|
|
||||||
* Nouvelle gestion utilisateurs:
|
|
||||||
objectif: dissocier l'authentification de la notion "d'enseignant"
|
|
||||||
On a une source externe "d'utilisateurs" (annuaire LDAP ou base SQL)
|
|
||||||
qui permet seulement de:
|
|
||||||
- authentifier un utilisateur (login, passwd)
|
|
||||||
- lister un utilisateur: login => firstname, lastname, email
|
|
||||||
- lister les utilisateurs
|
|
||||||
|
|
||||||
et une base interne ScoDoc "d'acteurs" (enseignants, administratifs).
|
|
||||||
Chaque acteur est défini par:
|
|
||||||
- actor_id, firstname, lastname
|
|
||||||
date_creation, date_expiration,
|
|
||||||
roles, departement,
|
|
||||||
email (+flag indiquant s'il faut utiliser ce mail ou celui de
|
|
||||||
l'utilisateur ?)
|
|
||||||
state (on, off) (pour desactiver avant expiration ?)
|
|
||||||
user_id (login) => lien avec base utilisateur
|
|
||||||
|
|
||||||
On offrira une source d'utilisateurs SQL (base partagée par tous les dept.
|
|
||||||
d'une instance ScoDoc), mais dans la plupart des cas les gens utiliseront
|
|
||||||
un annuaire LDAP.
|
|
||||||
|
|
||||||
La base d'acteurs remplace ScoUsers. Les objets ScoDoc (semestres,
|
|
||||||
modules etc) font référence à des acteurs (eg responsable_id est un actor_id).
|
|
||||||
|
|
||||||
Le lien entre les deux ?
|
|
||||||
Loger un utilisateur => authentification utilisateur + association d'un acteur
|
|
||||||
Cela doit se faire au niveau d'un UserFolder Zope, pour avoir les
|
|
||||||
bons rôles et le contrôle d'accès adéquat.
|
|
||||||
(Il faut donc coder notre propre UserFolder).
|
|
||||||
On ne peut associer qu'un acteur à l'état 'on' et non expiré.
|
|
||||||
|
|
||||||
Opérations ScoDoc:
|
|
||||||
- paramétrage: choisir et paramétrer source utilisateurs
|
|
||||||
- ajouter utilisateur: choisir un utilisateur dans la liste
|
|
||||||
et lui associer un nouvel acteur (choix des rôles, des dates)
|
|
||||||
+ éventuellement: synchro d'un ensemble d'utilisateurs, basé sur
|
|
||||||
une requête (eg LDAP) précise (quelle interface utilisateur proposer ?)
|
|
||||||
|
|
||||||
- régulièrement (cron) aviser quelqu'un (le chef) de l'expiration des acteurs.
|
|
||||||
- changer etat d'un acteur (on/off)
|
|
||||||
|
|
||||||
|
|
||||||
o-------------------------------------o
|
|
||||||
|
|
105
app/__init__.py
Normal file
105
app/__init__.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from logging.handlers import SMTPHandler, RotatingFileHandler
|
||||||
|
|
||||||
|
from flask import request
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from flask_mail import Mail
|
||||||
|
from flask_bootstrap import Bootstrap
|
||||||
|
from flask_moment import Moment
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(Config)
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
login = LoginManager()
|
||||||
|
login.login_view = "auth.login"
|
||||||
|
login.login_message = "Please log in to access this page."
|
||||||
|
mail = Mail()
|
||||||
|
bootstrap = Bootstrap(app)
|
||||||
|
moment = Moment()
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_class=Config):
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(config_class)
|
||||||
|
db.init_app(app)
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
login.init_app(app)
|
||||||
|
mail.init_app(app)
|
||||||
|
bootstrap.init_app(app)
|
||||||
|
moment.init_app(app)
|
||||||
|
|
||||||
|
from app.auth import bp as auth_bp
|
||||||
|
|
||||||
|
app.register_blueprint(auth_bp, url_prefix="/auth")
|
||||||
|
|
||||||
|
from app.views import essais_bp
|
||||||
|
|
||||||
|
app.register_blueprint(essais_bp, url_prefix="/Essais")
|
||||||
|
|
||||||
|
from app.views import scolar_bp
|
||||||
|
from app.views import notes_bp
|
||||||
|
from app.views import absences_bp
|
||||||
|
|
||||||
|
# https://scodoc.fr/ScoDoc/RT/Scolarite/...
|
||||||
|
app.register_blueprint(scolar_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite")
|
||||||
|
# https://scodoc.fr/ScoDoc/RT/Scolarite/Notes/...
|
||||||
|
app.register_blueprint(notes_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Notes")
|
||||||
|
# https://scodoc.fr/ScoDoc/RT/Scolarite/Absences/...
|
||||||
|
app.register_blueprint(
|
||||||
|
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.main import bp as main_bp
|
||||||
|
|
||||||
|
app.register_blueprint(main_bp)
|
||||||
|
|
||||||
|
if not app.debug and not app.testing:
|
||||||
|
if app.config["MAIL_SERVER"]:
|
||||||
|
auth = None
|
||||||
|
if app.config["MAIL_USERNAME"] or app.config["MAIL_PASSWORD"]:
|
||||||
|
auth = (app.config["MAIL_USERNAME"], app.config["MAIL_PASSWORD"])
|
||||||
|
secure = None
|
||||||
|
if app.config["MAIL_USE_TLS"]:
|
||||||
|
secure = ()
|
||||||
|
mail_handler = SMTPHandler(
|
||||||
|
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]),
|
||||||
|
fromaddr="no-reply@" + app.config["MAIL_SERVER"],
|
||||||
|
toaddrs=[app.config["ADMINS"]],
|
||||||
|
subject="ScoDoc8 Failure",
|
||||||
|
credentials=auth,
|
||||||
|
secure=secure,
|
||||||
|
)
|
||||||
|
mail_handler.setLevel(logging.ERROR)
|
||||||
|
app.logger.addHandler(mail_handler)
|
||||||
|
|
||||||
|
if not os.path.exists("logs"):
|
||||||
|
os.mkdir("logs")
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
"logs/scodoc.log", maxBytes=10240, backupCount=10
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(
|
||||||
|
logging.Formatter(
|
||||||
|
"%(asctime)s %(levelname)s: %(message)s " "[in %(pathname)s:%(lineno)d]"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
file_handler.setLevel(logging.INFO)
|
||||||
|
app.logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
app.logger.setLevel(logging.INFO)
|
||||||
|
app.logger.info("ScoDoc8 startup")
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# from app import models
|
6
app/auth/README.md
Normal file
6
app/auth/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# ScoDoc User Authentication Blueprint
|
||||||
|
|
||||||
|
Code borrowed and adapted from
|
||||||
|
https://courses.miguelgrinberg.com/p/flask-mega-tutorial
|
||||||
|
|
||||||
|
|
8
app/auth/__init__.py
Normal file
8
app/auth/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""auth.__init__
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
from app.auth import routes
|
15
app/auth/email.py
Normal file
15
app/auth/email.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
from flask import render_template, current_app
|
||||||
|
from flask_babel import _
|
||||||
|
from app.email import send_email
|
||||||
|
|
||||||
|
|
||||||
|
def send_password_reset_email(user):
|
||||||
|
token = user.get_reset_password_token()
|
||||||
|
send_email(
|
||||||
|
"[ScoDoc] Reset Your Password",
|
||||||
|
sender=current_app.config["ADMINS"][0],
|
||||||
|
recipients=[user.email],
|
||||||
|
text_body=render_template("email/reset_password.txt", user=user, token=token),
|
||||||
|
html_body=render_template("email/reset_password.html", user=user, token=token),
|
||||||
|
)
|
55
app/auth/forms.py
Normal file
55
app/auth/forms.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
|
"""Formulaires authentification
|
||||||
|
|
||||||
|
TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentification
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||||
|
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
||||||
|
from app.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
_ = lambda x: x # sans babel
|
||||||
|
_l = _
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = StringField(_l("Username"), validators=[DataRequired()])
|
||||||
|
password = PasswordField(_l("Password"), validators=[DataRequired()])
|
||||||
|
remember_me = BooleanField(_l("Remember Me"))
|
||||||
|
submit = SubmitField(_l("Sign In"))
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreationForm(FlaskForm):
|
||||||
|
username = StringField(_l("Username"), validators=[DataRequired()])
|
||||||
|
email = StringField(_l("Email"), validators=[DataRequired(), Email()])
|
||||||
|
password = PasswordField(_l("Password"), validators=[DataRequired()])
|
||||||
|
password2 = PasswordField(
|
||||||
|
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")]
|
||||||
|
)
|
||||||
|
submit = SubmitField(_l("Register"))
|
||||||
|
|
||||||
|
def validate_username(self, username):
|
||||||
|
user = User.query.filter_by(username=username.data).first()
|
||||||
|
if user is not None:
|
||||||
|
raise ValidationError(_("Please use a different username."))
|
||||||
|
|
||||||
|
def validate_email(self, email):
|
||||||
|
user = User.query.filter_by(email=email.data).first()
|
||||||
|
if user is not None:
|
||||||
|
raise ValidationError(_("Please use a different email address."))
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordRequestForm(FlaskForm):
|
||||||
|
email = StringField(_l("Email"), validators=[DataRequired(), Email()])
|
||||||
|
submit = SubmitField(_l("Request Password Reset"))
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordForm(FlaskForm):
|
||||||
|
password = PasswordField(_l("Password"), validators=[DataRequired()])
|
||||||
|
password2 = PasswordField(
|
||||||
|
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")]
|
||||||
|
)
|
||||||
|
submit = SubmitField(_l("Request Password Reset"))
|
262
app/auth/models.py
Normal file
262
app/auth/models.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
|
"""Users and Roles models for ScoDoc
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from hashlib import md5
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from flask import current_app, url_for
|
||||||
|
from flask_login import UserMixin, AnonymousUserMixin
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
from app import db, login
|
||||||
|
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(64), index=True, unique=True)
|
||||||
|
email = db.Column(db.String(120), index=True, unique=True)
|
||||||
|
password_hash = db.Column(db.String(128))
|
||||||
|
about_me = db.Column(db.String(140))
|
||||||
|
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
token = db.Column(db.String(32), index=True, unique=True)
|
||||||
|
token_expiration = db.Column(db.DateTime)
|
||||||
|
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
||||||
|
Permission = Permission
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.roles = []
|
||||||
|
super(User, self).__init__(**kwargs)
|
||||||
|
if (
|
||||||
|
not self.roles
|
||||||
|
and self.email
|
||||||
|
and self.email == current_app.config["SCODOC_ADMIN_MAIL"]
|
||||||
|
):
|
||||||
|
# super-admin
|
||||||
|
admin_role = Role.query.filter_by(name="Admin").first()
|
||||||
|
assert admin_role
|
||||||
|
self.add_role(admin_role, None)
|
||||||
|
db.session.commit()
|
||||||
|
current_app.logger.info("creating user with roles={}".format(self.roles))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<User {}>".format(self.username)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
"Set password"
|
||||||
|
if password:
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
else:
|
||||||
|
self.password_hash = None
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
"""Check given password vs current one.
|
||||||
|
Returns `True` if the password matched, `False` otherwise.
|
||||||
|
"""
|
||||||
|
if not self.password_hash: # user without password can't login
|
||||||
|
return False
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def get_reset_password_token(self, expires_in=600):
|
||||||
|
return jwt.encode(
|
||||||
|
{"reset_password": self.id, "exp": time() + expires_in},
|
||||||
|
current_app.config["SECRET_KEY"],
|
||||||
|
algorithm="HS256",
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def verify_reset_password_token(token):
|
||||||
|
try:
|
||||||
|
id = jwt.decode(
|
||||||
|
token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
|
||||||
|
)["reset_password"]
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
return User.query.get(id)
|
||||||
|
|
||||||
|
def to_dict(self, include_email=False):
|
||||||
|
data = {
|
||||||
|
"id": self.id,
|
||||||
|
"username": self.username,
|
||||||
|
"last_seen": self.last_seen.isoformat() + "Z",
|
||||||
|
"about_me": self.about_me,
|
||||||
|
}
|
||||||
|
if include_email:
|
||||||
|
data["email"] = self.email
|
||||||
|
return data
|
||||||
|
|
||||||
|
def from_dict(self, data, new_user=False):
|
||||||
|
for field in ["username", "email", "about_me"]:
|
||||||
|
if field in data:
|
||||||
|
setattr(self, field, data[field])
|
||||||
|
if new_user and "password" in data:
|
||||||
|
self.set_password(data["password"])
|
||||||
|
|
||||||
|
def get_token(self, expires_in=3600):
|
||||||
|
now = datetime.utcnow()
|
||||||
|
if self.token and self.token_expiration > now + timedelta(seconds=60):
|
||||||
|
return self.token
|
||||||
|
self.token = base64.b64encode(os.urandom(24)).decode("utf-8")
|
||||||
|
self.token_expiration = now + timedelta(seconds=expires_in)
|
||||||
|
db.session.add(self)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def revoke_token(self):
|
||||||
|
self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_token(token):
|
||||||
|
user = User.query.filter_by(token=token).first()
|
||||||
|
if user is None or user.token_expiration < datetime.utcnow():
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Permissions management:
|
||||||
|
def has_permission(self, perm, dept):
|
||||||
|
"""Check if user has permission `perm` in given `dept`.
|
||||||
|
Emulate Zope `has_permission``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
perm: integer, one of the value defined in Permission class.
|
||||||
|
context:
|
||||||
|
"""
|
||||||
|
# les role liés à ce département, et les roles avec dept=None (super-admin)
|
||||||
|
roles_in_dept = (
|
||||||
|
UserRole.query.filter_by(user_id=self.id)
|
||||||
|
.filter((UserRole.dept == dept) | (UserRole.dept == None))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for user_role in roles_in_dept:
|
||||||
|
if user_role.role.has_permission(perm):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Role management
|
||||||
|
def add_role(self, role, dept):
|
||||||
|
"""Add a role to this user.
|
||||||
|
:param role: Role to add.
|
||||||
|
"""
|
||||||
|
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
|
||||||
|
|
||||||
|
def add_roles(self, roles, dept):
|
||||||
|
"""Add roles to this user.
|
||||||
|
:param roles: Roles to add.
|
||||||
|
"""
|
||||||
|
for role in roles:
|
||||||
|
self.add_role(role, dept)
|
||||||
|
|
||||||
|
def set_roles(self, roles, dept):
|
||||||
|
self.user_roles = [UserRole(user=self, role=r, dept=dept) for r in roles]
|
||||||
|
|
||||||
|
def get_roles(self):
|
||||||
|
for role in self.roles:
|
||||||
|
yield role
|
||||||
|
|
||||||
|
def is_administrator(self):
|
||||||
|
return self.has_permission(Permission.ScoSuperAdmin, None)
|
||||||
|
|
||||||
|
|
||||||
|
class AnonymousUser(AnonymousUserMixin):
|
||||||
|
def has_permission(self, perm, dept=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_administrator(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
login.anonymous_user = AnonymousUser
|
||||||
|
|
||||||
|
|
||||||
|
class Role(db.Model):
|
||||||
|
"""Roles for ScoDoc"""
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(64), unique=True)
|
||||||
|
default = db.Column(db.Boolean, default=False, index=True)
|
||||||
|
permissions = db.Column(db.BigInteger) # 64 bits
|
||||||
|
users = db.relationship("User", secondary="user_role", viewonly=True)
|
||||||
|
# __table_args__ = (db.UniqueConstraint("name", "dept", name="_rolename_dept_uc"),)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Role, self).__init__(**kwargs)
|
||||||
|
if self.permissions is None:
|
||||||
|
self.permissions = 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Role {} perm={:0{w}b}>".format(
|
||||||
|
self.name,
|
||||||
|
self.permissions & ((1 << Permission.NBITS) - 1),
|
||||||
|
w=Permission.NBITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_permission(self, perm):
|
||||||
|
self.permissions |= perm
|
||||||
|
|
||||||
|
def remove_permission(self, perm):
|
||||||
|
self.permissions = self.permissions & ~perm
|
||||||
|
|
||||||
|
def reset_permissions(self):
|
||||||
|
self.permissions = 0
|
||||||
|
|
||||||
|
def has_permission(self, perm):
|
||||||
|
return self.permissions & perm == perm
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def insert_roles():
|
||||||
|
"""Create default roles"""
|
||||||
|
default_role = "Observateur"
|
||||||
|
for r, permissions in SCO_ROLES_DEFAULTS.items():
|
||||||
|
role = Role.query.filter_by(name=r).first()
|
||||||
|
if role is None:
|
||||||
|
role = Role(name=r)
|
||||||
|
role.reset_permissions()
|
||||||
|
for perm in permissions:
|
||||||
|
role.add_permission(perm)
|
||||||
|
role.default = role.name == default_role
|
||||||
|
db.session.add(role)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_named_role(name):
|
||||||
|
"""Returns existing role with given name, or None."""
|
||||||
|
return Role.query.filter_by(name=name).first()
|
||||||
|
|
||||||
|
|
||||||
|
class UserRole(db.Model):
|
||||||
|
"""Associate user to role, in a dept.
|
||||||
|
If dept is None, the role applies to all departments (eg super admin).
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||||
|
role_id = db.Column(db.Integer, db.ForeignKey("role.id"))
|
||||||
|
dept = db.Column(db.String(64))
|
||||||
|
user = db.relationship(
|
||||||
|
User, backref=db.backref("user_roles", cascade="all, delete-orphan")
|
||||||
|
)
|
||||||
|
role = db.relationship(
|
||||||
|
Role, backref=db.backref("user_roles", cascade="all, delete-orphan")
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<UserRole u={} r={} dept={}>".format(self.user, self.role, self.dept)
|
||||||
|
|
||||||
|
|
||||||
|
@login.user_loader
|
||||||
|
def load_user(id):
|
||||||
|
return User.query.get(int(id))
|
100
app/auth/routes.py
Normal file
100
app/auth/routes.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
"""
|
||||||
|
auth.routes.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import render_template, redirect, url_for, current_app, flash, request
|
||||||
|
from werkzeug.urls import url_parse
|
||||||
|
from flask_login import login_user, logout_user, current_user
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.auth import bp
|
||||||
|
from app.auth.forms import (
|
||||||
|
LoginForm,
|
||||||
|
UserCreationForm,
|
||||||
|
ResetPasswordRequestForm,
|
||||||
|
ResetPasswordForm,
|
||||||
|
)
|
||||||
|
from app.auth.models import User
|
||||||
|
from app.auth.email import send_password_reset_email
|
||||||
|
from app.decorators import scodoc7func, admin_required
|
||||||
|
|
||||||
|
_ = lambda x: x # sans babel
|
||||||
|
_l = _
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
form = LoginForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
|
if user is None or not user.check_password(form.password.data):
|
||||||
|
flash(_("Invalid username or password"))
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
login_user(user, remember=form.remember_me.data)
|
||||||
|
next_page = request.args.get("next")
|
||||||
|
if not next_page or url_parse(next_page).netloc != "":
|
||||||
|
next_page = url_for("main.index")
|
||||||
|
return redirect(next_page)
|
||||||
|
return render_template("auth/login.html", title=_("Sign In"), form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/logout")
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/create_user", methods=["GET", "POST"])
|
||||||
|
@admin_required
|
||||||
|
def create_user():
|
||||||
|
"Form creating new user"
|
||||||
|
form = UserCreationForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User(username=form.username.data, email=form.email.data)
|
||||||
|
user.set_password(form.password.data)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
flash("User {} created".format(user.username))
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
return render_template(
|
||||||
|
"auth/register.html", title=u"Création utilisateur", form=form
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/reset_password_request", methods=["GET", "POST"])
|
||||||
|
def reset_password_request():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
form = ResetPasswordRequestForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User.query.filter_by(email=form.email.data).first()
|
||||||
|
if user:
|
||||||
|
send_password_reset_email(user)
|
||||||
|
else:
|
||||||
|
current_app.logger.info(
|
||||||
|
"reset_password_request: for unkown user '{}'".format(form.email.data)
|
||||||
|
)
|
||||||
|
flash(_("Check your email for the instructions to reset your password"))
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
return render_template(
|
||||||
|
"auth/reset_password_request.html", title=_("Reset Password"), form=form
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/reset_password/<token>", methods=["GET", "POST"])
|
||||||
|
def reset_password(token):
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
user = User.verify_reset_password_token(token)
|
||||||
|
if not user:
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
form = ResetPasswordForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user.set_password(form.password.data)
|
||||||
|
db.session.commit()
|
||||||
|
flash(_("Your password has been reset."))
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
return render_template("auth/reset_password.html", form=form)
|
7
app/cli.py
Normal file
7
app/cli.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
import os
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
pass
|
206
app/decorators.py
Normal file
206
app/decorators.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
"""Decorators for permissions, roles and ScoDoc7 Zope compatibility
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
from functools import wraps
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import flask
|
||||||
|
from flask import g
|
||||||
|
from flask import abort, current_app
|
||||||
|
from flask import request
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_login import login_required
|
||||||
|
from flask import current_app
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
from app.auth.models import Permission
|
||||||
|
|
||||||
|
|
||||||
|
class ZUser(object):
|
||||||
|
"Emulating Zope User"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"create, based on `flask_login.current_user`"
|
||||||
|
self.username = current_user.username
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
def has_permission(self, perm, context):
|
||||||
|
"""check if this user as the permission `perm`
|
||||||
|
in departement given by `g.scodoc_dept`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class ZRequest(object):
|
||||||
|
"Emulating Zope 2 REQUEST"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.URL = request.base_url
|
||||||
|
self.URL0 = self.URL
|
||||||
|
self.BASE0 = request.url_root
|
||||||
|
self.QUERY_STRING = request.query_string
|
||||||
|
self.REQUEST_METHOD = request.method
|
||||||
|
self.AUTHENTICATED_USER = current_user
|
||||||
|
if request.method == "POST":
|
||||||
|
self.form = request.form
|
||||||
|
if request.files:
|
||||||
|
# Add files in form: must copy to get a mutable version
|
||||||
|
# request.form is a werkzeug.datastructures.ImmutableMultiDict
|
||||||
|
self.form = self.form.copy()
|
||||||
|
self.form.update(request.files)
|
||||||
|
elif request.method == "GET":
|
||||||
|
self.form = request.args
|
||||||
|
self.RESPONSE = ZResponse()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return """REQUEST
|
||||||
|
URL={r.URL}
|
||||||
|
QUERY_STRING={r.QUERY_STRING}
|
||||||
|
REQUEST_METHOD={r.REQUEST_METHOD}
|
||||||
|
AUTHENTICATED_USER={r.AUTHENTICATED_USER}
|
||||||
|
form={r.form}
|
||||||
|
""".format(
|
||||||
|
r=self
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ZResponse(object):
|
||||||
|
"Emulating Zope 2 RESPONSE"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.headers = {}
|
||||||
|
|
||||||
|
def redirect(self, url):
|
||||||
|
return flask.redirect(url) # http 302
|
||||||
|
|
||||||
|
def setHeader(self, header, value):
|
||||||
|
self.headers[header.tolower()] = value
|
||||||
|
|
||||||
|
|
||||||
|
def permission_required(permission):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if "scodoc_dept" in kwargs:
|
||||||
|
g.scodoc_dept = kwargs["scodoc_dept"]
|
||||||
|
del kwargs["scodoc_dept"]
|
||||||
|
current_app.logger.info(
|
||||||
|
"permission_required: %s in %s" % (permission, g.scodoc_dept)
|
||||||
|
)
|
||||||
|
if not current_user.has_permission(permission, g.scodoc_dept):
|
||||||
|
abort(403)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(f):
|
||||||
|
return permission_required(Permission.ScoSuperAdmin)(f)
|
||||||
|
|
||||||
|
|
||||||
|
def scodoc7func(context):
|
||||||
|
"""Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7.
|
||||||
|
Si on a un kwarg `scodoc_dept`(venant de la route), le stocke dans `g.scodoc_dept`.
|
||||||
|
Ajoute l'argument REQUEST s'il est dans la signature de la fonction.
|
||||||
|
Les paramètres de la query string deviennent des (keywords) paramètres de la fonction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def s7_decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def scodoc7func_decorator(*args, **kwargs):
|
||||||
|
"""Decorator allowing legacy Zope published methods to be called via Flask
|
||||||
|
routes without modification.
|
||||||
|
|
||||||
|
There are two cases: the function can be called
|
||||||
|
1. via a Flask route ("top level call")
|
||||||
|
2. or be called directly from Python.
|
||||||
|
|
||||||
|
If called via a route, this decorator setups a REQUEST object (emulating Zope2 REQUEST)
|
||||||
|
and `g.scodoc_dept` if present in the argument (for routes like `/<scodoc_dept>/Scolarite/sco_exemple`).
|
||||||
|
"""
|
||||||
|
assert not args
|
||||||
|
# Détermine si on est appelé via une route ("toplevel")
|
||||||
|
# ou par un appel de fonction python normal.
|
||||||
|
top_level = not hasattr(g, "zrequest")
|
||||||
|
if top_level:
|
||||||
|
g.zrequest = None
|
||||||
|
#
|
||||||
|
if "scodoc_dept" in kwargs:
|
||||||
|
g.scodoc_dept = kwargs["scodoc_dept"]
|
||||||
|
del kwargs["scodoc_dept"]
|
||||||
|
elif not hasattr(g, "scodoc_dept"): # if toplevel call
|
||||||
|
g.scodoc_dept = None
|
||||||
|
# --- Emulate Zope's REQUEST
|
||||||
|
REQUEST = ZRequest()
|
||||||
|
g.zrequest = REQUEST
|
||||||
|
req_args = REQUEST.form # args from query string (get) or form (post)
|
||||||
|
# --- Add positional arguments
|
||||||
|
pos_arg_values = []
|
||||||
|
# PY3 à remplacer par inspect.getfullargspec en py3:
|
||||||
|
argspec = inspect.getargspec(func)
|
||||||
|
current_app.logger.info("argspec=%s" % str(argspec))
|
||||||
|
nb_default_args = len(argspec.defaults) if argspec.defaults else 0
|
||||||
|
if nb_default_args:
|
||||||
|
arg_names = argspec.args[:-nb_default_args]
|
||||||
|
else:
|
||||||
|
arg_names = argspec.args
|
||||||
|
for arg_name in arg_names:
|
||||||
|
if arg_name == "REQUEST": # special case
|
||||||
|
pos_arg_values.append(REQUEST)
|
||||||
|
elif arg_name == "context":
|
||||||
|
pos_arg_values.append(context)
|
||||||
|
else:
|
||||||
|
pos_arg_values.append(req_args[arg_name])
|
||||||
|
current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
||||||
|
# Add keyword arguments
|
||||||
|
if nb_default_args:
|
||||||
|
for arg_name in argspec.args[-nb_default_args:]:
|
||||||
|
if arg_name == "REQUEST": # special case
|
||||||
|
kwargs[arg_name] = REQUEST
|
||||||
|
elif arg_name in req_args:
|
||||||
|
# set argument kw optionnel
|
||||||
|
kwargs[arg_name] = req_args[arg_name]
|
||||||
|
current_app.logger.info(
|
||||||
|
"scodoc7func_decorator: top_level=%s, pos_arg_values=%s, kwargs=%s"
|
||||||
|
% (top_level, pos_arg_values, kwargs)
|
||||||
|
)
|
||||||
|
value = func(*pos_arg_values, **kwargs)
|
||||||
|
|
||||||
|
if not top_level:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
# Build response, adding collected http headers:
|
||||||
|
headers = []
|
||||||
|
kw = {"response": value, "status": 200}
|
||||||
|
if g.zrequest:
|
||||||
|
headers = g.zrequest.RESPONSE.headers
|
||||||
|
if not headers:
|
||||||
|
# no customized header, speedup:
|
||||||
|
return value
|
||||||
|
if "content-type" in headers:
|
||||||
|
kw["mimetype"] = headers["content-type"]
|
||||||
|
r = flask.Response(**kw)
|
||||||
|
for h in headers:
|
||||||
|
r.headers[h] = headers[h]
|
||||||
|
return r
|
||||||
|
|
||||||
|
return scodoc7func_decorator
|
||||||
|
|
||||||
|
return s7_decorator
|
||||||
|
|
||||||
|
|
||||||
|
# Le "context" de ScoDoc7
|
||||||
|
class ScoDoc7Context(object):
|
||||||
|
"""Context object for legacy Zope methods.
|
||||||
|
Mainly used to call published methods, as context.function(...)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, globals_dict):
|
||||||
|
self.__dict__ = globals_dict
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "ScoDoc7Context()"
|
19
app/email.py
Normal file
19
app/email.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
from threading import Thread
|
||||||
|
from flask import current_app
|
||||||
|
from flask_mail import Message
|
||||||
|
from app import mail
|
||||||
|
|
||||||
|
|
||||||
|
def send_async_email(app, msg):
|
||||||
|
with app.app_context():
|
||||||
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(subject, sender, recipients, text_body, html_body):
|
||||||
|
msg = Message(subject, sender=sender, recipients=recipients)
|
||||||
|
msg.body = text_body
|
||||||
|
msg.html = html_body
|
||||||
|
Thread(
|
||||||
|
target=send_async_email, args=(current_app._get_current_object(), msg)
|
||||||
|
).start()
|
8
app/main/README.md
Normal file
8
app/main/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# main Blueprint
|
||||||
|
|
||||||
|
Quelques essais pour la migration.
|
||||||
|
|
||||||
|
TODO: Ne sera pas conservé.
|
||||||
|
|
||||||
|
|
||||||
|
|
6
app/main/__init__.py
Normal file
6
app/main/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint("main", __name__)
|
||||||
|
|
||||||
|
from app.main import routes
|
145
app/main/routes.py
Normal file
145
app/main/routes.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
import pprint
|
||||||
|
from pprint import pprint as pp
|
||||||
|
import functools
|
||||||
|
import thread # essai
|
||||||
|
from zipfile import ZipFile
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
import flask
|
||||||
|
from flask import request, render_template, redirect
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from app.main import bp
|
||||||
|
|
||||||
|
from app.decorators import scodoc7func, admin_required
|
||||||
|
|
||||||
|
context = None
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/")
|
||||||
|
@bp.route("/index")
|
||||||
|
def index():
|
||||||
|
return render_template("main/index.html", title=u"Essai Flask")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/test_vue")
|
||||||
|
@login_required
|
||||||
|
def test_vue():
|
||||||
|
return """Vous avez vu. <a href="/">Retour à l'accueil</a>"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_infos():
|
||||||
|
return [
|
||||||
|
"<p>request.base_url=%s</p>" % request.base_url,
|
||||||
|
"<p>request.url_root=%s</p>" % request.url_root,
|
||||||
|
"<p>request.query_string=%s</p>" % request.query_string,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
D = {"count": 0}
|
||||||
|
|
||||||
|
# @app.route("/")
|
||||||
|
# @app.route("/index")
|
||||||
|
# def index():
|
||||||
|
# sleep(8)
|
||||||
|
# D["count"] = D.get("count", 0) + 1
|
||||||
|
# return "Hello, World! %s count=%s" % (thread.get_ident(), D["count"])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/zopefunction", methods=["POST", "GET"])
|
||||||
|
@login_required
|
||||||
|
@scodoc7func(context)
|
||||||
|
def a_zope_function(y, x="defaut", REQUEST=None):
|
||||||
|
"""Une fonction typique de ScoDoc7"""
|
||||||
|
H = get_request_infos() + [
|
||||||
|
"<p><b>x=<tt>%s</tt></b></p>" % x,
|
||||||
|
"<p><b>y=<tt>%s</tt></b></p>" % y,
|
||||||
|
"<p><b>URL=<tt>%s</tt></b></p>" % REQUEST.URL,
|
||||||
|
"<p><b>QUERY_STRING=<tt>%s</tt></b></p>" % REQUEST.QUERY_STRING,
|
||||||
|
"<p><b>AUTHENTICATED_USER=<tt>%s</tt></b></p>" % REQUEST.AUTHENTICATED_USER,
|
||||||
|
]
|
||||||
|
H.append("<p><b>form=<tt>%s</tt></b></p>" % REQUEST.form)
|
||||||
|
H.append("<p><b>form[x]=<tt>%s</tt></b></p>" % REQUEST.form.get("x", "non fourni"))
|
||||||
|
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/zopeform_get")
|
||||||
|
@scodoc7func(context)
|
||||||
|
def a_zope_form_get(REQUEST=None):
|
||||||
|
H = [
|
||||||
|
"""<h2>Formulaire GET</h2>
|
||||||
|
<form action="%s" method="get">
|
||||||
|
x : <input type="text" name="x"/><br/>
|
||||||
|
y : <input type="text" name="y"/><br/>
|
||||||
|
fichier : <input type="file" name="fichier"/><br/>
|
||||||
|
<input type="submit" value="Envoyer"/>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
% flask.url_for("main.a_zope_function")
|
||||||
|
]
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/zopeform_post")
|
||||||
|
@scodoc7func(context)
|
||||||
|
def a_zope_form_post(REQUEST=None):
|
||||||
|
H = [
|
||||||
|
"""<h2>Formulaire POST</h2>
|
||||||
|
<form action="%s" method="post" enctype="multipart/form-data">
|
||||||
|
x : <input type="text" name="x"/><br/>
|
||||||
|
y : <input type="text" name="y"/><br/>
|
||||||
|
fichier : <input type="file" name="fichier"/><br/>
|
||||||
|
<input type="submit" value="Envoyer"/>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
% flask.url_for("main.a_zope_function")
|
||||||
|
]
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/ScoDoc/<dept_id>/Scolarite/Notes/formsemestre_status")
|
||||||
|
@scodoc7func(context)
|
||||||
|
def formsemestre_status(dept_id=None, formsemestre_id=None, REQUEST=None):
|
||||||
|
"""Essai méthode de département
|
||||||
|
Le contrôle d'accès doit vérifier les bons rôles : ici Ens<dept_id>
|
||||||
|
"""
|
||||||
|
return u"""dept_id=%s , formsemestre_id=%s <a href="/">Retour à l'accueil</a>""" % (
|
||||||
|
dept_id,
|
||||||
|
formsemestre_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/hello/world")
|
||||||
|
def hello():
|
||||||
|
H = get_request_infos() + [
|
||||||
|
"<p>Hello, World! %s count=%s</p>" % (thread.get_ident(), D["count"]),
|
||||||
|
]
|
||||||
|
# print(pprint.pformat(dir(request)))
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/getzip")
|
||||||
|
def getzip():
|
||||||
|
"""Essai renvoi d'un ZIP en Flask"""
|
||||||
|
# La version Zope:
|
||||||
|
# REQUEST.RESPONSE.setHeader("content-type", "application/zip")
|
||||||
|
# REQUEST.RESPONSE.setHeader("content-length", size)
|
||||||
|
# REQUEST.RESPONSE.setHeader(
|
||||||
|
# "content-disposition", 'attachement; filename="monzip.zip"'
|
||||||
|
# )
|
||||||
|
zipdata = StringIO()
|
||||||
|
zipfile = ZipFile(zipdata, "w")
|
||||||
|
zipfile.writestr("fichier1", "un contenu")
|
||||||
|
zipfile.writestr("fichier2", "deux contenus")
|
||||||
|
zipfile.close()
|
||||||
|
data = zipdata.getvalue()
|
||||||
|
size = len(data)
|
||||||
|
# open("/tmp/toto.zip", "w").write(data)
|
||||||
|
# Flask response:
|
||||||
|
r = flask.Response(response=data, status=200, mimetype="application/zip")
|
||||||
|
r.headers["Content-Type"] = "application/zip"
|
||||||
|
r.headers["content-length"] = size
|
||||||
|
r.headers["content-disposition"] = 'attachement; filename="monzip.zip"'
|
||||||
|
return r
|
7
app/models.py
Normal file
7
app/models.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
|
"""ScoDoc8 models
|
||||||
|
"""
|
||||||
|
|
||||||
|
# None, at this point
|
||||||
|
# see auth.models for user/role related models
|
@ -108,7 +108,7 @@ ADMISSION_MODIFIABLE_FIELDS = (
|
|||||||
def sco_import_format(with_codesemestre=True):
|
def sco_import_format(with_codesemestre=True):
|
||||||
"returns tuples (Attribut, Type, Table, AllowNulls, Description)"
|
"returns tuples (Attribut, Type, Table, AllowNulls, Description)"
|
||||||
r = []
|
r = []
|
||||||
for l in open(scu.SCO_SRCDIR + "/" + FORMAT_FILE):
|
for l in open(scu.SCO_SRC_DIR + "/" + FORMAT_FILE):
|
||||||
l = l.strip()
|
l = l.strip()
|
||||||
if l and l[0] != "#":
|
if l and l[0] != "#":
|
||||||
fs = l.split(";")
|
fs = l.split(";")
|
@ -1,13 +1,14 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "7.24"
|
SCOVERSION = "8.01a"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
SCONEWS = """
|
SCONEWS = """
|
||||||
<h4>Année 2021</h4>
|
<h4>Année 2021</h4>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>Version mobile (en test)</li>
|
||||||
<li>Évaluations de type "deuxième session"</li>
|
<li>Évaluations de type "deuxième session"</li>
|
||||||
<li>Gestion du genre neutre (pas d'affichage de la civilité)</li>
|
<li>Gestion du genre neutre (pas d'affichage de la civilité)</li>
|
||||||
<li>Diverses corrections (PV de jurys, ...)</li>
|
<li>Diverses corrections (PV de jurys, ...)</li>
|
@ -720,7 +720,7 @@ class ZAbsences(
|
|||||||
+ self.sco_footer(REQUEST)
|
+ self.sco_footer(REQUEST)
|
||||||
)
|
)
|
||||||
|
|
||||||
base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % (
|
base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % (
|
||||||
datelundi,
|
datelundi,
|
||||||
groups_infos.groups_query_args,
|
groups_infos.groups_query_args,
|
||||||
urllib.quote(destination),
|
urllib.quote(destination),
|
||||||
@ -904,15 +904,16 @@ class ZAbsences(
|
|||||||
etuds = [e for e in etuds if e["etudid"] in mod_inscrits]
|
etuds = [e for e in etuds if e["etudid"] in mod_inscrits]
|
||||||
if not moduleimpl_id:
|
if not moduleimpl_id:
|
||||||
moduleimpl_id = None
|
moduleimpl_id = None
|
||||||
base_url_noweeks = "SignaleAbsenceGrSemestre?datedebut=%s&datefin=%s&%s&destination=%s" % (
|
base_url_noweeks = (
|
||||||
datedebut,
|
"SignaleAbsenceGrSemestre?datedebut=%s&datefin=%s&%s&destination=%s"
|
||||||
datefin,
|
% (
|
||||||
groups_infos.groups_query_args,
|
datedebut,
|
||||||
urllib.quote(destination),
|
datefin,
|
||||||
|
groups_infos.groups_query_args,
|
||||||
|
urllib.quote(destination),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
base_url = (
|
base_url = base_url_noweeks + "&nbweeks=%s" % nbweeks # sans le moduleimpl_id
|
||||||
base_url_noweeks + "&nbweeks=%s" % nbweeks
|
|
||||||
) # sans le moduleimpl_id
|
|
||||||
|
|
||||||
if etuds:
|
if etuds:
|
||||||
nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id)
|
nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id)
|
||||||
@ -952,9 +953,9 @@ class ZAbsences(
|
|||||||
dates = dates[-nbweeks:]
|
dates = dates[-nbweeks:]
|
||||||
msg = "Montrer toutes les semaines"
|
msg = "Montrer toutes les semaines"
|
||||||
nwl = 0
|
nwl = 0
|
||||||
url_link_semaines = base_url_noweeks + "&nbweeks=%s" % nwl
|
url_link_semaines = base_url_noweeks + "&nbweeks=%s" % nwl
|
||||||
if moduleimpl_id:
|
if moduleimpl_id:
|
||||||
url_link_semaines += "&moduleimpl_id=" + moduleimpl_id
|
url_link_semaines += "&moduleimpl_id=" + moduleimpl_id
|
||||||
#
|
#
|
||||||
dates = [x.ISO() for x in dates]
|
dates = [x.ISO() for x in dates]
|
||||||
dayname = sco_abs.day_names(self)[jourdebut.weekday]
|
dayname = sco_abs.day_names(self)[jourdebut.weekday]
|
||||||
@ -1027,7 +1028,7 @@ class ZAbsences(
|
|||||||
"""<p>
|
"""<p>
|
||||||
Module concerné par ces absences (%(optionel_txt)s):
|
Module concerné par ces absences (%(optionel_txt)s):
|
||||||
<select id="moduleimpl_id" name="moduleimpl_id"
|
<select id="moduleimpl_id" name="moduleimpl_id"
|
||||||
onchange="document.location='%(url)s&moduleimpl_id='+document.getElementById('moduleimpl_id').value">
|
onchange="document.location='%(url)s&moduleimpl_id='+document.getElementById('moduleimpl_id').value">
|
||||||
<option value="" %(sel)s>non spécifié</option>
|
<option value="" %(sel)s>non spécifié</option>
|
||||||
%(menu_module)s
|
%(menu_module)s
|
||||||
</select>
|
</select>
|
||||||
@ -1327,7 +1328,7 @@ class ZAbsences(
|
|||||||
for a in absnonjust:
|
for a in absnonjust:
|
||||||
a["justlink"] = "<em>justifier</em>"
|
a["justlink"] = "<em>justifier</em>"
|
||||||
a["_justlink_target"] = (
|
a["_justlink_target"] = (
|
||||||
"doJustifAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s"
|
"doJustifAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s"
|
||||||
% (etudid, a["datedmy"], a["datedmy"], a["ampm"])
|
% (etudid, a["datedmy"], a["datedmy"], a["ampm"])
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
@ -1463,7 +1464,7 @@ class ZAbsences(
|
|||||||
)
|
)
|
||||||
+ "<p>Période du %s au %s (nombre de <b>demi-journées</b>)<br/>"
|
+ "<p>Période du %s au %s (nombre de <b>demi-journées</b>)<br/>"
|
||||||
% (debut, fin),
|
% (debut, fin),
|
||||||
base_url="%s&formsemestre_id=%s&debut=%s&fin=%s"
|
base_url="%s&formsemestre_id=%s&debut=%s&fin=%s"
|
||||||
% (groups_infos.base_url, formsemestre_id, debut, fin),
|
% (groups_infos.base_url, formsemestre_id, debut, fin),
|
||||||
filename="etat_abs_"
|
filename="etat_abs_"
|
||||||
+ scu.make_filename(
|
+ scu.make_filename(
|
||||||
@ -1700,7 +1701,7 @@ ou entrez une date pour visualiser les absents un jour donné :
|
|||||||
"ProcessBilletAbsenceForm?billet_id=%s" % b["billet_id"]
|
"ProcessBilletAbsenceForm?billet_id=%s" % b["billet_id"]
|
||||||
)
|
)
|
||||||
if etud:
|
if etud:
|
||||||
b["_etat_str_target"] += "&etudid=%s" % etud["etudid"]
|
b["_etat_str_target"] += "&etudid=%s" % etud["etudid"]
|
||||||
b["_billet_id_target"] = b["_etat_str_target"]
|
b["_billet_id_target"] = b["_etat_str_target"]
|
||||||
else:
|
else:
|
||||||
b["etat_str"] = "ok"
|
b["etat_str"] = "ok"
|
@ -55,7 +55,17 @@ from email.MIMEBase import MIMEBase # pylint: disable=no-name-in-module,import-
|
|||||||
from email.Header import Header # pylint: disable=no-name-in-module,import-error
|
from email.Header import Header # pylint: disable=no-name-in-module,import-error
|
||||||
from email import Encoders # pylint: disable=no-name-in-module,import-error
|
from email import Encoders # pylint: disable=no-name-in-module,import-error
|
||||||
|
|
||||||
from sco_zope import * # pylint: disable=unused-wildcard-import
|
from sco_zope import (
|
||||||
|
ObjectManager,
|
||||||
|
PropertyManager,
|
||||||
|
RoleManager,
|
||||||
|
Item,
|
||||||
|
Persistent,
|
||||||
|
Implicit,
|
||||||
|
ClassSecurityInfo,
|
||||||
|
DTMLFile,
|
||||||
|
Globals,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import Products.ZPsycopgDA.DA as ZopeDA
|
import Products.ZPsycopgDA.DA as ZopeDA
|
||||||
@ -504,6 +514,11 @@ class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Imp
|
|||||||
% REQUEST.BASE0
|
% REQUEST.BASE0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Lien expérimental temporaire:
|
||||||
|
H.append(
|
||||||
|
'<p><a href="/ScoDoc/static/mobile">Version mobile (expérimentale, à vos risques et périls)</a></p>'
|
||||||
|
)
|
||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
"""
|
"""
|
||||||
<div id="scodoc_attribution">
|
<div id="scodoc_attribution">
|
||||||
@ -753,7 +768,6 @@ Problème de connexion (identifiant, mot de passe): <em>contacter votre responsa
|
|||||||
% params
|
% params
|
||||||
)
|
)
|
||||||
# display error traceback (? may open a security risk via xss attack ?)
|
# display error traceback (? may open a security risk via xss attack ?)
|
||||||
# log('exc B')
|
|
||||||
params["txt_html"] = self._report_request(REQUEST, fmt="html")
|
params["txt_html"] = self._report_request(REQUEST, fmt="html")
|
||||||
H.append(
|
H.append(
|
||||||
"""<h4 class="scodoc">Zope Traceback (à envoyer par mail à <a href="mailto:%(sco_dev_mail)s">%(sco_dev_mail)s</a>)</h4><div style="background-color: rgb(153,153,204); border: 1px;">
|
"""<h4 class="scodoc">Zope Traceback (à envoyer par mail à <a href="mailto:%(sco_dev_mail)s">%(sco_dev_mail)s</a>)</h4><div style="background-color: rgb(153,153,204); border: 1px;">
|
||||||
@ -827,8 +841,6 @@ REFERER: %(REFERER)s
|
|||||||
Form: %(form)s
|
Form: %(form)s
|
||||||
Origin: %(HTTP_X_FORWARDED_FOR)s
|
Origin: %(HTTP_X_FORWARDED_FOR)s
|
||||||
Agent: %(HTTP_USER_AGENT)s
|
Agent: %(HTTP_USER_AGENT)s
|
||||||
|
|
||||||
subversion: %(svn_version)s
|
|
||||||
"""
|
"""
|
||||||
% params
|
% params
|
||||||
)
|
)
|
@ -261,7 +261,7 @@ class ZScoUsers(
|
|||||||
|
|
||||||
security.declareProtected(ScoUsersAdmin, "user_info")
|
security.declareProtected(ScoUsersAdmin, "user_info")
|
||||||
|
|
||||||
def user_info(self, user_name=None, user=None):
|
def user_info(self, user_name=None, user=None, format=None, REQUEST=None):
|
||||||
"""Donne infos sur l'utilisateur (qui peut ne pas etre dans notre base).
|
"""Donne infos sur l'utilisateur (qui peut ne pas etre dans notre base).
|
||||||
Si user_name est specifie, interroge la BD. Sinon, user doit etre un dict.
|
Si user_name est specifie, interroge la BD. Sinon, user doit etre un dict.
|
||||||
"""
|
"""
|
||||||
@ -322,7 +322,7 @@ class ZScoUsers(
|
|||||||
# nomnoacc est le nom en minuscules sans accents
|
# nomnoacc est le nom en minuscules sans accents
|
||||||
info["nomnoacc"] = scu.suppress_accents(scu.strlower(info["nom"]))
|
info["nomnoacc"] = scu.suppress_accents(scu.strlower(info["nom"]))
|
||||||
|
|
||||||
return info
|
return scu.sendResult(REQUEST, info, name="user", format=format)
|
||||||
|
|
||||||
def _can_handle_passwd(self, authuser, user_name, allow_admindepts=False):
|
def _can_handle_passwd(self, authuser, user_name, allow_admindepts=False):
|
||||||
"""true if authuser can see or change passwd of user_name.
|
"""true if authuser can see or change passwd of user_name.
|
||||||
@ -523,7 +523,7 @@ class ZScoUsers(
|
|||||||
if authuser.has_permission(ScoUsersAdmin, self):
|
if authuser.has_permission(ScoUsersAdmin, self):
|
||||||
H.append(
|
H.append(
|
||||||
"""
|
"""
|
||||||
<li><a class="stdlink" href="create_user_form?user_name=%(user_name)s&edit=1">modifier/déactiver ce compte</a></li>
|
<li><a class="stdlink" href="create_user_form?user_name=%(user_name)s&edit=1">modifier/déactiver ce compte</a></li>
|
||||||
<li><a class="stdlink" href="delete_user_form?user_name=%(user_name)s">supprimer cet utilisateur</a> <em>(à n'utiliser qu'en cas d'erreur !)</em></li>
|
<li><a class="stdlink" href="delete_user_form?user_name=%(user_name)s">supprimer cet utilisateur</a> <em>(à n'utiliser qu'en cas d'erreur !)</em></li>
|
||||||
"""
|
"""
|
||||||
% info[0]
|
% info[0]
|
@ -279,7 +279,7 @@ class ZScolar(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Imp
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<head>
|
<head>
|
||||||
<title>Programme DUT R&T</title>
|
<title>Programme DUT TEST</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||||
<meta name="LANG" content="fr" />
|
<meta name="LANG" content="fr" />
|
||||||
@ -416,11 +416,6 @@ REQUEST.URL0=%s<br/>
|
|||||||
# GESTION DE LA BD
|
# GESTION DE LA BD
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
security.declareProtected(ScoSuperAdmin, "GetDBConnexionString")
|
|
||||||
|
|
||||||
def GetDBConnexionString(self):
|
|
||||||
# should not be published (but used from contained classes via acquisition)
|
|
||||||
return self._db_cnx_string
|
|
||||||
|
|
||||||
security.declareProtected(ScoSuperAdmin, "GetDBConnexion")
|
security.declareProtected(ScoSuperAdmin, "GetDBConnexion")
|
||||||
GetDBConnexion = ndb.GetDBConnexion
|
GetDBConnexion = ndb.GetDBConnexion
|
||||||
@ -467,9 +462,9 @@ REQUEST.URL0=%s<br/>
|
|||||||
H = [
|
H = [
|
||||||
"""<h2>Système de gestion scolarité</h2>
|
"""<h2>Système de gestion scolarité</h2>
|
||||||
<p>© Emmanuel Viennet 1997-2021</p>
|
<p>© Emmanuel Viennet 1997-2021</p>
|
||||||
<p>Version %s (subversion %s)</p>
|
<p>Version %s</p>
|
||||||
"""
|
"""
|
||||||
% (SCOVERSION, scu.get_svn_version(file_path))
|
% (scu.get_scodoc_version())
|
||||||
]
|
]
|
||||||
H.append(
|
H.append(
|
||||||
'<p>Logiciel libre écrit en <a href="http://www.python.org">Python</a>.</p><p>Utilise <a href="http://www.reportlab.org/">ReportLab</a> pour générer les documents PDF, et <a href="http://sourceforge.net/projects/pyexcelerator">pyExcelerator</a> pour le traitement des documents Excel.</p>'
|
'<p>Logiciel libre écrit en <a href="http://www.python.org">Python</a>.</p><p>Utilise <a href="http://www.reportlab.org/">ReportLab</a> pour générer les documents PDF, et <a href="http://sourceforge.net/projects/pyexcelerator">pyExcelerator</a> pour le traitement des documents Excel.</p>'
|
||||||
@ -679,7 +674,7 @@ REQUEST.URL0=%s<br/>
|
|||||||
date = date.next()
|
date = date.next()
|
||||||
FA.append("</select>")
|
FA.append("</select>")
|
||||||
FA.append(
|
FA.append(
|
||||||
'<a href="Absences/EtatAbsencesGr?group_ids=%%(group_id)s&debut=%(date_debut)s&fin=%(date_fin)s">état</a>'
|
'<a href="Absences/EtatAbsencesGr?group_ids=%%(group_id)s&debut=%(date_debut)s&fin=%(date_fin)s">état</a>'
|
||||||
% sem
|
% sem
|
||||||
)
|
)
|
||||||
FA.append("</form></td>")
|
FA.append("</form></td>")
|
||||||
@ -715,8 +710,8 @@ REQUEST.URL0=%s<br/>
|
|||||||
"""<td>
|
"""<td>
|
||||||
<a href="%(url)s/groups_view?group_ids=%(group_id)s">%(label)s</a>
|
<a href="%(url)s/groups_view?group_ids=%(group_id)s">%(label)s</a>
|
||||||
</td><td>
|
</td><td>
|
||||||
(<a href="%(url)s/groups_view?group_ids=%(group_id)s&format=xls">format tableur</a>)
|
(<a href="%(url)s/groups_view?group_ids=%(group_id)s&format=xls">format tableur</a>)
|
||||||
<a href="%(url)s/groups_view?curtab=tab-photos&group_ids=%(group_id)s&etat=I">Photos</a>
|
<a href="%(url)s/groups_view?curtab=tab-photos&group_ids=%(group_id)s&etat=I">Photos</a>
|
||||||
</td>"""
|
</td>"""
|
||||||
% group
|
% group
|
||||||
)
|
)
|
||||||
@ -780,7 +775,9 @@ REQUEST.URL0=%s<br/>
|
|||||||
# -------------------------- INFOS SUR ETUDIANTS --------------------------
|
# -------------------------- INFOS SUR ETUDIANTS --------------------------
|
||||||
security.declareProtected(ScoView, "getEtudInfo")
|
security.declareProtected(ScoView, "getEtudInfo")
|
||||||
|
|
||||||
def getEtudInfo(self, etudid=False, code_nip=False, filled=False, REQUEST=None, format=None):
|
def getEtudInfo(
|
||||||
|
self, etudid=False, code_nip=False, filled=False, REQUEST=None, format=None
|
||||||
|
):
|
||||||
"""infos sur un etudiant pour utilisation en Zope DTML
|
"""infos sur un etudiant pour utilisation en Zope DTML
|
||||||
On peut specifier etudid
|
On peut specifier etudid
|
||||||
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine
|
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine
|
||||||
@ -1174,7 +1171,7 @@ REQUEST.URL0=%s<br/>
|
|||||||
scolars.etud_annotations_delete(cnx, annotation_id)
|
scolars.etud_annotations_delete(cnx, annotation_id)
|
||||||
|
|
||||||
return REQUEST.RESPONSE.redirect(
|
return REQUEST.RESPONSE.redirect(
|
||||||
"ficheEtud?etudid=%s&head_message=Annotation%%20supprimée" % (etudid)
|
"ficheEtud?etudid=%s&head_message=Annotation%%20supprimée" % (etudid)
|
||||||
)
|
)
|
||||||
|
|
||||||
security.declareProtected(ScoEtudChangeAdr, "formChangeCoordonnees")
|
security.declareProtected(ScoEtudChangeAdr, "formChangeCoordonnees")
|
||||||
@ -2776,7 +2773,7 @@ def _simple_error_page(context, msg, DeptId=None):
|
|||||||
H = [context.standard_html_header(context), "<h2>Erreur !</h2>", "<p>", msg, "</p>"]
|
H = [context.standard_html_header(context), "<h2>Erreur !</h2>", "<p>", msg, "</p>"]
|
||||||
if DeptId:
|
if DeptId:
|
||||||
H.append(
|
H.append(
|
||||||
'<p><a href="delete_dept?DeptId=%s&force=1">Supprimer le dossier %s</a>(très recommandé !)</p>'
|
'<p><a href="delete_dept?DeptId=%s&force=1">Supprimer le dossier %s</a>(très recommandé !)</p>'
|
||||||
% (DeptId, DeptId)
|
% (DeptId, DeptId)
|
||||||
)
|
)
|
||||||
H.append(context.standard_html_footer(context))
|
H.append(context.standard_html_footer(context))
|
@ -25,33 +25,6 @@
|
|||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
from ZScolar import ZScolar, manage_addZScolarForm, manage_addZScolar
|
"""ScoDoc core package
|
||||||
|
"""
|
||||||
from ZScoDoc import ZScoDoc, manage_addZScoDoc
|
# from app.scodoc import sco_core
|
||||||
|
|
||||||
# from sco_zope import *
|
|
||||||
# from notes_log import log
|
|
||||||
# log.set_log_directory( INSTANCE_HOME + '/log' )
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
"""initialize the Scolar products"""
|
|
||||||
# called at each startup (context is a ProductContext instance, basically useless)
|
|
||||||
|
|
||||||
# --- ZScolars
|
|
||||||
context.registerClass(
|
|
||||||
ZScolar,
|
|
||||||
constructors=(
|
|
||||||
manage_addZScolarForm, # this is called when someone adds the product
|
|
||||||
manage_addZScolar,
|
|
||||||
),
|
|
||||||
icon="static/icons/sco_icon.png",
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- ZScoDoc
|
|
||||||
context.registerClass(
|
|
||||||
ZScoDoc, constructors=(manage_addZScoDoc,), icon="static/icons/sco_icon.png"
|
|
||||||
)
|
|
@ -45,9 +45,16 @@ def bonus_iutv(notes_sport, coefs, infos=None):
|
|||||||
return bonus
|
return bonus
|
||||||
|
|
||||||
|
|
||||||
def bonus_iut_stdenis(notes_sport, coefs, infos=None):
|
def bonus_direct(notes_sport, coefs, infos=None):
|
||||||
"""Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points.
|
"""Un bonus direct et sans chichis: les points sont directement ajoutés à la moyenne générale.
|
||||||
|
Les coefficients sont ignorés: tous les points de bonus sont sommés.
|
||||||
|
(rappel: la note est ramenée sur 20 avant application).
|
||||||
"""
|
"""
|
||||||
|
return sum(notes_sport)
|
||||||
|
|
||||||
|
|
||||||
|
def bonus_iut_stdenis(notes_sport, coefs, infos=None):
|
||||||
|
"""Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points."""
|
||||||
points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10
|
points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10
|
||||||
bonus = points * 0.05 # ou / 20
|
bonus = points * 0.05 # ou / 20
|
||||||
return min(bonus, 0.5) # bonus limité à 1/2 point
|
return min(bonus, 0.5) # bonus limité à 1/2 point
|
||||||
@ -62,7 +69,7 @@ def bonus_colmar(notes_sport, coefs, infos=None):
|
|||||||
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
||||||
dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à
|
dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à
|
||||||
la moyenne générale du semestre déjà obtenue par l'étudiant.
|
la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# les coefs sont ignorés
|
# les coefs sont ignorés
|
||||||
points = sum([x - 10 for x in notes_sport if x > 10])
|
points = sum([x - 10 for x in notes_sport if x > 10])
|
||||||
@ -73,7 +80,7 @@ def bonus_colmar(notes_sport, coefs, infos=None):
|
|||||||
|
|
||||||
def bonus_iutva(notes_sport, coefs, infos=None):
|
def bonus_iutva(notes_sport, coefs, infos=None):
|
||||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray
|
"""Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
|
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
|
||||||
Si la note est >= 10 et < 12, bonus de 0.1 point
|
Si la note est >= 10 et < 12, bonus de 0.1 point
|
||||||
@ -93,42 +100,13 @@ def bonus_iutva(notes_sport, coefs, infos=None):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
# XXX Inutilisé (mai 2020) ? à confirmer avant suppression XXX
|
|
||||||
# def bonus_iut1grenoble_v0(notes_sport, coefs, infos=None):
|
|
||||||
# """Calcul bonus sport IUT Grenoble sur la moyenne générale
|
|
||||||
#
|
|
||||||
# La note de sport de nos étudiants va de 0 à 5 points.
|
|
||||||
# Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale.
|
|
||||||
# Par exemple : note de sport 2/5 : chaque UE sera augmentée de 2%, ainsi que la moyenne générale.
|
|
||||||
#
|
|
||||||
# Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées.
|
|
||||||
# """
|
|
||||||
# # les coefs sont ignorés
|
|
||||||
# # notes de 0 à 5
|
|
||||||
# points = sum([x for x in notes_sport])
|
|
||||||
# factor = (points / 4.0) / 100.0
|
|
||||||
# bonus = infos["moy"] * factor
|
|
||||||
# # Modifie les moyennes de toutes les UE:
|
|
||||||
# for ue_id in infos["moy_ues"]:
|
|
||||||
# ue_status = infos["moy_ues"][ue_id]
|
|
||||||
# if ue_status["sum_coefs"] > 0:
|
|
||||||
# # modifie moyenne UE ds semestre courant
|
|
||||||
# ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * (1.0 + factor)
|
|
||||||
# if not ue_status["is_capitalized"]:
|
|
||||||
# # si non capitalisee, modifie moyenne prise en compte
|
|
||||||
# ue_status["moy"] = ue_status["cur_moy_ue"]
|
|
||||||
#
|
|
||||||
# # open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
|
|
||||||
# return bonus
|
|
||||||
|
|
||||||
|
|
||||||
def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None):
|
def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None):
|
||||||
"""Calcul bonus sport IUT Grenoble sur la moyenne générale (version 2017)
|
"""Calcul bonus sport IUT Grenoble sur la moyenne générale (version 2017)
|
||||||
|
|
||||||
La note de sport de nos étudiants va de 0 à 5 points.
|
La note de sport de nos étudiants va de 0 à 5 points.
|
||||||
Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale.
|
Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale.
|
||||||
Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%.
|
Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%.
|
||||||
|
|
||||||
Calcul ici du bonus sur moyenne générale
|
Calcul ici du bonus sur moyenne générale
|
||||||
"""
|
"""
|
||||||
# les coefs sont ignorés
|
# les coefs sont ignorés
|
||||||
@ -162,14 +140,14 @@ def bonus_lille(notes_sport, coefs, infos=None):
|
|||||||
def bonus_iutlh(notes_sport, coefs, infos=None):
|
def bonus_iutlh(notes_sport, coefs, infos=None):
|
||||||
"""Calcul bonus sport IUT du Havre sur moyenne générale et UE
|
"""Calcul bonus sport IUT du Havre sur moyenne générale et UE
|
||||||
|
|
||||||
La note de sport de nos étudiants va de 0 à 20 points.
|
La note de sport de nos étudiants va de 0 à 20 points.
|
||||||
m2=m1*(1+0.005*((10-N1)+(10-N2))
|
m2=m1*(1+0.005*((10-N1)+(10-N2))
|
||||||
m2 : Nouvelle moyenne de l'unité d'enseignement si note de sport et/ou de langue supérieure à 10
|
m2 : Nouvelle moyenne de l'unité d'enseignement si note de sport et/ou de langue supérieure à 10
|
||||||
m1 : moyenne de l'unité d'enseignement avant bonification
|
m1 : moyenne de l'unité d'enseignement avant bonification
|
||||||
N1 : note de sport si supérieure à 10
|
N1 : note de sport si supérieure à 10
|
||||||
N2 : note de seconde langue si supérieure à 10
|
N2 : note de seconde langue si supérieure à 10
|
||||||
Par exemple : sport 15/20 et langue 12/20 : chaque UE sera multipliée par 1+0.005*7, ainsi que la moyenne générale.
|
Par exemple : sport 15/20 et langue 12/20 : chaque UE sera multipliée par 1+0.005*7, ainsi que la moyenne générale.
|
||||||
Calcul ici de la moyenne générale et moyennes d'UE non capitalisées.
|
Calcul ici de la moyenne générale et moyennes d'UE non capitalisées.
|
||||||
"""
|
"""
|
||||||
# les coefs sont ignorés
|
# les coefs sont ignorés
|
||||||
points = sum([x - 10 for x in notes_sport if x > 10])
|
points = sum([x - 10 for x in notes_sport if x > 10])
|
||||||
@ -205,8 +183,8 @@ def bonus_tours(notes_sport, coefs, infos=None):
|
|||||||
def bonus_iutr(notes_sport, coefs, infos=None):
|
def bonus_iutr(notes_sport, coefs, infos=None):
|
||||||
"""Calcul du bonus , regle de l'IUT de Roanne (contribuée par Raphael C., nov 2012)
|
"""Calcul du bonus , regle de l'IUT de Roanne (contribuée par Raphael C., nov 2012)
|
||||||
|
|
||||||
Le bonus est compris entre 0 et 0.35 point.
|
Le bonus est compris entre 0 et 0.35 point.
|
||||||
cette procédure modifie la moyenne de chaque UE capitalisable.
|
cette procédure modifie la moyenne de chaque UE capitalisable.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# modifie les moyennes de toutes les UE:
|
# modifie les moyennes de toutes les UE:
|
||||||
@ -260,7 +238,7 @@ def bonus_saint_etienne(notes_sport, coefs, infos=None):
|
|||||||
"""IUT de Saint-Etienne (jan 2014)
|
"""IUT de Saint-Etienne (jan 2014)
|
||||||
Nous avons différents types de bonification
|
Nous avons différents types de bonification
|
||||||
bonfication Sport / Associations
|
bonfication Sport / Associations
|
||||||
coopératives de département / Bureau Des Étudiants
|
coopératives de département / Bureau Des Étudiants
|
||||||
/ engagement citoyen / Langues optionnelles
|
/ engagement citoyen / Langues optionnelles
|
||||||
Nous ajoutons sur le bulletin une bonification qui varie entre 0,1 et 0,3 ou 0,35 pour chaque item
|
Nous ajoutons sur le bulletin une bonification qui varie entre 0,1 et 0,3 ou 0,35 pour chaque item
|
||||||
la bonification totale ne doit pas excéder les 0,6 point.
|
la bonification totale ne doit pas excéder les 0,6 point.
|
||||||
@ -278,9 +256,9 @@ def bonus_saint_etienne(notes_sport, coefs, infos=None):
|
|||||||
|
|
||||||
|
|
||||||
def bonus_iutTarbes(notes_sport, coefs, infos=None):
|
def bonus_iutTarbes(notes_sport, coefs, infos=None):
|
||||||
"""Calcul bonus modules optionnels
|
"""Calcul bonus modules optionnels
|
||||||
(sport, Langues, action sociale, Théâtre), règle IUT Tarbes
|
(sport, Langues, action sociale, Théâtre), règle IUT Tarbes
|
||||||
Les coefficients ne sont pas pris en compte,
|
Les coefficients ne sont pas pris en compte,
|
||||||
seule la meilleure note est prise en compte
|
seule la meilleure note est prise en compte
|
||||||
le 1/30ème des points au-dessus de 10 sur 20 est retenu et s'ajoute à
|
le 1/30ème des points au-dessus de 10 sur 20 est retenu et s'ajoute à
|
||||||
la moyenne générale du semestre déjà obtenue par l'étudiant.
|
la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
@ -408,7 +386,7 @@ def bonus_iutbethune(notes_sport, coefs, infos=None):
|
|||||||
def bonus_demo(notes_sport, coefs, infos=None):
|
def bonus_demo(notes_sport, coefs, infos=None):
|
||||||
"""Fausse fonction "bonus" pour afficher les informations disponibles
|
"""Fausse fonction "bonus" pour afficher les informations disponibles
|
||||||
et aider les développeurs.
|
et aider les développeurs.
|
||||||
Les informations sont placées dans le fichier /tmp/scodoc_bonus.log
|
Les informations sont placées dans le fichier /tmp/scodoc_bonus.log
|
||||||
qui est ECRASE à chaque appel.
|
qui est ECRASE à chaque appel.
|
||||||
*** Ne pas utiliser en production !!! ***
|
*** Ne pas utiliser en production !!! ***
|
||||||
"""
|
"""
|
@ -67,7 +67,11 @@ def go(app, n=0, verbose=True):
|
|||||||
def go_dept(app, dept, verbose=True):
|
def go_dept(app, dept, verbose=True):
|
||||||
objs = app.ScoDoc.objectValues("Folder")
|
objs = app.ScoDoc.objectValues("Folder")
|
||||||
for o in objs:
|
for o in objs:
|
||||||
context = o.Scolarite
|
try:
|
||||||
|
context = o.Scolarite
|
||||||
|
except AttributeError:
|
||||||
|
# ignore other folders, like old "icons"
|
||||||
|
continue
|
||||||
if context.DeptId() == dept:
|
if context.DeptId() == dept:
|
||||||
if verbose:
|
if verbose:
|
||||||
print("context in dept ", context.DeptId())
|
print("context in dept ", context.DeptId())
|
@ -445,14 +445,14 @@ class GenTable:
|
|||||||
if self.base_url:
|
if self.base_url:
|
||||||
if self.xls_link:
|
if self.xls_link:
|
||||||
H.append(
|
H.append(
|
||||||
' <a href="%s&format=xls">%s</a>'
|
' <a href="%s&format=xls">%s</a>'
|
||||||
% (self.base_url, scu.ICON_XLS)
|
% (self.base_url, scu.ICON_XLS)
|
||||||
)
|
)
|
||||||
if self.xls_link and self.pdf_link:
|
if self.xls_link and self.pdf_link:
|
||||||
H.append(" ")
|
H.append(" ")
|
||||||
if self.pdf_link:
|
if self.pdf_link:
|
||||||
H.append(
|
H.append(
|
||||||
' <a href="%s&format=pdf">%s</a>'
|
' <a href="%s&format=pdf">%s</a>'
|
||||||
% (self.base_url, scu.ICON_PDF)
|
% (self.base_url, scu.ICON_PDF)
|
||||||
)
|
)
|
||||||
H.append("</p>")
|
H.append("</p>")
|
@ -8,6 +8,10 @@ import re
|
|||||||
import inspect
|
import inspect
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.base import MIMEBase
|
||||||
|
from email.header import Header
|
||||||
|
|
||||||
from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error
|
from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error
|
||||||
MIMEMultipart,
|
MIMEMultipart,
|
@ -462,11 +462,11 @@ def get_templates_from_distrib(template="avis"):
|
|||||||
|
|
||||||
if template in ["avis", "footer"]:
|
if template in ["avis", "footer"]:
|
||||||
# pas de preference pour le template: utilise fichier du serveur
|
# pas de preference pour le template: utilise fichier du serveur
|
||||||
p = os.path.join(scu.SCO_SRCDIR, pe_local_tmpl)
|
p = os.path.join(scu.SCO_SRC_DIR, pe_local_tmpl)
|
||||||
if os.path.exists(p):
|
if os.path.exists(p):
|
||||||
template_latex = get_code_latex_from_modele(p)
|
template_latex = get_code_latex_from_modele(p)
|
||||||
else:
|
else:
|
||||||
p = os.path.join(scu.SCO_SRCDIR, pe_default_tmpl)
|
p = os.path.join(scu.SCO_SRC_DIR, pe_default_tmpl)
|
||||||
if os.path.exists(p):
|
if os.path.exists(p):
|
||||||
template_latex = get_code_latex_from_modele(p)
|
template_latex = get_code_latex_from_modele(p)
|
||||||
else:
|
else:
|
@ -177,7 +177,7 @@ def add_pe_stuff_to_zip(context, zipfile, ziproot):
|
|||||||
|
|
||||||
Also copy logos
|
Also copy logos
|
||||||
"""
|
"""
|
||||||
PE_AUX_DIR = os.path.join(scu.SCO_SRCDIR, "config/doc_poursuites_etudes")
|
PE_AUX_DIR = os.path.join(scu.SCO_SRC_DIR, "config/doc_poursuites_etudes")
|
||||||
distrib_dir = os.path.join(PE_AUX_DIR, "distrib")
|
distrib_dir = os.path.join(PE_AUX_DIR, "distrib")
|
||||||
distrib_pathnames = list_directory_filenames(
|
distrib_pathnames = list_directory_filenames(
|
||||||
distrib_dir
|
distrib_dir
|
@ -32,13 +32,11 @@
|
|||||||
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error
|
|
||||||
MIMEMultipart,
|
from email.mime.multipart import MIMEMultipart
|
||||||
)
|
from email.mime.text import MIMEText
|
||||||
from email.MIMEText import MIMEText # pylint: disable=no-name-in-module,import-error
|
from email.header import Header
|
||||||
from email.MIMEBase import MIMEBase # pylint: disable=no-name-in-module,import-error
|
|
||||||
from email.Header import Header # pylint: disable=no-name-in-module,import-error
|
|
||||||
from email import Encoders # pylint: disable=no-name-in-module,import-error
|
|
||||||
|
|
||||||
import notesdb as ndb
|
import notesdb as ndb
|
||||||
import sco_utils as scu
|
import sco_utils as scu
|
@ -733,8 +733,8 @@ def ListeAbsEtud(
|
|||||||
etudid, datedebut, with_evals=with_evals, format=format
|
etudid, datedebut, with_evals=with_evals, format=format
|
||||||
)
|
)
|
||||||
if REQUEST:
|
if REQUEST:
|
||||||
base_url_nj = "%s?etudid=%s&absjust_only=0" % (REQUEST.URL0, etudid)
|
base_url_nj = "%s?etudid=%s&absjust_only=0" % (REQUEST.URL0, etudid)
|
||||||
base_url_j = "%s?etudid=%s&absjust_only=1" % (REQUEST.URL0, etudid)
|
base_url_j = "%s?etudid=%s&absjust_only=1" % (REQUEST.URL0, etudid)
|
||||||
else:
|
else:
|
||||||
base_url_nj = base_url_j = ""
|
base_url_nj = base_url_j = ""
|
||||||
tab_absnonjust = GenTable(
|
tab_absnonjust = GenTable(
|
@ -53,6 +53,7 @@ import shutil
|
|||||||
import glob
|
import glob
|
||||||
|
|
||||||
import sco_utils as scu
|
import sco_utils as scu
|
||||||
|
from config import Config
|
||||||
import notesdb as ndb
|
import notesdb as ndb
|
||||||
from notes_log import log
|
from notes_log import log
|
||||||
import sco_formsemestre
|
import sco_formsemestre
|
||||||
@ -71,7 +72,7 @@ from sco_exceptions import (
|
|||||||
|
|
||||||
class BaseArchiver:
|
class BaseArchiver:
|
||||||
def __init__(self, archive_type=""):
|
def __init__(self, archive_type=""):
|
||||||
dirs = [os.environ["INSTANCE_HOME"], "var", "scodoc", "archives"]
|
dirs = [Config.INSTANCE_HOME, "var", "scodoc", "archives"]
|
||||||
if archive_type:
|
if archive_type:
|
||||||
dirs.append(archive_type)
|
dirs.append(archive_type)
|
||||||
self.root = os.path.join(*dirs)
|
self.root = os.path.join(*dirs)
|
||||||
@ -484,7 +485,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
|||||||
|
|
||||||
# submitted or cancelled:
|
# submitted or cancelled:
|
||||||
return REQUEST.RESPONSE.redirect(
|
return REQUEST.RESPONSE.redirect(
|
||||||
"formsemestre_list_archives?formsemestre_id=%s&head_message=%s"
|
"formsemestre_list_archives?formsemestre_id=%s&head_message=%s"
|
||||||
% (formsemestre_id, msg)
|
% (formsemestre_id, msg)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -510,7 +511,7 @@ def formsemestre_list_archives(context, REQUEST, formsemestre_id):
|
|||||||
for a in L:
|
for a in L:
|
||||||
archive_name = PVArchive.get_archive_name(a["archive_id"])
|
archive_name = PVArchive.get_archive_name(a["archive_id"])
|
||||||
H.append(
|
H.append(
|
||||||
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
|
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
|
||||||
% (
|
% (
|
||||||
a["date"].strftime("%d/%m/%Y %H:%M"),
|
a["date"].strftime("%d/%m/%Y %H:%M"),
|
||||||
a["description"],
|
a["description"],
|
||||||
@ -520,7 +521,7 @@ def formsemestre_list_archives(context, REQUEST, formsemestre_id):
|
|||||||
)
|
)
|
||||||
for filename in a["content"]:
|
for filename in a["content"]:
|
||||||
H.append(
|
H.append(
|
||||||
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
|
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
|
||||||
% (formsemestre_id, archive_name, filename, filename)
|
% (formsemestre_id, archive_name, filename, filename)
|
||||||
)
|
)
|
||||||
if not a["content"]:
|
if not a["content"]:
|
||||||
@ -556,7 +557,8 @@ def formsemestre_delete_archive(
|
|||||||
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)
|
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)
|
||||||
|
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"""<h2>Confirmer la suppression de l'archive du %s ?</h2>
|
"""<h2>Confirmer la suppression de l'archive du %s ?</h2>
|
||||||
<p>La suppression sera définitive.</p>"""
|
<p>La suppression sera définitive.</p>"""
|
||||||
% PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
|
% PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
|
||||||
@ -570,4 +572,4 @@ def formsemestre_delete_archive(
|
|||||||
)
|
)
|
||||||
|
|
||||||
PVArchive.delete_archive(archive_id)
|
PVArchive.delete_archive(archive_id)
|
||||||
return REQUEST.RESPONSE.redirect(dest_url + "&head_message=Archive%20supprimée")
|
return REQUEST.RESPONSE.redirect(dest_url + "&head_message=Archive%20supprimée")
|
@ -83,14 +83,14 @@ def etud_list_archives_html(context, REQUEST, etudid):
|
|||||||
)
|
)
|
||||||
for filename in a["content"]:
|
for filename in a["content"]:
|
||||||
H.append(
|
H.append(
|
||||||
"""<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&archive_name=%s&filename=%s">%s</a>"""
|
"""<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&archive_name=%s&filename=%s">%s</a>"""
|
||||||
% (etudid, archive_name, filename, filename)
|
% (etudid, archive_name, filename, filename)
|
||||||
)
|
)
|
||||||
if not a["content"]:
|
if not a["content"]:
|
||||||
H.append("<em>aucun fichier !</em>")
|
H.append("<em>aucun fichier !</em>")
|
||||||
if can_edit:
|
if can_edit:
|
||||||
H.append(
|
H.append(
|
||||||
'<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&archive_name=%s">%s</a></span>'
|
'<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&archive_name=%s">%s</a></span>'
|
||||||
% (etudid, archive_name, delete_icon)
|
% (etudid, archive_name, delete_icon)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -201,7 +201,8 @@ def etud_delete_archive(context, REQUEST, etudid, archive_name, dialog_confirmed
|
|||||||
archive_id = EtudsArchive.get_id_from_name(context, etudid, archive_name)
|
archive_id = EtudsArchive.get_id_from_name(context, etudid, archive_name)
|
||||||
dest_url = "ficheEtud?etudid=%s" % etudid
|
dest_url = "ficheEtud?etudid=%s" % etudid
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"""<h2>Confirmer la suppression des fichiers ?</h2>
|
"""<h2>Confirmer la suppression des fichiers ?</h2>
|
||||||
<p>Fichier associé le %s à l'étudiant %s</p>
|
<p>Fichier associé le %s à l'étudiant %s</p>
|
||||||
<p>La suppression sera définitive.</p>"""
|
<p>La suppression sera définitive.</p>"""
|
||||||
@ -216,7 +217,7 @@ def etud_delete_archive(context, REQUEST, etudid, archive_name, dialog_confirmed
|
|||||||
)
|
)
|
||||||
|
|
||||||
EtudsArchive.delete_archive(archive_id)
|
EtudsArchive.delete_archive(archive_id)
|
||||||
return REQUEST.RESPONSE.redirect(dest_url + "&head_message=Archive%20supprimée")
|
return REQUEST.RESPONSE.redirect(dest_url + "&head_message=Archive%20supprimée")
|
||||||
|
|
||||||
|
|
||||||
def etud_get_archived_file(context, REQUEST, etudid, archive_name, filename):
|
def etud_get_archived_file(context, REQUEST, etudid, archive_name, filename):
|
@ -28,19 +28,17 @@
|
|||||||
"""Génération des bulletins de notes
|
"""Génération des bulletins de notes
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
from types import StringType
|
from types import StringType
|
||||||
import pprint
|
import pprint
|
||||||
import urllib
|
import urllib
|
||||||
from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error
|
|
||||||
MIMEMultipart,
|
|
||||||
)
|
|
||||||
from email.MIMEText import MIMEText # pylint: disable=no-name-in-module,import-error
|
|
||||||
from email.MIMEBase import MIMEBase # pylint: disable=no-name-in-module,import-error
|
|
||||||
from email.Header import Header # pylint: disable=no-name-in-module,import-error
|
|
||||||
from email import Encoders # pylint: disable=no-name-in-module,import-error
|
|
||||||
|
|
||||||
import time
|
|
||||||
import htmlutils
|
import htmlutils
|
||||||
|
import email
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.base import MIMEBase
|
||||||
|
from email.header import Header
|
||||||
|
|
||||||
from reportlab.lib.colors import Color
|
from reportlab.lib.colors import Color
|
||||||
|
|
||||||
import sco_utils as scu
|
import sco_utils as scu
|
||||||
@ -329,7 +327,7 @@ def formsemestre_bulletinetud_dict(
|
|||||||
)
|
)
|
||||||
u[
|
u[
|
||||||
"ue_descr_html"
|
"ue_descr_html"
|
||||||
] = '<a href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s" class="bull_link">%s</a>' % (
|
] = '<a href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s" class="bull_link">%s</a>' % (
|
||||||
sem_origin["formsemestre_id"],
|
sem_origin["formsemestre_id"],
|
||||||
etudid,
|
etudid,
|
||||||
sem_origin["titreannee"],
|
sem_origin["titreannee"],
|
||||||
@ -522,7 +520,7 @@ def _ue_mod_bulletin(context, etudid, formsemestre_id, ue_id, modimpls, nt, vers
|
|||||||
else:
|
else:
|
||||||
e["name"] = e["description"] or "le %s" % e["jour"]
|
e["name"] = e["description"] or "le %s" % e["jour"]
|
||||||
e["target_html"] = (
|
e["target_html"] = (
|
||||||
"evaluation_listenotes?evaluation_id=%s&format=html&tf-submitted=1"
|
"evaluation_listenotes?evaluation_id=%s&format=html&tf-submitted=1"
|
||||||
% e["evaluation_id"]
|
% e["evaluation_id"]
|
||||||
)
|
)
|
||||||
e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
|
e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
|
||||||
@ -571,7 +569,7 @@ def _ue_mod_bulletin(context, etudid, formsemestre_id, ue_id, modimpls, nt, vers
|
|||||||
mod["evaluations_incompletes"].append(e)
|
mod["evaluations_incompletes"].append(e)
|
||||||
e["name"] = (e["description"] or "") + " (%s)" % e["jour"]
|
e["name"] = (e["description"] or "") + " (%s)" % e["jour"]
|
||||||
e["target_html"] = (
|
e["target_html"] = (
|
||||||
"evaluation_listenotes?evaluation_id=%s&format=html&tf-submitted=1"
|
"evaluation_listenotes?evaluation_id=%s&format=html&tf-submitted=1"
|
||||||
% e["evaluation_id"]
|
% e["evaluation_id"]
|
||||||
)
|
)
|
||||||
e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
|
e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
|
||||||
@ -816,7 +814,7 @@ def formsemestre_bulletinetud(
|
|||||||
if sem["modalite"] == "EXT":
|
if sem["modalite"] == "EXT":
|
||||||
R.append(
|
R.append(
|
||||||
"""<p><a
|
"""<p><a
|
||||||
href="formsemestre_ext_edit_ue_validations?formsemestre_id=%s&etudid=%s"
|
href="formsemestre_ext_edit_ue_validations?formsemestre_id=%s&etudid=%s"
|
||||||
class="stdlink">
|
class="stdlink">
|
||||||
Editer les validations d'UE dans ce semestre extérieur
|
Editer les validations d'UE dans ce semestre extérieur
|
||||||
</a></p>"""
|
</a></p>"""
|
||||||
@ -1009,7 +1007,7 @@ def mail_bulletin(context, formsemestre_id, I, pdfdata, filename, recipient_addr
|
|||||||
att = MIMEBase("application", "pdf")
|
att = MIMEBase("application", "pdf")
|
||||||
att.add_header("Content-Disposition", "attachment", filename=filename)
|
att.add_header("Content-Disposition", "attachment", filename=filename)
|
||||||
att.set_payload(pdfdata)
|
att.set_payload(pdfdata)
|
||||||
Encoders.encode_base64(att)
|
email.encoders.encode_base64(att)
|
||||||
msg.attach(att)
|
msg.attach(att)
|
||||||
log("mail bulletin a %s" % msg["To"])
|
log("mail bulletin a %s" % msg["To"])
|
||||||
context.sendEmail(msg)
|
context.sendEmail(msg)
|
||||||
@ -1076,7 +1074,7 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
menuBul = [
|
menuBul = [
|
||||||
{
|
{
|
||||||
"title": "Réglages bulletins",
|
"title": "Réglages bulletins",
|
||||||
"url": "formsemestre_edit_options?formsemestre_id=%s&target_url=%s"
|
"url": "formsemestre_edit_options?formsemestre_id=%s&target_url=%s"
|
||||||
% (formsemestre_id, qurl),
|
% (formsemestre_id, qurl),
|
||||||
"enabled": (uid in sem["responsables"])
|
"enabled": (uid in sem["responsables"])
|
||||||
or authuser.has_permission(ScoImplement, context),
|
or authuser.has_permission(ScoImplement, context),
|
||||||
@ -1087,13 +1085,13 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
context, formsemestre_id
|
context, formsemestre_id
|
||||||
),
|
),
|
||||||
"url": url
|
"url": url
|
||||||
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
||||||
% (formsemestre_id, etudid, version),
|
% (formsemestre_id, etudid, version),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Envoi par mail à %s" % etud["email"],
|
"title": "Envoi par mail à %s" % etud["email"],
|
||||||
"url": url
|
"url": url
|
||||||
+ "?formsemestre_id=%s&etudid=%s&format=pdfmail&version=%s"
|
+ "?formsemestre_id=%s&etudid=%s&format=pdfmail&version=%s"
|
||||||
% (formsemestre_id, etudid, version),
|
% (formsemestre_id, etudid, version),
|
||||||
"enabled": etud["email"]
|
"enabled": etud["email"]
|
||||||
and can_send_bulletin_by_mail(
|
and can_send_bulletin_by_mail(
|
||||||
@ -1103,7 +1101,7 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
{
|
{
|
||||||
"title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
|
"title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
|
||||||
"url": url
|
"url": url
|
||||||
+ "?formsemestre_id=%s&etudid=%s&format=pdfmail&version=%s&prefer_mail_perso=1"
|
+ "?formsemestre_id=%s&etudid=%s&format=pdfmail&version=%s&prefer_mail_perso=1"
|
||||||
% (formsemestre_id, etudid, version),
|
% (formsemestre_id, etudid, version),
|
||||||
"enabled": etud["emailperso"]
|
"enabled": etud["emailperso"]
|
||||||
and can_send_bulletin_by_mail(
|
and can_send_bulletin_by_mail(
|
||||||
@ -1113,12 +1111,12 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
{
|
{
|
||||||
"title": "Version XML",
|
"title": "Version XML",
|
||||||
"url": url
|
"url": url
|
||||||
+ "?formsemestre_id=%s&etudid=%s&format=xml&version=%s"
|
+ "?formsemestre_id=%s&etudid=%s&format=xml&version=%s"
|
||||||
% (formsemestre_id, etudid, version),
|
% (formsemestre_id, etudid, version),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Ajouter une appréciation",
|
"title": "Ajouter une appréciation",
|
||||||
"url": "appreciation_add_form?etudid=%s&formsemestre_id=%s"
|
"url": "appreciation_add_form?etudid=%s&formsemestre_id=%s"
|
||||||
% (etudid, formsemestre_id),
|
% (etudid, formsemestre_id),
|
||||||
"enabled": (
|
"enabled": (
|
||||||
(authuser in sem["responsables"])
|
(authuser in sem["responsables"])
|
||||||
@ -1127,31 +1125,31 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Enregistrer un semestre effectué ailleurs",
|
"title": "Enregistrer un semestre effectué ailleurs",
|
||||||
"url": "formsemestre_ext_create_form?etudid=%s&formsemestre_id=%s"
|
"url": "formsemestre_ext_create_form?etudid=%s&formsemestre_id=%s"
|
||||||
% (etudid, formsemestre_id),
|
% (etudid, formsemestre_id),
|
||||||
"enabled": authuser.has_permission(ScoImplement, context),
|
"enabled": authuser.has_permission(ScoImplement, context),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Enregistrer une validation d'UE antérieure",
|
"title": "Enregistrer une validation d'UE antérieure",
|
||||||
"url": "formsemestre_validate_previous_ue?etudid=%s&formsemestre_id=%s"
|
"url": "formsemestre_validate_previous_ue?etudid=%s&formsemestre_id=%s"
|
||||||
% (etudid, formsemestre_id),
|
% (etudid, formsemestre_id),
|
||||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Enregistrer note d'une UE externe",
|
"title": "Enregistrer note d'une UE externe",
|
||||||
"url": "external_ue_create_form?etudid=%s&formsemestre_id=%s"
|
"url": "external_ue_create_form?etudid=%s&formsemestre_id=%s"
|
||||||
% (etudid, formsemestre_id),
|
% (etudid, formsemestre_id),
|
||||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Entrer décisions jury",
|
"title": "Entrer décisions jury",
|
||||||
"url": "formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s"
|
"url": "formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s"
|
||||||
% (formsemestre_id, etudid),
|
% (formsemestre_id, etudid),
|
||||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Editer PV jury",
|
"title": "Editer PV jury",
|
||||||
"url": "formsemestre_pvjury_pdf?formsemestre_id=%s&etudid=%s"
|
"url": "formsemestre_pvjury_pdf?formsemestre_id=%s&etudid=%s"
|
||||||
% (formsemestre_id, etudid),
|
% (formsemestre_id, etudid),
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
},
|
},
|
||||||
@ -1164,7 +1162,7 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
'<td> <a href="%s">%s</a></td>'
|
'<td> <a href="%s">%s</a></td>'
|
||||||
% (
|
% (
|
||||||
url
|
url
|
||||||
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
||||||
% (formsemestre_id, etudid, version),
|
% (formsemestre_id, etudid, version),
|
||||||
scu.ICON_PDF,
|
scu.ICON_PDF,
|
||||||
)
|
)
|
@ -324,7 +324,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
|||||||
for app in I["appreciations_list"]:
|
for app in I["appreciations_list"]:
|
||||||
if can_edit_app:
|
if can_edit_app:
|
||||||
mlink = (
|
mlink = (
|
||||||
'<a class="stdlink" href="appreciation_add_form?id=%s">modifier</a> <a class="stdlink" href="appreciation_add_form?id=%s&suppress=1">supprimer</a>'
|
'<a class="stdlink" href="appreciation_add_form?id=%s">modifier</a> <a class="stdlink" href="appreciation_add_form?id=%s&suppress=1">supprimer</a>'
|
||||||
% (app["id"], app["id"])
|
% (app["id"], app["id"])
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -335,7 +335,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
|||||||
)
|
)
|
||||||
if can_edit_app:
|
if can_edit_app:
|
||||||
H.append(
|
H.append(
|
||||||
'<p><a class="stdlink" href="appreciation_add_form?etudid=%(etudid)s&formsemestre_id=%(formsemestre_id)s">Ajouter une appréciation</a></p>'
|
'<p><a class="stdlink" href="appreciation_add_form?etudid=%(etudid)s&formsemestre_id=%(formsemestre_id)s">Ajouter une appréciation</a></p>'
|
||||||
% self.infos
|
% self.infos
|
||||||
)
|
)
|
||||||
H.append("</div>")
|
H.append("</div>")
|
@ -159,7 +159,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
for app in self.infos["appreciations_list"]:
|
for app in self.infos["appreciations_list"]:
|
||||||
if can_edit_app:
|
if can_edit_app:
|
||||||
mlink = (
|
mlink = (
|
||||||
'<a class="stdlink" href="appreciation_add_form?id=%s">modifier</a> <a class="stdlink" href="appreciation_add_form?id=%s&suppress=1">supprimer</a>'
|
'<a class="stdlink" href="appreciation_add_form?id=%s">modifier</a> <a class="stdlink" href="appreciation_add_form?id=%s&suppress=1">supprimer</a>'
|
||||||
% (app["id"], app["id"])
|
% (app["id"], app["id"])
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -170,7 +170,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
)
|
)
|
||||||
if can_edit_app:
|
if can_edit_app:
|
||||||
H.append(
|
H.append(
|
||||||
'<p><a class="stdlink" href="appreciation_add_form?etudid=%(etudid)s&formsemestre_id=%(formsemestre_id)s">Ajouter une appréciation</a></p>'
|
'<p><a class="stdlink" href="appreciation_add_form?etudid=%(etudid)s&formsemestre_id=%(formsemestre_id)s">Ajouter une appréciation</a></p>'
|
||||||
% self.infos
|
% self.infos
|
||||||
)
|
)
|
||||||
H.append("</div>")
|
H.append("</div>")
|
@ -116,7 +116,8 @@ CODES_EXPL = {
|
|||||||
RAT: "En attente d'un rattrapage",
|
RAT: "En attente d'un rattrapage",
|
||||||
DEF: "Défaillant",
|
DEF: "Défaillant",
|
||||||
}
|
}
|
||||||
# Nota: ces explications sont personnalisables via le fichier de config scodoc_config.py
|
# Nota: ces explications sont personnalisables via le fichier
|
||||||
|
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
|
||||||
# variable: CONFIG.CODES_EXP
|
# variable: CONFIG.CODES_EXP
|
||||||
|
|
||||||
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
|
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
|
109
app/scodoc/sco_config.py
Normal file
109
app/scodoc/sco_config.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""Configuration de ScoDoc (version 2020)
|
||||||
|
NE PAS MODIFIER localement ce fichier !
|
||||||
|
mais éditer /opt/scodoc/var/scodoc/config/scodoc_local.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from attrdict import AttrDict
|
||||||
|
import bonus_sport
|
||||||
|
|
||||||
|
CONFIG = AttrDict()
|
||||||
|
|
||||||
|
# set to 1 if you want to require INE:
|
||||||
|
CONFIG.always_require_ine = 0
|
||||||
|
|
||||||
|
# The base URL, use only if you are behind a proxy
|
||||||
|
# eg "https://scodoc.example.net/ScoDoc"
|
||||||
|
CONFIG.ABSOLUTE_URL = ""
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# -------------- Documents PDF
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
|
# Taille du l'image logo: largeur/hauteur (ne pas oublier le . !!!)
|
||||||
|
# W/H XXX provisoire: utilisera PIL pour connaitre la taille de l'image
|
||||||
|
CONFIG.LOGO_FOOTER_ASPECT = 326 / 96.0
|
||||||
|
# Taille dans le document en millimetres
|
||||||
|
CONFIG.LOGO_FOOTER_HEIGHT = 10
|
||||||
|
# Proportions logo (donné ici pour IUTV)
|
||||||
|
CONFIG.LOGO_HEADER_ASPECT = 549 / 346.0
|
||||||
|
# Taille verticale dans le document en millimetres
|
||||||
|
CONFIG.LOGO_HEADER_HEIGHT = 28
|
||||||
|
|
||||||
|
|
||||||
|
# Pied de page PDF : un format Python, %(xxx)s est remplacé par la variable xxx.
|
||||||
|
# Les variables définies sont:
|
||||||
|
# day : Day of the month as a decimal number [01,31]
|
||||||
|
# month : Month as a decimal number [01,12].
|
||||||
|
# year : Year without century as a decimal number [00,99].
|
||||||
|
# Year : Year with century as a decimal number.
|
||||||
|
# hour : Hour (24-hour clock) as a decimal number [00,23].
|
||||||
|
# minute: Minute as a decimal number [00,59].
|
||||||
|
#
|
||||||
|
# server_url: URL du serveur ScoDoc
|
||||||
|
# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir VERSION.py)
|
||||||
|
CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s"
|
||||||
|
|
||||||
|
#
|
||||||
|
# ------------- Calcul bonus modules optionnels (sport, culture...) -------------
|
||||||
|
#
|
||||||
|
|
||||||
|
CONFIG.compute_bonus = bonus_sport.bonus_iutv
|
||||||
|
# Mettre "bonus_demo" pour logguer des informations utiles au developpement...
|
||||||
|
|
||||||
|
# ------------- Capitalisation des UEs -------------
|
||||||
|
# Deux écoles:
|
||||||
|
# - règle "DUT": capitalisation des UE obtenues avec moyenne UE >= 10 ET de toutes les UE
|
||||||
|
# des semestres validés (ADM, ADC, AJ). (conforme à l'arrêté d'août 2005)
|
||||||
|
#
|
||||||
|
# - règle "LMD": capitalisation uniquement des UE avec moy. > 10
|
||||||
|
|
||||||
|
# Si vrai, capitalise toutes les UE des semestres validés (règle "DUT").
|
||||||
|
# CONFIG.CAPITALIZE_ALL_UES = True
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# -------------- Personnalisation des pages
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# Nom (chemin complet) d'un fichier .html à inclure juste après le <body>
|
||||||
|
# le <body> des pages ScoDoc
|
||||||
|
CONFIG.CUSTOM_HTML_HEADER = ""
|
||||||
|
|
||||||
|
# Fichier html a inclure en fin des pages (juste avant le </body>)
|
||||||
|
CONFIG.CUSTOM_HTML_FOOTER = ""
|
||||||
|
|
||||||
|
# Fichier .html à inclure dans la pages connexion/déconnexion (accueil)
|
||||||
|
# si on veut que ce soit différent (par défaut la même chose)
|
||||||
|
CONFIG.CUSTOM_HTML_HEADER_CNX = CONFIG.CUSTOM_HTML_HEADER
|
||||||
|
CONFIG.CUSTOM_HTML_FOOTER_CNX = CONFIG.CUSTOM_HTML_FOOTER
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# -------------- Noms de Lycées
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
|
# Fichier de correspondance codelycee -> noms
|
||||||
|
# (chemin relatif au repertoire d'install des sources)
|
||||||
|
CONFIG.ETABL_FILENAME = "config/etablissements.csv"
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# -------------- Divers:
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# True for UCAC (étudiants camerounais sans prénoms)
|
||||||
|
CONFIG.ALLOW_NULL_PRENOM = False
|
||||||
|
|
||||||
|
# Taille max des fichiers archive etudiants (en octets)
|
||||||
|
# CONFIG.ETUD_MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
# Si pas de photo et portail, publie l'url (était vrai jusqu'en oct 2016)
|
||||||
|
CONFIG.PUBLISH_PORTAL_PHOTO_URL = False
|
||||||
|
|
||||||
|
# Si > 0: longueur minimale requise des nouveaux mots de passe
|
||||||
|
# (le test cracklib.FascistCheck s'appliquera dans tous les cas)
|
||||||
|
CONFIG.MIN_PASSWORD_LENGTH = 0
|
||||||
|
|
||||||
|
# Ce dictionnaire est fusionné à celui de sco_codes_parcours
|
||||||
|
# pour définir les codes jury et explications associées
|
||||||
|
CONFIG.CODES_EXPL = {
|
||||||
|
# AJ : 'Ajourné (échec)',
|
||||||
|
}
|
43
app/scodoc/sco_config_load.py
Normal file
43
app/scodoc/sco_config_load.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""Chargement de la configuration locale
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from notes_log import log
|
||||||
|
import sco_config
|
||||||
|
|
||||||
|
# scodoc_local defines a CONFIG object
|
||||||
|
# here we check if there is a local config file
|
||||||
|
|
||||||
|
|
||||||
|
def load_local_configuration(scodoc_cfg_dir):
|
||||||
|
"""Load local configuration file (if exists)
|
||||||
|
and merge it with CONFIG.
|
||||||
|
"""
|
||||||
|
# this path should be synced with upgrade.sh
|
||||||
|
LOCAL_CONFIG_FILENAME = os.path.join(scodoc_cfg_dir, "scodoc_local.py")
|
||||||
|
LOCAL_CONFIG = None
|
||||||
|
if os.path.exists(LOCAL_CONFIG_FILENAME):
|
||||||
|
if not scodoc_cfg_dir in sys.path:
|
||||||
|
sys.path.insert(1, scodoc_cfg_dir)
|
||||||
|
try:
|
||||||
|
from scodoc_local import CONFIG as LOCAL_CONFIG
|
||||||
|
|
||||||
|
log("imported %s" % LOCAL_CONFIG_FILENAME)
|
||||||
|
except ImportError:
|
||||||
|
log("Error: can't import %s" % LOCAL_CONFIG_FILENAME)
|
||||||
|
del sys.path[1]
|
||||||
|
if LOCAL_CONFIG is None:
|
||||||
|
return
|
||||||
|
# Now merges local config in our CONFIG
|
||||||
|
for x in [x for x in dir(LOCAL_CONFIG) if x[0] != "_"]:
|
||||||
|
v = getattr(LOCAL_CONFIG, x)
|
||||||
|
if not v in sco_config.CONFIG:
|
||||||
|
log("Warning: local config setting unused parameter %s (skipped)" % x)
|
||||||
|
else:
|
||||||
|
if v != sco_config.CONFIG[x]:
|
||||||
|
log("Setting parameter %s from %s" % (x, LOCAL_CONFIG_FILENAME))
|
||||||
|
sco_config.CONFIG[x] = v
|
19
app/scodoc/sco_core.py
Normal file
19
app/scodoc/sco_core.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""essai: ceci serait un module scodoc/sco_xxx.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import types
|
||||||
|
|
||||||
|
import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
def sco_get_version(context, REQUEST=None):
|
||||||
|
"""Une fonction typique de ScoDoc7"""
|
||||||
|
return """<html><body><p>%s</p></body></html>""" % scu.SCOVERSION
|
||||||
|
|
||||||
|
|
||||||
|
def test_refactor(context, x=1):
|
||||||
|
x = context.toto()
|
||||||
|
y = ("context=" + context.module_is_locked("alpha")) + "23"
|
@ -194,7 +194,7 @@ def formsemestre_estim_cost(
|
|||||||
)
|
)
|
||||||
tab.html_before_table = h
|
tab.html_before_table = h
|
||||||
tab.base_url = (
|
tab.base_url = (
|
||||||
"%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s"
|
"%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s"
|
||||||
% (REQUEST.URL0, formsemestre_id, n_group_td, n_group_tp, coef_tp)
|
% (REQUEST.URL0, formsemestre_id, n_group_td, n_group_tp, coef_tp)
|
||||||
)
|
)
|
||||||
|
|
@ -51,14 +51,10 @@ import fcntl
|
|||||||
import subprocess
|
import subprocess
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error
|
from email.mime.multipart import MIMEMultipart
|
||||||
MIMEMultipart,
|
from email.mime.text import MIMEText
|
||||||
)
|
from email.mime.base import MIMEBase
|
||||||
from email.MIMEText import MIMEText # pylint: disable=no-name-in-module,import-error
|
from email.header import Header
|
||||||
from email.MIMEBase import MIMEBase # pylint: disable=no-name-in-module,import-error
|
|
||||||
from email.Header import Header # pylint: disable=no-name-in-module,import-error
|
|
||||||
from email import Encoders # pylint: disable=no-name-in-module,import-error
|
|
||||||
|
|
||||||
|
|
||||||
import notesdb as ndb
|
import notesdb as ndb
|
||||||
import sco_utils as scu
|
import sco_utils as scu
|
||||||
@ -122,8 +118,7 @@ def sco_dump_and_send_db(context, REQUEST=None):
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Drop anonymized database
|
# Drop anonymized database
|
||||||
_drop_ano_db(ano_db_name)
|
# XXX _drop_ano_db(ano_db_name)
|
||||||
|
|
||||||
# Remove lock
|
# Remove lock
|
||||||
fcntl.flock(x, fcntl.LOCK_UN)
|
fcntl.flock(x, fcntl.LOCK_UN)
|
||||||
|
|
||||||
@ -158,7 +153,7 @@ def _duplicate_db(db_name, ano_db_name):
|
|||||||
|
|
||||||
def _anonymize_db(ano_db_name):
|
def _anonymize_db(ano_db_name):
|
||||||
"""Anonymize a departement database"""
|
"""Anonymize a departement database"""
|
||||||
cmd = os.path.join(scu.SCO_CONFIG_DIR, "anonymize_db.py")
|
cmd = os.path.join(scu.SCO_TOOLS_DIR, "anonymize_db.py")
|
||||||
log("_anonymize_db: {}".format(cmd))
|
log("_anonymize_db: {}".format(cmd))
|
||||||
try:
|
try:
|
||||||
_ = subprocess.check_output([cmd, ano_db_name])
|
_ = subprocess.check_output([cmd, ano_db_name])
|
||||||
@ -200,7 +195,7 @@ def _send_db(context, REQUEST, ano_db_name):
|
|||||||
"nomcomplet"
|
"nomcomplet"
|
||||||
],
|
],
|
||||||
"sco_version": scu.SCOVERSION,
|
"sco_version": scu.SCOVERSION,
|
||||||
"sco_subversion": scu.get_svn_version(scu.SCO_CONFIG_DIR),
|
"sco_fullversion": scu.get_scodoc_version(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return r
|
return r
|
@ -65,7 +65,8 @@ def formation_delete(context, formation_id=None, dialog_confirmed=False, REQUEST
|
|||||||
H.append('</ul><p><a href="%s">Revenir</a></p>' % context.NotesURL())
|
H.append('</ul><p><a href="%s">Revenir</a></p>' % context.NotesURL())
|
||||||
else:
|
else:
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"""<h2>Confirmer la suppression de la formation %(titre)s (%(acronyme)s) ?</h2>
|
"""<h2>Confirmer la suppression de la formation %(titre)s (%(acronyme)s) ?</h2>
|
||||||
<p><b>Attention:</b> la suppression d'une formation est <b>irréversible</b> et implique la supression de toutes les UE, matières et modules de la formation !
|
<p><b>Attention:</b> la suppression d'une formation est <b>irréversible</b> et implique la supression de toutes les UE, matières et modules de la formation !
|
||||||
</p>
|
</p>
|
@ -267,7 +267,8 @@ def ue_delete(
|
|||||||
ue = ue[0]
|
ue = ue[0]
|
||||||
|
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
|
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
|
||||||
dest_url="",
|
dest_url="",
|
||||||
REQUEST=REQUEST,
|
REQUEST=REQUEST,
|
||||||
@ -435,14 +436,14 @@ Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module
|
|||||||
H.append('<li class="notes_ue_list">')
|
H.append('<li class="notes_ue_list">')
|
||||||
if iue != 0 and editable:
|
if iue != 0 and editable:
|
||||||
H.append(
|
H.append(
|
||||||
'<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
|
'<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
|
||||||
% (UE["ue_id"], arrow_up)
|
% (UE["ue_id"], arrow_up)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append(arrow_none)
|
H.append(arrow_none)
|
||||||
if iue < len(ue_list) - 1 and editable:
|
if iue < len(ue_list) - 1 and editable:
|
||||||
H.append(
|
H.append(
|
||||||
'<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
|
'<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
|
||||||
% (UE["ue_id"], arrow_down)
|
% (UE["ue_id"], arrow_down)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -500,14 +501,14 @@ Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module
|
|||||||
H.append('<span class="notes_module_list_buts">')
|
H.append('<span class="notes_module_list_buts">')
|
||||||
if im != 0 and editable:
|
if im != 0 and editable:
|
||||||
H.append(
|
H.append(
|
||||||
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
|
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
|
||||||
% (Mod["module_id"], arrow_up)
|
% (Mod["module_id"], arrow_up)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append(arrow_none)
|
H.append(arrow_none)
|
||||||
if im < len(Modlist) - 1 and editable:
|
if im < len(Modlist) - 1 and editable:
|
||||||
H.append(
|
H.append(
|
||||||
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
|
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
|
||||||
% (Mod["module_id"], arrow_down)
|
% (Mod["module_id"], arrow_down)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -620,9 +621,9 @@ Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module
|
|||||||
"""
|
"""
|
||||||
<li><a class="stdlink" href="formation_table_recap?formation_id=%(formation_id)s">Table récapitulative de la formation</a></li>
|
<li><a class="stdlink" href="formation_table_recap?formation_id=%(formation_id)s">Table récapitulative de la formation</a></li>
|
||||||
|
|
||||||
<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=xml">Export XML de la formation</a> (permet de la sauvegarder pour l'échanger avec un autre site)</li>
|
<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=xml">Export XML de la formation</a> (permet de la sauvegarder pour l'échanger avec un autre site)</li>
|
||||||
|
|
||||||
<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=json">Export JSON de la formation</a></li>
|
<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=json">Export JSON de la formation</a></li>
|
||||||
|
|
||||||
<li><a class="stdlink" href="module_list?formation_id=%(formation_id)s">Liste détaillée des modules de la formation</a> (debug) </li>
|
<li><a class="stdlink" href="module_list?formation_id=%(formation_id)s">Liste détaillée des modules de la formation</a> (debug) </li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -646,7 +647,7 @@ Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module
|
|||||||
H.append(" [verrouillé]")
|
H.append(" [verrouillé]")
|
||||||
else:
|
else:
|
||||||
H.append(
|
H.append(
|
||||||
' <a class="stdlink" href="formsemestre_editwithmodules?formation_id=%(formation_id)s&formsemestre_id=%(formsemestre_id)s">Modifier</a>'
|
' <a class="stdlink" href="formsemestre_editwithmodules?formation_id=%(formation_id)s&formsemestre_id=%(formsemestre_id)s">Modifier</a>'
|
||||||
% sem
|
% sem
|
||||||
)
|
)
|
||||||
H.append("</li>")
|
H.append("</li>")
|
||||||
@ -655,7 +656,7 @@ Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module
|
|||||||
if authuser.has_permission(ScoImplement, context):
|
if authuser.has_permission(ScoImplement, context):
|
||||||
H.append(
|
H.append(
|
||||||
"""<ul>
|
"""<ul>
|
||||||
<li><a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">Mettre en place un nouveau semestre de formation %(acronyme)s</a>
|
<li><a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">Mettre en place un nouveau semestre de formation %(acronyme)s</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>"""
|
</ul>"""
|
@ -177,8 +177,8 @@ def apo_semset_maq_status(
|
|||||||
H.append("""<li>Il y a plusieurs années scolaires !</li>""")
|
H.append("""<li>Il y a plusieurs années scolaires !</li>""")
|
||||||
if nips_no_sco: # seulement un warning
|
if nips_no_sco: # seulement un warning
|
||||||
url_list = (
|
url_list = (
|
||||||
"view_apo_etuds?semset_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&nips=%s"
|
"view_apo_etuds?semset_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&nips=%s"
|
||||||
% (semset_id, "&nips=".join(nips_no_sco))
|
% (semset_id, "&nips=".join(nips_no_sco))
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
'<li class="apo_csv_warning">Attention: il y a <a href="%s">%d étudiant(s)</a> dans les maquettes Apogée chargées non inscrit(s) dans ce semestre ScoDoc;</li>'
|
'<li class="apo_csv_warning">Attention: il y a <a href="%s">%d étudiant(s)</a> dans les maquettes Apogée chargées non inscrit(s) dans ce semestre ScoDoc;</li>'
|
||||||
@ -196,8 +196,8 @@ def apo_semset_maq_status(
|
|||||||
|
|
||||||
if nips_no_apo:
|
if nips_no_apo:
|
||||||
url_list = (
|
url_list = (
|
||||||
"view_scodoc_etuds?semset_id=%s&title=Etudiants%%20ScoDoc%%20non%%20listés%%20dans%%20les%%20maquettes%%20Apogée%%20chargées&nips=%s"
|
"view_scodoc_etuds?semset_id=%s&title=Etudiants%%20ScoDoc%%20non%%20listés%%20dans%%20les%%20maquettes%%20Apogée%%20chargées&nips=%s"
|
||||||
% (semset_id, "&nips=".join(nips_no_apo))
|
% (semset_id, "&nips=".join(nips_no_apo))
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
'<li><a href="%s">%d étudiants</a> dans ce semestre non présents dans les maquettes Apogée chargées</li>'
|
'<li><a href="%s">%d étudiants</a> dans ce semestre non présents dans les maquettes Apogée chargées</li>'
|
||||||
@ -206,8 +206,8 @@ def apo_semset_maq_status(
|
|||||||
|
|
||||||
if nips_no_sco: # seulement un warning
|
if nips_no_sco: # seulement un warning
|
||||||
url_list = (
|
url_list = (
|
||||||
"view_apo_etuds?semset_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&nips=%s"
|
"view_apo_etuds?semset_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&nips=%s"
|
||||||
% (semset_id, "&nips=".join(nips_no_sco))
|
% (semset_id, "&nips=".join(nips_no_sco))
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
'<li class="apo_csv_warning">Attention: il reste <a href="%s">%d étudiants</a> dans les maquettes Apogée chargées mais pas inscrits dans ce semestre ScoDoc</li>'
|
'<li class="apo_csv_warning">Attention: il reste <a href="%s">%d étudiants</a> dans les maquettes Apogée chargées mais pas inscrits dans ce semestre ScoDoc</li>'
|
||||||
@ -215,9 +215,9 @@ def apo_semset_maq_status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if apo_dups:
|
if apo_dups:
|
||||||
url_list = (
|
url_list = "view_apo_etuds?semset_id=%s&title=Doublons%%20Apogee&nips=%s" % (
|
||||||
"view_apo_etuds?semset_id=%s&title=Doublons%%20Apogee&nips=%s"
|
semset_id,
|
||||||
% (semset_id, "&nips=".join(apo_dups))
|
"&nips=".join(apo_dups),
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
'<li><a href="%s">%d étudiants</a> présents dans les <em>plusieurs</em> maquettes Apogée chargées</li>'
|
'<li><a href="%s">%d étudiants</a> présents dans les <em>plusieurs</em> maquettes Apogée chargées</li>'
|
||||||
@ -659,7 +659,8 @@ def view_apo_csv_delete(
|
|||||||
semset = sco_semset.SemSet(context, semset_id=semset_id)
|
semset = sco_semset.SemSet(context, semset_id=semset_id)
|
||||||
dest_url = "apo_semset_maq_status?semset_id=" + semset_id
|
dest_url = "apo_semset_maq_status?semset_id=" + semset_id
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
|
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
|
||||||
<p>La suppression sera définitive.</p>"""
|
<p>La suppression sera définitive.</p>"""
|
||||||
% (etape_apo,),
|
% (etape_apo,),
|
||||||
@ -673,7 +674,7 @@ def view_apo_csv_delete(
|
|||||||
context, etape_apo, semset["annee_scolaire"], semset["sem_id"]
|
context, etape_apo, semset["annee_scolaire"], semset["sem_id"]
|
||||||
)
|
)
|
||||||
sco_etape_apogee.apo_csv_delete(context, info["archive_id"])
|
sco_etape_apogee.apo_csv_delete(context, info["archive_id"])
|
||||||
return REQUEST.RESPONSE.redirect(dest_url + "&head_message=Archive%20supprimée")
|
return REQUEST.RESPONSE.redirect(dest_url + "&head_message=Archive%20supprimée")
|
||||||
|
|
||||||
|
|
||||||
def view_apo_csv(context, etape_apo="", semset_id="", format="html", REQUEST=None):
|
def view_apo_csv(context, etape_apo="", semset_id="", format="html", REQUEST=None):
|
@ -66,6 +66,18 @@ class FormatError(ScoValueError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ScoInvalidDept(ScoValueError):
|
||||||
|
"""departement invalide"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ScoConfigurationError(ScoValueError):
|
||||||
|
"""Configuration invalid"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ScoLockedFormError(ScoException):
|
class ScoLockedFormError(ScoException):
|
||||||
def __init__(self, msg="", REQUEST=None):
|
def __init__(self, msg="", REQUEST=None):
|
||||||
msg = (
|
msg = (
|
@ -236,11 +236,11 @@ def scodoc_table_results(
|
|||||||
tab, semlist = _build_results_table(
|
tab, semlist = _build_results_table(
|
||||||
context, start_date_iso, end_date_iso, types_parcours
|
context, start_date_iso, end_date_iso, types_parcours
|
||||||
)
|
)
|
||||||
tab.base_url = "%s?start_date=%s&end_date=%s&types_parcours=%s" % (
|
tab.base_url = "%s?start_date=%s&end_date=%s&types_parcours=%s" % (
|
||||||
REQUEST.URL0,
|
REQUEST.URL0,
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
"&types_parcours=".join([str(x) for x in types_parcours]),
|
"&types_parcours=".join([str(x) for x in types_parcours]),
|
||||||
)
|
)
|
||||||
if format != "html":
|
if format != "html":
|
||||||
return tab.make_page(
|
return tab.make_page(
|
@ -141,7 +141,7 @@ def search_etud_in_dept(context, expnom="", REQUEST=None):
|
|||||||
if len(etuds) > 0:
|
if len(etuds) > 0:
|
||||||
# Choix dans la liste des résultats:
|
# Choix dans la liste des résultats:
|
||||||
for e in etuds:
|
for e in etuds:
|
||||||
target = dest_url + "?etudid=%s&" % e["etudid"]
|
target = dest_url + "?etudid=%s&" % e["etudid"]
|
||||||
e["_nomprenom_target"] = target
|
e["_nomprenom_target"] = target
|
||||||
e["inscription_target"] = target
|
e["inscription_target"] = target
|
||||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
@ -241,7 +241,7 @@ def formation_list_table(context, formation_id=None, args={}, REQUEST=None):
|
|||||||
for s in f["sems"]
|
for s in f["sems"]
|
||||||
]
|
]
|
||||||
+ [
|
+ [
|
||||||
'<a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">ajouter</a>'
|
'<a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">ajouter</a>'
|
||||||
% f
|
% f
|
||||||
]
|
]
|
||||||
)
|
)
|
@ -711,7 +711,7 @@ def do_formsemestre_createwithmodules(context, REQUEST=None, edit=False):
|
|||||||
}
|
}
|
||||||
_ = sco_moduleimpl.do_moduleimpl_create(context, modargs)
|
_ = sco_moduleimpl.do_moduleimpl_create(context, modargs)
|
||||||
return REQUEST.RESPONSE.redirect(
|
return REQUEST.RESPONSE.redirect(
|
||||||
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
|
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
|
||||||
% formsemestre_id
|
% formsemestre_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -811,7 +811,7 @@ def do_formsemestre_createwithmodules(context, REQUEST=None, edit=False):
|
|||||||
return msg_html
|
return msg_html
|
||||||
else:
|
else:
|
||||||
return REQUEST.RESPONSE.redirect(
|
return REQUEST.RESPONSE.redirect(
|
||||||
"formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
|
"formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
|
||||||
% formsemestre_id
|
% formsemestre_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -965,7 +965,7 @@ def formsemestre_clone(context, formsemestre_id, REQUEST=None):
|
|||||||
REQUEST=REQUEST,
|
REQUEST=REQUEST,
|
||||||
)
|
)
|
||||||
return REQUEST.RESPONSE.redirect(
|
return REQUEST.RESPONSE.redirect(
|
||||||
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
|
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
|
||||||
% new_formsemestre_id
|
% new_formsemestre_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1129,7 +1129,8 @@ def formsemestre_associate_new_version(
|
|||||||
% (s["formsemestre_id"], checked, disabled, s["titremois"])
|
% (s["formsemestre_id"], checked, disabled, s["titremois"])
|
||||||
)
|
)
|
||||||
|
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"""<h2>Associer à une nouvelle version de formation non verrouillée ?</h2>
|
"""<h2>Associer à une nouvelle version de formation non verrouillée ?</h2>
|
||||||
<p>Le programme pédagogique ("formation") va être dupliqué pour que vous puissiez le modifier sans affecter les autres semestres. Les autres paramètres (étudiants, notes...) du semestre seront inchangés.</p>
|
<p>Le programme pédagogique ("formation") va être dupliqué pour que vous puissiez le modifier sans affecter les autres semestres. Les autres paramètres (étudiants, notes...) du semestre seront inchangés.</p>
|
||||||
<p>Veillez à ne pas abuser de cette possibilité, car créer trop de versions de formations va vous compliquer la gestion (à vous de garder trace des différences et à ne pas vous tromper par la suite...).
|
<p>Veillez à ne pas abuser de cette possibilité, car créer trop de versions de formations va vous compliquer la gestion (à vous de garder trace des différences et à ne pas vous tromper par la suite...).
|
||||||
@ -1148,7 +1149,7 @@ def formsemestre_associate_new_version(
|
|||||||
context, [formsemestre_id] + other_formsemestre_ids, REQUEST=REQUEST
|
context, [formsemestre_id] + other_formsemestre_ids, REQUEST=REQUEST
|
||||||
)
|
)
|
||||||
return REQUEST.RESPONSE.redirect(
|
return REQUEST.RESPONSE.redirect(
|
||||||
"formsemestre_status?formsemestre_id=%s&head_message=Formation%%20dupliquée"
|
"formsemestre_status?formsemestre_id=%s&head_message=Formation%%20dupliquée"
|
||||||
% formsemestre_id
|
% formsemestre_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1280,7 +1281,8 @@ def formsemestre_delete2(
|
|||||||
"""Delete a formsemestre (confirmation)"""
|
"""Delete a formsemestre (confirmation)"""
|
||||||
# Confirmation dialog
|
# Confirmation dialog
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""",
|
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""",
|
||||||
dest_url="",
|
dest_url="",
|
||||||
REQUEST=REQUEST,
|
REQUEST=REQUEST,
|
||||||
@ -1423,7 +1425,8 @@ def formsemestre_change_lock(
|
|||||||
msg = "déverrouillage"
|
msg = "déverrouillage"
|
||||||
else:
|
else:
|
||||||
msg = "verrouillage"
|
msg = "verrouillage"
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"<h2>Confirmer le %s du semestre ?</h2>" % msg,
|
"<h2>Confirmer le %s du semestre ?</h2>" % msg,
|
||||||
helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
|
helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
|
||||||
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
|
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
|
||||||
@ -1462,7 +1465,8 @@ def formsemestre_change_publication_bul(
|
|||||||
msg = "non"
|
msg = "non"
|
||||||
else:
|
else:
|
||||||
msg = ""
|
msg = ""
|
||||||
return context.confirmDialog(
|
return scu.confirm_dialog(
|
||||||
|
context,
|
||||||
"<h2>Confirmer la %s publication des bulletins ?</h2>" % msg,
|
"<h2>Confirmer la %s publication des bulletins ?</h2>" % msg,
|
||||||
helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
|
helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
|
||||||
par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
|
par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
|
@ -86,7 +86,7 @@ def formsemestre_ext_create_form(context, etudid, formsemestre_id, REQUEST=None)
|
|||||||
<p class="help">
|
<p class="help">
|
||||||
Notez que si un semestre extérieur similaire a déjà été créé pour un autre étudiant,
|
Notez que si un semestre extérieur similaire a déjà été créé pour un autre étudiant,
|
||||||
il est préférable d'utiliser la fonction
|
il est préférable d'utiliser la fonction
|
||||||
"<a href="formsemestre_inscription_with_modules_form?etudid=%s&only_ext=1">
|
"<a href="formsemestre_inscription_with_modules_form?etudid=%s&only_ext=1">
|
||||||
inscrire à un autre semestre</a>"
|
inscrire à un autre semestre</a>"
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
@ -191,7 +191,7 @@ def formsemestre_ext_create_form(context, etudid, formsemestre_id, REQUEST=None)
|
|||||||
return "\n".join(H) + "\n" + tf[1] + F
|
return "\n".join(H) + "\n" + tf[1] + F
|
||||||
elif tf[0] == -1:
|
elif tf[0] == -1:
|
||||||
return REQUEST.RESPONSE.redirect(
|
return REQUEST.RESPONSE.redirect(
|
||||||
"%s/formsemestre_bulletinetud?formsemestre_id==%s&etudid=%s"
|
"%s/formsemestre_bulletinetud?formsemestre_id==%s&etudid=%s"
|
||||||
% (context.ScoURL(), formsemestre_id, etudid)
|
% (context.ScoURL(), formsemestre_id, etudid)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
@ -147,7 +147,7 @@ def formsemestre_inscription_with_modules_form(
|
|||||||
if (not only_ext) or (sem["modalite"] == "EXT"):
|
if (not only_ext) or (sem["modalite"] == "EXT"):
|
||||||
H.append(
|
H.append(
|
||||||
"""
|
"""
|
||||||
<li><a class="stdlink" href="formsemestre_inscription_with_modules?etudid=%s&formsemestre_id=%s">%s</a>
|
<li><a class="stdlink" href="formsemestre_inscription_with_modules?etudid=%s&formsemestre_id=%s">%s</a>
|
||||||
"""
|
"""
|
||||||
% (etudid, sem["formsemestre_id"], sem["titremois"])
|
% (etudid, sem["formsemestre_id"], sem["titremois"])
|
||||||
)
|
)
|
||||||
@ -217,12 +217,12 @@ def formsemestre_inscription_with_modules(
|
|||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
for s in others:
|
for s in others:
|
||||||
H.append(
|
H.append(
|
||||||
'<li><a href="formsemestre_desinscription?formsemestre_id=%s&etudid=%s">déinscrire de %s</li>'
|
'<li><a href="formsemestre_desinscription?formsemestre_id=%s&etudid=%s">déinscrire de %s</li>'
|
||||||
% (s["formsemestre_id"], etudid, s["titreannee"])
|
% (s["formsemestre_id"], etudid, s["titreannee"])
|
||||||
)
|
)
|
||||||
H.append("</ul>")
|
H.append("</ul>")
|
||||||
H.append(
|
H.append(
|
||||||
"""<p><a href="formsemestre_inscription_with_modules?etudid=%s&formsemestre_id=%s&multiple_ok=1&%s">Continuer quand même l'inscription</a></p>"""
|
"""<p><a href="formsemestre_inscription_with_modules?etudid=%s&formsemestre_id=%s&multiple_ok=1&%s">Continuer quand même l'inscription</a></p>"""
|
||||||
% (etudid, formsemestre_id, sco_groups.make_query_groups(group_ids))
|
% (etudid, formsemestre_id, sco_groups.make_query_groups(group_ids))
|
||||||
)
|
)
|
||||||
return "\n".join(H) + F
|
return "\n".join(H) + F
|
||||||
@ -332,7 +332,7 @@ def formsemestre_inscription_option(context, etudid, formsemestre_id, REQUEST=No
|
|||||||
sem_origin = sco_formsemestre.get_formsemestre(
|
sem_origin = sco_formsemestre.get_formsemestre(
|
||||||
context, ue_status["formsemestre_id"]
|
context, ue_status["formsemestre_id"]
|
||||||
)
|
)
|
||||||
ue_descr += ' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)' % (
|
ue_descr += ' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)' % (
|
||||||
sem_origin["formsemestre_id"],
|
sem_origin["formsemestre_id"],
|
||||||
etudid,
|
etudid,
|
||||||
sem_origin["titreannee"],
|
sem_origin["titreannee"],
|
@ -154,7 +154,7 @@ def formsemestre_status_menubar(context, sem, REQUEST):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Modifier le semestre",
|
"title": "Modifier le semestre",
|
||||||
"url": "formsemestre_editwithmodules?formation_id=%(formation_id)s&formsemestre_id=%(formsemestre_id)s"
|
"url": "formsemestre_editwithmodules?formation_id=%(formation_id)s&formsemestre_id=%(formsemestre_id)s"
|
||||||
% sem,
|
% sem,
|
||||||
"enabled": (
|
"enabled": (
|
||||||
authuser.has_permission(ScoImplement, context)
|
authuser.has_permission(ScoImplement, context)
|
||||||
@ -292,7 +292,7 @@ def formsemestre_status_menubar(context, sem, REQUEST):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Exporter table des étudiants",
|
"title": "Exporter table des étudiants",
|
||||||
"url": "groups_view?format=allxls&group_ids="
|
"url": "groups_view?format=allxls&group_ids="
|
||||||
+ sco_groups.get_default_group(
|
+ sco_groups.get_default_group(
|
||||||
context, formsemestre_id, fix_if_missing=True, REQUEST=REQUEST
|
context, formsemestre_id, fix_if_missing=True, REQUEST=REQUEST
|
||||||
),
|
),
|
||||||
@ -388,7 +388,7 @@ def formsemestre_status_menubar(context, sem, REQUEST):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Saisie des décisions du jury",
|
"title": "Saisie des décisions du jury",
|
||||||
"url": "formsemestre_recapcomplet?modejury=1&hidemodules=1&hidebac=1&pref_override=0&formsemestre_id="
|
"url": "formsemestre_recapcomplet?modejury=1&hidemodules=1&hidebac=1&pref_override=0&formsemestre_id="
|
||||||
+ formsemestre_id,
|
+ formsemestre_id,
|
||||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||||
},
|
},
|
||||||
@ -684,7 +684,7 @@ def formsemestre_description_table(
|
|||||||
caption=title,
|
caption=title,
|
||||||
html_caption=title,
|
html_caption=title,
|
||||||
html_class="table_leftalign formsemestre_description",
|
html_class="table_leftalign formsemestre_description",
|
||||||
base_url="%s?formsemestre_id=%s&with_evals=%s"
|
base_url="%s?formsemestre_id=%s&with_evals=%s"
|
||||||
% (REQUEST.URL0, formsemestre_id, with_evals),
|
% (REQUEST.URL0, formsemestre_id, with_evals),
|
||||||
page_title=title,
|
page_title=title,
|
||||||
html_title=context.html_sem_header(
|
html_title=context.html_sem_header(
|
||||||
@ -725,12 +725,130 @@ def formsemestre_lists(context, formsemestre_id, REQUEST=None):
|
|||||||
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
|
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
|
||||||
H = [
|
H = [
|
||||||
context.html_sem_header(REQUEST, "", sem),
|
context.html_sem_header(REQUEST, "", sem),
|
||||||
context.make_listes_sem(sem, REQUEST),
|
_make_listes_sem(context, sem, REQUEST),
|
||||||
context.sco_footer(REQUEST),
|
context.sco_footer(REQUEST),
|
||||||
]
|
]
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
# genere liste html pour accès aux groupes de ce semestre
|
||||||
|
# XXX #sco8 vérifier si c'est encore utilisé !
|
||||||
|
def _make_listes_sem(context, sem, REQUEST=None, with_absences=True):
|
||||||
|
context = context
|
||||||
|
authuser = REQUEST.AUTHENTICATED_USER
|
||||||
|
r = context.ScoURL() # root url
|
||||||
|
# construit l'URL "destination"
|
||||||
|
# (a laquelle on revient apres saisie absences)
|
||||||
|
query_args = cgi.parse_qs(REQUEST.QUERY_STRING)
|
||||||
|
if "head_message" in query_args:
|
||||||
|
del query_args["head_message"]
|
||||||
|
destination = "%s?%s" % (REQUEST.URL, urllib.urlencode(query_args, True))
|
||||||
|
destination = destination.replace(
|
||||||
|
"%", "%%"
|
||||||
|
) # car ici utilisee dans un format string !
|
||||||
|
|
||||||
|
#
|
||||||
|
H = []
|
||||||
|
# pas de menu absences si pas autorise:
|
||||||
|
if with_absences and not authuser.has_permission(ScoAbsChange, context):
|
||||||
|
with_absences = False
|
||||||
|
|
||||||
|
#
|
||||||
|
H.append(
|
||||||
|
'<h3>Listes de %(titre)s <span class="infostitresem">(%(mois_debut)s - %(mois_fin)s)</span></h3>'
|
||||||
|
% sem
|
||||||
|
)
|
||||||
|
|
||||||
|
formsemestre_id = sem["formsemestre_id"]
|
||||||
|
|
||||||
|
# calcule dates 1er jour semaine pour absences
|
||||||
|
try:
|
||||||
|
if with_absences:
|
||||||
|
first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday()
|
||||||
|
FA = [] # formulaire avec menu saisi absences
|
||||||
|
FA.append(
|
||||||
|
'<td><form action="Absences/SignaleAbsenceGrSemestre" method="get">'
|
||||||
|
)
|
||||||
|
FA.append(
|
||||||
|
'<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem
|
||||||
|
)
|
||||||
|
FA.append('<input type="hidden" name="group_ids" value="%(group_id)s"/>')
|
||||||
|
|
||||||
|
FA.append(
|
||||||
|
'<input type="hidden" name="destination" value="%s"/>' % destination
|
||||||
|
)
|
||||||
|
FA.append('<input type="submit" value="Saisir absences du" />')
|
||||||
|
FA.append('<select name="datedebut" class="noprint">')
|
||||||
|
date = first_monday
|
||||||
|
for jour in sco_abs.day_names(context):
|
||||||
|
FA.append('<option value="%s">%s</option>' % (date, jour))
|
||||||
|
date = date.next()
|
||||||
|
FA.append("</select>")
|
||||||
|
FA.append(
|
||||||
|
'<a href="Absences/EtatAbsencesGr?group_ids=%%(group_id)s&debut=%(date_debut)s&fin=%(date_fin)s">état</a>'
|
||||||
|
% sem
|
||||||
|
)
|
||||||
|
FA.append("</form></td>")
|
||||||
|
FormAbs = "\n".join(FA)
|
||||||
|
else:
|
||||||
|
FormAbs = ""
|
||||||
|
except ScoInvalidDateError: # dates incorrectes dans semestres ?
|
||||||
|
FormAbs = ""
|
||||||
|
#
|
||||||
|
H.append('<div id="grouplists">')
|
||||||
|
# Genere liste pour chaque partition (categorie de groupes)
|
||||||
|
for partition in sco_groups.get_partitions_list(context, sem["formsemestre_id"]):
|
||||||
|
if not partition["partition_name"]:
|
||||||
|
H.append("<h4>Tous les étudiants</h4>" % partition)
|
||||||
|
else:
|
||||||
|
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
|
||||||
|
groups = sco_groups.get_partition_groups(context, partition)
|
||||||
|
if groups:
|
||||||
|
H.append("<table>")
|
||||||
|
for group in groups:
|
||||||
|
n_members = len(
|
||||||
|
sco_groups.get_group_members(context, group["group_id"])
|
||||||
|
)
|
||||||
|
group["url"] = r
|
||||||
|
if group["group_name"]:
|
||||||
|
group["label"] = "groupe %(group_name)s" % group
|
||||||
|
else:
|
||||||
|
group["label"] = "liste"
|
||||||
|
H.append('<tr class="listegroupelink">')
|
||||||
|
H.append(
|
||||||
|
"""<td>
|
||||||
|
<a href="%(url)s/groups_view?group_ids=%(group_id)s">%(label)s</a>
|
||||||
|
</td><td>
|
||||||
|
(<a href="%(url)s/groups_view?group_ids=%(group_id)s&format=xls">format tableur</a>)
|
||||||
|
<a href="%(url)s/groups_view?curtab=tab-photos&group_ids=%(group_id)s&etat=I">Photos</a>
|
||||||
|
</td>"""
|
||||||
|
% group
|
||||||
|
)
|
||||||
|
H.append("<td>(%d étudiants)</td>" % n_members)
|
||||||
|
|
||||||
|
if with_absences:
|
||||||
|
H.append(FormAbs % group)
|
||||||
|
|
||||||
|
H.append("</tr>")
|
||||||
|
H.append("</table>")
|
||||||
|
else:
|
||||||
|
H.append('<p class="help indent">Aucun groupe dans cette partition')
|
||||||
|
if sco_groups.can_change_groups(context, REQUEST, formsemestre_id):
|
||||||
|
H.append(
|
||||||
|
' (<a href="affectGroups?partition_id=%s" class="stdlink">créer</a>)'
|
||||||
|
% partition["partition_id"]
|
||||||
|
)
|
||||||
|
H.append("</p>")
|
||||||
|
if sco_groups.can_change_groups(context, REQUEST, formsemestre_id):
|
||||||
|
H.append(
|
||||||
|
'<h4><a href="editPartitionForm?formsemestre_id=%s">Ajouter une partition</a></h4>'
|
||||||
|
% formsemestre_id
|
||||||
|
)
|
||||||
|
|
||||||
|
H.append("</div>")
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
def html_expr_diagnostic(context, diagnostics):
|
def html_expr_diagnostic(context, diagnostics):
|
||||||
"""Affiche messages d'erreur des formules utilisateurs"""
|
"""Affiche messages d'erreur des formules utilisateurs"""
|
||||||
H = []
|
H = []
|
||||||
@ -917,7 +1035,7 @@ def formsemestre_status(context, formsemestre_id=None, REQUEST=None):
|
|||||||
|
|
||||||
if can_edit:
|
if can_edit:
|
||||||
H.append(
|
H.append(
|
||||||
' <a href="edit_ue_expr?formsemestre_id=%s&ue_id=%s">'
|
' <a href="edit_ue_expr?formsemestre_id=%s&ue_id=%s">'
|
||||||
% (formsemestre_id, ue["ue_id"])
|
% (formsemestre_id, ue["ue_id"])
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user