diff --git a/.gitignore b/.gitignore
index f69b51fe4..6d49cf2c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -131,6 +131,7 @@ venv/
ENV/
env.bak/
venv.bak/
+envsco8/
# Spyder project settings
.spyderproject
diff --git a/README.md b/README.md
index b5a53fcac..e9c058b16 100644
--- a/README.md
+++ b/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
Documentation utilisateur:
-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
diff --git a/TODO b/TODO
deleted file mode 100644
index 4041532f0..000000000
--- a/TODO
+++ /dev/null
@@ -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
-
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100755
index 000000000..61fa4045c
--- /dev/null
+++ b/app/__init__.py
@@ -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
diff --git a/app/auth/README.md b/app/auth/README.md
new file mode 100644
index 000000000..7bb621abd
--- /dev/null
+++ b/app/auth/README.md
@@ -0,0 +1,6 @@
+# ScoDoc User Authentication Blueprint
+
+Code borrowed and adapted from
+https://courses.miguelgrinberg.com/p/flask-mega-tutorial
+
+
diff --git a/app/auth/__init__.py b/app/auth/__init__.py
new file mode 100644
index 000000000..4fb9575d9
--- /dev/null
+++ b/app/auth/__init__.py
@@ -0,0 +1,8 @@
+"""auth.__init__
+"""
+
+from flask import Blueprint
+
+bp = Blueprint("auth", __name__)
+
+from app.auth import routes
diff --git a/app/auth/email.py b/app/auth/email.py
new file mode 100644
index 000000000..d067bef27
--- /dev/null
+++ b/app/auth/email.py
@@ -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),
+ )
diff --git a/app/auth/forms.py b/app/auth/forms.py
new file mode 100644
index 000000000..4dcb11831
--- /dev/null
+++ b/app/auth/forms.py
@@ -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"))
diff --git a/app/auth/models.py b/app/auth/models.py
new file mode 100644
index 000000000..57622ba8f
--- /dev/null
+++ b/app/auth/models.py
@@ -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 "".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 "".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 "".format(self.user, self.role, self.dept)
+
+
+@login.user_loader
+def load_user(id):
+ return User.query.get(int(id))
diff --git a/app/auth/routes.py b/app/auth/routes.py
new file mode 100644
index 000000000..96cc40c6f
--- /dev/null
+++ b/app/auth/routes.py
@@ -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/", 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)
diff --git a/app/cli.py b/app/cli.py
new file mode 100644
index 000000000..25ae4d488
--- /dev/null
+++ b/app/cli.py
@@ -0,0 +1,7 @@
+# -*- coding: UTF-8 -*
+import os
+import click
+
+
+def register(app):
+ pass
diff --git a/app/decorators.py b/app/decorators.py
new file mode 100644
index 000000000..9816e53a6
--- /dev/null
+++ b/app/decorators.py
@@ -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 `//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
diff --git a/app/email.py b/app/email.py
new file mode 100644
index 000000000..f2d8164d8
--- /dev/null
+++ b/app/email.py
@@ -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()
diff --git a/app/main/README.md b/app/main/README.md
new file mode 100644
index 000000000..0f4acc044
--- /dev/null
+++ b/app/main/README.md
@@ -0,0 +1,8 @@
+# main Blueprint
+
+Quelques essais pour la migration.
+
+TODO: Ne sera pas conservé.
+
+
+
diff --git a/app/main/__init__.py b/app/main/__init__.py
new file mode 100644
index 000000000..a27dd27d7
--- /dev/null
+++ b/app/main/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: UTF-8 -*
+from flask import Blueprint
+
+bp = Blueprint("main", __name__)
+
+from app.main import routes
diff --git a/app/main/routes.py b/app/main/routes.py
new file mode 100644
index 000000000..d36545b94
--- /dev/null
+++ b/app/main/routes.py
@@ -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. Retour à l'accueil"""
+
+
+def get_request_infos():
+ return [
+ "request.base_url=%s
" % request.base_url,
+ "request.url_root=%s
" % request.url_root,
+ "request.query_string=%s
" % 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() + [
+ "x=%s
" % x,
+ "y=%s
" % y,
+ "URL=%s
" % REQUEST.URL,
+ "QUERY_STRING=%s
" % REQUEST.QUERY_STRING,
+ "AUTHENTICATED_USER=%s
" % REQUEST.AUTHENTICATED_USER,
+ ]
+ H.append("form=%s
" % REQUEST.form)
+ H.append("form[x]=%s
" % REQUEST.form.get("x", "non fourni"))
+
+ return "\n".join(H)
+
+
+@bp.route("/zopeform_get")
+@scodoc7func
+def a_zope_form_get(REQUEST=None):
+ H = [
+ """Formulaire GET
+
+ """
+ % flask.url_for("main.a_zope_function")
+ ]
+ return "\n".join(H)
+
+
+@bp.route("/zopeform_post")
+@scodoc7func
+def a_zope_form_post(REQUEST=None):
+ H = [
+ """Formulaire POST
+
+ """
+ % flask.url_for("main.a_zope_function")
+ ]
+ return "\n".join(H)
+
+
+@bp.route("/ScoDoc//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
+ """
+ return u"""dept_id=%s , formsemestre_id=%s Retour à l'accueil""" % (
+ dept_id,
+ formsemestre_id,
+ )
+
+
+@bp.route("/hello/world")
+def hello():
+ H = get_request_infos() + [
+ "Hello, World! %s count=%s
" % (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
diff --git a/app/models.py b/app/models.py
new file mode 100644
index 000000000..0205be7bf
--- /dev/null
+++ b/app/models.py
@@ -0,0 +1,7 @@
+# -*- coding: UTF-8 -*
+
+"""ScoDoc8 models
+"""
+
+# None, at this point
+# see auth.models for user/role related models
diff --git a/ImportScolars.py b/app/scodoc/ImportScolars.py
similarity index 100%
rename from ImportScolars.py
rename to app/scodoc/ImportScolars.py
diff --git a/SuppressAccents.py b/app/scodoc/SuppressAccents.py
similarity index 100%
rename from SuppressAccents.py
rename to app/scodoc/SuppressAccents.py
diff --git a/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py
similarity index 100%
rename from TrivialFormulator.py
rename to app/scodoc/TrivialFormulator.py
diff --git a/VERSION.py b/app/scodoc/VERSION.py
similarity index 100%
rename from VERSION.py
rename to app/scodoc/VERSION.py
diff --git a/ZAbsences.py b/app/scodoc/ZAbsences.py
similarity index 100%
rename from ZAbsences.py
rename to app/scodoc/ZAbsences.py
diff --git a/ZEntreprises.py b/app/scodoc/ZEntreprises.py
similarity index 100%
rename from ZEntreprises.py
rename to app/scodoc/ZEntreprises.py
diff --git a/ZNotes.py b/app/scodoc/ZNotes.py
similarity index 100%
rename from ZNotes.py
rename to app/scodoc/ZNotes.py
diff --git a/ZScoDoc.py b/app/scodoc/ZScoDoc.py
similarity index 100%
rename from ZScoDoc.py
rename to app/scodoc/ZScoDoc.py
diff --git a/ZScoUsers.py b/app/scodoc/ZScoUsers.py
similarity index 100%
rename from ZScoUsers.py
rename to app/scodoc/ZScoUsers.py
diff --git a/ZScolar.py b/app/scodoc/ZScolar.py
similarity index 100%
rename from ZScolar.py
rename to app/scodoc/ZScolar.py
diff --git a/__init__.py b/app/scodoc/__init__.py
similarity index 57%
rename from __init__.py
rename to app/scodoc/__init__.py
index 98fd40238..56e94e321 100644
--- a/__init__.py
+++ b/app/scodoc/__init__.py
@@ -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
diff --git a/bonus_sport.py b/app/scodoc/bonus_sport.py
similarity index 100%
rename from bonus_sport.py
rename to app/scodoc/bonus_sport.py
diff --git a/debug.py b/app/scodoc/debug.py
similarity index 100%
rename from debug.py
rename to app/scodoc/debug.py
diff --git a/dutrules.py b/app/scodoc/dutrules.py
similarity index 100%
rename from dutrules.py
rename to app/scodoc/dutrules.py
diff --git a/gen_tables.py b/app/scodoc/gen_tables.py
similarity index 100%
rename from gen_tables.py
rename to app/scodoc/gen_tables.py
diff --git a/html_sco_header.py b/app/scodoc/html_sco_header.py
similarity index 100%
rename from html_sco_header.py
rename to app/scodoc/html_sco_header.py
diff --git a/html_sidebar.py b/app/scodoc/html_sidebar.py
similarity index 100%
rename from html_sidebar.py
rename to app/scodoc/html_sidebar.py
diff --git a/htmlutils.py b/app/scodoc/htmlutils.py
similarity index 100%
rename from htmlutils.py
rename to app/scodoc/htmlutils.py
diff --git a/imageresize.py b/app/scodoc/imageresize.py
similarity index 100%
rename from imageresize.py
rename to app/scodoc/imageresize.py
diff --git a/intervals.py b/app/scodoc/intervals.py
similarity index 100%
rename from intervals.py
rename to app/scodoc/intervals.py
diff --git a/listhistogram.py b/app/scodoc/listhistogram.py
similarity index 100%
rename from listhistogram.py
rename to app/scodoc/listhistogram.py
diff --git a/notes_cache.py b/app/scodoc/notes_cache.py
similarity index 100%
rename from notes_cache.py
rename to app/scodoc/notes_cache.py
diff --git a/notes_log.py b/app/scodoc/notes_log.py
similarity index 100%
rename from notes_log.py
rename to app/scodoc/notes_log.py
diff --git a/notes_table.py b/app/scodoc/notes_table.py
similarity index 100%
rename from notes_table.py
rename to app/scodoc/notes_table.py
diff --git a/notes_users.py b/app/scodoc/notes_users.py
similarity index 100%
rename from notes_users.py
rename to app/scodoc/notes_users.py
diff --git a/notesdb.py b/app/scodoc/notesdb.py
similarity index 100%
rename from notesdb.py
rename to app/scodoc/notesdb.py
diff --git a/pe_avislatex.py b/app/scodoc/pe_avislatex.py
similarity index 100%
rename from pe_avislatex.py
rename to app/scodoc/pe_avislatex.py
diff --git a/pe_jurype.py b/app/scodoc/pe_jurype.py
similarity index 100%
rename from pe_jurype.py
rename to app/scodoc/pe_jurype.py
diff --git a/pe_semestretag.py b/app/scodoc/pe_semestretag.py
similarity index 100%
rename from pe_semestretag.py
rename to app/scodoc/pe_semestretag.py
diff --git a/pe_settag.py b/app/scodoc/pe_settag.py
similarity index 100%
rename from pe_settag.py
rename to app/scodoc/pe_settag.py
diff --git a/pe_tagtable.py b/app/scodoc/pe_tagtable.py
similarity index 100%
rename from pe_tagtable.py
rename to app/scodoc/pe_tagtable.py
diff --git a/pe_tools.py b/app/scodoc/pe_tools.py
similarity index 100%
rename from pe_tools.py
rename to app/scodoc/pe_tools.py
diff --git a/pe_view.py b/app/scodoc/pe_view.py
similarity index 100%
rename from pe_view.py
rename to app/scodoc/pe_view.py
diff --git a/safehtml.py b/app/scodoc/safehtml.py
similarity index 100%
rename from safehtml.py
rename to app/scodoc/safehtml.py
diff --git a/sco_abs.py b/app/scodoc/sco_abs.py
similarity index 100%
rename from sco_abs.py
rename to app/scodoc/sco_abs.py
diff --git a/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py
similarity index 100%
rename from sco_abs_notification.py
rename to app/scodoc/sco_abs_notification.py
diff --git a/sco_abs_views.py b/app/scodoc/sco_abs_views.py
similarity index 100%
rename from sco_abs_views.py
rename to app/scodoc/sco_abs_views.py
diff --git a/sco_apogee_compare.py b/app/scodoc/sco_apogee_compare.py
similarity index 100%
rename from sco_apogee_compare.py
rename to app/scodoc/sco_apogee_compare.py
diff --git a/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
similarity index 100%
rename from sco_apogee_csv.py
rename to app/scodoc/sco_apogee_csv.py
diff --git a/sco_archives.py b/app/scodoc/sco_archives.py
similarity index 99%
rename from sco_archives.py
rename to app/scodoc/sco_archives.py
index 70a0553b0..2650f0e92 100644
--- a/sco_archives.py
+++ b/app/scodoc/sco_archives.py
@@ -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)
diff --git a/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py
similarity index 100%
rename from sco_archives_etud.py
rename to app/scodoc/sco_archives_etud.py
diff --git a/sco_bac.py b/app/scodoc/sco_bac.py
similarity index 100%
rename from sco_bac.py
rename to app/scodoc/sco_bac.py
diff --git a/sco_bulletins.py b/app/scodoc/sco_bulletins.py
similarity index 100%
rename from sco_bulletins.py
rename to app/scodoc/sco_bulletins.py
diff --git a/sco_bulletins_example.py b/app/scodoc/sco_bulletins_example.py
similarity index 100%
rename from sco_bulletins_example.py
rename to app/scodoc/sco_bulletins_example.py
diff --git a/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py
similarity index 100%
rename from sco_bulletins_generator.py
rename to app/scodoc/sco_bulletins_generator.py
diff --git a/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py
similarity index 100%
rename from sco_bulletins_json.py
rename to app/scodoc/sco_bulletins_json.py
diff --git a/sco_bulletins_legacy.py b/app/scodoc/sco_bulletins_legacy.py
similarity index 100%
rename from sco_bulletins_legacy.py
rename to app/scodoc/sco_bulletins_legacy.py
diff --git a/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py
similarity index 100%
rename from sco_bulletins_pdf.py
rename to app/scodoc/sco_bulletins_pdf.py
diff --git a/sco_bulletins_signature.py b/app/scodoc/sco_bulletins_signature.py
similarity index 100%
rename from sco_bulletins_signature.py
rename to app/scodoc/sco_bulletins_signature.py
diff --git a/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py
similarity index 100%
rename from sco_bulletins_standard.py
rename to app/scodoc/sco_bulletins_standard.py
diff --git a/sco_bulletins_ucac.py b/app/scodoc/sco_bulletins_ucac.py
similarity index 100%
rename from sco_bulletins_ucac.py
rename to app/scodoc/sco_bulletins_ucac.py
diff --git a/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py
similarity index 100%
rename from sco_bulletins_xml.py
rename to app/scodoc/sco_bulletins_xml.py
diff --git a/sco_cache.py b/app/scodoc/sco_cache.py
similarity index 100%
rename from sco_cache.py
rename to app/scodoc/sco_cache.py
diff --git a/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py
similarity index 100%
rename from sco_codes_parcours.py
rename to app/scodoc/sco_codes_parcours.py
diff --git a/sco_compute_moy.py b/app/scodoc/sco_compute_moy.py
similarity index 100%
rename from sco_compute_moy.py
rename to app/scodoc/sco_compute_moy.py
diff --git a/sco_config.py b/app/scodoc/sco_config.py
similarity index 100%
rename from sco_config.py
rename to app/scodoc/sco_config.py
diff --git a/sco_config_load.py b/app/scodoc/sco_config_load.py
similarity index 82%
rename from sco_config_load.py
rename to app/scodoc/sco_config_load.py
index b980c9d49..e91e2f203 100644
--- a/sco_config_load.py
+++ b/app/scodoc/sco_config_load.py
@@ -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
diff --git a/app/scodoc/sco_core.py b/app/scodoc/sco_core.py
new file mode 100644
index 000000000..8e8c2e5f2
--- /dev/null
+++ b/app/scodoc/sco_core.py
@@ -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 """%s
""" % scu.SCOVERSION
+
diff --git a/sco_cost_formation.py b/app/scodoc/sco_cost_formation.py
similarity index 100%
rename from sco_cost_formation.py
rename to app/scodoc/sco_cost_formation.py
diff --git a/sco_debouche.py b/app/scodoc/sco_debouche.py
similarity index 100%
rename from sco_debouche.py
rename to app/scodoc/sco_debouche.py
diff --git a/sco_dept.py b/app/scodoc/sco_dept.py
similarity index 100%
rename from sco_dept.py
rename to app/scodoc/sco_dept.py
diff --git a/sco_dump_db.py b/app/scodoc/sco_dump_db.py
similarity index 100%
rename from sco_dump_db.py
rename to app/scodoc/sco_dump_db.py
diff --git a/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py
similarity index 100%
rename from sco_edit_formation.py
rename to app/scodoc/sco_edit_formation.py
diff --git a/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py
similarity index 100%
rename from sco_edit_matiere.py
rename to app/scodoc/sco_edit_matiere.py
diff --git a/sco_edit_module.py b/app/scodoc/sco_edit_module.py
similarity index 100%
rename from sco_edit_module.py
rename to app/scodoc/sco_edit_module.py
diff --git a/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
similarity index 100%
rename from sco_edit_ue.py
rename to app/scodoc/sco_edit_ue.py
diff --git a/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py
similarity index 100%
rename from sco_edt_cal.py
rename to app/scodoc/sco_edt_cal.py
diff --git a/sco_entreprises.py b/app/scodoc/sco_entreprises.py
similarity index 100%
rename from sco_entreprises.py
rename to app/scodoc/sco_entreprises.py
diff --git a/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py
similarity index 100%
rename from sco_etape_apogee.py
rename to app/scodoc/sco_etape_apogee.py
diff --git a/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py
similarity index 100%
rename from sco_etape_apogee_view.py
rename to app/scodoc/sco_etape_apogee_view.py
diff --git a/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py
similarity index 100%
rename from sco_etape_bilan.py
rename to app/scodoc/sco_etape_bilan.py
diff --git a/sco_evaluations.py b/app/scodoc/sco_evaluations.py
similarity index 100%
rename from sco_evaluations.py
rename to app/scodoc/sco_evaluations.py
diff --git a/sco_excel.py b/app/scodoc/sco_excel.py
similarity index 100%
rename from sco_excel.py
rename to app/scodoc/sco_excel.py
diff --git a/sco_exceptions.py b/app/scodoc/sco_exceptions.py
similarity index 100%
rename from sco_exceptions.py
rename to app/scodoc/sco_exceptions.py
diff --git a/sco_export_results.py b/app/scodoc/sco_export_results.py
similarity index 100%
rename from sco_export_results.py
rename to app/scodoc/sco_export_results.py
diff --git a/sco_find_etud.py b/app/scodoc/sco_find_etud.py
similarity index 100%
rename from sco_find_etud.py
rename to app/scodoc/sco_find_etud.py
diff --git a/sco_formations.py b/app/scodoc/sco_formations.py
similarity index 100%
rename from sco_formations.py
rename to app/scodoc/sco_formations.py
diff --git a/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py
similarity index 100%
rename from sco_formsemestre.py
rename to app/scodoc/sco_formsemestre.py
diff --git a/sco_formsemestre_custommenu.py b/app/scodoc/sco_formsemestre_custommenu.py
similarity index 100%
rename from sco_formsemestre_custommenu.py
rename to app/scodoc/sco_formsemestre_custommenu.py
diff --git a/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
similarity index 100%
rename from sco_formsemestre_edit.py
rename to app/scodoc/sco_formsemestre_edit.py
diff --git a/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py
similarity index 100%
rename from sco_formsemestre_exterieurs.py
rename to app/scodoc/sco_formsemestre_exterieurs.py
diff --git a/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
similarity index 100%
rename from sco_formsemestre_inscriptions.py
rename to app/scodoc/sco_formsemestre_inscriptions.py
diff --git a/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
similarity index 100%
rename from sco_formsemestre_status.py
rename to app/scodoc/sco_formsemestre_status.py
diff --git a/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
similarity index 100%
rename from sco_formsemestre_validation.py
rename to app/scodoc/sco_formsemestre_validation.py
diff --git a/sco_formulas.py b/app/scodoc/sco_formulas.py
similarity index 100%
rename from sco_formulas.py
rename to app/scodoc/sco_formulas.py
diff --git a/sco_groups.py b/app/scodoc/sco_groups.py
similarity index 100%
rename from sco_groups.py
rename to app/scodoc/sco_groups.py
diff --git a/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py
similarity index 100%
rename from sco_groups_edit.py
rename to app/scodoc/sco_groups_edit.py
diff --git a/sco_groups_view.py b/app/scodoc/sco_groups_view.py
similarity index 100%
rename from sco_groups_view.py
rename to app/scodoc/sco_groups_view.py
diff --git a/sco_import_users.py b/app/scodoc/sco_import_users.py
similarity index 100%
rename from sco_import_users.py
rename to app/scodoc/sco_import_users.py
diff --git a/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py
similarity index 100%
rename from sco_inscr_passage.py
rename to app/scodoc/sco_inscr_passage.py
diff --git a/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py
similarity index 100%
rename from sco_liste_notes.py
rename to app/scodoc/sco_liste_notes.py
diff --git a/sco_lycee.py b/app/scodoc/sco_lycee.py
similarity index 100%
rename from sco_lycee.py
rename to app/scodoc/sco_lycee.py
diff --git a/sco_modalites.py b/app/scodoc/sco_modalites.py
similarity index 100%
rename from sco_modalites.py
rename to app/scodoc/sco_modalites.py
diff --git a/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py
similarity index 100%
rename from sco_moduleimpl.py
rename to app/scodoc/sco_moduleimpl.py
diff --git a/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py
similarity index 100%
rename from sco_moduleimpl_inscriptions.py
rename to app/scodoc/sco_moduleimpl_inscriptions.py
diff --git a/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py
similarity index 100%
rename from sco_moduleimpl_status.py
rename to app/scodoc/sco_moduleimpl_status.py
diff --git a/sco_news.py b/app/scodoc/sco_news.py
similarity index 100%
rename from sco_news.py
rename to app/scodoc/sco_news.py
diff --git a/sco_page_etud.py b/app/scodoc/sco_page_etud.py
similarity index 100%
rename from sco_page_etud.py
rename to app/scodoc/sco_page_etud.py
diff --git a/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py
similarity index 100%
rename from sco_parcours_dut.py
rename to app/scodoc/sco_parcours_dut.py
diff --git a/sco_pdf.py b/app/scodoc/sco_pdf.py
similarity index 100%
rename from sco_pdf.py
rename to app/scodoc/sco_pdf.py
diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py
new file mode 100644
index 000000000..9d8cf8933
--- /dev/null
+++ b/app/scodoc/sco_permissions.py
@@ -0,0 +1,56 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Definition of ScoDoc 8 permissions
+ used by auth
+"""
+# Définition des permissions: ne pas changer les numéros ou l'ordre des lignes !
+_SCO_PERMISSIONS = (
+ # permission bit, symbol, description
+ # ScoSuperAdmin est utilisé pour:
+ # - ZScoDoc: add/delete departments
+ # - tous rôles lors creation utilisateurs
+ (1 << 1, "ScoSuperAdmin", "Super Administrateur"),
+ (1 << 2, "ScoView", "Voir"),
+ (1 << 3, "ScoEnsView", "Voir les parties pour les enseignants"),
+ (1 << 4, "ScoObservateur", "Observer (accès lecture restreint aux bulletins)"),
+ (1 << 5, "ScoUsersAdmin", "Gérer les utilisateurs"),
+ (1 << 6, "ScoUsersView", "Voir les utilisateurs"),
+ (1 << 7, "ScoChangePreferences", "Modifier les préférences"),
+ (1 << 8, "ScoChangeFormation", "Changer les formations"),
+ (1 << 9, "ScoEditFormationTags", "Tagguer les formations"),
+ (1 << 10, "ScoEditAllNotes", "Modifier toutes les notes"),
+ (1 << 11, "ScoEditAllEvals", "Modifier toutes les evaluations"),
+ (1 << 12, "ScoImplement", "Mettre en place une formation (créer un semestre)"),
+ (1 << 13, "ScoAbsChange", "Saisir des absences"),
+ (1 << 14, "ScoAbsAddBillet", "Saisir des billets d'absences"),
+ # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
+ (1 << 15, "ScoEtudChangeAdr", "Changer les addresses d'étudiants"),
+ (1 << 16, "ScoEtudChangeGroups", "Modifier les groupes"),
+ # aussi pour demissions, diplomes:
+ (1 << 17, "ScoEtudInscrit", "Inscrire des étudiants"),
+ # aussi pour archives:
+ (1 << 18, "ScoEtudAddAnnotations", "Éditer les annotations"),
+ (1 << 19, "ScoEntrepriseView", "Voir la section 'entreprises'"),
+ (1 << 20, "ScoEntrepriseChange", "Modifier les entreprises"),
+ (1 << 21, "ScoEditPVJury", "Éditer les PV de jury"),
+ # ajouter maquettes Apogee (=> chef dept et secr):
+ (1 << 22, "ScoEditApo", "Ajouter des maquettes Apogées"),
+)
+
+
+class Permission:
+ "Permissions for ScoDoc"
+ NBITS = 1 # maximum bits used (for formatting)
+ ALL_PERMISSIONS = [-1]
+ description = {} # { symbol : blah blah }
+
+ @staticmethod
+ def init_permissions():
+ for (perm, symbol, description) in _SCO_PERMISSIONS:
+ setattr(Permission, symbol, perm)
+ Permission.description[symbol] = description
+ Permission.NBITS = len(_SCO_PERMISSIONS)
+
+
+Permission.init_permissions()
diff --git a/sco_photos.py b/app/scodoc/sco_photos.py
similarity index 99%
rename from sco_photos.py
rename to app/scodoc/sco_photos.py
index 9b48b9d14..d6ecb9188 100644
--- a/sco_photos.py
+++ b/app/scodoc/sco_photos.py
@@ -53,6 +53,7 @@ from PIL import Image as PILImage
from cStringIO import StringIO
import glob
+from config import Config
from sco_utils import CONFIG, SCO_SRC_DIR
from notes_log import log
@@ -61,7 +62,7 @@ import sco_portal_apogee
from scolog import logdb
# Full paths on server's filesystem. Something like "/opt/scodoc/var/scodoc/photos"
-PHOTO_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc", "photos")
+PHOTO_DIR = os.path.join(Config.INSTANCE_HOME, "var", "scodoc", "photos")
ICONS_DIR = os.path.join(SCO_SRC_DIR, "static", "icons")
UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg")
UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown face image
diff --git a/sco_placement.py b/app/scodoc/sco_placement.py
similarity index 100%
rename from sco_placement.py
rename to app/scodoc/sco_placement.py
diff --git a/sco_portal_apogee.py b/app/scodoc/sco_portal_apogee.py
similarity index 100%
rename from sco_portal_apogee.py
rename to app/scodoc/sco_portal_apogee.py
diff --git a/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py
similarity index 100%
rename from sco_poursuite_dut.py
rename to app/scodoc/sco_poursuite_dut.py
diff --git a/sco_preferences.py b/app/scodoc/sco_preferences.py
similarity index 100%
rename from sco_preferences.py
rename to app/scodoc/sco_preferences.py
diff --git a/sco_prepajury.py b/app/scodoc/sco_prepajury.py
similarity index 100%
rename from sco_prepajury.py
rename to app/scodoc/sco_prepajury.py
diff --git a/sco_pvjury.py b/app/scodoc/sco_pvjury.py
similarity index 100%
rename from sco_pvjury.py
rename to app/scodoc/sco_pvjury.py
diff --git a/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py
similarity index 100%
rename from sco_pvpdf.py
rename to app/scodoc/sco_pvpdf.py
diff --git a/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
similarity index 100%
rename from sco_recapcomplet.py
rename to app/scodoc/sco_recapcomplet.py
diff --git a/sco_report.py b/app/scodoc/sco_report.py
similarity index 100%
rename from sco_report.py
rename to app/scodoc/sco_report.py
diff --git a/app/scodoc/sco_roles_default.py b/app/scodoc/sco_roles_default.py
new file mode 100644
index 000000000..406daee19
--- /dev/null
+++ b/app/scodoc/sco_roles_default.py
@@ -0,0 +1,58 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Definition of ScoDoc default roles
+"""
+
+from sco_permissions import Permission as p
+
+SCO_ROLES_DEFAULTS = {
+ "Observateur": (p.ScoObservateur,),
+ "Ens": (
+ p.ScoObservateur,
+ p.ScoView,
+ p.ScoEnsView,
+ p.ScoUsersView,
+ p.ScoEtudAddAnnotations,
+ p.ScoAbsChange,
+ p.ScoAbsAddBillet,
+ p.ScoEntrepriseView,
+ ),
+ "Secr": (
+ p.ScoObservateur,
+ p.ScoView,
+ p.ScoUsersView,
+ p.ScoEtudAddAnnotations,
+ p.ScoAbsChange,
+ p.ScoAbsAddBillet,
+ p.ScoEntrepriseView,
+ p.ScoEntrepriseChange,
+ p.ScoEtudChangeAdr,
+ ),
+ # Admin est le chef du département, pas le "super admin"
+ # on dit donc lister toutes ses permissions:
+ "Admin": (
+ p.ScoObservateur,
+ p.ScoView,
+ p.ScoEnsView,
+ p.ScoUsersView,
+ p.ScoEtudAddAnnotations,
+ p.ScoAbsChange,
+ p.ScoAbsAddBillet,
+ p.ScoEntrepriseView,
+ p.ScoEntrepriseChange,
+ p.ScoEtudChangeAdr,
+ p.ScoChangeFormation,
+ p.ScoEditFormationTags,
+ p.ScoEditAllNotes,
+ p.ScoEditAllEvals,
+ p.ScoImplement,
+ p.ScoEtudChangeGroups,
+ p.ScoEtudInscrit,
+ p.ScoUsersAdmin,
+ p.ScoChangePreferences,
+ ),
+ # Super Admin est un root: création/suppression de départements
+ # _tous_ les droits
+ "SuperAdmin": p.ALL_PERMISSIONS,
+}
\ No newline at end of file
diff --git a/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
similarity index 100%
rename from sco_saisie_notes.py
rename to app/scodoc/sco_saisie_notes.py
diff --git a/sco_semset.py b/app/scodoc/sco_semset.py
similarity index 100%
rename from sco_semset.py
rename to app/scodoc/sco_semset.py
diff --git a/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py
similarity index 100%
rename from sco_synchro_etuds.py
rename to app/scodoc/sco_synchro_etuds.py
diff --git a/sco_tag_module.py b/app/scodoc/sco_tag_module.py
similarity index 100%
rename from sco_tag_module.py
rename to app/scodoc/sco_tag_module.py
diff --git a/sco_trombino.py b/app/scodoc/sco_trombino.py
similarity index 100%
rename from sco_trombino.py
rename to app/scodoc/sco_trombino.py
diff --git a/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py
similarity index 100%
rename from sco_trombino_tours.py
rename to app/scodoc/sco_trombino_tours.py
diff --git a/sco_ue_external.py b/app/scodoc/sco_ue_external.py
similarity index 100%
rename from sco_ue_external.py
rename to app/scodoc/sco_ue_external.py
diff --git a/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py
similarity index 100%
rename from sco_undo_notes.py
rename to app/scodoc/sco_undo_notes.py
diff --git a/sco_up_to_date.py b/app/scodoc/sco_up_to_date.py
similarity index 100%
rename from sco_up_to_date.py
rename to app/scodoc/sco_up_to_date.py
diff --git a/sco_users.py b/app/scodoc/sco_users.py
similarity index 100%
rename from sco_users.py
rename to app/scodoc/sco_users.py
diff --git a/sco_utils.py b/app/scodoc/sco_utils.py
similarity index 96%
rename from sco_utils.py
rename to app/scodoc/sco_utils.py
index 022a3075d..4a78f7783 100644
--- a/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -58,6 +58,8 @@ from PIL import Image as PILImage
from VERSION import SCOVERSION
import VERSION
+from config import Config
+
from SuppressAccents import suppression_diacritics
from notes_log import log
from sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL
@@ -216,32 +218,31 @@ def group_by_key(d, key):
# ----- Global lock for critical sections (except notes_tables caches)
GSL = thread.allocate_lock() # Global ScoDoc Lock
-if "INSTANCE_HOME" in os.environ:
- # ----- Repertoire "var" (local)
- SCODOC_VAR_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc")
- # ----- Repertoire "config" modifiable
- # /opt/scodoc/var/scodoc/config
- SCODOC_CFG_DIR = os.path.join(SCODOC_VAR_DIR, "config")
- # ----- Version information
- SCODOC_VERSION_DIR = os.path.join(SCODOC_CFG_DIR, "version")
- # ----- Repertoire tmp
- SCO_TMP_DIR = os.path.join(SCODOC_VAR_DIR, "tmp")
- if not os.path.exists(SCO_TMP_DIR):
- os.mkdir(SCO_TMP_DIR, 0o755)
- # ----- Les logos: /opt/scodoc/var/scodoc/config/logos
- SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos")
+# ----- Repertoire "var" (local)
+SCODOC_VAR_DIR = os.path.join(Config.INSTANCE_HOME, "var", "scodoc")
+# ----- Repertoire "config" modifiable
+# /opt/scodoc/var/scodoc/config
+SCODOC_CFG_DIR = os.path.join(SCODOC_VAR_DIR, "config")
+# ----- Version information
+SCODOC_VERSION_DIR = os.path.join(SCODOC_CFG_DIR, "version")
+# ----- Repertoire tmp
+SCO_TMP_DIR = os.path.join(SCODOC_VAR_DIR, "tmp")
+if not os.path.exists(SCO_TMP_DIR):
+ os.mkdir(SCO_TMP_DIR, 0o755)
+# ----- Les logos: /opt/scodoc/var/scodoc/config/logos
+SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos")
- # Dans les sources:
- SCO_SRC_DIR = os.path.join(os.environ["INSTANCE_HOME"], "Products", "ScoDoc")
- # - Les outils distribués
- SCO_TOOLS_DIR = os.path.join(SCO_SRC_DIR, "config")
+# Dans les sources:
+SCO_SRC_DIR = os.path.join(Config.INSTANCE_HOME, "Products", "ScoDoc")
+# - Les outils distribués
+SCO_TOOLS_DIR = os.path.join(SCO_SRC_DIR, "config")
# ----- Lecture du fichier de configuration
import sco_config
import sco_config_load
-sco_config_load.load_local_configuration()
+sco_config_load.load_local_configuration(SCODOC_CFG_DIR)
CONFIG = sco_config.CONFIG
if hasattr(CONFIG, "CODES_EXPL"):
CODES_EXPL.update(
diff --git a/sco_zope.py b/app/scodoc/sco_zope.py
similarity index 100%
rename from sco_zope.py
rename to app/scodoc/sco_zope.py
diff --git a/scolars.py b/app/scodoc/scolars.py
similarity index 100%
rename from scolars.py
rename to app/scodoc/scolars.py
diff --git a/scolog.py b/app/scodoc/scolog.py
similarity index 100%
rename from scolog.py
rename to app/scodoc/scolog.py
diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html
new file mode 100644
index 000000000..6b775b738
--- /dev/null
+++ b/app/templates/auth/login.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block app_content %}
+Sign In
+
+
+ {{ wtf.quick_form(form) }}
+
+
+
+Forgot Your Password?
+Click to Reset It
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html
new file mode 100644
index 000000000..35e6a2abd
--- /dev/null
+++ b/app/templates/auth/register.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block app_content %}
+Création utilisateur
+
+
+ {{ wtf.quick_form(form) }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/auth/reset_password.html b/app/templates/auth/reset_password.html
new file mode 100644
index 000000000..d054674f6
--- /dev/null
+++ b/app/templates/auth/reset_password.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block app_content %}
+Reset Your Password
+
+
+ {{ wtf.quick_form(form) }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/auth/reset_password_request.html b/app/templates/auth/reset_password_request.html
new file mode 100644
index 000000000..6fc7329f3
--- /dev/null
+++ b/app/templates/auth/reset_password_request.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block app_content %}
+Reset Password
+
+
+ {{ wtf.quick_form(form) }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/base.html b/app/templates/base.html
new file mode 100644
index 000000000..15a9ee46b
--- /dev/null
+++ b/app/templates/base.html
@@ -0,0 +1,60 @@
+{% extends 'bootstrap/base.html' %}
+
+{% block title %}
+{% if title %}{{ title }} - ScoDoc{% else %}Welcome to ScoDoc{% endif %}
+{% endblock %}
+
+{% block navbar %}
+
+{% endblock %}
+
+{% block content %}
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+ {# application content needs to be provided in the app_content block #}
+ {% block app_content %}{% endblock %}
+
+{% endblock %}
+
+{% block scripts %}
+{{ super() }}
+{{ moment.include_moment() }}
+{{ moment.lang(g.locale) }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/email/reset_password.html b/app/templates/email/reset_password.html
new file mode 100644
index 000000000..e928c6c9f
--- /dev/null
+++ b/app/templates/email/reset_password.html
@@ -0,0 +1,16 @@
+Bonjour {{ user.username }},
+
+ Pour réinitialiser votre mot de passe ScoDoc,
+
+ cliquez sur ce lien
+ .
+
+Vous pouvez aussi copier ce lien dans votre navigateur Web::
+{{ url_for('auth.reset_password', token=token, _external=True) }}
+
+Si vous n'avez pas demandé à réinitialiser votre mot de passe sur
+ ScoDoc, vous pouvez simplement ignorer ce message.
+
+
+
+A bientôt !
\ No newline at end of file
diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt
new file mode 100644
index 000000000..ea5f14fda
--- /dev/null
+++ b/app/templates/email/reset_password.txt
@@ -0,0 +1,12 @@
+Bonjour {{ user.username }},
+
+Pour réinitialiser votre mot de passe ScoDoc, suivre le lien:
+
+{{ url_for('auth.reset_password', token=token, _external=True) }}
+
+
+Si vous n'avez pas demandé à réinitialiser votre mot de passe sur
+ScoDoc, vous pouvez simplement ignorer ce message.
+
+A bientôt !
+
diff --git a/app/templates/main/index.html b/app/templates/main/index.html
new file mode 100644
index 000000000..efee516ed
--- /dev/null
+++ b/app/templates/main/index.html
@@ -0,0 +1,43 @@
+{% extends 'base.html' %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block app_content %}
+Essais Flask pour ScoDoc 8: accueil
+
+
Avec login requis
+
+
Sans login
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/views/__init__.py b/app/views/__init__.py
new file mode 100644
index 000000000..a42092477
--- /dev/null
+++ b/app/views/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: UTF-8 -*
+"""ScoDoc Flask views
+"""
+from flask import Blueprint
+
+notes_bp = Blueprint("notes", __name__)
+
+from app.views import notes
diff --git a/app/views/notes.py b/app/views/notes.py
new file mode 100644
index 000000000..f2aa981d8
--- /dev/null
+++ b/app/views/notes.py
@@ -0,0 +1,61 @@
+# -*- coding: UTF-8 -*
+"""Module scodoc: un exemple de fonctions
+"""
+from flask import g
+from flask import current_app
+
+from app.decorators import (
+ scodoc7func,
+ ScoDoc7Context,
+ permission_required,
+ admin_required,
+ login_required,
+)
+from app.auth.models import Permission
+
+from app.views import notes_bp as bp
+
+# import sco_core deviendra:
+from app.ScoDoc import sco_core
+
+context = ScoDoc7Context(globals())
+
+
+@bp.route("//Scolarite/sco_exemple")
+@scodoc7func
+def sco_exemple(etudid="NON"):
+ """Un exemple de fonction ScoDoc 7"""
+ return """
+ ScoDoc 7 rules !
+ etudid=%(etudid)s
+ g.scodoc_dept=%(scodoc_dept)s
+
+
+ """ % {
+ "etudid": etudid,
+ "scodoc_dept": g.scodoc_dept,
+ }
+
+
+# En ScoDoc 7, on a souvent des vues qui en appellent d'autres
+# avec context.sco_exemple( etudid="E12" )
+@bp.route("//Scolarite/sco_exemple2")
+@login_required
+@scodoc7func
+def sco_exemple2():
+ return "Exemple 2" + context.sco_exemple(etudid="deux")
+
+
+# Test avec un seul argument REQUEST positionnel
+@bp.route("//Scolarite/sco_get_version")
+@scodoc7func
+def sco_get_version(REQUEST):
+ return sco_core.sco_get_version(REQUEST)
+
+
+# Fonction ressemblant à une méthode Zope protégée
+@bp.route("//Scolarite/sco_test_view")
+@scodoc7func
+@permission_required(Permission.ScoView)
+def sco_test_view(REQUEST=None):
+ return """Vous avez vu sco_test_view !"""
diff --git a/config.py b/config.py
new file mode 100644
index 000000000..d6db8dd9d
--- /dev/null
+++ b/config.py
@@ -0,0 +1,31 @@
+# -*- coding: UTF-8 -*
+
+import os
+from dotenv import load_dotenv
+
+BASEDIR = os.path.abspath(os.path.dirname(__file__))
+load_dotenv(os.path.join(BASEDIR, ".env"))
+
+
+class Config(object):
+ """General configution. Mostly loaded from environment via .env"""
+
+ SECRET_KEY = os.environ.get("SECRET_KEY") or "un-grand-secret-introuvable"
+ SQLALCHEMY_DATABASE_URI = (
+ os.environ.get("DATABASE_URL") or "postgresql://scodoc@localhost:5432/SCO8USERS"
+ )
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
+ LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT")
+ MAIL_SERVER = os.environ.get("MAIL_SERVER")
+ MAIL_PORT = int(os.environ.get("MAIL_PORT") or 25)
+ MAIL_USE_TLS = os.environ.get("MAIL_USE_TLS") is not None
+ MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
+ MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
+ LANGUAGES = ["fr", "en"] # unused for now
+ SCODOC_ADMIN_MAIL = os.environ.get("SCODOC_ADMIN_MAIL")
+ SCODOC_ADMIN_LOGIN = os.environ.get("SCODOC_ADMIN_LOGIN") or "admin"
+ ADMINS = [SCODOC_ADMIN_MAIL]
+ SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_MAIL")
+ BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL")
+ # for ScoDoc 7 compat (à changer)
+ INSTANCE_HOME = os.environ.get("INSTANCE_HOME", "/opt/scodoc")
\ No newline at end of file
diff --git a/dtml/docLogin.dtml b/dtml/docLogin.dtml
deleted file mode 100644
index fd46ec9f8..000000000
--- a/dtml/docLogin.dtml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Added by Emmanuel for ScoDoc
-
-
-
-
-Ok ">
-
-
-
-
-
-
diff --git a/dtml/docLogout.dtml b/dtml/docLogout.dtml
deleted file mode 100644
index 5210fc745..000000000
--- a/dtml/docLogout.dtml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-Vous êtes déconnecté de ScoDoc.
-
-
-">revenir à l'accueil
-
-
-(Attention: si vous êtes administrateur, vous ne pouvez vous déconnecter complètement qu'en relançant votre navigateur)
-
-
-
-
-
-
diff --git a/dtml/manage_addZNotesForm.dtml b/dtml/manage_addZNotesForm.dtml
deleted file mode 100644
index e58b49e02..000000000
--- a/dtml/manage_addZNotesForm.dtml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-