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

Reviewed-on: https://scodoc.org/git/lehmann/ScoDoc-Front/pulls/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 -*
# 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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 "<p>Aucune évaluation !</p>"
return "<p>Aucune évaluation !</p>", 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"]

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.
"""
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: <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=""):
"""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

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)
# ---- CODES INSCRIPTION AUX SEMESTRES
# (champ etat de FormsemestreInscription)
# (champ etat de FormSemestreInscription)
INSCRIT = "I"
DEMISSION = "D"
DEF = "DEF"

View File

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

View File

@ -1739,7 +1739,7 @@ def evaluation_listenotes():
mode = "module"
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
)
if format == "html":
@ -1750,9 +1750,9 @@ def evaluation_listenotes():
init_qtip=True,
)
F = html_sco_header.sco_footer()
return H + B + F
return H + html_content + F
else:
return B
return html_content
sco_publish(

View File

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

View File

@ -49,6 +49,7 @@ from app.decorators import (
admin_required,
login_required,
)
from app.models.etudiants import Identite
from app.views import scolar_bp as bp
@ -944,21 +945,21 @@ def formChangePhoto(etudid=None):
@scodoc7func
def formSuppressPhoto(etudid=None, dialog_confirmed=False):
"""Formulaire suppression photo étudiant"""
etud = sco_etud.get_etud_info(filled=True)[0]
etud = Identite.query.get_or_404(etudid)
if not dialog_confirmed:
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="",
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)
)

View File

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

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 -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.1.11"
SCOVERSION = "9.1.12"
SCONAME = "ScoDoc"

View File

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

View File

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