Compare commits

...

41 Commits

Author SHA1 Message Date
Emmanuel Viennet dcb53e9c35 WIP migration vues en cours / tout est en vrac ! 2021-06-02 22:40:34 +02:00
Emmanuel Viennet 77f68d1c4c WIP: prepare migration (remove zope context) 2021-06-02 14:50:41 +02:00
Emmanuel Viennet 5e8c837fb2 user_info: ajout arg format 2021-05-31 22:01:52 +02:00
Emmanuel Viennet 3da9bb6914 commandes Flask pour creer utilisateurs de test 2021-05-31 09:57:23 +02:00
Emmanuel Viennet 369b45a8c4 WIP: migration de ZNotes, decorateurs, etc. 2021-05-31 00:14:15 +02:00
Emmanuel Viennet 4864fa5040 Application Flask pour ScoDoc 8 2021-05-29 18:22:51 +02:00
Emmanuel Viennet 078e0e85e0 update from master 2021-05-29 10:57:56 +02:00
Emmanuel Viennet c3e5a5d188 Systemd script for Debian 9 or 10, with postgresql 11 or not. 2021-05-28 22:24:37 +02:00
Emmanuel Viennet 2ee1a386b9 Mobile v1.1 2021-05-25 21:47:22 +02:00
Emmanuel Viennet 1e53aa21cb debug script ignore non scodoc folders in Zope 2021-05-19 15:15:25 +02:00
Emmanuel Viennet 66859c53ba Ajout bonus "sport": bonus_direct 2021-05-19 11:48:00 +02:00
Emmanuel Viennet e943e7f283 Mobile v1+ 2021-05-18 22:47:33 +02:00
Emmanuel Viennet be30cf66fa intègre build v1 mobile 2021-05-18 14:03:15 +02:00
Emmanuel Viennet 2c5e59120c minor fix install script 2021-05-16 17:03:46 +02:00
Emmanuel Viennet e52ffb8357 & in generated urls 2021-05-11 11:48:32 +02:00
Emmanuel Viennet d84102657f up to 7.24 2021-05-01 21:52:26 +02:00
IDK 64f74800ee Enhance API inspection tools 2021-04-26 08:53:12 +02:00
Emmanuel Viennet e61a9752d3 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8
Update to rel. 1997.
2021-04-25 21:44:40 +02:00
IDK dc4bfe4d2e Merge ScoDoc 7.23
Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8
2021-02-13 23:18:32 +01:00
Emmanuel Viennet 2154b60cde Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-29 09:18:20 +01:00
Emmanuel Viennet f1ec103b25 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-23 22:58:27 +01:00
Emmanuel Viennet b76200ac87 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-17 23:04:55 +01:00
Emmanuel Viennet 2bfa7eb4a8 List and check usage of old zope methods 2021-01-16 19:33:35 +01:00
Emmanuel Viennet 0e7857e5ca Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-16 14:04:37 +01:00
Emmanuel Viennet 390141f145 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-16 13:18:46 +01:00
Emmanuel Viennet bb589ae3ae Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-16 11:05:23 +01:00
Emmanuel Viennet ae752bc581 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-10 22:41:06 +01:00
Emmanuel Viennet fcd34c3bdf Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-10 18:57:17 +01:00
viennet af5b946b46 more tools for migration to Flask 2021-01-10 11:43:17 +01:00
viennet 8018a0b092 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2021-01-08 22:25:29 +01:00
viennet cde43621f9 use new SCO_SRC_DIR 2021-01-02 00:10:29 +01:00
viennet aec2d58dbf Up-to-date with 7.20a 2021-01-01 23:46:51 +01:00
viennet 2bf4449dce Version display 2020-12-21 18:42:02 +01:00
viennet bdc0ad488a script getting ScoDoc version infos 2020-12-21 18:15:01 +01:00
viennet 225c97b6dc version 8.00a 2020-12-21 17:11:12 +01:00
viennet aba522a60d Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8 2020-12-21 16:31:38 +01:00
viennet f89fa0bf68 WIP - shellchek config scripts 2020-12-19 19:22:22 +01:00
viennet 5f425da6c0 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8
Apply fixes.
2020-12-16 12:00:46 +01:00
viennet 19913ce89a Fix comment 2020-12-15 08:50:19 +01:00
viennet 31c40b6492 updated email imports 2020-12-15 08:48:29 +01:00
viennet 415810496f Work in progress: new updater and code cleaning 2020-12-15 08:35:44 +01:00
210 changed files with 11137 additions and 4512 deletions

1
.gitignore vendored
View File

@ -131,6 +131,7 @@ venv/
ENV/
env.bak/
venv.bak/
envsco8/
# Spyder project settings
.spyderproject

View File

@ -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
View File

@ -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

3307
ZNotes.py

File diff suppressed because it is too large Load Diff

105
app/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
# -*- coding: UTF-8 -*
import os
import click
def register(app):
pass

206
app/decorators.py Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
# main Blueprint
Quelques essais pour la migration.
TODO: Ne sera pas conservé.

6
app/main/__init__.py Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
# -*- coding: UTF-8 -*
"""ScoDoc8 models
"""
# None, at this point
# see auth.models for user/role related models

View File

@ -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(";")

View File

@ -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>

View File

@ -720,7 +720,7 @@ class ZAbsences(
+ self.sco_footer(REQUEST)
)
base_url = "SignaleAbsenceGrHebdo?datelundi=%s&amp;%s&amp;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&amp;datefin=%s&amp;%s&amp;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 + "&amp;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 + "&amp;nbweeks=%s" % nwl
url_link_semaines = base_url_noweeks + "&nbweeks=%s" % nwl
if moduleimpl_id:
url_link_semaines += "&amp;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&amp;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&amp;datedebut=%s&amp;datefin=%s&amp;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&amp;formsemestre_id=%s&amp;debut=%s&amp;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é&nbsp;:
"ProcessBilletAbsenceForm?billet_id=%s" % b["billet_id"]
)
if etud:
b["_etat_str_target"] += "&amp;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"

View File

@ -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
)

View File

@ -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&amp;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]

View File

@ -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&amp;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>&copy; 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&amp;debut=%(date_debut)s&amp;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&amp;format=xls">format tableur</a>)
<a href="%(url)s/groups_view?curtab=tab-photos&amp;group_ids=%(group_id)s&amp;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&amp;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&amp;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))

View File

@ -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

View File

@ -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 !!! ***
"""

View File

@ -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())

View File

@ -445,14 +445,14 @@ class GenTable:
if self.base_url:
if self.xls_link:
H.append(
' <a href="%s&amp;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("&nbsp;&nbsp;")
if self.pdf_link:
H.append(
' <a href="%s&amp;format=pdf">%s</a>'
' <a href="%s&format=pdf">%s</a>'
% (self.base_url, scu.ICON_PDF)
)
H.append("</p>")

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -733,8 +733,8 @@ def ListeAbsEtud(
etudid, datedebut, with_evals=with_evals, format=format
)
if REQUEST:
base_url_nj = "%s?etudid=%s&amp;absjust_only=0" % (REQUEST.URL0, etudid)
base_url_j = "%s?etudid=%s&amp;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(

View File

@ -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&amp;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&amp;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&amp;archive_name=%s&amp;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 + "&amp;head_message=Archive%20supprimée")
return REQUEST.RESPONSE.redirect(dest_url + "&head_message=Archive%20supprimée")

View File

@ -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&amp;archive_name=%s&amp;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&amp;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 + "&amp;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):

View File

@ -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&amp;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&amp;format=html&amp;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&amp;format=html&amp;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&amp;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&amp;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&amp;etudid=%s&amp;format=pdf&amp;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&amp;etudid=%s&amp;format=pdfmail&amp;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&amp;etudid=%s&amp;format=pdfmail&amp;version=%s&amp;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&amp;etudid=%s&amp;format=xml&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;etudid=%s&amp;format=pdf&amp;version=%s"
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
% (formsemestre_id, etudid, version),
scu.ICON_PDF,
)

View File

@ -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&amp;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&amp;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>")

View File

@ -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&amp;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&amp;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>")

View File

@ -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
View 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)',
}

View 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
View 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"

View File

@ -194,7 +194,7 @@ def formsemestre_estim_cost(
)
tab.html_before_table = h
tab.base_url = (
"%s?formsemestre_id=%s&amp;n_group_td=%s&amp;n_group_tp=%s&amp;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)
)

View File

@ -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

View File

@ -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>

View File

@ -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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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>"""

View File

@ -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&amp;title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&amp;nips=%s"
% (semset_id, "&amp;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&amp;title=Etudiants%%20ScoDoc%%20non%%20listés%%20dans%%20les%%20maquettes%%20Apogée%%20chargées&amp;nips=%s"
% (semset_id, "&amp;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&amp;title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&amp;nips=%s"
% (semset_id, "&amp;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&amp;title=Doublons%%20Apogee&amp;nips=%s"
% (semset_id, "&amp;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 + "&amp;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):

View File

@ -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 = (

View File

@ -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&amp;end_date=%s&amp;types_parcours=%s" % (
tab.base_url = "%s?start_date=%s&end_date=%s&types_parcours=%s" % (
REQUEST.URL0,
start_date,
end_date,
"&amp;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(

View File

@ -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&amp;" % 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"])

View File

@ -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&amp;semestre_id=1">ajouter</a>'
'<a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">ajouter</a>'
% f
]
)

View File

@ -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&amp;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&amp;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&amp;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&amp;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.

View File

@ -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&amp;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&amp;etudid=%s"
"%s/formsemestre_bulletinetud?formsemestre_id==%s&etudid=%s"
% (context.ScoURL(), formsemestre_id, etudid)
)
else:

View File

@ -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&amp;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&amp;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&amp;formsemestre_id=%s&amp;multiple_ok=1&amp;%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&amp;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"],

View File

@ -154,7 +154,7 @@ def formsemestre_status_menubar(context, sem, REQUEST):
},
{
"title": "Modifier le semestre",
"url": "formsemestre_editwithmodules?formation_id=%(formation_id)s&amp;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&amp;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&amp;hidemodules=1&amp;hidebac=1&amp;pref_override=0&amp;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&amp;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&amp;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