forked from ScoDoc/ScoDoc
update
This commit is contained in:
commit
39a9f353d2
@ -6,3 +6,4 @@ from flask import Blueprint
|
||||
bp = Blueprint("api", __name__)
|
||||
|
||||
from app.api import sco_api
|
||||
from app.api import tokens
|
||||
|
@ -33,6 +33,7 @@ token_auth = HTTPTokenAuth()
|
||||
|
||||
@basic_auth.verify_password
|
||||
def verify_password(username, password):
|
||||
# breakpoint()
|
||||
user = User.query.filter_by(user_name=username).first()
|
||||
if user and user.check_password(password):
|
||||
return user
|
||||
@ -51,3 +52,17 @@ def verify_token(token):
|
||||
@token_auth.error_handler
|
||||
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)
|
||||
|
||||
return login_required(decorated_function)
|
||||
|
||||
return decorator
|
||||
|
@ -48,9 +48,9 @@ from app.api.errors import bad_request
|
||||
from app import models
|
||||
|
||||
|
||||
@bp.route("/ScoDoc/api/list_depts", methods=["GET"])
|
||||
@bp.route("list_depts", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def list_depts():
|
||||
depts = models.Departement.query.filter_by(visible=True).all()
|
||||
data = {"items": [d.to_dict() for d in depts]}
|
||||
data = [d.to_dict() for d in depts]
|
||||
return jsonify(data)
|
||||
|
@ -213,6 +213,9 @@ class User(UserMixin, db.Model):
|
||||
|
||||
@staticmethod
|
||||
def check_token(token):
|
||||
"""Retreive user for given token, chek token's validity
|
||||
and returns the user object.
|
||||
"""
|
||||
user = User.query.filter_by(token=token).first()
|
||||
if user is None or user.token_expiration < datetime.utcnow():
|
||||
return None
|
||||
|
@ -50,9 +50,19 @@ def scodoc(func):
|
||||
|
||||
@wraps(func)
|
||||
def scodoc_function(*args, **kwargs):
|
||||
# print("@scodoc")
|
||||
# interdit les POST si pas loggué
|
||||
if request.method == "POST" and not current_user.is_authenticated:
|
||||
current_app.logger.info("POST by non authenticated user")
|
||||
if (
|
||||
request.method == "POST"
|
||||
and not current_user.is_authenticated
|
||||
and not request.form.get(
|
||||
"__ac_password"
|
||||
) # exception pour compat API ScoDoc7
|
||||
):
|
||||
current_app.logger.info(
|
||||
"POST by non authenticated user (request.form=%s)",
|
||||
str(request.form)[:2048],
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
"auth.login",
|
||||
@ -68,6 +78,7 @@ def scodoc(func):
|
||||
# current_app.logger.info("setting dept to None")
|
||||
g.scodoc_dept = None
|
||||
g.scodoc_dept_id = -1 # invalide
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return scodoc_function
|
||||
@ -97,8 +108,8 @@ def permission_required_compat_scodoc7(permission):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# current_app.logger.warning("PERMISSION; kwargs=%s" % str(kwargs))
|
||||
# cherche les paramètre d'auth:
|
||||
# print("@permission_required_compat_scodoc7")
|
||||
auth_ok = False
|
||||
if request.method == "GET":
|
||||
user_name = request.args.get("__ac_name")
|
||||
@ -113,7 +124,6 @@ def permission_required_compat_scodoc7(permission):
|
||||
if u and u.check_password(user_password):
|
||||
auth_ok = True
|
||||
flask_login.login_user(u)
|
||||
|
||||
# reprend le chemin classique:
|
||||
scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
|
||||
@ -150,6 +160,7 @@ def scodoc7func(func):
|
||||
2. or be called directly from Python.
|
||||
|
||||
"""
|
||||
# print("@scodoc7func")
|
||||
# Détermine si on est appelé via une route ("toplevel")
|
||||
# ou par un appel de fonction python normal.
|
||||
top_level = not hasattr(g, "scodoc7_decorated")
|
||||
|
@ -41,7 +41,7 @@ class Identite(db.Model):
|
||||
code_nip = db.Column(db.Text())
|
||||
code_ine = db.Column(db.Text())
|
||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archive
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||
#
|
||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||
|
@ -137,6 +137,12 @@ class NotesTag(db.Model):
|
||||
# Association tag <-> module
|
||||
notes_modules_tags = db.Table(
|
||||
"notes_modules_tags",
|
||||
db.Column("tag_id", db.Integer, db.ForeignKey("notes_tags.id")),
|
||||
db.Column("module_id", db.Integer, db.ForeignKey("notes_modules.id")),
|
||||
db.Column(
|
||||
"tag_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
|
||||
),
|
||||
db.Column(
|
||||
"module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
|
||||
),
|
||||
)
|
||||
|
@ -79,7 +79,7 @@ class FormSemestre(db.Model):
|
||||
)
|
||||
|
||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archive
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
@ -987,7 +987,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
|
||||
bcc = copy_addr.strip()
|
||||
else:
|
||||
bcc = ""
|
||||
msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc)
|
||||
msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc])
|
||||
msg.body = hea
|
||||
|
||||
# Attach pdf
|
||||
|
@ -1949,6 +1949,15 @@ class BasePreferences(object):
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
if len(pdb) > 1:
|
||||
# suppress buggy duplicates (may come from corrupted database for ice ages)
|
||||
log(
|
||||
f"**oups** detected duplicated preference !\n({self.dept_id}, {formsemestre_id}, {name}, {value})"
|
||||
)
|
||||
for obj in pdb[1:]:
|
||||
self._editor.delete(cnx, obj["id"])
|
||||
pdb = [pdb[0]]
|
||||
|
||||
if not pdb:
|
||||
# crée préférence
|
||||
log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
|
||||
@ -1962,10 +1971,8 @@ class BasePreferences(object):
|
||||
},
|
||||
)
|
||||
modif = True
|
||||
log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
|
||||
else:
|
||||
# edit existing value
|
||||
|
||||
existing_value = pdb[0]["value"] # old stored value
|
||||
if (
|
||||
(existing_value != value)
|
||||
|
@ -1,9 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% import 'bootstrap/wtf.html' as wtf %}
|
||||
|
||||
{% macro render_field(field) %}
|
||||
{% macro render_field(field, auth_name=None) %}
|
||||
<tr style="">
|
||||
<td class="wtf-field">{{ field.label }}</td>
|
||||
{% if auth_name %}
|
||||
<td class="wtf-field"> {{ field.label }}<span style="font-weight:700;"> ({{ auth_name }}):</span></td>
|
||||
{% else %}
|
||||
<td class="wtf-field">{{ field.label }}</td>
|
||||
{% endif %}
|
||||
<td class="wtf-field">{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul class=errors>
|
||||
@ -20,16 +24,20 @@
|
||||
<h1>Modification du compte ScoDoc <tt>{{form.user_name.data}}</tt></h1>
|
||||
<div class="help">
|
||||
<p>Identifiez-vous avez votre mot de passe actuel</p>
|
||||
<p>Vous pouvez changer le mot de passe et/ou l'adresse email.</p>
|
||||
<p>Les champs vides ne seront pas changés.</p>
|
||||
</div>
|
||||
<form method=post>
|
||||
{{ form.user_name }}
|
||||
{{ form.csrf_token }}
|
||||
<table class="tf"><tbody>
|
||||
{{ render_field(form.old_password, size=14,
|
||||
{{ render_field(form.old_password, size=14, auth_name=auth_username,
|
||||
style="padding:1px; margin-left: 1em; margin-top: 4px;") }}
|
||||
{{ render_field(form.new_password, size=14,
|
||||
<tr>
|
||||
<td colspan=""2">
|
||||
<p>Vous pouvez changer le mot de passe et/ou l'adresse email.</p>
|
||||
<p>Les champs laissés vides ne seront pas modifiés.</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{ render_field(form.new_password, size=14,
|
||||
style="padding:1px; margin-left: 1em; margin-top: 12px;") }}
|
||||
{{ render_field(form.bis_password, size=14,
|
||||
style="padding:1px; margin-left: 1em; margin-top: 4px;") }}
|
||||
|
@ -15,4 +15,6 @@
|
||||
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p>A bientôt !</p>
|
||||
<p>A bientôt !</p>
|
||||
|
||||
<p>Ce message a été généré automatiquement par le serveur ScoDoc.</p>
|
@ -8,4 +8,6 @@ Votre identifiant de connexion est: {{ user.user_name }}
|
||||
{{ url_for('auth.reset_password', token=token, _external=True) }}
|
||||
{% endif %}
|
||||
|
||||
<p>A bientôt !</p>
|
||||
A bientôt !
|
||||
|
||||
Ce message a été généré automatiquement par le serveur ScoDoc.
|
@ -1058,7 +1058,8 @@ def AddBilletAbsence(
|
||||
code_nip=None,
|
||||
code_ine=None,
|
||||
justified=True,
|
||||
xml_reply=True,
|
||||
format="json",
|
||||
xml_reply=True, # deprecated
|
||||
):
|
||||
"""Mémorise un "billet"
|
||||
begin et end sont au format ISO (eg "1999-01-08 04:05:06")
|
||||
@ -1082,6 +1083,7 @@ def AddBilletAbsence(
|
||||
raise ValueError("invalid dates")
|
||||
#
|
||||
justified = bool(justified)
|
||||
xml_reply = bool(xml_reply)
|
||||
#
|
||||
cnx = ndb.GetDBConnexion()
|
||||
billet_id = sco_abs.billet_absence_create(
|
||||
@ -1095,17 +1097,17 @@ def AddBilletAbsence(
|
||||
"justified": justified,
|
||||
},
|
||||
)
|
||||
if xml_reply:
|
||||
# Renvoie le nouveau billet en XML
|
||||
billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
|
||||
tab = _tableBillets(billets, etud=etud)
|
||||
log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0))
|
||||
return tab.make_page(format="xml")
|
||||
else:
|
||||
return billet_id
|
||||
if xml_reply: # backward compat
|
||||
format = "xml"
|
||||
|
||||
# Renvoie le nouveau billet au format demandé
|
||||
billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
|
||||
tab = _tableBillets(billets, etud=etud)
|
||||
log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0))
|
||||
return tab.make_page(format=format)
|
||||
|
||||
|
||||
@bp.route("/AddBilletAbsenceForm")
|
||||
@bp.route("/AddBilletAbsenceForm", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoAbsAddBillet)
|
||||
@scodoc7func
|
||||
|
@ -264,7 +264,7 @@ sco_publish(
|
||||
|
||||
|
||||
@bp.route(
|
||||
"formsemestre_bulletinetud", methods=["GET", "POST"]
|
||||
"/formsemestre_bulletinetud", methods=["GET", "POST"]
|
||||
) # POST pour compat anciens clients PHP (deprecated)
|
||||
@scodoc
|
||||
@permission_required_compat_scodoc7(Permission.ScoView)
|
||||
|
@ -110,7 +110,7 @@ def get_etud_dept():
|
||||
# il peut y avoir plusieurs réponses si l'étudiant est passé par plusieurs départements
|
||||
etuds = Identite.query.filter_by(code_nip=request.args["code_nip"]).all()
|
||||
elif "code_ine" in request.args:
|
||||
etuds = Identite.query.filter_by(code_nip=request.args["code_ine"]).all()
|
||||
etuds = Identite.query.filter_by(code_ine=request.args["code_ine"]).all()
|
||||
else:
|
||||
raise BadRequest(
|
||||
"missing argument (expected one among: etudid, code_nip or code_ine)"
|
||||
|
@ -302,7 +302,34 @@ sco_publish(
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
|
||||
sco_publish("/groups_view", sco_groups_view.groups_view, Permission.ScoView)
|
||||
|
||||
@bp.route("/groups_view")
|
||||
@scodoc
|
||||
@permission_required_compat_scodoc7(Permission.ScoView)
|
||||
@scodoc7func
|
||||
def groups_view(
|
||||
group_ids=(),
|
||||
format="html",
|
||||
# Options pour listes:
|
||||
with_codes=0,
|
||||
etat=None,
|
||||
with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
|
||||
with_archives=0, # ajoute colonne avec noms fichiers archivés
|
||||
with_annotations=0,
|
||||
formsemestre_id=None,
|
||||
):
|
||||
return sco_groups_view.groups_view(
|
||||
group_ids=(),
|
||||
format=format,
|
||||
# Options pour listes:
|
||||
with_codes=with_codes,
|
||||
etat=etat,
|
||||
with_paiement=with_paiement, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
|
||||
with_archives=with_archives, # ajoute colonne avec noms fichiers archivés
|
||||
with_annotations=with_annotations,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
|
||||
|
||||
sco_publish(
|
||||
"/export_groups_as_moodle_csv",
|
||||
|
@ -35,6 +35,7 @@ Emmanuel Viennet, 2021
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
from enum import auto, IntEnum
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import flask
|
||||
@ -116,6 +117,12 @@ class ChangePasswordForm(FlaskForm):
|
||||
raise ValidationError("Mot de passe actuel incorrect, ré-essayez")
|
||||
|
||||
|
||||
class Mode(IntEnum):
|
||||
WELCOME_AND_CHANGE_PASSWORD = auto()
|
||||
WELCOME_ONLY = auto()
|
||||
SILENT = auto()
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/index_html")
|
||||
@scodoc
|
||||
@ -145,6 +152,8 @@ def user_info(user_name, format="json"):
|
||||
def create_user_form(user_name=None, edit=0, all_roles=1):
|
||||
"form. création ou edition utilisateur"
|
||||
auth_dept = current_user.dept
|
||||
auth_username = current_user.user_name
|
||||
from_mail = current_user.email
|
||||
initvalues = {}
|
||||
edit = int(edit)
|
||||
all_roles = int(all_roles)
|
||||
@ -529,19 +538,20 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
|
||||
)
|
||||
return "\n".join(H) + msg + "\n" + tf[1] + F
|
||||
# Traitement initial (mode) : 3 cas
|
||||
# cf énumération Mode
|
||||
# A: envoi de welcome + procedure de reset
|
||||
# B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
|
||||
# C: Aucun envoi (mot de passe saisi dans le formulaire)
|
||||
if vals["welcome:list"] == "1":
|
||||
if vals["reset_password:list"] == "1":
|
||||
mode = "A"
|
||||
mode = Mode.WELCOME_AND_CHANGE_PASSWORD
|
||||
else:
|
||||
mode = "B"
|
||||
mode = Mode.WELCOME_ONLY
|
||||
else:
|
||||
mode = "C"
|
||||
mode = Mode.SILENT
|
||||
|
||||
# check passwords
|
||||
if mode == "A":
|
||||
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD:
|
||||
vals["password"] = generate_password()
|
||||
else:
|
||||
if vals["password"]:
|
||||
@ -567,14 +577,14 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
|
||||
db.session.add(u)
|
||||
db.session.commit()
|
||||
# envoi éventuel d'un message
|
||||
if mode == "A" or mode == "B":
|
||||
if mode == "A":
|
||||
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY:
|
||||
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD:
|
||||
token = u.get_reset_password_token()
|
||||
else:
|
||||
token = None
|
||||
send_email(
|
||||
"[ScoDoc] Création de votre compte",
|
||||
sender=current_app.config["ADMINS"][0],
|
||||
sender=from_mail, # current_app.config["ADMINS"][0],
|
||||
recipients=[u.email],
|
||||
text_body=render_template("email/welcome.txt", user=u, token=token),
|
||||
html_body=render_template(
|
||||
@ -787,7 +797,10 @@ def form_change_password(user_name=None):
|
||||
return redirect(destination)
|
||||
|
||||
return render_template(
|
||||
"auth/change_password.html", form=form, title="Modification compte ScoDoc"
|
||||
"auth/change_password.html",
|
||||
form=form,
|
||||
title="Modification compte ScoDoc",
|
||||
auth_username=current_user.user_name,
|
||||
)
|
||||
|
||||
|
||||
|
34
migrations/versions/75cf18659984_cascade_tags_modules.py
Normal file
34
migrations/versions/75cf18659984_cascade_tags_modules.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""cascade tags modules
|
||||
|
||||
Revision ID: 75cf18659984
|
||||
Revises: d74b4e16fb3c
|
||||
Create Date: 2021-10-26 10:17:15.547905
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '75cf18659984'
|
||||
down_revision = 'd74b4e16fb3c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('notes_modules_tags_tag_id_fkey', 'notes_modules_tags', type_='foreignkey')
|
||||
op.drop_constraint('notes_modules_tags_module_id_fkey', 'notes_modules_tags', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'notes_modules_tags', 'notes_tags', ['tag_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key(None, 'notes_modules_tags', 'notes_modules', ['module_id'], ['id'], ondelete='CASCADE')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'notes_modules_tags', type_='foreignkey')
|
||||
op.drop_constraint(None, 'notes_modules_tags', type_='foreignkey')
|
||||
op.create_foreign_key('notes_modules_tags_module_id_fkey', 'notes_modules_tags', 'notes_modules', ['module_id'], ['id'])
|
||||
op.create_foreign_key('notes_modules_tags_tag_id_fkey', 'notes_modules_tags', 'notes_tags', ['tag_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Exemple connexion sur ScoDoc et utilisation de l'API
|
||||
|
||||
- Ouverture session
|
||||
- Liste semestres
|
||||
- Liste modules
|
||||
- Creation d'une évaluation
|
||||
- Saisie d'une note
|
||||
|
||||
Attention: cet exemple est en Python 3 (>= 3.6)
|
||||
"""
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
import pdb
|
||||
from pprint import pprint as pp
|
||||
from flask import g, url_for
|
||||
|
||||
# A modifier pour votre serveur:
|
||||
CHECK_CERTIFICATE = False # set to True in production
|
||||
BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite"
|
||||
USER = "XXX"
|
||||
PASSWORD = "XXX"
|
||||
|
||||
# ---
|
||||
if not CHECK_CERTIFICATE:
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
class ScoError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def GET(s, path, errmsg=None):
|
||||
"""Get and returns as JSON"""
|
||||
r = s.get(BASEURL + "/" + path, verify=CHECK_CERTIFICATE)
|
||||
if r.status_code != 200:
|
||||
raise ScoError(errmsg or "erreur !")
|
||||
return r.json() # decode la reponse JSON
|
||||
|
||||
|
||||
def POST(s, path, data, errmsg=None):
|
||||
"""Post"""
|
||||
r = s.post(BASEURL + "/" + path, data=data, verify=CHECK_CERTIFICATE)
|
||||
if r.status_code != 200:
|
||||
raise ScoError(errmsg or "erreur !")
|
||||
return r.text
|
||||
|
||||
|
||||
# --- Ouverture session (login)
|
||||
s = requests.Session()
|
||||
s.post(
|
||||
"https://deb11.viennet.net/api/auth/login",
|
||||
data={"user_name": USER, "password": PASSWORD},
|
||||
)
|
||||
r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE)
|
||||
if r.status_code != 200:
|
||||
raise ScoError("erreur de connection: vérifier adresse et identifiants")
|
||||
|
||||
# --- Recupere la liste de tous les semestres:
|
||||
sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")
|
||||
|
||||
# sems est une liste de semestres (dictionnaires)
|
||||
for sem in sems:
|
||||
if sem["etat"]:
|
||||
break
|
||||
|
||||
if sem["etat"] == "0":
|
||||
raise ScoError("Aucun semestre non verrouillé !")
|
||||
|
||||
# Affiche le semestre trouvé:
|
||||
pp(sem)
|
||||
|
||||
# ---- Récupère la description de ce semestre:
|
||||
# semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" )
|
||||
|
||||
# ---- Liste les modules et prend le premier
|
||||
mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}")
|
||||
print(f"{len(mods)} modules dans le semestre {sem['titre']}")
|
||||
|
||||
mod = mods[0]
|
||||
|
||||
# ---- Etudiants inscrits dans ce module
|
||||
inscrits = GET(
|
||||
s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}"
|
||||
)
|
||||
print(f"{len(inscrits)} inscrits dans ce module")
|
||||
# prend le premier inscrit, au hasard:
|
||||
etudid = inscrits[0]["etudid"]
|
||||
|
||||
# ---- Création d'une evaluation le dernier jour du semestre
|
||||
jour = sem["date_fin"]
|
||||
evaluation_id = POST(
|
||||
s,
|
||||
"/Notes/do_evaluation_create",
|
||||
data={
|
||||
"moduleimpl_id": mod["moduleimpl_id"],
|
||||
"coefficient": 1,
|
||||
"jour": jour, # "5/9/2019",
|
||||
"heure_debut": "9h00",
|
||||
"heure_fin": "10h00",
|
||||
"note_max": 20, # notes sur 20
|
||||
"description": "essai",
|
||||
},
|
||||
errmsg="échec création évaluation",
|
||||
)
|
||||
|
||||
print(
|
||||
f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
|
||||
)
|
||||
print(
|
||||
"Pour vérifier, aller sur: ",
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept="DEPT",
|
||||
moduleimpl_id=mod["moduleimpl_id"],
|
||||
),
|
||||
)
|
||||
|
||||
# ---- Saisie d'une note
|
||||
junk = POST(
|
||||
s,
|
||||
"/Notes/save_note",
|
||||
data={
|
||||
"etudid": etudid,
|
||||
"evaluation_id": evaluation_id,
|
||||
"value": 16.66, # la note !
|
||||
"comment": "test API",
|
||||
},
|
||||
)
|
@ -319,9 +319,9 @@ def import_scodoc7_dept(dept: str, dept_db_name: str = ""): # import-scodoc7-de
|
||||
@app.cli.command()
|
||||
@click.argument("dept", default="")
|
||||
@with_appcontext
|
||||
def migrate_scodoc7_dept_archive(dept: str): # migrate-scodoc7-dept-archive
|
||||
def migrate_scodoc7_dept_archives(dept: str): # migrate-scodoc7-dept-archives
|
||||
"""Post-migration: renomme les archives en fonction des id de ScoDoc 9"""
|
||||
tools.migrate_scodoc7_dept_archive(dept)
|
||||
tools.migrate_scodoc7_dept_archives(dept)
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
|
149
tests/api/exemple-api-basic.py
Normal file
149
tests/api/exemple-api-basic.py
Normal file
@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic athentication
|
||||
|
||||
|
||||
Utilisation: créer les variables d'environnement: (indiquer les valeurs
|
||||
pour le serveur ScoDoc que vous voulez interroger)
|
||||
|
||||
export SCODOC_URL="https://scodoc.xxx.net/"
|
||||
export SCODOC_USER="xxx"
|
||||
export SCODOC_PASSWD="xxx"
|
||||
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
|
||||
|
||||
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
|
||||
|
||||
|
||||
Travail en cours, un seul point d'API (list_depts).
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import pdb
|
||||
import requests
|
||||
import urllib3
|
||||
from pprint import pprint as pp
|
||||
|
||||
# --- Lecture configuration (variables d'env ou .env)
|
||||
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
||||
load_dotenv(os.path.join(BASEDIR, ".env"))
|
||||
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
|
||||
SCODOC_URL = os.environ["SCODOC_URL"]
|
||||
SCODOC_DEPT = os.environ["SCODOC_DEPT"]
|
||||
DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite/"
|
||||
SCODOC_USER = os.environ["SCODOC_USER"]
|
||||
SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"]
|
||||
print(f"SCODOC_URL={SCODOC_URL}")
|
||||
|
||||
# ---
|
||||
if not CHECK_CERTIFICATE:
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
class ScoError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def GET(path: str, headers={}, errmsg=None):
|
||||
"""Get and returns as JSON"""
|
||||
r = requests.get(
|
||||
DEPT_URL + "/" + path, headers=headers or HEADERS, verify=CHECK_CERTIFICATE
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise ScoError(errmsg or "erreur !")
|
||||
return r.json() # decode la reponse JSON
|
||||
|
||||
|
||||
def POST(s, path: str, data: dict, errmsg=None):
|
||||
"""Post"""
|
||||
r = s.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE)
|
||||
if r.status_code != 200:
|
||||
raise ScoError(errmsg or "erreur !")
|
||||
return r.text
|
||||
|
||||
|
||||
# --- Obtention du jeton (token)
|
||||
r = requests.post(
|
||||
SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
|
||||
)
|
||||
assert r.status_code == 200
|
||||
token = r.json()["token"]
|
||||
HEADERS = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
r = requests.get(
|
||||
SCODOC_URL + "/ScoDoc/api/list_depts", headers=HEADERS, verify=CHECK_CERTIFICATE
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise ScoError("erreur de connexion: vérifier adresse et identifiants")
|
||||
|
||||
pp(r.json())
|
||||
|
||||
|
||||
# # --- Recupere la liste de tous les semestres:
|
||||
# sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")
|
||||
|
||||
# # sems est une liste de semestres (dictionnaires)
|
||||
# for sem in sems:
|
||||
# if sem["etat"]:
|
||||
# break
|
||||
|
||||
# if sem["etat"] == "0":
|
||||
# raise ScoError("Aucun semestre non verrouillé !")
|
||||
|
||||
# # Affiche le semestre trouvé:
|
||||
# pp(sem)
|
||||
|
||||
# # ---- Récupère la description de ce semestre:
|
||||
# # semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" )
|
||||
|
||||
# # ---- Liste les modules et prend le premier
|
||||
# mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}")
|
||||
# print(f"{len(mods)} modules dans le semestre {sem['titre']}")
|
||||
|
||||
# mod = mods[0]
|
||||
|
||||
# # ---- Etudiants inscrits dans ce module
|
||||
# inscrits = GET(
|
||||
# s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}"
|
||||
# )
|
||||
# print(f"{len(inscrits)} inscrits dans ce module")
|
||||
# # prend le premier inscrit, au hasard:
|
||||
# etudid = inscrits[0]["etudid"]
|
||||
|
||||
# # ---- Création d'une evaluation le dernier jour du semestre
|
||||
# jour = sem["date_fin"]
|
||||
# evaluation_id = POST(
|
||||
# s,
|
||||
# "/Notes/do_evaluation_create",
|
||||
# data={
|
||||
# "moduleimpl_id": mod["moduleimpl_id"],
|
||||
# "coefficient": 1,
|
||||
# "jour": jour, # "5/9/2019",
|
||||
# "heure_debut": "9h00",
|
||||
# "heure_fin": "10h00",
|
||||
# "note_max": 20, # notes sur 20
|
||||
# "description": "essai",
|
||||
# },
|
||||
# errmsg="échec création évaluation",
|
||||
# )
|
||||
|
||||
# print(
|
||||
# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
|
||||
# )
|
||||
# print(
|
||||
# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}",
|
||||
# )
|
||||
|
||||
# # ---- Saisie d'une note
|
||||
# junk = POST(
|
||||
# s,
|
||||
# "/Notes/save_note",
|
||||
# data={
|
||||
# "etudid": etudid,
|
||||
# "evaluation_id": evaluation_id,
|
||||
# "value": 16.66, # la note !
|
||||
# "comment": "test API",
|
||||
# },
|
||||
# )
|
197
tests/api/exemple-api-scodoc7.py
Normal file
197
tests/api/exemple-api-scodoc7.py
Normal file
@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Exemple connexion sur ScoDoc 9 et utilisation de l'ancienne API ScoDoc 7
|
||||
à la mode "PHP": les gens passaient directement __ac_name et __ac_password
|
||||
dans chaque requête, en POST ou en GET.
|
||||
|
||||
Cela n'a jamais été documenté mais était implicitement supporté. C'est "deprecated"
|
||||
et ne sera plus supporté à partir de juillet 2022.
|
||||
|
||||
Ce script va tester:
|
||||
- Liste semestres
|
||||
- Liste modules
|
||||
- Creation d'une évaluation
|
||||
- Saisie d'une note
|
||||
|
||||
Utilisation: créer les variables d'environnement: (indiquer les valeurs
|
||||
pour le serveur ScoDoc que vous voulez interroger)
|
||||
|
||||
export SCODOC_URL="https://scodoc.xxx.net/"
|
||||
export SCODOC_USER="xxx"
|
||||
export SCODOC_PASSWD="xxx"
|
||||
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
|
||||
|
||||
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import json
|
||||
import os
|
||||
import pdb
|
||||
import requests
|
||||
import urllib3
|
||||
from pprint import pprint as pp
|
||||
|
||||
# --- Lecture configuration (variables d'env ou .env)
|
||||
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
||||
load_dotenv(os.path.join(BASEDIR, ".env"))
|
||||
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
|
||||
SCODOC_URL = os.environ["SCODOC_URL"]
|
||||
SCODOC_DEPT = os.environ["SCODOC_DEPT"]
|
||||
DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite"
|
||||
SCODOC_USER = os.environ["SCODOC_USER"]
|
||||
SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"]
|
||||
print(f"SCODOC_URL={SCODOC_URL}")
|
||||
|
||||
# ---
|
||||
if not CHECK_CERTIFICATE:
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
class ScoError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def GET(path: str, params=None, errmsg=None):
|
||||
"""Get and returns as JSON"""
|
||||
# ajoute auth
|
||||
params["__ac_name"] = SCODOC_USER
|
||||
params["__ac_password"] = SCODOC_PASSWORD
|
||||
r = requests.get(DEPT_URL + "/" + path, params=params, verify=CHECK_CERTIFICATE)
|
||||
if r.status_code != 200:
|
||||
raise ScoError(errmsg or "erreur !")
|
||||
return r.json() # decode la reponse JSON
|
||||
|
||||
|
||||
def POST(path: str, data: dict, errmsg=None):
|
||||
"""Post"""
|
||||
data["__ac_name"] = data.get("__ac_name", SCODOC_USER)
|
||||
data["__ac_password"] = data.get("__ac_password", SCODOC_PASSWORD)
|
||||
r = requests.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE)
|
||||
return r
|
||||
|
||||
|
||||
# ---
|
||||
# pas besoin d'ouvrir une session, on y va directement:
|
||||
|
||||
# --- Recupere la liste de tous les semestres:
|
||||
sems = GET("Notes/formsemestre_list", params={"format": "json"})
|
||||
|
||||
# sems est une liste de semestres (dictionnaires)
|
||||
for sem in sems:
|
||||
if sem["etat"]:
|
||||
break
|
||||
|
||||
if sem["etat"] == "0":
|
||||
raise ScoError("Aucun semestre non verrouillé !")
|
||||
|
||||
# Affiche le semestre trouvé:
|
||||
pp(sem)
|
||||
|
||||
# Liste des étudiants dans le 1er semestre non verrouillé:
|
||||
group_list = GET(
|
||||
"groups_view",
|
||||
params={
|
||||
"formsemestre_id": sem["formsemestre_id"],
|
||||
"with_codes": 1,
|
||||
"format": "json",
|
||||
},
|
||||
)
|
||||
if not group_list:
|
||||
# config inadaptée pour les tests...
|
||||
raise ScoError("aucun étudiant inscrit dans le semestre")
|
||||
|
||||
etud = group_list[0] # le premier étudiant inscrit ici
|
||||
# test un POST
|
||||
r = POST(
|
||||
"Absences/AddBilletAbsence",
|
||||
{
|
||||
"begin": "2021-10-25",
|
||||
"end": "2021-10-26",
|
||||
"description": "test API scodoc7",
|
||||
"etudid": etud["etudid"],
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.text.startswith('<?xml version="1.0" encoding="utf-8"?>')
|
||||
assert "billet_id" in r.text
|
||||
# Essai avec un compte invalide
|
||||
r_invalid = POST(
|
||||
"Absences/AddBilletAbsence",
|
||||
{
|
||||
"__ac_name": "xxx",
|
||||
"begin": "2021-10-25",
|
||||
"end": "2021-10-26",
|
||||
"description": "test API scodoc7",
|
||||
"etudid": etud["etudid"],
|
||||
},
|
||||
)
|
||||
assert r_invalid.status_code == 403 # compte invalide => not authorized
|
||||
|
||||
# AddBilletAbsence en json
|
||||
r = POST(
|
||||
"Absences/AddBilletAbsence",
|
||||
{
|
||||
"begin": "2021-10-25",
|
||||
"end": "2021-10-26",
|
||||
"description": "test API scodoc7",
|
||||
"etudid": etud["etudid"],
|
||||
"xml_reply": 0,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert isinstance(json.loads(r.text)[0]["billet_id"], int)
|
||||
|
||||
# Les fonctions ci-dessous ne fonctionnent plus en ScoDoc 9
|
||||
# Voir https://scodoc.org/git/viennet/ScoDoc/issues/149
|
||||
|
||||
# # ---- Liste les modules et prend le premier
|
||||
# mods = GET("/Notes/moduleimpl_list", params={"formsemestre_id": sem["formsemestre_id"]})
|
||||
# print(f"{len(mods)} modules dans le semestre {sem['titre']}")
|
||||
|
||||
# mod = mods[0]
|
||||
|
||||
# # ---- Etudiants inscrits dans ce module
|
||||
# inscrits = GET(
|
||||
# "Notes/do_moduleimpl_inscription_list",
|
||||
# params={"moduleimpl_id": mod["moduleimpl_id"]},
|
||||
# )
|
||||
# print(f"{len(inscrits)} inscrits dans ce module")
|
||||
# # prend le premier inscrit, au hasard:
|
||||
# etudid = inscrits[0]["etudid"]
|
||||
|
||||
# # ---- Création d'une evaluation le dernier jour du semestre
|
||||
# jour = sem["date_fin"]
|
||||
# evaluation_id = POST(
|
||||
# "/Notes/do_evaluation_create",
|
||||
# data={
|
||||
# "moduleimpl_id": mod["moduleimpl_id"],
|
||||
# "coefficient": 1,
|
||||
# "jour": jour, # "5/9/2019",
|
||||
# "heure_debut": "9h00",
|
||||
# "heure_fin": "10h00",
|
||||
# "note_max": 20, # notes sur 20
|
||||
# "description": "essai",
|
||||
# },
|
||||
# errmsg="échec création évaluation",
|
||||
# )
|
||||
|
||||
# print(
|
||||
# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
|
||||
# )
|
||||
# print(
|
||||
# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}",
|
||||
# )
|
||||
|
||||
# # ---- Saisie d'une note
|
||||
# junk = POST(
|
||||
# "/Notes/save_note",
|
||||
# data={
|
||||
# "etudid": etudid,
|
||||
# "evaluation_id": evaluation_id,
|
||||
# "value": 16.66, # la note !
|
||||
# "comment": "test API",
|
||||
# },
|
||||
# )
|
307
tests/unit/test_notes_modules.py
Normal file
307
tests/unit/test_notes_modules.py
Normal file
@ -0,0 +1,307 @@
|
||||
"""Test calculs moyennes de modules
|
||||
Vérif moyennes de modules des bulletins
|
||||
et aussi moyennes modules et UE internes (via nt)
|
||||
"""
|
||||
|
||||
from config import TestConfig
|
||||
from tests.unit import sco_fake_gen
|
||||
|
||||
from flask import g
|
||||
|
||||
import app
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
DEPT = TestConfig.DEPT_TEST
|
||||
|
||||
|
||||
def check_nt(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
ue_id,
|
||||
moduleimpl_id,
|
||||
expected_moy_ue=False,
|
||||
expected_mod_moy=False,
|
||||
expected_sum_coefs_ue=False,
|
||||
):
|
||||
"""Vérification bas niveau: vérif resultat avec l'API internet "nt"
|
||||
(peut changer dans le futur, ne pas utiliser hors ScoDoc !)
|
||||
ne vérifie que les valeurs expected non False
|
||||
"""
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
mod_moy = nt.get_etud_mod_moy(moduleimpl_id, etudid)
|
||||
if expected_moy_ue is not False:
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
assert expected_moy_ue == ue_status["moy"]
|
||||
if expected_mod_moy is not False:
|
||||
assert expected_mod_moy == mod_moy
|
||||
if expected_sum_coefs_ue is not False:
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
assert expected_sum_coefs_ue == ue_status["sum_coefs"]
|
||||
|
||||
|
||||
def test_notes_modules(test_client):
|
||||
"""Test calcul des moyennes de modules et d'UE
|
||||
Création étudiant, formation, semestre, inscription etudiant,
|
||||
création evaluation, saisie de notes.
|
||||
Vérifie calcul moyenne avec absences (ABS), excuse (EXC), attente (ATT)
|
||||
"""
|
||||
app.set_sco_dept(DEPT)
|
||||
|
||||
G = sco_fake_gen.ScoFake(verbose=False)
|
||||
etuds = [G.create_etud(code_nip=None) for i in range(2)] # 2 étudiants
|
||||
|
||||
f = G.create_formation(acronyme="")
|
||||
ue = G.create_ue(formation_id=f["formation_id"], acronyme="TST1", titre="ue test")
|
||||
ue_id = ue["ue_id"]
|
||||
mat = G.create_matiere(ue_id=ue_id, titre="matière test")
|
||||
coef_mod_1 = 1.5
|
||||
mod = G.create_module(
|
||||
matiere_id=mat["matiere_id"],
|
||||
code="TSM1",
|
||||
coefficient=coef_mod_1,
|
||||
titre="module test",
|
||||
ue_id=ue["ue_id"],
|
||||
formation_id=f["formation_id"],
|
||||
)
|
||||
|
||||
# --- Mise place d'un semestre
|
||||
sem = G.create_formsemestre(
|
||||
formation_id=f["formation_id"],
|
||||
semestre_id=1,
|
||||
date_debut="01/01/2020",
|
||||
date_fin="30/06/2020",
|
||||
)
|
||||
formsemestre_id = sem["formsemestre_id"]
|
||||
mi = G.create_moduleimpl(
|
||||
module_id=mod["module_id"],
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
moduleimpl_id = mi["moduleimpl_id"]
|
||||
# --- Inscription des étudiants
|
||||
for etud in etuds:
|
||||
G.inscrit_etudiant(sem, etud)
|
||||
etud = etuds[0]
|
||||
etudid = etud["etudid"]
|
||||
# --- Creation évaluations: e1, e2
|
||||
coef_1 = 1.0
|
||||
coef_2 = 2.0
|
||||
e1 = G.create_evaluation(
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
jour="01/01/2020",
|
||||
description="evaluation 1",
|
||||
coefficient=coef_1,
|
||||
)
|
||||
e2 = G.create_evaluation(
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
jour="01/01/2020",
|
||||
description="evaluation 2",
|
||||
coefficient=coef_2,
|
||||
)
|
||||
# --- Notes ordinaires
|
||||
note_1 = 12.0
|
||||
note_2 = 13.0
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etuds[0], note=note_1)
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etuds[0], note=note_2)
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=note_1 / 2)
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etuds[1], note=note_2 / 3)
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
# Vérifie structure du bulletin:
|
||||
assert b["etudid"] == etud["etudid"]
|
||||
assert len(b["ues"][0]["modules"][0]["evaluations"]) == 2
|
||||
assert len(b["ues"][0]["modules"]) == 1
|
||||
# Note moyenne:
|
||||
note_th = (coef_1 * note_1 + coef_2 * note_2) / (coef_1 + coef_2)
|
||||
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th)
|
||||
check_nt(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
ue_id,
|
||||
moduleimpl_id,
|
||||
expected_mod_moy=note_th,
|
||||
expected_moy_ue=note_th,
|
||||
expected_sum_coefs_ue=coef_mod_1,
|
||||
)
|
||||
|
||||
# Absence à une évaluation
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=None) # abs
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etud, note=note_2)
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
note_th = (coef_1 * 0.0 + coef_2 * note_2) / (coef_1 + coef_2)
|
||||
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th)
|
||||
# Absences aux deux évaluations
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=None) # abs
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etud, note=None) # abs
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(0.0)
|
||||
check_nt(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
ue_id,
|
||||
moduleimpl_id,
|
||||
expected_mod_moy=0.0,
|
||||
expected_moy_ue=0.0,
|
||||
expected_sum_coefs_ue=0.0,
|
||||
)
|
||||
|
||||
# Note excusée EXC <-> scu.NOTES_NEUTRALISE
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1)
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_1)
|
||||
check_nt(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
ue_id,
|
||||
moduleimpl_id,
|
||||
expected_mod_moy=note_1,
|
||||
expected_moy_ue=note_1,
|
||||
expected_sum_coefs_ue=coef_mod_1,
|
||||
)
|
||||
# Note en attente ATT <-> scu.NOTES_ATTENTE
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1)
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_ATTENTE) # ATT
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_1)
|
||||
check_nt(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
ue_id,
|
||||
moduleimpl_id,
|
||||
expected_mod_moy=note_1,
|
||||
expected_moy_ue=note_1,
|
||||
expected_sum_coefs_ue=coef_mod_1,
|
||||
)
|
||||
# Neutralisation (EXC) des 2 évals
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == "-"
|
||||
check_nt(
|
||||
etudid,
|
||||
sem["formsemestre_id"],
|
||||
ue["ue_id"],
|
||||
mi["moduleimpl_id"],
|
||||
expected_mod_moy="NA0",
|
||||
expected_moy_ue=0.0,
|
||||
expected_sum_coefs_ue=0.0,
|
||||
)
|
||||
# Attente (ATT) sur les 2 evals
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_ATTENTE) # ATT
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_ATTENTE) # ATT
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == "-"
|
||||
check_nt(
|
||||
etudid,
|
||||
sem["formsemestre_id"],
|
||||
ue["ue_id"],
|
||||
mi["moduleimpl_id"],
|
||||
expected_mod_moy="NA0",
|
||||
expected_moy_ue=0.0,
|
||||
expected_sum_coefs_ue=0.0,
|
||||
)
|
||||
# Non inscrit
|
||||
# - désinscrit notre étudiant:
|
||||
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=mi["moduleimpl_id"], etudid=etud["etudid"]
|
||||
)
|
||||
assert len(inscr) == 1
|
||||
oid = inscr[0]["moduleimpl_inscription_id"]
|
||||
sco_moduleimpl.do_moduleimpl_inscription_delete(
|
||||
oid, formsemestre_id=mi["formsemestre_id"]
|
||||
)
|
||||
# -
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
assert b["ues"] == [] # inscrit à aucune UE !
|
||||
check_nt(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
ue_id,
|
||||
moduleimpl_id,
|
||||
expected_mod_moy="NI",
|
||||
expected_moy_ue=0.0,
|
||||
expected_sum_coefs_ue=0.0,
|
||||
)
|
||||
# --- Maintenant avec 2 modules dans l'UE
|
||||
mod2 = G.create_module(
|
||||
matiere_id=mat["matiere_id"],
|
||||
code="TSM2",
|
||||
coefficient=coef_mod_2,
|
||||
titre="module test 2",
|
||||
ue_id=ue_id,
|
||||
formation_id=f["formation_id"],
|
||||
)
|
||||
mi2 = G.create_moduleimpl(
|
||||
module_id=mod2["module_id"],
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
# Re-inscription au premier module de l'UE
|
||||
sco_moduleimpl.do_moduleimpl_inscription_create(
|
||||
{"etudid": etudid, "moduleimpl_id": mi["moduleimpl_id"]},
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=12.5)
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
assert ue_status["nb_missing"] == 1 # 1 même si etud non inscrit à l'autre module
|
||||
assert ue_status["nb_notes"] == 1
|
||||
assert not ue_status["was_capitalized"]
|
||||
# Inscription au deuxième module de l'UE
|
||||
sco_moduleimpl.do_moduleimpl_inscription_create(
|
||||
{"etudid": etudid, "moduleimpl_id": mi2["moduleimpl_id"]},
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
assert ue_status["nb_missing"] == 1 # mi2 n'a pas encore de note
|
||||
assert ue_status["nb_notes"] == 1
|
||||
# Note dans module 2:
|
||||
e_m2 = G.create_evaluation(
|
||||
moduleimpl_id=mi2["moduleimpl_id"],
|
||||
jour="01/01/2020",
|
||||
description="evaluation mod 2",
|
||||
coefficient=1.0,
|
||||
)
|
||||
_, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=19.5)
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
assert ue_status["nb_missing"] == 0
|
||||
assert ue_status["nb_notes"] == 2
|
||||
|
||||
# Moyenne d'UE si l'un des modules est EXC ("NA0")
|
||||
# 2 modules, notes EXC dans le premier, note valide n dans le second
|
||||
# la moyenne de l'UE doit être n
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
|
||||
_, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=12.5)
|
||||
_, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=11.0)
|
||||
_, _, _ = G.create_note(evaluation=e2, etud=etuds[1], note=11.0)
|
||||
_, _, _ = G.create_note(evaluation=e_m2, etud=etuds[1], note=11.0)
|
||||
b = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etud["etudid"]
|
||||
)
|
||||
assert b["ues"][0]["ue_status"]["cur_moy_ue"] == 12.5
|
||||
assert b["ues"][0]["ue_status"]["moy"] == 12.5
|
||||
b2 = sco_bulletins.formsemestre_bulletinetud_dict(
|
||||
sem["formsemestre_id"], etuds[1]["etudid"]
|
||||
)
|
||||
assert b2["ues"][0]["ue_status"]["cur_moy_ue"] == 11.0
|
||||
assert b2["ues"][0]["ue_status"]["moy"] == 11
|
@ -15,8 +15,8 @@ DEPT = TestConfig.DEPT_TEST
|
||||
|
||||
def test_notes_rattrapage(test_client):
|
||||
"""Test quelques opérations élémentaires de ScoDoc
|
||||
Création 10 étudiants, formation, semestre, inscription etudiant,
|
||||
creation 1 evaluation, saisie 10 notes.
|
||||
Création 1 étudiant, formation, semestre, inscription etudiant,
|
||||
creation 1 evaluation, saisie notes.
|
||||
"""
|
||||
app.set_sco_dept(DEPT)
|
||||
|
||||
|
@ -6,4 +6,4 @@
|
||||
|
||||
from tools.import_scodoc7_user_db import import_scodoc7_user_db
|
||||
from tools.import_scodoc7_dept import import_scodoc7_dept
|
||||
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archive
|
||||
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives
|
||||
|
@ -233,7 +233,7 @@ do
|
||||
done
|
||||
|
||||
# ----- Post-Migration: renomme archives en fonction des nouveaux ids
|
||||
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask migrate-scodoc7-dept-archive)" "$SCODOC_USER" || die "Erreur de la post-migration des archives"
|
||||
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask migrate-scodoc7-dept-archives)" "$SCODOC_USER" || die "Erreur de la post-migration des archives"
|
||||
|
||||
|
||||
# --- Si migration "en place", désactive ScoDoc 7
|
||||
|
@ -11,7 +11,7 @@ from app.models.formsemestre import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
|
||||
def migrate_scodoc7_dept_archive(dept_name=""):
|
||||
def migrate_scodoc7_dept_archives(dept_name=""):
|
||||
if dept_name:
|
||||
depts = Departement.query.filter_by(acronym=dept_name)
|
||||
else:
|
||||
|
Loading…
Reference in New Issue
Block a user