diff --git a/app/api/auth.py b/app/api/auth.py index 958001eea..331cd388d 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -* # Authentication code borrowed from Miguel Grinberg's Mega Tutorial # (see https://github.com/miguelgrinberg/microblog) +# and modified for ScoDoc # Under The MIT License (MIT) @@ -23,6 +24,7 @@ # 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. +from flask import g from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth from app.auth.models import User from app.api.errors import error_response @@ -35,6 +37,7 @@ token_auth = HTTPTokenAuth() def verify_password(username, password): user = User.query.filter_by(user_name=username).first() if user and user.check_password(password): + g.current_user = user return user @@ -45,7 +48,9 @@ def basic_auth_error(status): @token_auth.verify_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 @@ -53,15 +58,20 @@ def token_auth_error(status): return error_response(status) -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) +@token_auth.get_user_roles +def get_user_roles(user): + return user.roles - 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 diff --git a/app/api/sco_api.py b/app/api/sco_api.py index c3ee74240..65bc8c12f 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -39,13 +39,18 @@ # Scolarite/Notes/moduleimpl_status # Scolarite/setGroups -from flask import jsonify, request, url_for, abort -from app import db +from flask import jsonify, request, url_for, abort, g +from flask_login import current_user +from sqlalchemy.sql import func + +from app import db, log from app.api import bp 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.models import FormSemestre, FormSemestreInscription, Identite +from app.scodoc.sco_permissions import Permission @bp.route("list_depts", methods=["GET"]) @@ -54,3 +59,23 @@ def list_depts(): depts = models.Departement.query.filter_by(visible=True).all() data = [d.to_dict() for d in depts] 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]) diff --git a/app/auth/models.py b/app/auth/models.py index 86ebdb83c..d9c5455b7 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -65,7 +65,7 @@ class User(UserMixin, db.Model): date_created = db.Column(db.DateTime, default=datetime.utcnow) date_expiration = db.Column(db.DateTime, default=None) 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) 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) 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): "True if i'm an active SuperAdmin" diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index cd1c59a5d..4ff2849f8 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -25,7 +25,6 @@ class ResultatsSemestreBUT: """Structure légère pour stocker les résultats du semestre et générer les bulletins. __init__ : charge depuis le cache ou calcule - invalidate(): invalide données cachées """ _cached_attrs = ( diff --git a/app/models/__init__.py b/app/models/__init__.py index 66e22432a..0fee7bc48 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -35,12 +35,12 @@ from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tag from app.models.ues import UniteEns from app.models.formsemestre import ( FormSemestre, - FormsemestreEtape, + FormSemestreEtape, FormationModalite, - FormsemestreUECoef, - FormsemestreUEComputationExpr, - FormsemestreCustomMenu, - FormsemestreInscription, + FormSemestreUECoef, + FormSemestreUEComputationExpr, + FormSemestreCustomMenu, + FormSemestreInscription, notes_formsemestre_responsables, NotesSemSet, notes_semset_formsemestre, @@ -57,7 +57,7 @@ from app.models.evaluations import ( from app.models.groups import Partition, GroupDescr, group_membership from app.models.notes import ( ScolarEvent, - ScolarFormsemestreValidation, + ScolarFormSemestreValidation, ScolarAutorisationInscription, BulAppreciations, NotesNotes, diff --git a/app/models/etudiants.py b/app/models/etudiants.py index cddac171c..5b49d68f7 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -9,7 +9,6 @@ from app import models from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN -from app.scodoc import sco_photos class Identite(db.Model): @@ -71,12 +70,14 @@ class Identite(db.Model): "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 - def to_dict_bul(self): + def to_dict_bul(self, include_photo=True): """Infos exportées dans les bulletins""" - return { + from app.scodoc import sco_photos + + d = { "civilite": self.civilite, - "code_ine": self.code_nip, - "code_nip": self.code_ine, + "code_ine": self.code_ine, + "code_nip": self.code_nip, "date_naissance": self.date_naissance.isoformat() if self.date_naissance else None, @@ -84,9 +85,11 @@ class Identite(db.Model): "emailperso": self.get_first_email("emailperso"), "etudid": self.id, "nom": self.nom_disp(), - "photo_url": sco_photos.get_etud_photo_url(self.id), "prenom": self.prenom, } + if include_photo: + d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),) + return d def inscription_courante(self): """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 """ # 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 ).first() if ins: diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 3d5ddd304..450ea2ccf 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -82,7 +82,7 @@ class FormSemestre(db.Model): # Relations: etapes = db.relationship( - "FormsemestreEtape", cascade="all,delete", backref="formsemestre" + "FormSemestreEtape", cascade="all,delete", backref="formsemestre" ) modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic") etuds = db.relationship( @@ -119,7 +119,7 @@ class FormSemestre(db.Model): return d 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 les modules mis en place dans ce semestre. - 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""" __tablename__ = "notes_formsemestre_etapes" @@ -331,7 +331,7 @@ class FormationModalite(db.Model): raise -class FormsemestreUECoef(db.Model): +class FormSemestreUECoef(db.Model): """Coef des UE capitalisees arrivant dans ce semestre""" __tablename__ = "notes_formsemestre_uecoef" @@ -350,7 +350,7 @@ class FormsemestreUECoef(db.Model): coefficient = db.Column(db.Float, nullable=False) -class FormsemestreUEComputationExpr(db.Model): +class FormSemestreUEComputationExpr(db.Model): """Formules utilisateurs pour calcul moyenne UE""" __tablename__ = "notes_formsemestre_ue_computation_expr" @@ -370,7 +370,7 @@ class FormsemestreUEComputationExpr(db.Model): computation_expr = db.Column(db.Text()) -class FormsemestreCustomMenu(db.Model): +class FormSemestreCustomMenu(db.Model): """Menu custom associe au semestre""" __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 -class FormsemestreInscription(db.Model): +class FormSemestreInscription(db.Model): """Inscription à un semestre de formation""" __tablename__ = "notes_formsemestre_inscription" @@ -410,7 +410,7 @@ class FormsemestreInscription(db.Model): backref=db.backref( "inscriptions", cascade="all, delete-orphan", - order_by="FormsemestreInscription.etudid", + order_by="FormSemestreInscription.etudid", ), ) # I inscrit, D demission en cours de semestre, DEF si "defaillant" diff --git a/app/models/notes.py b/app/models/notes.py index c196596fb..df2766d04 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -40,7 +40,7 @@ class ScolarEvent(db.Model): ) -class ScolarFormsemestreValidation(db.Model): +class ScolarFormSemestreValidation(db.Model): """Décisions de jury""" __tablename__ = "scolar_formsemestre_validation" diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 3e23414ca..0d129d813 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -32,7 +32,7 @@ from flask_login import current_user from app import db 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 from app.scodoc import sco_groups from app.scodoc.sco_utils import ModuleType @@ -152,7 +152,7 @@ def html_ue_infos(ue): ) .all() ) - nb_etuds_valid_ue = ScolarFormsemestreValidation.query.filter_by( + nb_etuds_valid_ue = ScolarFormSemestreValidation.query.filter_by( ue_id=ue.id ).count() can_safely_be_suppressed = ( diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 09e5480f7..6da2e3f8c 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -56,7 +56,7 @@ from app.scodoc.htmlutils import histogram_notes def do_evaluation_listenotes( evaluation_id=None, moduleimpl_id=None, format="html" -) -> str: +) -> tuple[str, str]: """ Affichage des notes d'une évaluation (si evaluation_id) ou de toutes les évaluations d'un module (si moduleimpl_id) @@ -71,7 +71,7 @@ def do_evaluation_listenotes( else: raise ValueError("missing argument: evaluation or module") if not evals: - return "
Aucune évaluation !
" + return "Aucune évaluation !
", f"ScoDoc" E = evals[0] # il y a au moins une evaluation modimpl = ModuleImpl.query.get(E["moduleimpl_id"]) @@ -189,9 +189,12 @@ def do_evaluation_listenotes( if tf[0] == 0: return "\n".join(H) + "\n" + tf[1], page_title elif tf[0] == -1: - return flask.redirect( - "%s/Notes/moduleimpl_status?moduleimpl_id=%s" - % (scu.ScoURL(), E["moduleimpl_id"]) + return ( + flask.redirect( + "%s/Notes/moduleimpl_status?moduleimpl_id=%s" + % (scu.ScoURL(), E["moduleimpl_id"]) + ), + "", ) else: anonymous_listing = tf[2]["anonymous_listing"] diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py index b76e001d7..5d70168b6 100644 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -42,9 +42,6 @@ Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx - 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 glob import io @@ -52,24 +49,26 @@ import os import random import requests import time -import traceback import PIL from PIL import Image as PILImage 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_portal_apogee from app.scodoc import sco_preferences -from app import log +from app.scodoc.sco_exceptions import ScoGenError from app.scodoc.scolog import logdb import app.scodoc.notesdb as ndb 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") ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons") 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. 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) if fast: return photo_url - path = photo_pathname(etud, size=size) + path = photo_pathname(etud["photo_filename"], size=size) if not path: # Portail ? ext_url = photo_portal_url(etud) @@ -131,8 +131,8 @@ def get_photo_image(etudid=None, size="small"): if not etudid: filename = UNKNOWN_IMAGE_PATH else: - etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] - filename = photo_pathname(etud, size=size) + etud = Identite.query.get_or_404(etudid) + filename = photo_pathname(etud.photo_filename, size=size) if not filename: filename = UNKNOWN_IMAGE_PATH return _http_jpeg_file(filename) @@ -171,8 +171,8 @@ def _http_jpeg_file(filename): return response -def etud_photo_is_local(etud, size="small"): - return photo_pathname(etud, size=size) +def etud_photo_is_local(etud: dict, size="small"): + return photo_pathname(etud["photo_filename"], size=size) 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") -def photo_pathname(etud, size="orig"): - """Returns full path of image file if etud has a photo (in the filesystem), or False. +def photo_pathname(photo_filename: str, size="orig"): + """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. + Argument: photo_filename (Identite attribute) + Resultat: False or str """ if size == "small": version = H90 @@ -225,9 +228,9 @@ def photo_pathname(etud, size="orig"): version = "" else: raise ValueError("invalid size parameter for photo") - if not etud["photo_filename"]: + if not photo_filename: 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): return path else: @@ -264,15 +267,14 @@ def store_photo(etud, data): return 1, "ok" -def suppress_photo(etud): +def suppress_photo(etud: Identite) -> None: """Suppress a photo""" - log("suppress_photo etudid=%s" % etud["etudid"]) - rel_path = photo_pathname(etud) + log("suppress_photo etudid=%s" % etud.id) + rel_path = photo_pathname(etud.photo_filename) # 1- remove ref. from database - etud["photo_filename"] = None - cnx = ndb.GetDBConnexion() - sco_etud.identite_edit_nocheck(cnx, etud) - cnx.commit() + etud.photo_filename = None + db.session.add(etud) + # 2- erase images files if rel_path: # remove extension and glob @@ -281,8 +283,10 @@ def suppress_photo(etud): for filename in filenames: log("removing file %s" % filename) os.remove(filename) + db.session.commit() # 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)!") if status == 1: 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: return None, "%s: %s" % (etud["nomprenom"], diag) diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index b5bfb6b52..0849b0374 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -183,10 +183,11 @@ def trombino_html(groups_infos): def check_local_photos_availability(groups_infos, format=""): - """Verifie que toutes les photos (des gropupes indiqués) sont copiées localement - dans ScoDoc (seules les photos dont nous disposons localement peuvent être exportées - en pdf ou en zip). - Si toutes ne sont pas dispo, retourne un dialogue d'avertissement pour l'utilisateur. + """Vérifie que toutes les photos (des groupes indiqués) sont copiées + localement dans ScoDoc (seules les photos dont nous disposons localement + peuvent être exportées en pdf ou en zip). + Si toutes ne sont pas dispo, retourne un dialogue d'avertissement + pour l'utilisateur. """ nb_missing = 0 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) # Archive originals (not reduced) images, in JPEG 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: continue 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): - """Returns aplatypus object for the photo of student t""" + """Returns a platypus object for the photo of student t""" try: - path = sco_photos.photo_pathname(t, size="small") + path = sco_photos.photo_pathname(t["photo_filename"], size="small") if not path: # log('> unknown') path = sco_photos.UNKNOWN_IMAGE_PATH diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 35d13cbf3..3b6b065ce 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -72,7 +72,7 @@ NOTES_SUPPRESS = -1001.0 # note a supprimer NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) # ---- CODES INSCRIPTION AUX SEMESTRES -# (champ etat de FormsemestreInscription) +# (champ etat de FormSemestreInscription) INSCRIT = "I" DEMISSION = "D" DEF = "DEF" diff --git a/app/templates/but/bulletin.html b/app/templates/but/bulletin.html index 819b6bd7d..727a144d7 100644 --- a/app/templates/but/bulletin.html +++ b/app/templates/but/bulletin.html @@ -9,7 +9,7 @@Confirmer la suppression de la photo de %(nomprenom)s ?
" % etud, + f"Confirmer la suppression de la photo de {etud.nom_disp()} ?
", dest_url="", 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) 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) ) diff --git a/migrations/versions/4f98a8b02c89_index_in_formsemestreinscription.py b/migrations/versions/4f98a8b02c89_index_in_formsemestreinscription.py index 179ed98cf..b867e291f 100644 --- a/migrations/versions/4f98a8b02c89_index_in_formsemestreinscription.py +++ b/migrations/versions/4f98a8b02c89_index_in_formsemestreinscription.py @@ -1,4 +1,4 @@ -"""index in FormsemestreInscription +"""index in FormSemestreInscription Revision ID: 4f98a8b02c89 Revises: a57a6ee2e3cb @@ -10,23 +10,47 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '4f98a8b02c89' -down_revision = 'a57a6ee2e3cb' +revision = "4f98a8b02c89" +down_revision = "a57a6ee2e3cb" branch_labels = None depends_on = None def upgrade(): # ### 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.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) + op.create_index( + op.f("ix_notes_formsemestre_inscription_etat"), + "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 ### def downgrade(): # ### 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.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') + op.drop_index( + op.f("ix_notes_formsemestre_inscription_formsemestre_id"), + 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 ### diff --git a/migrations/versions/91be8a06d423_user_token_size_limit.py b/migrations/versions/91be8a06d423_user_token_size_limit.py new file mode 100644 index 000000000..a9caa0d51 --- /dev/null +++ b/migrations/versions/91be8a06d423_user_token_size_limit.py @@ -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 ### diff --git a/sco_version.py b/sco_version.py index b33f520ab..8baaca5ee 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.11" +SCOVERSION = "9.1.12" SCONAME = "ScoDoc" diff --git a/scodoc.py b/scodoc.py index 26598fc59..482501e0e 100755 --- a/scodoc.py +++ b/scodoc.py @@ -24,7 +24,7 @@ from app.auth.models import User, Role, UserRole from app.models import ScoPreference from app.scodoc.sco_logos import make_logo_local 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 Identite from app.models.evaluations import Evaluation @@ -57,7 +57,7 @@ def make_shell_context(): "flask": flask, "Formation": Formation, "FormSemestre": FormSemestre, - "FormsemestreInscription": FormsemestreInscription, + "FormSemestreInscription": FormSemestreInscription, "Identite": Identite, "login_user": login_user, "logout_user": logout_user, @@ -248,6 +248,35 @@ def edit_role(rolename, addpermissionname=None, removepermissionname=None): # e 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() @click.argument("dept") def delete_dept(dept): # delete-dept diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index 529c379e5..0975e1003 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -2,7 +2,7 @@ # -*- mode: python -*- # -*- 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 @@ -80,6 +80,15 @@ if r.status_code != 200: 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: # sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")