Application Flask pour ScoDoc 8
This commit is contained in:
parent
078e0e85e0
commit
4864fa5040
1
.gitignore
vendored
1
.gitignore
vendored
@ -131,6 +131,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
envsco8/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
37
README.md
37
README.md
@ -1,5 +1,5 @@
|
||||
|
||||
# SCODOC - gestion de la scolarité
|
||||
# ScoDoc - Gestion de la scolarité
|
||||
|
||||
(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt)
|
||||
|
||||
@ -8,7 +8,40 @@ 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
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
|
||||
|
238
TODO
238
TODO
@ -1,238 +0,0 @@
|
||||
|
||||
NOTES EN VRAC / Brouillon / Trucs obsoletes
|
||||
|
||||
|
||||
#do_moduleimpl_list\(\{"([a-z_]*)"\s*:\s*(.*)\}\)
|
||||
#do_moduleimpl_list( $1 = $2 )
|
||||
|
||||
#do_moduleimpl_list\([\s\n]*args[\s\n]*=[\s\n]*\{"([a-z_]*)"[\s\n]*:[\s\n]*(.*)[\s\n]*\}[\s\n]*\)
|
||||
|
||||
Upgrade JavaScript
|
||||
- jquery-ui-1.12.1 introduit un problème d'affichage de la barre de menu.
|
||||
Il faudrait la revoir entièrement pour upgrader.
|
||||
On reste donc à jquery-ui-1.10.4.custom
|
||||
Or cette version est incompatible avec jQuery 3 (messages d'erreur dans la console)
|
||||
On reste donc avec jQuery 1.12.14
|
||||
|
||||
|
||||
Suivi des requêtes utilisateurs:
|
||||
table sql: id, ip, authuser, request
|
||||
|
||||
|
||||
* Optim:
|
||||
porcodeb4, avant memorisation des moy_ue:
|
||||
S1 SEM14133 cold start: min 9s, max 12s, avg > 11s
|
||||
inval (add note): 1.33s (pas de recalcul des autres)
|
||||
inval (add abs) : min8s, max 12s (recalcule tout :-()
|
||||
LP SEM14946 cold start: 0.7s - 0.86s
|
||||
|
||||
|
||||
|
||||
----------------- LISTE OBSOLETE (très ancienne, à trier) -----------------------
|
||||
BUGS
|
||||
----
|
||||
|
||||
- formsemestre_inscription_with_modules
|
||||
si inscription 'un etud deja inscrit, IntegrityError
|
||||
|
||||
FEATURES REQUESTS
|
||||
-----------------
|
||||
|
||||
* Bulletins:
|
||||
. logos IUT et Univ sur bull PDF
|
||||
. nom departement: nom abbrégé (CJ) ou complet (Carrière Juridiques)
|
||||
. bulletin: deplacer la barre indicateur (cf OLDGEA S2: gêne)
|
||||
. bulletin: click nom titre -> ficheEtud
|
||||
|
||||
. formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5"
|
||||
et valider correctement le form !
|
||||
|
||||
* Jury
|
||||
. recapcomplet: revenir avec qq lignes au dessus de l'étudiant en cours
|
||||
|
||||
|
||||
* Divers
|
||||
. formsemestre_editwithmodules: confirmer suppression modules
|
||||
(et pour l'instant impossible si evaluations dans le module)
|
||||
|
||||
* Modules et UE optionnelles:
|
||||
. UE capitalisées: donc dispense possible dans semestre redoublé.
|
||||
traitable en n'inscrivant pas l'etudiant au modules
|
||||
de cette UE: faire interface utilisateur
|
||||
|
||||
. page pour inscription d'un etudiant a un module
|
||||
. page pour visualiser les modules auquel un etudiant est inscrit,
|
||||
et le desinscrire si besoin.
|
||||
|
||||
. ficheEtud indiquer si inscrit au module sport
|
||||
|
||||
* Absences
|
||||
. EtatAbsences : verifier dates (en JS)
|
||||
. Listes absences pdf et listes groupes pdf + emargements (cf mail Nathalie)
|
||||
. absences par demi-journées sur EtatAbsencesDate (? à vérifier)
|
||||
. formChoixSemestreGroupe: utilisé par Absences/index_html
|
||||
a améliorer
|
||||
|
||||
|
||||
* Notes et évaluations:
|
||||
. Exception "Not an OLE file": generer page erreur plus explicite
|
||||
. Dates evaluation: utiliser JS pour calendrier
|
||||
. Saisie des notes: si une note invalide, l'indiquer dans le listing (JS ?)
|
||||
. et/ou: notes invalides: afficher les noms des etudiants concernes
|
||||
dans le message d'erreur.
|
||||
. upload excel: message erreur peu explicite:
|
||||
* Feuille "Saisie notes", 17 lignes
|
||||
* Erreur: la feuille contient 1 notes invalides
|
||||
* Notes invalides pour les id: ['10500494']
|
||||
(pas de notes modifiées)
|
||||
Notes chargées. <<< CONTRADICTOIRE !!
|
||||
|
||||
. recap complet semestre:
|
||||
Options:
|
||||
- choix groupes
|
||||
- critère de tri (moy ou alphab)
|
||||
- nb de chiffres a afficher
|
||||
|
||||
+ definir des "catégories" d'évaluations (eg "théorie","pratique")
|
||||
afin de n'afficher que des moyennes "de catégorie" dans
|
||||
le bulletin.
|
||||
|
||||
. liste des absents à une eval et croisement avec BD absences
|
||||
|
||||
. notes_evaluation_listenotes
|
||||
- afficher groupes, moyenne, #inscrits, #absents, #manquantes dans l'en-tete.
|
||||
- lien vers modif notes (selon role)
|
||||
|
||||
. Export excel des notes d'evaluation: indiquer date, et autres infos en haut.
|
||||
. Génération PDF listes notes
|
||||
. Page recap notes moyennes par groupes (choisir type de groupe?)
|
||||
|
||||
. (GEA) edition tableau notes avec tous les evals d'un module
|
||||
(comme notes_evaluation_listenotes mais avec tt les evals)
|
||||
|
||||
|
||||
* Non prioritaire:
|
||||
. optimiser scolar_news_summary
|
||||
. recapitulatif des "nouvelles"
|
||||
- dernieres notes
|
||||
- changement de statuts (demissions,inscriptions)
|
||||
- annotations
|
||||
- entreprises
|
||||
|
||||
. notes_table: pouvoir changer decision sans invalider tout le cache
|
||||
. navigation: utiliser Session pour montrer historique pages vues ?
|
||||
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
|
||||
A faire:
|
||||
- fiche etud: code dec jury sur ligne 1
|
||||
si ancien, indiquer autorisation inscription sous le parcours
|
||||
|
||||
- saisie notes: undo
|
||||
- saisie notes: validation
|
||||
- ticket #18:
|
||||
UE capitalisées: donc dispense possible dans semestre redoublé. Traitable en n'inscrivant pas l'etudiant aux modules de cette UE: faire interface utilisateur.
|
||||
|
||||
Prévoir d'entrer une UE capitalisée avec sa note, date d'obtention et un commentaire. Coupler avec la désincription aux modules (si l'étudiant a été inscrit avec ses condisciples).
|
||||
|
||||
|
||||
- Ticket #4: Afin d'éviter les doublons, vérifier qu'il n'existe pas d'homonyme proche lors de la création manuelle d'un étudiant. (confirmé en ScoDoc 6, vérifier aussi les imports Excel)
|
||||
|
||||
- Ticket #74: Il est possible d'inscrire un étudiant sans prénom par un import excel !!!
|
||||
|
||||
- Ticket #64: saisir les absences pour la promo entiere (et pas par groupe). Des fois, je fais signer une feuille de presence en amphi a partir de la liste de tous les etudiants. Ensuite pour reporter les absents par groupe, c'est galere.
|
||||
|
||||
- Ticket #62: Lors des exports Excel, le format des cellules n'est pas reconnu comme numérique sous Windows (pas de problèmes avec Macintosh et Linux).
|
||||
|
||||
A confirmer et corriger.
|
||||
|
||||
- Ticket #75: On peut modifier une décision de jury (et les autorisations de passage associées), mais pas la supprimer purement et simplement.
|
||||
Ajoute ce choix dans les "décisions manuelles".
|
||||
|
||||
- Ticket #37: Page recap notes moyennes par groupes
|
||||
Construire une page avec les moyennes dans chaque UE ou module par groupe d'étudiants.
|
||||
Et aussi pourquoi pas ventiler par type de bac, sexe, parcours (nombre de semestre de parcours) ?
|
||||
redemandé par CJ: à faire avant mai 2008 !
|
||||
|
||||
- Ticket #75: Synchro Apogée: choisir les etudiants
|
||||
Sur la page de syncho Apogée (formsemestre_synchro_etuds), on peut choisir (cocher) les étudiants Apogée à importer. mais on ne peut pas le faire s'ils sont déjà dans ScoDoc: il faudrait ajouter des checkboxes dans toutes les listes.
|
||||
|
||||
- Ticket #9: Format des valeurs de marges des bulletins.
|
||||
formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5" et valider correctement le form !
|
||||
|
||||
- Ticket #17: Suppression modules dans semestres
|
||||
formsemestre_editwithmodules: confirmer suppression modules
|
||||
|
||||
- Ticket #29: changer le stoquage des photos, garder une version HD.
|
||||
|
||||
- bencher NotesTable sans calcul de moyennes. Etudier un cache des moyennes de modules.
|
||||
- listes d'utilisateurs (modules): remplacer menus par champs texte + completions javascript
|
||||
- documenter archives sur Wiki
|
||||
- verifier paquet Debian pour font pdf (reportab: helvetica ... plante si font indisponible)
|
||||
- chercher comment obtenir une page d'erreur correcte pour les pages POST
|
||||
(eg: si le font n'existe pas, archive semestre echoue sans page d'erreur)
|
||||
? je ne crois pas que le POST soit en cause. HTTP status=500
|
||||
ne se produit pas avec Safari
|
||||
- essayer avec IE / Win98
|
||||
- faire apparaitre les diplômés sur le graphe des parcours
|
||||
- démission: formulaire: vérifier que la date est bien dans le semestre
|
||||
|
||||
+ graphe parcours: aligner en colonnes selon les dates (de fin), placer les diplomes
|
||||
dans la même colone que le semestre terminal.
|
||||
|
||||
- modif gestion utilisateurs (donner droits en fct du dept. d'appartenance, bug #57)
|
||||
- modif form def. utilisateur (dept appartenance)
|
||||
- utilisateurs: source externe
|
||||
- archivage des semestres
|
||||
|
||||
|
||||
o-------------------------------------o
|
||||
|
||||
* Nouvelle gestion utilisateurs:
|
||||
objectif: dissocier l'authentification de la notion "d'enseignant"
|
||||
On a une source externe "d'utilisateurs" (annuaire LDAP ou base SQL)
|
||||
qui permet seulement de:
|
||||
- authentifier un utilisateur (login, passwd)
|
||||
- lister un utilisateur: login => firstname, lastname, email
|
||||
- lister les utilisateurs
|
||||
|
||||
et une base interne ScoDoc "d'acteurs" (enseignants, administratifs).
|
||||
Chaque acteur est défini par:
|
||||
- actor_id, firstname, lastname
|
||||
date_creation, date_expiration,
|
||||
roles, departement,
|
||||
email (+flag indiquant s'il faut utiliser ce mail ou celui de
|
||||
l'utilisateur ?)
|
||||
state (on, off) (pour desactiver avant expiration ?)
|
||||
user_id (login) => lien avec base utilisateur
|
||||
|
||||
On offrira une source d'utilisateurs SQL (base partagée par tous les dept.
|
||||
d'une instance ScoDoc), mais dans la plupart des cas les gens utiliseront
|
||||
un annuaire LDAP.
|
||||
|
||||
La base d'acteurs remplace ScoUsers. Les objets ScoDoc (semestres,
|
||||
modules etc) font référence à des acteurs (eg responsable_id est un actor_id).
|
||||
|
||||
Le lien entre les deux ?
|
||||
Loger un utilisateur => authentification utilisateur + association d'un acteur
|
||||
Cela doit se faire au niveau d'un UserFolder Zope, pour avoir les
|
||||
bons rôles et le contrôle d'accès adéquat.
|
||||
(Il faut donc coder notre propre UserFolder).
|
||||
On ne peut associer qu'un acteur à l'état 'on' et non expiré.
|
||||
|
||||
Opérations ScoDoc:
|
||||
- paramétrage: choisir et paramétrer source utilisateurs
|
||||
- ajouter utilisateur: choisir un utilisateur dans la liste
|
||||
et lui associer un nouvel acteur (choix des rôles, des dates)
|
||||
+ éventuellement: synchro d'un ensemble d'utilisateurs, basé sur
|
||||
une requête (eg LDAP) précise (quelle interface utilisateur proposer ?)
|
||||
|
||||
- régulièrement (cron) aviser quelqu'un (le chef) de l'expiration des acteurs.
|
||||
- changer etat d'un acteur (on/off)
|
||||
|
||||
|
||||
o-------------------------------------o
|
||||
|
92
app/__init__.py
Executable file
92
app/__init__.py
Executable file
@ -0,0 +1,92 @@
|
||||
# -*- 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 notes_bp
|
||||
|
||||
app.register_blueprint(notes_bp, url_prefix="/ScoDoc")
|
||||
|
||||
from app.main import bp as main_bp
|
||||
|
||||
app.register_blueprint(main_bp)
|
||||
|
||||
if not app.debug and not app.testing:
|
||||
if app.config["MAIL_SERVER"]:
|
||||
auth = None
|
||||
if app.config["MAIL_USERNAME"] or app.config["MAIL_PASSWORD"]:
|
||||
auth = (app.config["MAIL_USERNAME"], app.config["MAIL_PASSWORD"])
|
||||
secure = None
|
||||
if app.config["MAIL_USE_TLS"]:
|
||||
secure = ()
|
||||
mail_handler = SMTPHandler(
|
||||
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]),
|
||||
fromaddr="no-reply@" + app.config["MAIL_SERVER"],
|
||||
toaddrs=[app.config["ADMINS"]],
|
||||
subject="ScoDoc8 Failure",
|
||||
credentials=auth,
|
||||
secure=secure,
|
||||
)
|
||||
mail_handler.setLevel(logging.ERROR)
|
||||
app.logger.addHandler(mail_handler)
|
||||
|
||||
if not os.path.exists("logs"):
|
||||
os.mkdir("logs")
|
||||
file_handler = RotatingFileHandler(
|
||||
"logs/scodoc.log", maxBytes=10240, backupCount=10
|
||||
)
|
||||
file_handler.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s %(levelname)s: %(message)s " "[in %(pathname)s:%(lineno)d]"
|
||||
)
|
||||
)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
app.logger.addHandler(file_handler)
|
||||
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.info("ScoDoc8 startup")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# from app import models
|
6
app/auth/README.md
Normal file
6
app/auth/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
# ScoDoc User Authentication Blueprint
|
||||
|
||||
Code borrowed and adapted from
|
||||
https://courses.miguelgrinberg.com/p/flask-mega-tutorial
|
||||
|
||||
|
8
app/auth/__init__.py
Normal file
8
app/auth/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""auth.__init__
|
||||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("auth", __name__)
|
||||
|
||||
from app.auth import routes
|
15
app/auth/email.py
Normal file
15
app/auth/email.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
from flask import render_template, current_app
|
||||
from flask_babel import _
|
||||
from app.email import send_email
|
||||
|
||||
|
||||
def send_password_reset_email(user):
|
||||
token = user.get_reset_password_token()
|
||||
send_email(
|
||||
"[ScoDoc] Reset Your Password",
|
||||
sender=current_app.config["ADMINS"][0],
|
||||
recipients=[user.email],
|
||||
text_body=render_template("email/reset_password.txt", user=user, token=token),
|
||||
html_body=render_template("email/reset_password.html", user=user, token=token),
|
||||
)
|
55
app/auth/forms.py
Normal file
55
app/auth/forms.py
Normal file
@ -0,0 +1,55 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""Formulaires authentification
|
||||
|
||||
TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentification
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
||||
from app.auth.models import User
|
||||
|
||||
|
||||
_ = lambda x: x # sans babel
|
||||
_l = _
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField(_l("Username"), validators=[DataRequired()])
|
||||
password = PasswordField(_l("Password"), validators=[DataRequired()])
|
||||
remember_me = BooleanField(_l("Remember Me"))
|
||||
submit = SubmitField(_l("Sign In"))
|
||||
|
||||
|
||||
class UserCreationForm(FlaskForm):
|
||||
username = StringField(_l("Username"), validators=[DataRequired()])
|
||||
email = StringField(_l("Email"), validators=[DataRequired(), Email()])
|
||||
password = PasswordField(_l("Password"), validators=[DataRequired()])
|
||||
password2 = PasswordField(
|
||||
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")]
|
||||
)
|
||||
submit = SubmitField(_l("Register"))
|
||||
|
||||
def validate_username(self, username):
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user is not None:
|
||||
raise ValidationError(_("Please use a different username."))
|
||||
|
||||
def validate_email(self, email):
|
||||
user = User.query.filter_by(email=email.data).first()
|
||||
if user is not None:
|
||||
raise ValidationError(_("Please use a different email address."))
|
||||
|
||||
|
||||
class ResetPasswordRequestForm(FlaskForm):
|
||||
email = StringField(_l("Email"), validators=[DataRequired(), Email()])
|
||||
submit = SubmitField(_l("Request Password Reset"))
|
||||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
password = PasswordField(_l("Password"), validators=[DataRequired()])
|
||||
password2 = PasswordField(
|
||||
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")]
|
||||
)
|
||||
submit = SubmitField(_l("Request Password Reset"))
|
262
app/auth/models.py
Normal file
262
app/auth/models.py
Normal file
@ -0,0 +1,262 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""Users and Roles models for ScoDoc
|
||||
"""
|
||||
|
||||
import base64
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import md5
|
||||
import json
|
||||
import os
|
||||
from time import time
|
||||
|
||||
from flask import current_app, url_for
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
import jwt
|
||||
|
||||
from app import db, login
|
||||
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), index=True, unique=True)
|
||||
email = db.Column(db.String(120), index=True, unique=True)
|
||||
password_hash = db.Column(db.String(128))
|
||||
about_me = db.Column(db.String(140))
|
||||
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
token = db.Column(db.String(32), index=True, unique=True)
|
||||
token_expiration = db.Column(db.DateTime)
|
||||
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
||||
Permission = Permission
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.roles = []
|
||||
super(User, self).__init__(**kwargs)
|
||||
if (
|
||||
not self.roles
|
||||
and self.email
|
||||
and self.email == current_app.config["SCODOC_ADMIN_MAIL"]
|
||||
):
|
||||
# super-admin
|
||||
admin_role = Role.query.filter_by(name="Admin").first()
|
||||
assert admin_role
|
||||
self.add_role(admin_role, None)
|
||||
db.session.commit()
|
||||
current_app.logger.info("creating user with roles={}".format(self.roles))
|
||||
|
||||
def __repr__(self):
|
||||
return "<User {}>".format(self.username)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
def set_password(self, password):
|
||||
"Set password"
|
||||
if password:
|
||||
self.password_hash = generate_password_hash(password)
|
||||
else:
|
||||
self.password_hash = None
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check given password vs current one.
|
||||
Returns `True` if the password matched, `False` otherwise.
|
||||
"""
|
||||
if not self.password_hash: # user without password can't login
|
||||
return False
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def get_reset_password_token(self, expires_in=600):
|
||||
return jwt.encode(
|
||||
{"reset_password": self.id, "exp": time() + expires_in},
|
||||
current_app.config["SECRET_KEY"],
|
||||
algorithm="HS256",
|
||||
).decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def verify_reset_password_token(token):
|
||||
try:
|
||||
id = jwt.decode(
|
||||
token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
|
||||
)["reset_password"]
|
||||
except:
|
||||
return
|
||||
return User.query.get(id)
|
||||
|
||||
def to_dict(self, include_email=False):
|
||||
data = {
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"last_seen": self.last_seen.isoformat() + "Z",
|
||||
"about_me": self.about_me,
|
||||
}
|
||||
if include_email:
|
||||
data["email"] = self.email
|
||||
return data
|
||||
|
||||
def from_dict(self, data, new_user=False):
|
||||
for field in ["username", "email", "about_me"]:
|
||||
if field in data:
|
||||
setattr(self, field, data[field])
|
||||
if new_user and "password" in data:
|
||||
self.set_password(data["password"])
|
||||
|
||||
def get_token(self, expires_in=3600):
|
||||
now = datetime.utcnow()
|
||||
if self.token and self.token_expiration > now + timedelta(seconds=60):
|
||||
return self.token
|
||||
self.token = base64.b64encode(os.urandom(24)).decode("utf-8")
|
||||
self.token_expiration = now + timedelta(seconds=expires_in)
|
||||
db.session.add(self)
|
||||
return self.token
|
||||
|
||||
def revoke_token(self):
|
||||
self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
|
||||
|
||||
@staticmethod
|
||||
def check_token(token):
|
||||
user = User.query.filter_by(token=token).first()
|
||||
if user is None or user.token_expiration < datetime.utcnow():
|
||||
return None
|
||||
return user
|
||||
|
||||
# Permissions management:
|
||||
def has_permission(self, perm, dept):
|
||||
"""Check if user has permission `perm` in given `dept`.
|
||||
Emulate Zope `has_permission``
|
||||
|
||||
Args:
|
||||
perm: integer, one of the value defined in Permission class.
|
||||
context:
|
||||
"""
|
||||
# les role liés à ce département, et les roles avec dept=None (super-admin)
|
||||
roles_in_dept = (
|
||||
UserRole.query.filter_by(user_id=self.id)
|
||||
.filter((UserRole.dept == dept) | (UserRole.dept == None))
|
||||
.all()
|
||||
)
|
||||
for user_role in roles_in_dept:
|
||||
if user_role.role.has_permission(perm):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Role management
|
||||
def add_role(self, role, dept):
|
||||
"""Add a role to this user.
|
||||
:param role: Role to add.
|
||||
"""
|
||||
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
|
||||
|
||||
def add_roles(self, roles, dept):
|
||||
"""Add roles to this user.
|
||||
:param roles: Roles to add.
|
||||
"""
|
||||
for role in roles:
|
||||
self.add_role(role, dept)
|
||||
|
||||
def set_roles(self, roles, dept):
|
||||
self.user_roles = [UserRole(user=self, role=r, dept=dept) for r in roles]
|
||||
|
||||
def get_roles(self):
|
||||
for role in self.roles:
|
||||
yield role
|
||||
|
||||
def is_administrator(self):
|
||||
return self.has_permission(Permission.ScoSuperAdmin, None)
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
def has_permission(self, perm, dept=None):
|
||||
return False
|
||||
|
||||
def is_administrator(self):
|
||||
return False
|
||||
|
||||
|
||||
login.anonymous_user = AnonymousUser
|
||||
|
||||
|
||||
class Role(db.Model):
|
||||
"""Roles for ScoDoc"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(64), unique=True)
|
||||
default = db.Column(db.Boolean, default=False, index=True)
|
||||
permissions = db.Column(db.BigInteger) # 64 bits
|
||||
users = db.relationship("User", secondary="user_role", viewonly=True)
|
||||
# __table_args__ = (db.UniqueConstraint("name", "dept", name="_rolename_dept_uc"),)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Role, self).__init__(**kwargs)
|
||||
if self.permissions is None:
|
||||
self.permissions = 0
|
||||
|
||||
def __repr__(self):
|
||||
return "<Role {} perm={:0{w}b}>".format(
|
||||
self.name,
|
||||
self.permissions & ((1 << Permission.NBITS) - 1),
|
||||
w=Permission.NBITS,
|
||||
)
|
||||
|
||||
def add_permission(self, perm):
|
||||
self.permissions |= perm
|
||||
|
||||
def remove_permission(self, perm):
|
||||
self.permissions = self.permissions & ~perm
|
||||
|
||||
def reset_permissions(self):
|
||||
self.permissions = 0
|
||||
|
||||
def has_permission(self, perm):
|
||||
return self.permissions & perm == perm
|
||||
|
||||
@staticmethod
|
||||
def insert_roles():
|
||||
"""Create default roles"""
|
||||
default_role = "Observateur"
|
||||
for r, permissions in SCO_ROLES_DEFAULTS.items():
|
||||
role = Role.query.filter_by(name=r).first()
|
||||
if role is None:
|
||||
role = Role(name=r)
|
||||
role.reset_permissions()
|
||||
for perm in permissions:
|
||||
role.add_permission(perm)
|
||||
role.default = role.name == default_role
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_named_role(name):
|
||||
"""Returns existing role with given name, or None."""
|
||||
return Role.query.filter_by(name=name).first()
|
||||
|
||||
|
||||
class UserRole(db.Model):
|
||||
"""Associate user to role, in a dept.
|
||||
If dept is None, the role applies to all departments (eg super admin).
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
role_id = db.Column(db.Integer, db.ForeignKey("role.id"))
|
||||
dept = db.Column(db.String(64))
|
||||
user = db.relationship(
|
||||
User, backref=db.backref("user_roles", cascade="all, delete-orphan")
|
||||
)
|
||||
role = db.relationship(
|
||||
Role, backref=db.backref("user_roles", cascade="all, delete-orphan")
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<UserRole u={} r={} dept={}>".format(self.user, self.role, self.dept)
|
||||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
100
app/auth/routes.py
Normal file
100
app/auth/routes.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
"""
|
||||
auth.routes.py
|
||||
"""
|
||||
|
||||
from flask import render_template, redirect, url_for, current_app, flash, request
|
||||
from werkzeug.urls import url_parse
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
|
||||
from app import db
|
||||
from app.auth import bp
|
||||
from app.auth.forms import (
|
||||
LoginForm,
|
||||
UserCreationForm,
|
||||
ResetPasswordRequestForm,
|
||||
ResetPasswordForm,
|
||||
)
|
||||
from app.auth.models import User
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.decorators import scodoc7func, admin_required
|
||||
|
||||
_ = lambda x: x # sans babel
|
||||
_l = _
|
||||
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("main.index"))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash(_("Invalid username or password"))
|
||||
return redirect(url_for("auth.login"))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
next_page = request.args.get("next")
|
||||
if not next_page or url_parse(next_page).netloc != "":
|
||||
next_page = url_for("main.index")
|
||||
return redirect(next_page)
|
||||
return render_template("auth/login.html", title=_("Sign In"), form=form)
|
||||
|
||||
|
||||
@bp.route("/logout")
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@bp.route("/create_user", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def create_user():
|
||||
"Form creating new user"
|
||||
form = UserCreationForm()
|
||||
if form.validate_on_submit():
|
||||
user = User(username=form.username.data, email=form.email.data)
|
||||
user.set_password(form.password.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash("User {} created".format(user.username))
|
||||
return redirect(url_for("main.index"))
|
||||
return render_template(
|
||||
"auth/register.html", title=u"Création utilisateur", form=form
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/reset_password_request", methods=["GET", "POST"])
|
||||
def reset_password_request():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("main.index"))
|
||||
form = ResetPasswordRequestForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
if user:
|
||||
send_password_reset_email(user)
|
||||
else:
|
||||
current_app.logger.info(
|
||||
"reset_password_request: for unkown user '{}'".format(form.email.data)
|
||||
)
|
||||
flash(_("Check your email for the instructions to reset your password"))
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template(
|
||||
"auth/reset_password_request.html", title=_("Reset Password"), form=form
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/reset_password/<token>", methods=["GET", "POST"])
|
||||
def reset_password(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("main.index"))
|
||||
user = User.verify_reset_password_token(token)
|
||||
if not user:
|
||||
return redirect(url_for("main.index"))
|
||||
form = ResetPasswordForm()
|
||||
if form.validate_on_submit():
|
||||
user.set_password(form.password.data)
|
||||
db.session.commit()
|
||||
flash(_("Your password has been reset."))
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("auth/reset_password.html", form=form)
|
7
app/cli.py
Normal file
7
app/cli.py
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
import os
|
||||
import click
|
||||
|
||||
|
||||
def register(app):
|
||||
pass
|
195
app/decorators.py
Normal file
195
app/decorators.py
Normal file
@ -0,0 +1,195 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
def permission_required(permission):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
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)
|
||||
|
||||
|
||||
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 scodoc7func(func):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@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
|
||||
if hasattr(g, "zrequest"):
|
||||
top_level = False
|
||||
else:
|
||||
g.zrequest = None
|
||||
top_level = True
|
||||
#
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
# 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
|
19
app/email.py
Normal file
19
app/email.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
from threading import Thread
|
||||
from flask import current_app
|
||||
from flask_mail import Message
|
||||
from app import mail
|
||||
|
||||
|
||||
def send_async_email(app, msg):
|
||||
with app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
|
||||
def send_email(subject, sender, recipients, text_body, html_body):
|
||||
msg = Message(subject, sender=sender, recipients=recipients)
|
||||
msg.body = text_body
|
||||
msg.html = html_body
|
||||
Thread(
|
||||
target=send_async_email, args=(current_app._get_current_object(), msg)
|
||||
).start()
|
8
app/main/README.md
Normal file
8
app/main/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# main Blueprint
|
||||
|
||||
Quelques essais pour la migration.
|
||||
|
||||
TODO: Ne sera pas conservé.
|
||||
|
||||
|
||||
|
6
app/main/__init__.py
Normal file
6
app/main/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("main", __name__)
|
||||
|
||||
from app.main import routes
|
143
app/main/routes.py
Normal file
143
app/main/routes.py
Normal file
@ -0,0 +1,143 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
@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
|
||||
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
|
||||
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
|
||||
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
|
||||
def formsemestre_status(dept_id=None, formsemestre_id=None, REQUEST=None):
|
||||
"""Essai méthode de département
|
||||
Le contrôle d'accès doit vérifier les bons rôles : ici Ens<dept_id>
|
||||
"""
|
||||
return u"""dept_id=%s , formsemestre_id=%s <a href="/">Retour à l'accueil</a>""" % (
|
||||
dept_id,
|
||||
formsemestre_id,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/hello/world")
|
||||
def hello():
|
||||
H = get_request_infos() + [
|
||||
"<p>Hello, World! %s count=%s</p>" % (thread.get_ident(), D["count"]),
|
||||
]
|
||||
# print(pprint.pformat(dir(request)))
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
@bp.route("/getzip")
|
||||
def getzip():
|
||||
"""Essai renvoi d'un ZIP en Flask"""
|
||||
# La version Zope:
|
||||
# REQUEST.RESPONSE.setHeader("content-type", "application/zip")
|
||||
# REQUEST.RESPONSE.setHeader("content-length", size)
|
||||
# REQUEST.RESPONSE.setHeader(
|
||||
# "content-disposition", 'attachement; filename="monzip.zip"'
|
||||
# )
|
||||
zipdata = StringIO()
|
||||
zipfile = ZipFile(zipdata, "w")
|
||||
zipfile.writestr("fichier1", "un contenu")
|
||||
zipfile.writestr("fichier2", "deux contenus")
|
||||
zipfile.close()
|
||||
data = zipdata.getvalue()
|
||||
size = len(data)
|
||||
# open("/tmp/toto.zip", "w").write(data)
|
||||
# Flask response:
|
||||
r = flask.Response(response=data, status=200, mimetype="application/zip")
|
||||
r.headers["Content-Type"] = "application/zip"
|
||||
r.headers["content-length"] = size
|
||||
r.headers["content-disposition"] = 'attachement; filename="monzip.zip"'
|
||||
return r
|
7
app/models.py
Normal file
7
app/models.py
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""ScoDoc8 models
|
||||
"""
|
||||
|
||||
# None, at this point
|
||||
# see auth.models for user/role related models
|
@ -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
|
||||
"""
|
||||
from app.ScoDoc import sco_core
|
@ -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)
|
@ -6,24 +6,23 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import sco_utils
|
||||
from sco_utils import log, SCODOC_CFG_DIR
|
||||
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():
|
||||
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_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)
|
||||
if not scodoc_cfg_dir in sys.path:
|
||||
sys.path.insert(1, scodoc_cfg_dir)
|
||||
try:
|
||||
from scodoc_local import CONFIG as LOCAL_CONFIG
|
||||
|
15
app/scodoc/sco_core.py
Normal file
15
app/scodoc/sco_core.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- 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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user