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.bak/
|
||||
venv.bak/
|
||||
envsco8/
|
||||
|
||||
# Spyder project settings
|
||||
.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)
|
||||
|
||||
@ -8,7 +8,68 @@ Installation: voir instructions à jour sur <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):
|
||||
"returns tuples (Attribut, Type, Table, AllowNulls, Description)"
|
||||
r = []
|
||||
for l in open(scu.SCO_SRCDIR + "/" + FORMAT_FILE):
|
||||
for l in open(scu.SCO_SRC_DIR + "/" + FORMAT_FILE):
|
||||
l = l.strip()
|
||||
if l and l[0] != "#":
|
||||
fs = l.split(";")
|
@ -1,13 +1,14 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "7.24"
|
||||
SCOVERSION = "8.01a"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
SCONEWS = """
|
||||
<h4>Année 2021</h4>
|
||||
<ul>
|
||||
<li>Version mobile (en test)</li>
|
||||
<li>Évaluations de type "deuxième session"</li>
|
||||
<li>Gestion du genre neutre (pas d'affichage de la civilité)</li>
|
||||
<li>Diverses corrections (PV de jurys, ...)</li>
|
@ -720,7 +720,7 @@ class ZAbsences(
|
||||
+ self.sco_footer(REQUEST)
|
||||
)
|
||||
|
||||
base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % (
|
||||
base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % (
|
||||
datelundi,
|
||||
groups_infos.groups_query_args,
|
||||
urllib.quote(destination),
|
||||
@ -904,15 +904,16 @@ class ZAbsences(
|
||||
etuds = [e for e in etuds if e["etudid"] in mod_inscrits]
|
||||
if not moduleimpl_id:
|
||||
moduleimpl_id = None
|
||||
base_url_noweeks = "SignaleAbsenceGrSemestre?datedebut=%s&datefin=%s&%s&destination=%s" % (
|
||||
datedebut,
|
||||
datefin,
|
||||
groups_infos.groups_query_args,
|
||||
urllib.quote(destination),
|
||||
base_url_noweeks = (
|
||||
"SignaleAbsenceGrSemestre?datedebut=%s&datefin=%s&%s&destination=%s"
|
||||
% (
|
||||
datedebut,
|
||||
datefin,
|
||||
groups_infos.groups_query_args,
|
||||
urllib.quote(destination),
|
||||
)
|
||||
)
|
||||
base_url = (
|
||||
base_url_noweeks + "&nbweeks=%s" % nbweeks
|
||||
) # sans le moduleimpl_id
|
||||
base_url = base_url_noweeks + "&nbweeks=%s" % nbweeks # sans le moduleimpl_id
|
||||
|
||||
if etuds:
|
||||
nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id)
|
||||
@ -952,9 +953,9 @@ class ZAbsences(
|
||||
dates = dates[-nbweeks:]
|
||||
msg = "Montrer toutes les semaines"
|
||||
nwl = 0
|
||||
url_link_semaines = base_url_noweeks + "&nbweeks=%s" % nwl
|
||||
url_link_semaines = base_url_noweeks + "&nbweeks=%s" % nwl
|
||||
if moduleimpl_id:
|
||||
url_link_semaines += "&moduleimpl_id=" + moduleimpl_id
|
||||
url_link_semaines += "&moduleimpl_id=" + moduleimpl_id
|
||||
#
|
||||
dates = [x.ISO() for x in dates]
|
||||
dayname = sco_abs.day_names(self)[jourdebut.weekday]
|
||||
@ -1027,7 +1028,7 @@ class ZAbsences(
|
||||
"""<p>
|
||||
Module concerné par ces absences (%(optionel_txt)s):
|
||||
<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>
|
||||
%(menu_module)s
|
||||
</select>
|
||||
@ -1327,7 +1328,7 @@ class ZAbsences(
|
||||
for a in absnonjust:
|
||||
a["justlink"] = "<em>justifier</em>"
|
||||
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"])
|
||||
)
|
||||
#
|
||||
@ -1463,7 +1464,7 @@ class ZAbsences(
|
||||
)
|
||||
+ "<p>Période du %s au %s (nombre de <b>demi-journées</b>)<br/>"
|
||||
% (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),
|
||||
filename="etat_abs_"
|
||||
+ 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"]
|
||||
)
|
||||
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"]
|
||||
else:
|
||||
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 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:
|
||||
import Products.ZPsycopgDA.DA as ZopeDA
|
||||
@ -504,6 +514,11 @@ class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Imp
|
||||
% 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(
|
||||
"""
|
||||
<div id="scodoc_attribution">
|
||||
@ -753,7 +768,6 @@ Problème de connexion (identifiant, mot de passe): <em>contacter votre responsa
|
||||
% params
|
||||
)
|
||||
# display error traceback (? may open a security risk via xss attack ?)
|
||||
# log('exc B')
|
||||
params["txt_html"] = self._report_request(REQUEST, fmt="html")
|
||||
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;">
|
||||
@ -827,8 +841,6 @@ REFERER: %(REFERER)s
|
||||
Form: %(form)s
|
||||
Origin: %(HTTP_X_FORWARDED_FOR)s
|
||||
Agent: %(HTTP_USER_AGENT)s
|
||||
|
||||
subversion: %(svn_version)s
|
||||
"""
|
||||
% params
|
||||
)
|
@ -261,7 +261,7 @@ class ZScoUsers(
|
||||
|
||||
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).
|
||||
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
|
||||
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):
|
||||
"""true if authuser can see or change passwd of user_name.
|
||||
@ -523,7 +523,7 @@ class ZScoUsers(
|
||||
if authuser.has_permission(ScoUsersAdmin, self):
|
||||
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>
|
||||
"""
|
||||
% 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">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<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-Style-Type" content="text/css" />
|
||||
<meta name="LANG" content="fr" />
|
||||
@ -416,11 +416,6 @@ REQUEST.URL0=%s<br/>
|
||||
# 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")
|
||||
GetDBConnexion = ndb.GetDBConnexion
|
||||
@ -467,9 +462,9 @@ REQUEST.URL0=%s<br/>
|
||||
H = [
|
||||
"""<h2>Système de gestion scolarité</h2>
|
||||
<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(
|
||||
'<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()
|
||||
FA.append("</select>")
|
||||
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
|
||||
)
|
||||
FA.append("</form></td>")
|
||||
@ -715,8 +710,8 @@ REQUEST.URL0=%s<br/>
|
||||
"""<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>
|
||||
(<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
|
||||
)
|
||||
@ -780,7 +775,9 @@ REQUEST.URL0=%s<br/>
|
||||
# -------------------------- INFOS SUR ETUDIANTS --------------------------
|
||||
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
|
||||
On peut specifier etudid
|
||||
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)
|
||||
|
||||
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")
|
||||
@ -2776,7 +2773,7 @@ def _simple_error_page(context, msg, DeptId=None):
|
||||
H = [context.standard_html_header(context), "<h2>Erreur !</h2>", "<p>", msg, "</p>"]
|
||||
if DeptId:
|
||||
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)
|
||||
)
|
||||
H.append(context.standard_html_footer(context))
|
@ -25,33 +25,6 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from ZScolar import ZScolar, manage_addZScolarForm, manage_addZScolar
|
||||
|
||||
from ZScoDoc import ZScoDoc, manage_addZScoDoc
|
||||
|
||||
# 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"
|
||||
)
|
||||
"""ScoDoc core package
|
||||
"""
|
||||
# from app.scodoc import sco_core
|
@ -45,9 +45,16 @@ def bonus_iutv(notes_sport, coefs, infos=None):
|
||||
return bonus
|
||||
|
||||
|
||||
def bonus_iut_stdenis(notes_sport, coefs, infos=None):
|
||||
"""Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points.
|
||||
def bonus_direct(notes_sport, coefs, infos=None):
|
||||
"""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
|
||||
bonus = points * 0.05 # ou / 20
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
"""
|
||||
# les coefs sont ignorés
|
||||
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):
|
||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray
|
||||
|
||||
|
||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||
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
|
||||
@ -93,42 +100,13 @@ def bonus_iutva(notes_sport, coefs, infos=None):
|
||||
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):
|
||||
"""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.
|
||||
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
|
||||
"""
|
||||
# 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):
|
||||
"""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.
|
||||
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
|
||||
m1 : moyenne de l'unité d'enseignement avant bonification
|
||||
N1 : note de sport 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.
|
||||
Calcul ici de la moyenne générale et moyennes d'UE non capitalisées.
|
||||
La note de sport de nos étudiants va de 0 à 20 points.
|
||||
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
|
||||
m1 : moyenne de l'unité d'enseignement avant bonification
|
||||
N1 : note de sport 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.
|
||||
Calcul ici de la moyenne générale et moyennes d'UE non capitalisées.
|
||||
"""
|
||||
# les coefs sont ignorés
|
||||
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):
|
||||
"""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.
|
||||
cette procédure modifie la moyenne de chaque UE capitalisable.
|
||||
Le bonus est compris entre 0 et 0.35 point.
|
||||
cette procédure modifie la moyenne de chaque UE capitalisable.
|
||||
|
||||
"""
|
||||
# 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)
|
||||
Nous avons différents types de bonification
|
||||
bonfication Sport / Associations
|
||||
coopératives de département / Bureau Des Étudiants
|
||||
coopératives de département / Bureau Des Étudiants
|
||||
/ 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
|
||||
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):
|
||||
"""Calcul bonus modules optionnels
|
||||
"""Calcul bonus modules optionnels
|
||||
(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
|
||||
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.
|
||||
@ -408,7 +386,7 @@ def bonus_iutbethune(notes_sport, coefs, infos=None):
|
||||
def bonus_demo(notes_sport, coefs, infos=None):
|
||||
"""Fausse fonction "bonus" pour afficher les informations disponibles
|
||||
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.
|
||||
*** Ne pas utiliser en production !!! ***
|
||||
"""
|
@ -67,7 +67,11 @@ def go(app, n=0, verbose=True):
|
||||
def go_dept(app, dept, verbose=True):
|
||||
objs = app.ScoDoc.objectValues("Folder")
|
||||
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 verbose:
|
||||
print("context in dept ", context.DeptId())
|
@ -445,14 +445,14 @@ class GenTable:
|
||||
if self.base_url:
|
||||
if self.xls_link:
|
||||
H.append(
|
||||
' <a href="%s&format=xls">%s</a>'
|
||||
' <a href="%s&format=xls">%s</a>'
|
||||
% (self.base_url, scu.ICON_XLS)
|
||||
)
|
||||
if self.xls_link and self.pdf_link:
|
||||
H.append(" ")
|
||||
if self.pdf_link:
|
||||
H.append(
|
||||
' <a href="%s&format=pdf">%s</a>'
|
||||
' <a href="%s&format=pdf">%s</a>'
|
||||
% (self.base_url, scu.ICON_PDF)
|
||||
)
|
||||
H.append("</p>")
|
@ -8,6 +8,10 @@ import re
|
||||
import inspect
|
||||
import time
|
||||
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
|
||||
MIMEMultipart,
|
@ -462,11 +462,11 @@ def get_templates_from_distrib(template="avis"):
|
||||
|
||||
if template in ["avis", "footer"]:
|
||||
# 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):
|
||||
template_latex = get_code_latex_from_modele(p)
|
||||
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):
|
||||
template_latex = get_code_latex_from_modele(p)
|
||||
else:
|
@ -177,7 +177,7 @@ def add_pe_stuff_to_zip(context, zipfile, ziproot):
|
||||
|
||||
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_pathnames = list_directory_filenames(
|
||||
distrib_dir
|
@ -32,13 +32,11 @@
|
||||
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
||||
"""
|
||||
import datetime
|
||||
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
|
||||
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
|
||||
|
||||
import notesdb as ndb
|
||||
import sco_utils as scu
|
@ -733,8 +733,8 @@ def ListeAbsEtud(
|
||||
etudid, datedebut, with_evals=with_evals, format=format
|
||||
)
|
||||
if REQUEST:
|
||||
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_nj = "%s?etudid=%s&absjust_only=0" % (REQUEST.URL0, etudid)
|
||||
base_url_j = "%s?etudid=%s&absjust_only=1" % (REQUEST.URL0, etudid)
|
||||
else:
|
||||
base_url_nj = base_url_j = ""
|
||||
tab_absnonjust = GenTable(
|
@ -53,6 +53,7 @@ import shutil
|
||||
import glob
|
||||
|
||||
import sco_utils as scu
|
||||
from config import Config
|
||||
import notesdb as ndb
|
||||
from notes_log import log
|
||||
import sco_formsemestre
|
||||
@ -71,7 +72,7 @@ from sco_exceptions import (
|
||||
|
||||
class BaseArchiver:
|
||||
def __init__(self, archive_type=""):
|
||||
dirs = [os.environ["INSTANCE_HOME"], "var", "scodoc", "archives"]
|
||||
dirs = [Config.INSTANCE_HOME, "var", "scodoc", "archives"]
|
||||
if archive_type:
|
||||
dirs.append(archive_type)
|
||||
self.root = os.path.join(*dirs)
|
||||
@ -484,7 +485,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
||||
|
||||
# submitted or cancelled:
|
||||
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)
|
||||
)
|
||||
|
||||
@ -510,7 +511,7 @@ def formsemestre_list_archives(context, REQUEST, formsemestre_id):
|
||||
for a in L:
|
||||
archive_name = PVArchive.get_archive_name(a["archive_id"])
|
||||
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["description"],
|
||||
@ -520,7 +521,7 @@ def formsemestre_list_archives(context, REQUEST, formsemestre_id):
|
||||
)
|
||||
for filename in a["content"]:
|
||||
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)
|
||||
)
|
||||
if not a["content"]:
|
||||
@ -556,7 +557,8 @@ def formsemestre_delete_archive(
|
||||
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)
|
||||
|
||||
if not dialog_confirmed:
|
||||
return context.confirmDialog(
|
||||
return scu.confirm_dialog(
|
||||
context,
|
||||
"""<h2>Confirmer la suppression de l'archive du %s ?</h2>
|
||||
<p>La suppression sera définitive.</p>"""
|
||||
% 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)
|
||||
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"]:
|
||||
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)
|
||||
)
|
||||
if not a["content"]:
|
||||
H.append("<em>aucun fichier !</em>")
|
||||
if can_edit:
|
||||
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)
|
||||
)
|
||||
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)
|
||||
dest_url = "ficheEtud?etudid=%s" % etudid
|
||||
if not dialog_confirmed:
|
||||
return context.confirmDialog(
|
||||
return scu.confirm_dialog(
|
||||
context,
|
||||
"""<h2>Confirmer la suppression des fichiers ?</h2>
|
||||
<p>Fichier associé le %s à l'étudiant %s</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)
|
||||
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):
|
@ -28,19 +28,17 @@
|
||||
"""Génération des bulletins de notes
|
||||
|
||||
"""
|
||||
import time
|
||||
from types import StringType
|
||||
import pprint
|
||||
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 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
|
||||
|
||||
import sco_utils as scu
|
||||
@ -329,7 +327,7 @@ def formsemestre_bulletinetud_dict(
|
||||
)
|
||||
u[
|
||||
"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"],
|
||||
etudid,
|
||||
sem_origin["titreannee"],
|
||||
@ -522,7 +520,7 @@ def _ue_mod_bulletin(context, etudid, formsemestre_id, ue_id, modimpls, nt, vers
|
||||
else:
|
||||
e["name"] = e["description"] or "le %s" % e["jour"]
|
||||
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["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)
|
||||
e["name"] = (e["description"] or "") + " (%s)" % e["jour"]
|
||||
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["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
|
||||
@ -816,7 +814,7 @@ def formsemestre_bulletinetud(
|
||||
if sem["modalite"] == "EXT":
|
||||
R.append(
|
||||
"""<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">
|
||||
Editer les validations d'UE dans ce semestre extérieur
|
||||
</a></p>"""
|
||||
@ -1009,7 +1007,7 @@ def mail_bulletin(context, formsemestre_id, I, pdfdata, filename, recipient_addr
|
||||
att = MIMEBase("application", "pdf")
|
||||
att.add_header("Content-Disposition", "attachment", filename=filename)
|
||||
att.set_payload(pdfdata)
|
||||
Encoders.encode_base64(att)
|
||||
email.encoders.encode_base64(att)
|
||||
msg.attach(att)
|
||||
log("mail bulletin a %s" % msg["To"])
|
||||
context.sendEmail(msg)
|
||||
@ -1076,7 +1074,7 @@ def _formsemestre_bulletinetud_header_html(
|
||||
menuBul = [
|
||||
{
|
||||
"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),
|
||||
"enabled": (uid in sem["responsables"])
|
||||
or authuser.has_permission(ScoImplement, context),
|
||||
@ -1087,13 +1085,13 @@ def _formsemestre_bulletinetud_header_html(
|
||||
context, formsemestre_id
|
||||
),
|
||||
"url": url
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
||||
% (formsemestre_id, etudid, version),
|
||||
},
|
||||
{
|
||||
"title": "Envoi par mail à %s" % etud["email"],
|
||||
"url": url
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=pdfmail&version=%s"
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=pdfmail&version=%s"
|
||||
% (formsemestre_id, etudid, version),
|
||||
"enabled": etud["email"]
|
||||
and can_send_bulletin_by_mail(
|
||||
@ -1103,7 +1101,7 @@ def _formsemestre_bulletinetud_header_html(
|
||||
{
|
||||
"title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
|
||||
"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),
|
||||
"enabled": etud["emailperso"]
|
||||
and can_send_bulletin_by_mail(
|
||||
@ -1113,12 +1111,12 @@ def _formsemestre_bulletinetud_header_html(
|
||||
{
|
||||
"title": "Version XML",
|
||||
"url": url
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=xml&version=%s"
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=xml&version=%s"
|
||||
% (formsemestre_id, etudid, version),
|
||||
},
|
||||
{
|
||||
"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),
|
||||
"enabled": (
|
||||
(authuser in sem["responsables"])
|
||||
@ -1127,31 +1125,31 @@ def _formsemestre_bulletinetud_header_html(
|
||||
},
|
||||
{
|
||||
"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),
|
||||
"enabled": authuser.has_permission(ScoImplement, context),
|
||||
},
|
||||
{
|
||||
"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),
|
||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||
},
|
||||
{
|
||||
"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),
|
||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||
},
|
||||
{
|
||||
"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),
|
||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||
},
|
||||
{
|
||||
"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),
|
||||
"enabled": True,
|
||||
},
|
||||
@ -1164,7 +1162,7 @@ def _formsemestre_bulletinetud_header_html(
|
||||
'<td> <a href="%s">%s</a></td>'
|
||||
% (
|
||||
url
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
||||
% (formsemestre_id, etudid, version),
|
||||
scu.ICON_PDF,
|
||||
)
|
@ -324,7 +324,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
||||
for app in I["appreciations_list"]:
|
||||
if can_edit_app:
|
||||
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"])
|
||||
)
|
||||
else:
|
||||
@ -335,7 +335,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
||||
)
|
||||
if can_edit_app:
|
||||
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
|
||||
)
|
||||
H.append("</div>")
|
@ -159,7 +159,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
for app in self.infos["appreciations_list"]:
|
||||
if can_edit_app:
|
||||
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"])
|
||||
)
|
||||
else:
|
||||
@ -170,7 +170,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
)
|
||||
if can_edit_app:
|
||||
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
|
||||
)
|
||||
H.append("</div>")
|
@ -116,7 +116,8 @@ CODES_EXPL = {
|
||||
RAT: "En attente d'un rattrapage",
|
||||
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
|
||||
|
||||
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.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)
|
||||
)
|
||||
|
@ -51,14 +51,10 @@ import fcntl
|
||||
import subprocess
|
||||
import requests
|
||||
|
||||
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
|
||||
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.base import MIMEBase
|
||||
from email.header import Header
|
||||
|
||||
import notesdb as ndb
|
||||
import sco_utils as scu
|
||||
@ -122,8 +118,7 @@ def sco_dump_and_send_db(context, REQUEST=None):
|
||||
|
||||
finally:
|
||||
# Drop anonymized database
|
||||
_drop_ano_db(ano_db_name)
|
||||
|
||||
# XXX _drop_ano_db(ano_db_name)
|
||||
# Remove lock
|
||||
fcntl.flock(x, fcntl.LOCK_UN)
|
||||
|
||||
@ -158,7 +153,7 @@ def _duplicate_db(db_name, ano_db_name):
|
||||
|
||||
def _anonymize_db(ano_db_name):
|
||||
"""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))
|
||||
try:
|
||||
_ = subprocess.check_output([cmd, ano_db_name])
|
||||
@ -200,7 +195,7 @@ def _send_db(context, REQUEST, ano_db_name):
|
||||
"nomcomplet"
|
||||
],
|
||||
"sco_version": scu.SCOVERSION,
|
||||
"sco_subversion": scu.get_svn_version(scu.SCO_CONFIG_DIR),
|
||||
"sco_fullversion": scu.get_scodoc_version(),
|
||||
},
|
||||
)
|
||||
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())
|
||||
else:
|
||||
if not dialog_confirmed:
|
||||
return context.confirmDialog(
|
||||
return scu.confirm_dialog(
|
||||
context,
|
||||
"""<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>
|
@ -267,7 +267,8 @@ def ue_delete(
|
||||
ue = ue[0]
|
||||
|
||||
if not dialog_confirmed:
|
||||
return context.confirmDialog(
|
||||
return scu.confirm_dialog(
|
||||
context,
|
||||
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
|
||||
dest_url="",
|
||||
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">')
|
||||
if iue != 0 and editable:
|
||||
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)
|
||||
)
|
||||
else:
|
||||
H.append(arrow_none)
|
||||
if iue < len(ue_list) - 1 and editable:
|
||||
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)
|
||||
)
|
||||
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">')
|
||||
if im != 0 and editable:
|
||||
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)
|
||||
)
|
||||
else:
|
||||
H.append(arrow_none)
|
||||
if im < len(Modlist) - 1 and editable:
|
||||
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)
|
||||
)
|
||||
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_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>
|
||||
</ul>
|
||||
@ -646,7 +647,7 @@ Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module
|
||||
H.append(" [verrouillé]")
|
||||
else:
|
||||
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
|
||||
)
|
||||
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):
|
||||
H.append(
|
||||
"""<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>
|
||||
|
||||
</ul>"""
|
@ -177,8 +177,8 @@ def apo_semset_maq_status(
|
||||
H.append("""<li>Il y a plusieurs années scolaires !</li>""")
|
||||
if nips_no_sco: # seulement un warning
|
||||
url_list = (
|
||||
"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))
|
||||
"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))
|
||||
)
|
||||
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>'
|
||||
@ -196,8 +196,8 @@ def apo_semset_maq_status(
|
||||
|
||||
if nips_no_apo:
|
||||
url_list = (
|
||||
"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))
|
||||
"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))
|
||||
)
|
||||
H.append(
|
||||
'<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
|
||||
url_list = (
|
||||
"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))
|
||||
"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))
|
||||
)
|
||||
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>'
|
||||
@ -215,9 +215,9 @@ def apo_semset_maq_status(
|
||||
)
|
||||
|
||||
if apo_dups:
|
||||
url_list = (
|
||||
"view_apo_etuds?semset_id=%s&title=Doublons%%20Apogee&nips=%s"
|
||||
% (semset_id, "&nips=".join(apo_dups))
|
||||
url_list = "view_apo_etuds?semset_id=%s&title=Doublons%%20Apogee&nips=%s" % (
|
||||
semset_id,
|
||||
"&nips=".join(apo_dups),
|
||||
)
|
||||
H.append(
|
||||
'<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)
|
||||
dest_url = "apo_semset_maq_status?semset_id=" + semset_id
|
||||
if not dialog_confirmed:
|
||||
return context.confirmDialog(
|
||||
return scu.confirm_dialog(
|
||||
context,
|
||||
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
|
||||
<p>La suppression sera définitive.</p>"""
|
||||
% (etape_apo,),
|
||||
@ -673,7 +674,7 @@ def view_apo_csv_delete(
|
||||
context, etape_apo, semset["annee_scolaire"], semset["sem_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):
|
@ -66,6 +66,18 @@ class FormatError(ScoValueError):
|
||||
pass
|
||||
|
||||
|
||||
class ScoInvalidDept(ScoValueError):
|
||||
"""departement invalide"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ScoConfigurationError(ScoValueError):
|
||||
"""Configuration invalid"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ScoLockedFormError(ScoException):
|
||||
def __init__(self, msg="", REQUEST=None):
|
||||
msg = (
|
@ -236,11 +236,11 @@ def scodoc_table_results(
|
||||
tab, semlist = _build_results_table(
|
||||
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,
|
||||
start_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":
|
||||
return tab.make_page(
|
@ -141,7 +141,7 @@ def search_etud_in_dept(context, expnom="", REQUEST=None):
|
||||
if len(etuds) > 0:
|
||||
# Choix dans la liste des résultats:
|
||||
for e in etuds:
|
||||
target = dest_url + "?etudid=%s&" % e["etudid"]
|
||||
target = dest_url + "?etudid=%s&" % e["etudid"]
|
||||
e["_nomprenom_target"] = target
|
||||
e["inscription_target"] = target
|
||||
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"]
|
||||
]
|
||||
+ [
|
||||
'<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
|
||||
]
|
||||
)
|
@ -711,7 +711,7 @@ def do_formsemestre_createwithmodules(context, REQUEST=None, edit=False):
|
||||
}
|
||||
_ = sco_moduleimpl.do_moduleimpl_create(context, modargs)
|
||||
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
|
||||
)
|
||||
else:
|
||||
@ -811,7 +811,7 @@ def do_formsemestre_createwithmodules(context, REQUEST=None, edit=False):
|
||||
return msg_html
|
||||
else:
|
||||
return REQUEST.RESPONSE.redirect(
|
||||
"formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
|
||||
"formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
|
||||
% formsemestre_id
|
||||
)
|
||||
|
||||
@ -965,7 +965,7 @@ def formsemestre_clone(context, formsemestre_id, REQUEST=None):
|
||||
REQUEST=REQUEST,
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
@ -1129,7 +1129,8 @@ def formsemestre_associate_new_version(
|
||||
% (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>
|
||||
<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...).
|
||||
@ -1148,7 +1149,7 @@ def formsemestre_associate_new_version(
|
||||
context, [formsemestre_id] + other_formsemestre_ids, REQUEST=REQUEST
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
@ -1280,7 +1281,8 @@ def formsemestre_delete2(
|
||||
"""Delete a formsemestre (confirmation)"""
|
||||
# Confirmation dialog
|
||||
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>""",
|
||||
dest_url="",
|
||||
REQUEST=REQUEST,
|
||||
@ -1423,7 +1425,8 @@ def formsemestre_change_lock(
|
||||
msg = "déverrouillage"
|
||||
else:
|
||||
msg = "verrouillage"
|
||||
return context.confirmDialog(
|
||||
return scu.confirm_dialog(
|
||||
context,
|
||||
"<h2>Confirmer le %s du semestre ?</h2>" % msg,
|
||||
helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
|
||||
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
|
||||
@ -1462,7 +1465,8 @@ def formsemestre_change_publication_bul(
|
||||
msg = "non"
|
||||
else:
|
||||
msg = ""
|
||||
return context.confirmDialog(
|
||||
return scu.confirm_dialog(
|
||||
context,
|
||||
"<h2>Confirmer la %s publication des bulletins ?</h2>" % msg,
|
||||
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.
|
@ -86,7 +86,7 @@ def formsemestre_ext_create_form(context, etudid, formsemestre_id, REQUEST=None)
|
||||
<p class="help">
|
||||
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
|
||||
"<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>"
|
||||
</p>
|
||||
"""
|
||||
@ -191,7 +191,7 @@ def formsemestre_ext_create_form(context, etudid, formsemestre_id, REQUEST=None)
|
||||
return "\n".join(H) + "\n" + tf[1] + F
|
||||
elif tf[0] == -1:
|
||||
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)
|
||||
)
|
||||
else:
|
@ -147,7 +147,7 @@ def formsemestre_inscription_with_modules_form(
|
||||
if (not only_ext) or (sem["modalite"] == "EXT"):
|
||||
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"])
|
||||
)
|
||||
@ -217,12 +217,12 @@ def formsemestre_inscription_with_modules(
|
||||
H.append("<ul>")
|
||||
for s in others:
|
||||
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"])
|
||||
)
|
||||
H.append("</ul>")
|
||||
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))
|
||||
)
|
||||
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(
|
||||
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"],
|
||||
etudid,
|
||||
sem_origin["titreannee"],
|
@ -154,7 +154,7 @@ def formsemestre_status_menubar(context, sem, REQUEST):
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"enabled": (
|
||||
authuser.has_permission(ScoImplement, context)
|
||||
@ -292,7 +292,7 @@ def formsemestre_status_menubar(context, sem, REQUEST):
|
||||
},
|
||||
{
|
||||
"title": "Exporter table des étudiants",
|
||||
"url": "groups_view?format=allxls&group_ids="
|
||||
"url": "groups_view?format=allxls&group_ids="
|
||||
+ sco_groups.get_default_group(
|
||||
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",
|
||||
"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,
|
||||
"enabled": context._can_validate_sem(REQUEST, formsemestre_id),
|
||||
},
|
||||
@ -684,7 +684,7 @@ def formsemestre_description_table(
|
||||
caption=title,
|
||||
html_caption=title,
|
||||
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),
|
||||
page_title=title,
|
||||
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)
|
||||
H = [
|
||||
context.html_sem_header(REQUEST, "", sem),
|
||||
context.make_listes_sem(sem, REQUEST),
|
||||
_make_listes_sem(context, sem, REQUEST),
|
||||
context.sco_footer(REQUEST),
|
||||
]
|
||||
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):
|
||||
"""Affiche messages d'erreur des formules utilisateurs"""
|
||||
H = []
|
||||
@ -917,7 +1035,7 @@ def formsemestre_status(context, formsemestre_id=None, REQUEST=None):
|
||||
|
||||
if can_edit:
|
||||
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"])
|
||||
)
|
||||
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