Merge pull request 'master' (#2) from ScoDoc/ScoDoc:master into master

Reviewed-on: lehmann/ScoDoc-Front#2
This commit is contained in:
Sébastien Lehmann 2021-12-21 15:18:31 +01:00
commit e0edde3f46
22 changed files with 258 additions and 108 deletions

View File

@ -1,6 +1,7 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
# Authentication code borrowed from Miguel Grinberg's Mega Tutorial # Authentication code borrowed from Miguel Grinberg's Mega Tutorial
# (see https://github.com/miguelgrinberg/microblog) # (see https://github.com/miguelgrinberg/microblog)
# and modified for ScoDoc
# Under The MIT License (MIT) # Under The MIT License (MIT)
@ -23,6 +24,7 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from flask import g
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from app.auth.models import User from app.auth.models import User
from app.api.errors import error_response from app.api.errors import error_response
@ -35,6 +37,7 @@ token_auth = HTTPTokenAuth()
def verify_password(username, password): def verify_password(username, password):
user = User.query.filter_by(user_name=username).first() user = User.query.filter_by(user_name=username).first()
if user and user.check_password(password): if user and user.check_password(password):
g.current_user = user
return user return user
@ -45,7 +48,9 @@ def basic_auth_error(status):
@token_auth.verify_token @token_auth.verify_token
def verify_token(token): def verify_token(token):
return User.check_token(token) if token else None user = User.check_token(token) if token else None
g.current_user = user
return user
@token_auth.error_handler @token_auth.error_handler
@ -53,15 +58,20 @@ def token_auth_error(status):
return error_response(status) return error_response(status)
def token_permission_required(permission): @token_auth.get_user_roles
def decorator(f): def get_user_roles(user):
@wraps(f) return user.roles
def decorated_function(*args, **kwargs):
scodoc_dept = getattr(g, "scodoc_dept", None)
if not current_user.has_permission(permission, scodoc_dept):
abort(403)
return f(*args, **kwargs)
return login_required(decorated_function)
return decorator # def token_permission_required(permission):
# def decorator(f):
# @wraps(f)
# def decorated_function(*args, **kwargs):
# scodoc_dept = getattr(g, "scodoc_dept", None)
# if not current_user.has_permission(permission, scodoc_dept):
# abort(403)
# return f(*args, **kwargs)
# return login_required(decorated_function)
# return decorator

View File

@ -39,13 +39,18 @@
# Scolarite/Notes/moduleimpl_status # Scolarite/Notes/moduleimpl_status
# Scolarite/setGroups # Scolarite/setGroups
from flask import jsonify, request, url_for, abort from flask import jsonify, request, url_for, abort, g
from app import db from flask_login import current_user
from sqlalchemy.sql import func
from app import db, log
from app.api import bp from app.api import bp
from app.api.auth import token_auth from app.api.auth import token_auth
from app.api.errors import bad_request from app.api.errors import bad_request, error_response
from app.decorators import permission_required
from app import models from app import models
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.scodoc.sco_permissions import Permission
@bp.route("list_depts", methods=["GET"]) @bp.route("list_depts", methods=["GET"])
@ -54,3 +59,23 @@ def list_depts():
depts = models.Departement.query.filter_by(visible=True).all() depts = models.Departement.query.filter_by(visible=True).all()
data = [d.to_dict() for d in depts] data = [d.to_dict() for d in depts]
return jsonify(data) return jsonify(data)
@bp.route("/etudiants/courant", methods=["GET"])
@token_auth.login_required
def etudiants():
"""Liste de tous les étudiants actuellement inscrits à un semestre
en cours.
"""
# Vérification de l'accès: permission Observateir sur tous les départements
# (c'est un exemple à compléter)
if not g.current_user.has_permission(Permission.ScoObservateur, None):
return error_response(401, message="accès interdit")
query = db.session.query(Identite).filter(
FormSemestreInscription.formsemestre_id == FormSemestre.id,
FormSemestreInscription.etudid == Identite.id,
FormSemestre.date_debut <= func.now(),
FormSemestre.date_fin >= func.now(),
)
return jsonify([e.to_dict_bul(include_photo=False) for e in query])

View File

@ -65,7 +65,7 @@ class User(UserMixin, db.Model):
date_created = db.Column(db.DateTime, default=datetime.utcnow) date_created = db.Column(db.DateTime, default=datetime.utcnow)
date_expiration = db.Column(db.DateTime, default=None) date_expiration = db.Column(db.DateTime, default=None)
passwd_temp = db.Column(db.Boolean, default=False) passwd_temp = db.Column(db.Boolean, default=False)
token = db.Column(db.String(32), index=True, unique=True) token = db.Column(db.Text(), index=True, unique=True)
token_expiration = db.Column(db.DateTime) token_expiration = db.Column(db.DateTime)
roles = db.relationship("Role", secondary="user_role", viewonly=True) roles = db.relationship("Role", secondary="user_role", viewonly=True)
@ -272,7 +272,7 @@ class User(UserMixin, db.Model):
"""string repr. of user's roles (with depts) """string repr. of user's roles (with depts)
e.g. "Ens_RT, Ens_Info, Secr_CJ" e.g. "Ens_RT, Ens_Info, Secr_CJ"
""" """
return ",".join(f"{r.role.name}_{r.dept or ''}" for r in self.user_roles) return ",".join(f"{r.role.name or ''}_{r.dept or ''}" for r in self.user_roles)
def is_administrator(self): def is_administrator(self):
"True if i'm an active SuperAdmin" "True if i'm an active SuperAdmin"

View File

@ -25,7 +25,6 @@ class ResultatsSemestreBUT:
"""Structure légère pour stocker les résultats du semestre et """Structure légère pour stocker les résultats du semestre et
générer les bulletins. générer les bulletins.
__init__ : charge depuis le cache ou calcule __init__ : charge depuis le cache ou calcule
invalidate(): invalide données cachées
""" """
_cached_attrs = ( _cached_attrs = (

View File

@ -35,12 +35,12 @@ from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tag
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.models.formsemestre import ( from app.models.formsemestre import (
FormSemestre, FormSemestre,
FormsemestreEtape, FormSemestreEtape,
FormationModalite, FormationModalite,
FormsemestreUECoef, FormSemestreUECoef,
FormsemestreUEComputationExpr, FormSemestreUEComputationExpr,
FormsemestreCustomMenu, FormSemestreCustomMenu,
FormsemestreInscription, FormSemestreInscription,
notes_formsemestre_responsables, notes_formsemestre_responsables,
NotesSemSet, NotesSemSet,
notes_semset_formsemestre, notes_semset_formsemestre,
@ -57,7 +57,7 @@ from app.models.evaluations import (
from app.models.groups import Partition, GroupDescr, group_membership from app.models.groups import Partition, GroupDescr, group_membership
from app.models.notes import ( from app.models.notes import (
ScolarEvent, ScolarEvent,
ScolarFormsemestreValidation, ScolarFormSemestreValidation,
ScolarAutorisationInscription, ScolarAutorisationInscription,
BulAppreciations, BulAppreciations,
NotesNotes, NotesNotes,

View File

@ -9,7 +9,6 @@ from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.scodoc import sco_photos
class Identite(db.Model): class Identite(db.Model):
@ -71,12 +70,14 @@ class Identite(db.Model):
"le mail associé à la première adrese de l'étudiant, ou None" "le mail associé à la première adrese de l'étudiant, ou None"
return self.adresses[0].email or None if self.adresses.count() > 0 else None return self.adresses[0].email or None if self.adresses.count() > 0 else None
def to_dict_bul(self): def to_dict_bul(self, include_photo=True):
"""Infos exportées dans les bulletins""" """Infos exportées dans les bulletins"""
return { from app.scodoc import sco_photos
d = {
"civilite": self.civilite, "civilite": self.civilite,
"code_ine": self.code_nip, "code_ine": self.code_ine,
"code_nip": self.code_ine, "code_nip": self.code_nip,
"date_naissance": self.date_naissance.isoformat() "date_naissance": self.date_naissance.isoformat()
if self.date_naissance if self.date_naissance
else None, else None,
@ -84,9 +85,11 @@ class Identite(db.Model):
"emailperso": self.get_first_email("emailperso"), "emailperso": self.get_first_email("emailperso"),
"etudid": self.id, "etudid": self.id,
"nom": self.nom_disp(), "nom": self.nom_disp(),
"photo_url": sco_photos.get_etud_photo_url(self.id),
"prenom": self.prenom, "prenom": self.prenom,
} }
if include_photo:
d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),)
return d
def inscription_courante(self): def inscription_courante(self):
"""La première inscription à un formsemestre _actuellement_ en cours. """La première inscription à un formsemestre _actuellement_ en cours.
@ -104,7 +107,7 @@ class Identite(db.Model):
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
""" """
# voir si ce n'est pas trop lent: # voir si ce n'est pas trop lent:
ins = models.FormsemestreInscription.query.filter_by( ins = models.FormSemestreInscription.query.filter_by(
etudid=self.id, formsemestre_id=formsemestre_id etudid=self.id, formsemestre_id=formsemestre_id
).first() ).first()
if ins: if ins:

View File

@ -82,7 +82,7 @@ class FormSemestre(db.Model):
# Relations: # Relations:
etapes = db.relationship( etapes = db.relationship(
"FormsemestreEtape", cascade="all,delete", backref="formsemestre" "FormSemestreEtape", cascade="all,delete", backref="formsemestre"
) )
modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic") modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic")
etuds = db.relationship( etuds = db.relationship(
@ -119,7 +119,7 @@ class FormSemestre(db.Model):
return d return d
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery: def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
"""UE des modules de ce semestre. """UE des modules de ce semestre, triées par numéro.
- Formations classiques: les UEs auxquelles appartiennent - Formations classiques: les UEs auxquelles appartiennent
les modules mis en place dans ce semestre. les modules mis en place dans ce semestre.
- Formations APC / BUT: les UEs de la formation qui ont - Formations APC / BUT: les UEs de la formation qui ont
@ -262,7 +262,7 @@ notes_formsemestre_responsables = db.Table(
) )
class FormsemestreEtape(db.Model): class FormSemestreEtape(db.Model):
"""Étape Apogée associées au semestre""" """Étape Apogée associées au semestre"""
__tablename__ = "notes_formsemestre_etapes" __tablename__ = "notes_formsemestre_etapes"
@ -331,7 +331,7 @@ class FormationModalite(db.Model):
raise raise
class FormsemestreUECoef(db.Model): class FormSemestreUECoef(db.Model):
"""Coef des UE capitalisees arrivant dans ce semestre""" """Coef des UE capitalisees arrivant dans ce semestre"""
__tablename__ = "notes_formsemestre_uecoef" __tablename__ = "notes_formsemestre_uecoef"
@ -350,7 +350,7 @@ class FormsemestreUECoef(db.Model):
coefficient = db.Column(db.Float, nullable=False) coefficient = db.Column(db.Float, nullable=False)
class FormsemestreUEComputationExpr(db.Model): class FormSemestreUEComputationExpr(db.Model):
"""Formules utilisateurs pour calcul moyenne UE""" """Formules utilisateurs pour calcul moyenne UE"""
__tablename__ = "notes_formsemestre_ue_computation_expr" __tablename__ = "notes_formsemestre_ue_computation_expr"
@ -370,7 +370,7 @@ class FormsemestreUEComputationExpr(db.Model):
computation_expr = db.Column(db.Text()) computation_expr = db.Column(db.Text())
class FormsemestreCustomMenu(db.Model): class FormSemestreCustomMenu(db.Model):
"""Menu custom associe au semestre""" """Menu custom associe au semestre"""
__tablename__ = "notes_formsemestre_custommenu" __tablename__ = "notes_formsemestre_custommenu"
@ -386,7 +386,7 @@ class FormsemestreCustomMenu(db.Model):
idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu
class FormsemestreInscription(db.Model): class FormSemestreInscription(db.Model):
"""Inscription à un semestre de formation""" """Inscription à un semestre de formation"""
__tablename__ = "notes_formsemestre_inscription" __tablename__ = "notes_formsemestre_inscription"
@ -410,7 +410,7 @@ class FormsemestreInscription(db.Model):
backref=db.backref( backref=db.backref(
"inscriptions", "inscriptions",
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="FormsemestreInscription.etudid", order_by="FormSemestreInscription.etudid",
), ),
) )
# I inscrit, D demission en cours de semestre, DEF si "defaillant" # I inscrit, D demission en cours de semestre, DEF si "defaillant"

View File

@ -40,7 +40,7 @@ class ScolarEvent(db.Model):
) )
class ScolarFormsemestreValidation(db.Model): class ScolarFormSemestreValidation(db.Model):
"""Décisions de jury""" """Décisions de jury"""
__tablename__ = "scolar_formsemestre_validation" __tablename__ = "scolar_formsemestre_validation"

View File

@ -32,7 +32,7 @@ from flask_login import current_user
from app import db from app import db
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl
from app.models.notes import ScolarFormsemestreValidation from app.models.notes import ScolarFormSemestreValidation
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -152,7 +152,7 @@ def html_ue_infos(ue):
) )
.all() .all()
) )
nb_etuds_valid_ue = ScolarFormsemestreValidation.query.filter_by( nb_etuds_valid_ue = ScolarFormSemestreValidation.query.filter_by(
ue_id=ue.id ue_id=ue.id
).count() ).count()
can_safely_be_suppressed = ( can_safely_be_suppressed = (

View File

@ -56,7 +56,7 @@ from app.scodoc.htmlutils import histogram_notes
def do_evaluation_listenotes( def do_evaluation_listenotes(
evaluation_id=None, moduleimpl_id=None, format="html" evaluation_id=None, moduleimpl_id=None, format="html"
) -> str: ) -> tuple[str, str]:
""" """
Affichage des notes d'une évaluation (si evaluation_id) Affichage des notes d'une évaluation (si evaluation_id)
ou de toutes les évaluations d'un module (si moduleimpl_id) ou de toutes les évaluations d'un module (si moduleimpl_id)
@ -71,7 +71,7 @@ def do_evaluation_listenotes(
else: else:
raise ValueError("missing argument: evaluation or module") raise ValueError("missing argument: evaluation or module")
if not evals: if not evals:
return "<p>Aucune évaluation !</p>" return "<p>Aucune évaluation !</p>", f"ScoDoc"
E = evals[0] # il y a au moins une evaluation E = evals[0] # il y a au moins une evaluation
modimpl = ModuleImpl.query.get(E["moduleimpl_id"]) modimpl = ModuleImpl.query.get(E["moduleimpl_id"])
@ -189,9 +189,12 @@ def do_evaluation_listenotes(
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1], page_title return "\n".join(H) + "\n" + tf[1], page_title
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return (
"%s/Notes/moduleimpl_status?moduleimpl_id=%s" flask.redirect(
% (scu.ScoURL(), E["moduleimpl_id"]) "%s/Notes/moduleimpl_status?moduleimpl_id=%s"
% (scu.ScoURL(), E["moduleimpl_id"])
),
"",
) )
else: else:
anonymous_listing = tf[2]["anonymous_listing"] anonymous_listing = tf[2]["anonymous_listing"]

View File

@ -42,9 +42,6 @@ Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx
- support for legacy ZODB removed in v1909. - support for legacy ZODB removed in v1909.
""" """
from flask.helpers import make_response, url_for
from app.scodoc.sco_exceptions import ScoGenError
import datetime import datetime
import glob import glob
import io import io
@ -52,24 +49,26 @@ import os
import random import random
import requests import requests
import time import time
import traceback
import PIL import PIL
from PIL import Image as PILImage from PIL import Image as PILImage
from flask import request, g from flask import request, g
from flask.helpers import make_response, url_for
from config import Config from app import log
from app import db
from app.models import Identite
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app import log from app.scodoc.sco_exceptions import ScoGenError
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from config import Config
# Full paths on server's filesystem. Something like "/opt/scodoc/var/scodoc/photos" # Full paths on server's filesystem. Something like "/opt/scodoc-data/photos"
PHOTO_DIR = os.path.join(Config.SCODOC_VAR_DIR, "photos") PHOTO_DIR = os.path.join(Config.SCODOC_VAR_DIR, "photos")
ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons") ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons")
UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg") UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg")
@ -97,14 +96,15 @@ def get_etud_photo_url(etudid, size="small"):
) )
def etud_photo_url(etud, size="small", fast=False): def etud_photo_url(etud: dict, size="small", fast=False) -> str:
"""url to the image of the student, in "small" size or "orig" size. """url to the image of the student, in "small" size or "orig" size.
If ScoDoc doesn't have an image and a portal is configured, link to it. If ScoDoc doesn't have an image and a portal is configured, link to it.
""" """
photo_url = get_etud_photo_url(etud["etudid"], size=size) photo_url = get_etud_photo_url(etud["etudid"], size=size)
if fast: if fast:
return photo_url return photo_url
path = photo_pathname(etud, size=size) path = photo_pathname(etud["photo_filename"], size=size)
if not path: if not path:
# Portail ? # Portail ?
ext_url = photo_portal_url(etud) ext_url = photo_portal_url(etud)
@ -131,8 +131,8 @@ def get_photo_image(etudid=None, size="small"):
if not etudid: if not etudid:
filename = UNKNOWN_IMAGE_PATH filename = UNKNOWN_IMAGE_PATH
else: else:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = Identite.query.get_or_404(etudid)
filename = photo_pathname(etud, size=size) filename = photo_pathname(etud.photo_filename, size=size)
if not filename: if not filename:
filename = UNKNOWN_IMAGE_PATH filename = UNKNOWN_IMAGE_PATH
return _http_jpeg_file(filename) return _http_jpeg_file(filename)
@ -171,8 +171,8 @@ def _http_jpeg_file(filename):
return response return response
def etud_photo_is_local(etud, size="small"): def etud_photo_is_local(etud: dict, size="small"):
return photo_pathname(etud, size=size) return photo_pathname(etud["photo_filename"], size=size)
def etud_photo_html(etud=None, etudid=None, title=None, size="small"): def etud_photo_html(etud=None, etudid=None, title=None, size="small"):
@ -215,9 +215,12 @@ def etud_photo_orig_html(etud=None, etudid=None, title=None):
return etud_photo_html(etud=etud, etudid=etudid, title=title, size="orig") return etud_photo_html(etud=etud, etudid=etudid, title=title, size="orig")
def photo_pathname(etud, size="orig"): def photo_pathname(photo_filename: str, size="orig"):
"""Returns full path of image file if etud has a photo (in the filesystem), or False. """Returns full path of image file if etud has a photo (in the filesystem),
or False.
Do not distinguish the cases: no photo, or file missing. Do not distinguish the cases: no photo, or file missing.
Argument: photo_filename (Identite attribute)
Resultat: False or str
""" """
if size == "small": if size == "small":
version = H90 version = H90
@ -225,9 +228,9 @@ def photo_pathname(etud, size="orig"):
version = "" version = ""
else: else:
raise ValueError("invalid size parameter for photo") raise ValueError("invalid size parameter for photo")
if not etud["photo_filename"]: if not photo_filename:
return False return False
path = os.path.join(PHOTO_DIR, etud["photo_filename"]) + version + IMAGE_EXT path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT
if os.path.exists(path): if os.path.exists(path):
return path return path
else: else:
@ -264,15 +267,14 @@ def store_photo(etud, data):
return 1, "ok" return 1, "ok"
def suppress_photo(etud): def suppress_photo(etud: Identite) -> None:
"""Suppress a photo""" """Suppress a photo"""
log("suppress_photo etudid=%s" % etud["etudid"]) log("suppress_photo etudid=%s" % etud.id)
rel_path = photo_pathname(etud) rel_path = photo_pathname(etud.photo_filename)
# 1- remove ref. from database # 1- remove ref. from database
etud["photo_filename"] = None etud.photo_filename = None
cnx = ndb.GetDBConnexion() db.session.add(etud)
sco_etud.identite_edit_nocheck(cnx, etud)
cnx.commit()
# 2- erase images files # 2- erase images files
if rel_path: if rel_path:
# remove extension and glob # remove extension and glob
@ -281,8 +283,10 @@ def suppress_photo(etud):
for filename in filenames: for filename in filenames:
log("removing file %s" % filename) log("removing file %s" % filename)
os.remove(filename) os.remove(filename)
db.session.commit()
# 3- log # 3- log
logdb(cnx, method="changePhoto", msg="suppression", etudid=etud["etudid"]) cnx = ndb.GetDBConnexion()
logdb(cnx, method="changePhoto", msg="suppression", etudid=etud.id)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -373,6 +377,9 @@ def copy_portal_photo_to_fs(etud):
log("copy_portal_photo_to_fs: failure (exception in store_photo)!") log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
if status == 1: if status == 1:
log("copy_portal_photo_to_fs: copied %s" % url) log("copy_portal_photo_to_fs: copied %s" % url)
return photo_pathname(etud), "%s: photo chargée" % etud["nomprenom"] return (
photo_pathname(etud["photo_filename"]),
f"{etud['nomprenom']}: photo chargée",
)
else: else:
return None, "%s: <b>%s</b>" % (etud["nomprenom"], diag) return None, "%s: <b>%s</b>" % (etud["nomprenom"], diag)

View File

@ -183,10 +183,11 @@ def trombino_html(groups_infos):
def check_local_photos_availability(groups_infos, format=""): def check_local_photos_availability(groups_infos, format=""):
"""Verifie que toutes les photos (des gropupes indiqués) sont copiées localement """Vérifie que toutes les photos (des groupes indiqués) sont copiées
dans ScoDoc (seules les photos dont nous disposons localement peuvent être exportées localement dans ScoDoc (seules les photos dont nous disposons localement
en pdf ou en zip). peuvent être exportées en pdf ou en zip).
Si toutes ne sont pas dispo, retourne un dialogue d'avertissement pour l'utilisateur. Si toutes ne sont pas dispo, retourne un dialogue d'avertissement
pour l'utilisateur.
""" """
nb_missing = 0 nb_missing = 0
for t in groups_infos.members: for t in groups_infos.members:
@ -221,7 +222,7 @@ def _trombino_zip(groups_infos):
# assume we have the photos (or the user acknowledged the fact) # assume we have the photos (or the user acknowledged the fact)
# Archive originals (not reduced) images, in JPEG # Archive originals (not reduced) images, in JPEG
for t in groups_infos.members: for t in groups_infos.members:
im_path = sco_photos.photo_pathname(t, size="orig") im_path = sco_photos.photo_pathname(t["photo_filename"], size="orig")
if not im_path: if not im_path:
continue continue
img = open(im_path, "rb").read() img = open(im_path, "rb").read()
@ -292,9 +293,9 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
def _get_etud_platypus_image(t, image_width=2 * cm): def _get_etud_platypus_image(t, image_width=2 * cm):
"""Returns aplatypus object for the photo of student t""" """Returns a platypus object for the photo of student t"""
try: try:
path = sco_photos.photo_pathname(t, size="small") path = sco_photos.photo_pathname(t["photo_filename"], size="small")
if not path: if not path:
# log('> unknown') # log('> unknown')
path = sco_photos.UNKNOWN_IMAGE_PATH path = sco_photos.UNKNOWN_IMAGE_PATH

View File

@ -72,7 +72,7 @@ NOTES_SUPPRESS = -1001.0 # note a supprimer
NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee)
# ---- CODES INSCRIPTION AUX SEMESTRES # ---- CODES INSCRIPTION AUX SEMESTRES
# (champ etat de FormsemestreInscription) # (champ etat de FormSemestreInscription)
INSCRIT = "I" INSCRIT = "I"
DEMISSION = "D" DEMISSION = "D"
DEF = "DEF" DEF = "DEF"

View File

@ -9,7 +9,7 @@
<releve-but></releve-but> <releve-but></releve-but>
<script src="/ScoDoc/static/js/releve-but.js"></script> <script src="/ScoDoc/static/js/releve-but.js"></script>
<script> <script>
let dataSrc = "{{bul_url|safe}}"; let dataSrc = "{{bul_url|safe}}";
fetch(dataSrc) fetch(dataSrc)
.then(r => { return r.json() }) .then(r => { return r.json() })
.then(json => { .then(json => {
@ -22,10 +22,9 @@
.dateInscription, .dateInscription,
.numerosEtudiant, .numerosEtudiant,
.dateNaissance{ .dateNaissance{
/*display: none;*/ display: none;
}`; }`;
releve.shadowRoot.appendChild(style); releve.shadowRoot.appendChild(style);
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1739,7 +1739,7 @@ def evaluation_listenotes():
mode = "module" mode = "module"
format = vals.get("format", "html") format = vals.get("format", "html")
B, page_title = sco_liste_notes.do_evaluation_listenotes( html_content, page_title = sco_liste_notes.do_evaluation_listenotes(
evaluation_id=evaluation_id, moduleimpl_id=moduleimpl_id, format=format evaluation_id=evaluation_id, moduleimpl_id=moduleimpl_id, format=format
) )
if format == "html": if format == "html":
@ -1750,9 +1750,9 @@ def evaluation_listenotes():
init_qtip=True, init_qtip=True,
) )
F = html_sco_header.sco_footer() F = html_sco_header.sco_footer()
return H + B + F return H + html_content + F
else: else:
return B return html_content
sco_publish( sco_publish(

View File

@ -54,7 +54,7 @@ from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
import app import app
from app.models import Departement, Identite from app.models import Departement, Identite
from app.models import FormSemestre, FormsemestreInscription from app.models import FormSemestre, FormSemestreInscription
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
import sco_version import sco_version
from app.scodoc import sco_logos, sco_config_form from app.scodoc import sco_logos, sco_config_form

View File

@ -49,6 +49,7 @@ from app.decorators import (
admin_required, admin_required,
login_required, login_required,
) )
from app.models.etudiants import Identite
from app.views import scolar_bp as bp from app.views import scolar_bp as bp
@ -944,21 +945,21 @@ def formChangePhoto(etudid=None):
@scodoc7func @scodoc7func
def formSuppressPhoto(etudid=None, dialog_confirmed=False): def formSuppressPhoto(etudid=None, dialog_confirmed=False):
"""Formulaire suppression photo étudiant""" """Formulaire suppression photo étudiant"""
etud = sco_etud.get_etud_info(filled=True)[0] etud = Identite.query.get_or_404(etudid)
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"<p>Confirmer la suppression de la photo de %(nomprenom)s ?</p>" % etud, f"<p>Confirmer la suppression de la photo de {etud.nom_disp()} ?</p>",
dest_url="", dest_url="",
cancel_url=url_for( cancel_url=url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
), ),
parameters={"etudid": etudid}, parameters={"etudid": etud.id},
) )
sco_photos.suppress_photo(etud) sco_photos.suppress_photo(etud)
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
) )

View File

@ -1,4 +1,4 @@
"""index in FormsemestreInscription """index in FormSemestreInscription
Revision ID: 4f98a8b02c89 Revision ID: 4f98a8b02c89
Revises: a57a6ee2e3cb Revises: a57a6ee2e3cb
@ -10,23 +10,47 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '4f98a8b02c89' revision = "4f98a8b02c89"
down_revision = 'a57a6ee2e3cb' down_revision = "a57a6ee2e3cb"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_index(op.f('ix_notes_formsemestre_inscription_etat'), 'notes_formsemestre_inscription', ['etat'], unique=False) op.create_index(
op.create_index(op.f('ix_notes_formsemestre_inscription_etudid'), 'notes_formsemestre_inscription', ['etudid'], unique=False) op.f("ix_notes_formsemestre_inscription_etat"),
op.create_index(op.f('ix_notes_formsemestre_inscription_formsemestre_id'), 'notes_formsemestre_inscription', ['formsemestre_id'], unique=False) "notes_formsemestre_inscription",
["etat"],
unique=False,
)
op.create_index(
op.f("ix_notes_formsemestre_inscription_etudid"),
"notes_formsemestre_inscription",
["etudid"],
unique=False,
)
op.create_index(
op.f("ix_notes_formsemestre_inscription_formsemestre_id"),
"notes_formsemestre_inscription",
["formsemestre_id"],
unique=False,
)
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_notes_formsemestre_inscription_formsemestre_id'), table_name='notes_formsemestre_inscription') op.drop_index(
op.drop_index(op.f('ix_notes_formsemestre_inscription_etudid'), table_name='notes_formsemestre_inscription') op.f("ix_notes_formsemestre_inscription_formsemestre_id"),
op.drop_index(op.f('ix_notes_formsemestre_inscription_etat'), table_name='notes_formsemestre_inscription') table_name="notes_formsemestre_inscription",
)
op.drop_index(
op.f("ix_notes_formsemestre_inscription_etudid"),
table_name="notes_formsemestre_inscription",
)
op.drop_index(
op.f("ix_notes_formsemestre_inscription_etat"),
table_name="notes_formsemestre_inscription",
)
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -0,0 +1,40 @@
"""user token size limit
Revision ID: 91be8a06d423
Revises: 4f98a8b02c89
Create Date: 2021-12-20 22:48:42.390743
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "91be8a06d423"
down_revision = "4f98a8b02c89"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"user",
"token",
existing_type=sa.VARCHAR(length=32),
type_=sa.Text(),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"user",
"token",
existing_type=sa.Text(),
type_=sa.VARCHAR(length=32),
existing_nullable=True,
)
# ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.1.11" SCOVERSION = "9.1.12"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -24,7 +24,7 @@ from app.auth.models import User, Role, UserRole
from app.models import ScoPreference from app.models import ScoPreference
from app.scodoc.sco_logos import make_logo_local from app.scodoc.sco_logos import make_logo_local
from app.models import Formation, UniteEns, Module from app.models import Formation, UniteEns, Module
from app.models import FormSemestre, FormsemestreInscription from app.models import FormSemestre, FormSemestreInscription
from app.models import ModuleImpl, ModuleImplInscription from app.models import ModuleImpl, ModuleImplInscription
from app.models import Identite from app.models import Identite
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
@ -57,7 +57,7 @@ def make_shell_context():
"flask": flask, "flask": flask,
"Formation": Formation, "Formation": Formation,
"FormSemestre": FormSemestre, "FormSemestre": FormSemestre,
"FormsemestreInscription": FormsemestreInscription, "FormSemestreInscription": FormSemestreInscription,
"Identite": Identite, "Identite": Identite,
"login_user": login_user, "login_user": login_user,
"logout_user": logout_user, "logout_user": logout_user,
@ -248,6 +248,35 @@ def edit_role(rolename, addpermissionname=None, removepermissionname=None): # e
db.session.commit() db.session.commit()
@app.cli.command()
@click.argument("username")
@click.option("-d", "--dept", "dept_acronym")
@click.option("-a", "--add", "add_role_name")
@click.option("-r", "--remove", "remove_role_name")
def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name=None):
"""Add or remove a role to the given user in the given dept"""
user = User.query.filter_by(user_name=username).first()
if not user:
sys.stderr.write(f"user_role: user {username} does not exists\n")
return 1
if dept_acronym:
dept = models.Departement.query.filter_by(acronym=dept_acronym).first()
if dept is None:
sys.stderr.write(f"Erreur: le departement {dept} n'existe pas !\n")
return 2
if add_role_name:
role = Role.query.filter_by(name=add_role_name).first()
user.add_role(role, dept_acronym)
if remove_role_name:
role = Role.query.filter_by(name=remove_role_name).first()
user_role = UserRole.query.filter(
UserRole.role == role, UserRole.user == user, UserRole.dept == dept_acronym
).first()
db.session.delete(user_role)
db.session.commit()
@app.cli.command() @app.cli.command()
@click.argument("dept") @click.argument("dept")
def delete_dept(dept): # delete-dept def delete_dept(dept): # delete-dept

View File

@ -2,7 +2,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic athentication """Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic authentication
Utilisation: créer les variables d'environnement: (indiquer les valeurs Utilisation: créer les variables d'environnement: (indiquer les valeurs
@ -80,6 +80,15 @@ if r.status_code != 200:
pp(r.json()) pp(r.json())
# Liste des tous les étudiants en cours (de tous les depts)
r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiants/courant",
headers=HEADERS,
verify=CHECK_CERTIFICATE,
)
if r.status_code != 200:
raise ScoError("erreur de connexion: vérifier adresse et identifiants")
# # --- Recupere la liste de tous les semestres: # # --- Recupere la liste de tous les semestres:
# sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") # sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")