diff --git a/app/api/__init__.py b/app/api/__init__.py index d397a93d..36da5c56 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -23,4 +23,13 @@ def requested_format(default_format="json", allowed_formats=None): from app.api import tokens from app.api import sco_api +from app.api import test_api +from app.api import departements +from app.api import etudiants +from app.api import formations +from app.api import formsemestres +from app.api import partitions +from app.api import evaluations +from app.api import jury +from app.api import absences from app.api import logos diff --git a/app/api/absences.py b/app/api/absences.py new file mode 100644 index 00000000..35abe34f --- /dev/null +++ b/app/api/absences.py @@ -0,0 +1,150 @@ +#################################################### Absences ######################################################### + +from flask import jsonify + +from app.api import bp +from app.api.errors import error_response +from app.api.auth import token_permission_required +from app.api.tools import get_etu_from_etudid_or_nip_or_ine +from app.scodoc import notesdb as ndb + +from app.scodoc import sco_abs +from app.scodoc.sco_groups import get_group_members +from app.scodoc.sco_permissions import Permission + + +@bp.route("/absences/etudid/", methods=["GET"]) +@bp.route("/absences/nip/", methods=["GET"]) +@bp.route("/absences/ine/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def absences(etudid: int = None, nip: int = None, ine: int = None): + """ + Retourne la liste des absences d'un étudiant donné + + etudid : l'etudid d'un étudiant + nip: le code nip d'un étudiant + ine : le code ine d'un étudiant + + Exemple de résultat: + [ + { + "jour": "2022-04-15", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "2022-04-15", + "matin": false, + "estabs": true, + "estjust": false, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + } + ] + """ + if etudid is None: + # Récupération de l'étudiant + etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etud is None: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel.\n " + "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + ) + etudid = etud.etudid + + # Récupération des absences de l'étudiant + ndb.open_db_connection() + absences = sco_abs.list_abs_date(etudid) + for absence in absences: + absence["jour"] = absence["jour"].isoformat() + return jsonify(absences) + + +@bp.route("/absences/etudid//just", methods=["GET"]) +@bp.route("/absences/nip//just", methods=["GET"]) +@bp.route("/absences/ine//just", methods=["GET"]) +@token_permission_required(Permission.APIView) +def absences_just(etudid: int = None, nip: int = None, ine: int = None): + """ + Retourne la liste des absences justifiées d'un étudiant donné + + etudid : l'etudid d'un étudiant + nip: le code nip d'un étudiant + ine : le code ine d'un étudiant + + Exemple de résultat : + [ + { + "jour": "2022-04-15", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "Fri, 15 Apr 2022 00:00:00 GMT", + "matin": false, + "estabs": true, + "estjust": false, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + } + ] + """ + if etudid is None: + # Récupération de l'étudiant + try: + etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + etudid = etu.etudid + except AttributeError: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel.\n " + "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + ) + + # Récupération des absences justifiées de l'étudiant + absences = sco_abs.list_abs_date(etudid) + for absence in [absence for absence in absences if absence["estjust"]]: + absence["jour"] = absence["jour"].isoformat() + return jsonify(absences) + + +@bp.route( + "/absences/abs_group_etat/?group_id=&date_debut=date_debut&date_fin=date_fin", + methods=["GET"], +) +@token_permission_required(Permission.APIView) +def abs_groupe_etat( # XXX A REVOIR XXX + group_id: int, date_debut, date_fin, with_boursier=True, format="html" +): + """ + Retoune la liste des absences d'un ou plusieurs groupes entre deux dates + """ + # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date() + + try: + # Utilisation de la fonction get_group_members + members = get_group_members(group_id) + except ValueError: + return error_response( + 409, message="La requête ne peut être traitée en l’état actuel" + ) + + data = [] + # Filtre entre les deux dates renseignées + for member in members: + abs = sco_abs.list_abs_date(member.id, date_debut, date_fin) + data.append(abs) + + # return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences) + return error_response(501, message="Not implemented") diff --git a/app/api/auth.py b/app/api/auth.py index 331cd388..20dd7ded 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -24,6 +24,10 @@ # 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 functools import wraps + + +from flask import abort from flask import g from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth from app.auth.models import User @@ -63,15 +67,17 @@ def get_user_roles(user): return user.roles -# 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) +def token_permission_required(permission): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + scodoc_dept = getattr(g, "scodoc_dept", None) + if hasattr(g, "current_user") and not g.current_user.has_permission( + permission, scodoc_dept + ): + abort(403) + return f(*args, **kwargs) -# return login_required(decorated_function) + return decorated_function # login_required(decorated_function) -# return decorator + return decorator diff --git a/app/api/departements.py b/app/api/departements.py new file mode 100644 index 00000000..924d1e44 --- /dev/null +++ b/app/api/departements.py @@ -0,0 +1,303 @@ +############################################### Departements ########################################################## +import app + +from app import models +from app.api import bp +from app.api.auth import token_permission_required +from app.api.errors import error_response +from app.scodoc.sco_permissions import Permission + +from flask import jsonify + + +@bp.route("/departements", methods=["GET"]) +@token_permission_required(Permission.APIView) +def departements(): + """ + Retourne la liste des ids de départements visibles + + Exemple de résultat : + [ + { + "id": 1, + "acronym": "TAPI", + "description": null, + "visible": true, + "date_creation": "Fri, 15 Apr 2022 12:19:28 GMT" + }, + { + "id": 2, + "acronym": "MMI", + "description": null, + "visible": false, + "date_creation": "Fri, 18 Apr 2022 11:20:8 GMT" + }, + ... + ] + """ + # Récupération de tous les départements + depts = models.Departement.query.all() + + # Mise en place de la liste avec tous les départements + data = [d.to_dict() for d in depts] + + return jsonify(data) + + +@bp.route("/departements//etudiants/liste", methods=["GET"]) +@bp.route( + "/departements//etudiants/liste/", methods=["GET"] +) +@token_permission_required(Permission.APIView) +def liste_etudiants(dept: str, formsemestre_id=None): + """ + Retourne la liste des étudiants d'un département + + dept: l'acronym d'un département + formsemestre_id: l'id d'un formesemestre + + Exemple de résultat : + [ + { + "civilite": "X", + "code_ine": null, + "code_nip": null, + "date_naissance": null, + "email": null, + "emailperso": null, + "etudid": 18, + "nom": "MOREL", + "prenom": "JACQUES" + }, + { + "civilite": "X", + "code_ine": null, + "code_nip": null, + "date_naissance": null, + "email": null, + "emailperso": null, + "etudid": 19, + "nom": "FOURNIER", + "prenom": "ANNE" + }, + ... + ] + """ + # Si le formsemestre_id a été renseigné + if formsemestre_id is not None: + # Récupération du formsemestre + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + # Récupération du département + departement = formsemestre.departement + + # Si le formsemestre_id n'a pas été renseigné + else: + # Récupération du formsemestre + departement = models.Departement.query.filter_by(acronym=dept).first_or_404() + + # Récupération des étudiants + etudiants = departement.etudiants.all() + + # Mise en forme des données + list_etu = [etu.to_dict_bul(include_urls=False) for etu in etudiants] + + return jsonify(list_etu) + + +@bp.route("/departements//semestres_courants", methods=["GET"]) +@token_permission_required(Permission.APIView) +def liste_semestres_courant(dept: str): + """ + Liste des semestres actifs d'un départements donné + + dept: l'acronym d'un département + + Exemple de résultat : + [ + { + "titre": "master machine info", + "gestion_semestrielle": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "bul_bgcolor": null, + "date_fin": "15/12/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": false, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "block_moyennes": false, + "formsemestre_id": 1, + "titre_num": "master machine info semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-12-15", + "responsables": [ + 3, + 2 + ] + }, + ... + ] + """ + # Récupération des départements comportant l'acronym mit en paramètre + dept = models.Departement.query.filter_by(acronym=dept).first_or_404() + + # Récupération des semestres suivant id_dept + semestres = models.FormSemestre.query.filter_by(dept_id=dept.id, etat=True) + + # Mise en forme des données + data = [d.to_dict() for d in semestres] + + return jsonify(data) + + +@bp.route( + "/departements//formations//referentiel_competences", + methods=["GET"], +) +@token_permission_required(Permission.APIView) +def referenciel_competences(dept: str, formation_id: int): + """ + Retourne le référentiel de compétences + + dept : l'acronym d'un département + formation_id : l'id d'une formation + """ + dept = models.Departement.query.filter_by(acronym=dept).first_or_404() + + formation = models.Formation.query.filter_by( + id=formation_id, dept_id=dept.id + ).first_or_404() + + ref_comp = formation.referentiel_competence_id + + if ref_comp is None: + return error_response( + 204, message="Pas de référenciel de compétences pour cette formation" + ) + else: + return jsonify(ref_comp) + + +@bp.route( + "/departements//formsemestre//programme", + methods=["GET"], +) +@token_permission_required(Permission.APIView) +def semestre_index(dept: str, formsemestre_id: int): + """ + Retourne la liste des Ues, ressources et SAE d'un semestre + + dept : l'acronym d'un département + formsemestre_id : l'id d'un formesemestre + + Exemple de résultat : + { + "ues": [ + { + "type": 0, + "formation_id": 1, + "ue_code": "UCOD11", + "id": 1, + "ects": 12.0, + "acronyme": "RT1.1", + "is_external": false, + "numero": 1, + "code_apogee": "", + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "coefficient": 0.0, + "semestre_idx": 1, + "color": "#B80004", + "ue_id": 1 + }, + ... + ], + "ressources": [ + { + "titre": "Fondamentaux de la programmation", + "coefficient": 1.0, + "module_type": 2, + "id": 17, + "ects": null, + "abbrev": null, + "ue_id": 3, + "code": "R107", + "formation_id": 1, + "heures_cours": 0.0, + "matiere_id": 3, + "heures_td": 0.0, + "semestre_id": 1, + "heures_tp": 0.0, + "numero": 70, + "code_apogee": "", + "module_id": 17 + }, + ... + ], + "saes": [ + { + "titre": "Se pr\u00e9senter sur Internet", + "coefficient": 1.0, + "module_type": 3, + "id": 14, + "ects": null, + "abbrev": null, + "ue_id": 3, + "code": "SAE14", + "formation_id": 1, + "heures_cours": 0.0, + "matiere_id": 3, + "heures_td": 0.0, + "semestre_id": 1, + "heures_tp": 0.0, + "numero": 40, + "code_apogee": "", + "module_id": 14 + }, + ... + ] + } + """ + + app.set_sco_dept(dept) + + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + ues = formsemestre.query_ues() + + ues_dict = [] + ressources = [] + saes = [] + + for ue in ues: + ues_dict.append(ue.to_dict()) + ressources = ue.get_ressources() + saes = ue.get_saes() + + data_ressources = [] + for ressource in ressources: + data_ressources.append(ressource.to_dict()) + + data_saes = [] + for sae in saes: + data_saes.append(sae.to_dict()) + + data = { + "ues": ues_dict, + "ressources": data_ressources, + "saes": data_saes, + } + + return data diff --git a/app/api/etudiants.py b/app/api/etudiants.py new file mode 100644 index 00000000..964dcde9 --- /dev/null +++ b/app/api/etudiants.py @@ -0,0 +1,432 @@ +#################################################### Etudiants ######################################################## + +from flask import jsonify + +import app +from app import models +from app.api import bp +from app.api.errors import error_response +from app.api.auth import token_permission_required +from app.api.tools import get_etu_from_etudid_or_nip_or_ine +from app.models import FormSemestreInscription, FormSemestre, Identite +from app.scodoc import sco_bulletins +from app.scodoc import sco_groups +from app.scodoc.sco_permissions import Permission +from app.scodoc import notesdb as ndb + + +@bp.route("/etudiants/courant", methods=["GET"]) +@token_permission_required(Permission.APIView) +def etudiants_courant(): + """ + Retourne la liste des étudiants courant + + Exemple de résultat : + { + { + "civilite": "X", + "code_ine": null, + "code_nip": null, + "date_naissance": null, + "email": null, + "emailperso": null, + "etudid": 18, + "nom": "MOREL", + "prenom": "JACQUES" + }, + { + "civilite": "X", + "code_ine": null, + "code_nip": null, + "date_naissance": null, + "email": null, + "emailperso": null, + "etudid": 19, + "nom": "FOURNIER", + "prenom": "ANNE" + }, + ... + } + """ + # Récupération de tous les étudiants + etuds = Identite.query.filter( + Identite.id == FormSemestreInscription.etudid, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + FormSemestre.date_debut <= app.db.func.now(), + FormSemestre.date_fin >= app.db.func.now(), + ) + + data = [etu.to_dict_bul(include_urls=False) for etu in etuds] + + return jsonify(data) + + +@bp.route("/etudiant/etudid/", methods=["GET"]) +@bp.route("/etudiant/nip/", methods=["GET"]) +@bp.route("/etudiant/ine/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def etudiant(etudid: int = None, nip: int = None, ine: int = None): + """ + Retourne les informations de l'étudiant correspondant à l'id passé en paramètres. + + etudid : l'etudid d'un étudiant + nip : le code nip d'un étudiant + ine : le code ine d'un étudiant + + Exemple de résultat : + { + "civilite": "X", + "code_ine": null, + "code_nip": null, + "date_naissance": null, + "email": null, + "emailperso": null, + "etudid": 18, + "nom": "MOREL", + "prenom": "JACQUES" + } + """ + # Récupération de l'étudiant + etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + + # Mise en forme des données + data = etud.to_dict_bul(include_urls=False) + + return jsonify(data) + + +@bp.route("/etudiant/etudid//formsemestres") +@bp.route("/etudiant/nip//formsemestres") +@bp.route("/etudiant/ine//formsemestres") +@token_permission_required(Permission.APIView) +def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): + """ + Retourne la liste des semestres qu'un étudiant a suivis, triés par ordre chronologique. + + etudid : l'etudid d'un étudiant + nip : le code nip d'un étudiant + ine : le code ine d'un étudiant + + Exemple de résultat : + [ + { + "titre": "master machine info", + "gestion_semestrielle": false, + "date_debut": "01/09/2021", + "bul_bgcolor": null, + "date_fin": "15/12/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": false, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "block_moyennes": false, + "formsemestre_id": 1, + "titre_num": "master machine info semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-12-15", + "responsables": [ + 3, + 2 + ] + }, + ... + ] + """ + # Récupération de l'étudiant + etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + + formsemestres = models.FormSemestre.query.filter( + models.FormSemestreInscription.etudid == etud.id, + models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id, + ).order_by(models.FormSemestre.date_debut) + + return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) + + +@bp.route( + "/etudiant/etudid//formsemestre//bulletin", + methods=["GET"], +) +@bp.route( + "/etudiant/nip//formsemestre//bulletin", + methods=["GET"], +) +@bp.route( + "/etudiant/ine//formsemestre//bulletin", + methods=["GET"], +) +@token_permission_required(Permission.APIView) +def etudiant_bulletin_semestre( + formsemestre_id, etudid: int = None, nip: int = None, ine: int = None +): + """ + Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné + + formsemestre_id : l'id d'un formsemestre + etudid : l'etudid d'un étudiant + nip : le code nip d'un étudiant + ine : le code ine d'un étudiant + Exemple de résultat : + { + "version": "0", + "type": "BUT", + "date": "2022-04-27T07:18:16.450634Z", + "publie": true, + "etudiant": { + "civilite": "X", + "code_ine": "1", + "code_nip": "1", + "date_naissance": "", + "email": "SACHA.COSTA@example.com", + "emailperso": "", + "etudid": 1, + "nom": "COSTA", + "prenom": "SACHA", + "nomprenom": "Sacha COSTA", + "lieu_naissance": "", + "dept_naissance": "", + "nationalite": "", + "boursier": "", + "fiche_url": "/ScoDoc/TAPI/Scolarite/ficheEtud?etudid=1", + "photo_url": "/ScoDoc/TAPI/Scolarite/get_photo_image?etudid=1&size=small", + "id": 1, + "codepostaldomicile": "", + "paysdomicile": "", + "telephonemobile": "", + "typeadresse": "domicile", + "domicile": "", + "villedomicile": "", + "telephone": "", + "fax": "", + "description": "" + }, + "formation": { + "id": 1, + "acronyme": "BUT R&T", + "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", + "titre": "BUT R&T" + }, + "formsemestre_id": 1, + "etat_inscription": "I", + "options": { + "show_abs": true, + "show_abs_modules": false, + "show_ects": true, + "show_codemodules": false, + "show_matieres": false, + "show_rangs": true, + "show_ue_rangs": true, + "show_mod_rangs": true, + "show_moypromo": false, + "show_minmax": false, + "show_minmax_mod": false, + "show_minmax_eval": false, + "show_coef": true, + "show_ue_cap_details": false, + "show_ue_cap_current": true, + "show_temporary": true, + "temporary_txt": "Provisoire", + "show_uevalid": true, + "show_date_inscr": true + }, + "ressources": { + "R101": { + "id": 1, + "titre": "Initiation aux r\u00e9seaux informatiques", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1", + "moyenne": {}, + "evaluations": [ + { + "id": 1, + "description": "eval1", + "date": "2022-04-20", + "heure_debut": "08:00", + "heure_fin": "09:00", + "coef": "01.00", + "poids": { + "RT1.1": 1.0, + }, + "note": { + "value": "12.00", + "min": "00.00", + "max": "18.00", + "moy": "10.88" + }, + "url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1" + } + ] + }, + }, + "saes": { + "SAE11": { + "id": 2, + "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2", + "moyenne": {}, + "evaluations": [] + }, + }, + "ues": { + "RT1.1": { + "id": 1, + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "numero": 1, + "type": 0, + "color": "#B80004", + "competence": null, + "moyenne": { + "value": "08.50", + "min": "06.00", + "max": "16.50", + "moy": "11.31", + "rang": "12", + "total": 16 + }, + "bonus": "00.00", + "malus": "00.00", + "capitalise": null, + "ressources": { + "R101": { + "id": 1, + "coef": 12.0, + "moyenne": "12.00" + }, + }, + "saes": { + "SAE11": { + "id": 2, + "coef": 16.0, + "moyenne": "~" + }, + }, + "ECTS": { + "acquis": 0.0, + "total": 12.0 + } + }, + "semestre": { + "etapes": [], + "date_debut": "2021-09-01", + "date_fin": "2022-08-31", + "annee_universitaire": "2021 - 2022", + "numero": 1, + "inscription": "", + "groupes": [], + "absences": { + "injustifie": 1, + "total": 2 + }, + "ECTS": { + "acquis": 0, + "total": 30.0 + }, + "notes": { + "value": "10.60", + "min": "02.40", + "moy": "11.05", + "max": "17.40" + }, + "rang": { + "value": "10", + "total": 16 + } + } + } + """ + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() + + app.set_sco_dept(dept.acronym) + + # Récupération de l'étudiant + try: + etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + except AttributeError: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel.\n " + "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + ) + + return sco_bulletins.get_formsemestre_bulletin_etud_json(formsemestre, etu) + + +@bp.route( + "/etudiant/etudid//semestre//groups", + methods=["GET"], +) +@bp.route( + "/etudiant/nip//semestre//groups", methods=["GET"] +) +@bp.route( + "/etudiant/ine//semestre//groups", methods=["GET"] +) +@token_permission_required(Permission.APIView) +def etudiant_groups( + formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None +): + """ + Retourne la liste des groupes auxquels appartient l'étudiant dans le semestre indiqué + + formsemestre_id : l'id d'un formsemestre + etudid : l'etudid d'un étudiant + nip : le code nip d'un étudiant + ine : le code ine d'un étudiant + + Exemple de résultat : + [ + { + "partition_id": 1, + "id": 1, + "formsemestre_id": 1, + "partition_name": null, + "numero": 0, + "bul_show_rank": false, + "show_in_lists": true, + "group_id": 1, + "group_name": null + }, + { + "partition_id": 2, + "id": 2, + "formsemestre_id": 1, + "partition_name": "TD", + "numero": 1, + "bul_show_rank": false, + "show_in_lists": true, + "group_id": 2, + "group_name": "A" + } + ] + """ + if etudid is None: + etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etud is None: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel.\n " + "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + ) + etudid = etud.etudid + + # Récupération du formsemestre + sem = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() + dept = models.Departement.query.get(sem.dept_id) + app.set_sco_dept(dept.acronym) + data = sco_groups.get_etud_groups(etudid, sem.id) + + return jsonify(data) diff --git a/app/api/evaluations.py b/app/api/evaluations.py new file mode 100644 index 00000000..2f6049d9 --- /dev/null +++ b/app/api/evaluations.py @@ -0,0 +1,111 @@ +############################################### Evaluations ########################################################### +from flask import jsonify + +import app + +from app import models +from app.api import bp +from app.api.auth import token_permission_required +from app.api.errors import error_response +from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes +from app.scodoc.sco_permissions import Permission + + +@bp.route("/evaluations/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def evaluations(moduleimpl_id: int): + """ + Retourne la liste des évaluations à partir de l'id d'un moduleimpl + + moduleimpl_id : l'id d'un moduleimpl + + Exemple de résultat : + [ + { + "moduleimpl_id": 1, + "jour": "20/04/2022", + "heure_debut": "08h00", + "description": "eval1", + "coefficient": 1.0, + "publish_incomplete": false, + "numero": 0, + "id": 1, + "heure_fin": "09h00", + "note_max": 20.0, + "visibulletin": true, + "evaluation_type": 0, + "evaluation_id": 1, + "jouriso": "2022-04-20", + "duree": "1h", + "descrheure": " de 08h00 \u00e0 09h00", + "matin": 1, + "apresmidi": 0 + }, + ... + ] + """ + # Récupération de toutes les évaluations + evals = models.Evaluation.query.filter_by(id=moduleimpl_id) + + # Mise en forme des données + data = [d.to_dict() for d in evals] + + return jsonify(data) + + +@bp.route("/evaluations/eval_notes/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def evaluation_notes(evaluation_id: int): + """ + Retourne la liste des notes à partir de l'id d'une évaluation donnée + + evaluation_id : l'id d'une évaluation + + Exemple de résultat : + { + "1": { + "id": 1, + "etudid": 10, + "evaluation_id": 1, + "value": 15.0, + "comment": "", + "date": "Wed, 20 Apr 2022 06:49:05 GMT", + "uid": 2 + }, + "2": { + "id": 2, + "etudid": 1, + "evaluation_id": 1, + "value": 12.0, + "comment": "", + "date": "Wed, 20 Apr 2022 06:49:06 GMT", + "uid": 2 + }, + ... + } + """ + # Fonction utilisée : app.scodoc.sco_evaluation_db.do_evaluation_get_all_notes() + + eval = models.Evaluation.query.filter_by(id=evaluation_id).first_or_404() + + moduleimpl = models.ModuleImpl.query.filter_by(id=eval.moduleimpl_id).first_or_404() + + formsemestre = models.FormSemestre.query.filter_by( + id=moduleimpl.formsemestre_id + ).first_or_404() + + dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() + + app.set_sco_dept(dept.acronym) + + try: + # Utilisation de la fonction do_evaluation_get_all_notes + data = do_evaluation_get_all_notes(evaluation_id) + except AttributeError: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel. \n" + "Veillez vérifier la conformité du 'evaluation_id'", + ) + + return jsonify(data) diff --git a/app/api/formations.py b/app/api/formations.py new file mode 100644 index 00000000..f7d1a642 --- /dev/null +++ b/app/api/formations.py @@ -0,0 +1,260 @@ +##############################################" Formations ############################################################ +from flask import jsonify + +from app import models +from app.api import bp +from app.api.errors import error_response +from app.api.auth import token_permission_required +from app.scodoc.sco_formations import formation_export +from app.scodoc.sco_permissions import Permission + + +@bp.route("/formations", methods=["GET"]) +@token_permission_required(Permission.APIView) +def formations(): + """ + Retourne la liste des formations + + Exemple de résultat : + [ + { + "id": 1, + "acronyme": "BUT R&T", + "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", + "formation_code": "V1RET", + "code_specialite": null, + "dept_id": 1, + "titre": "BUT R&T", + "version": 1, + "type_parcours": 700, + "referentiel_competence_id": null, + "formation_id": 1 + }, + ... + ] + + """ + # Récupération de toutes les formations + list_formations = models.Formation.query.all() + + # Mise en forme des données + data = [d.to_dict() for d in list_formations] + + return jsonify(data) + + +@bp.route("/formations/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def formations_by_id(formation_id: int): + """ + Retourne une formation en fonction d'un id donné + + formation_id : l'id d'une formation + + Exemple de résultat : + { + "id": 1, + "acronyme": "BUT R&T", + "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", + "formation_code": "V1RET", + "code_specialite": null, + "dept_id": 1, + "titre": "BUT R&T", + "version": 1, + "type_parcours": 700, + "referentiel_competence_id": null, + "formation_id": 1 + } + """ + # Récupération de la formation + forma = models.Formation.query.filter_by(id=formation_id).first_or_404() + + # Mise en forme des données + data = forma.to_dict() + + return jsonify(data) + + +@bp.route("/formations/formation_export/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def formation_export_by_formation_id(formation_id: int, export_ids=False): + """ + Retourne la formation, avec UE, matières, modules + + formation_id : l'id d'une formation + + Exemple de résultat : + { + "id": 1, + "acronyme": "BUT R&T", + "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", + "formation_code": "V1RET", + "code_specialite": null, + "dept_id": 1, + "titre": "BUT R&T", + "version": 1, + "type_parcours": 700, + "referentiel_competence_id": null, + "formation_id": 1, + "ue": [ + { + "acronyme": "RT1.1", + "numero": 1, + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "type": 0, + "ue_code": "UCOD11", + "ects": 12.0, + "is_external": false, + "code_apogee": "", + "coefficient": 0.0, + "semestre_idx": 1, + "color": "#B80004", + "reference": 1, + "matiere": [ + { + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "numero": 1, + "module": [ + { + "titre": "Initiation aux r\u00e9seaux informatiques", + "abbrev": "Init aux r\u00e9seaux informatiques", + "code": "R101", + "heures_cours": 0.0, + "heures_td": 0.0, + "heures_tp": 0.0, + "coefficient": 1.0, + "ects": "", + "semestre_id": 1, + "numero": 10, + "code_apogee": "", + "module_type": 2, + "coefficients": [ + { + "ue_reference": "1", + "coef": "12.0" + }, + { + "ue_reference": "2", + "coef": "4.0" + }, + { + "ue_reference": "3", + "coef": "4.0" + } + ] + }, + { + "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", + "abbrev": "Hygi\u00e8ne informatique", + "code": "SAE11", + "heures_cours": 0.0, + "heures_td": 0.0, + "heures_tp": 0.0, + "coefficient": 1.0, + "ects": "", + "semestre_id": 1, + "numero": 10, + "code_apogee": "", + "module_type": 3, + "coefficients": [ + { + "ue_reference": "1", + "coef": "16.0" + } + ] + }, + ... + ] + }, + ... + ] + }, + ] + } + + """ + # Fonction utilité : app.scodoc.sco_formations.formation_export() + + try: + # Utilisation de la fonction formation_export + data = formation_export(formation_id) + except ValueError: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel. \n" + "Veillez vérifier la conformité du 'formation_id'", + ) + + return jsonify(data) + + +@bp.route("/formations/moduleimpl/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def moduleimpls(moduleimpl_id: int): + """ + Retourne la liste des moduleimpl + + moduleimpl_id : l'id d'un moduleimpl + """ + # Récupération des tous les moduleimpl + list_moduleimpls = models.ModuleImpl.query.filter_by(id=moduleimpl_id) + + # Mise en forme des données + data = [moduleimpl.to_dict() for moduleimpl in list_moduleimpls] + + return jsonify(data) + + +@bp.route( + "/formations/moduleimpl/formsemestre//liste", + methods=["GET"], +) # XXX TODO penser à changer la route sur la doc +@token_permission_required(Permission.APIView) +def moduleimpls_sem(formsemestre_id: int): + """ + Retourne la liste des moduleimpl d'un semestre + + formsemestre_id : l'id d'un formsemestre + + Exemple d'utilisation : + [ + { + "id": 1, + "formsemestre_id": 1, + "computation_expr": null, + "module_id": 1, + "responsable_id": 2, + "module": { + "heures_tp": 0.0, + "code_apogee": "", + "titre": "Initiation aux r\u00e9seaux informatiques", + "coefficient": 1.0, + "module_type": 2, + "id": 1, + "ects": null, + "abbrev": "Init aux r\u00e9seaux informatiques", + "ue_id": 1, + "code": "R101", + "formation_id": 1, + "heures_cours": 0.0, + "matiere_id": 1, + "heures_td": 0.0, + "semestre_id": 1, + "numero": 10, + "module_id": 1 + }, + "moduleimpl_id": 1, + "ens": [] + }, + ... + ] + """ + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + moduleimpls = formsemestre.modimpls_sorted + + data = [moduleimpl.to_dict() for moduleimpl in moduleimpls] + + return jsonify(data) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py new file mode 100644 index 00000000..9704d753 --- /dev/null +++ b/app/api/formsemestres.py @@ -0,0 +1,472 @@ +########################################## Formsemestres ############################################################## +from flask import jsonify + +import app +from app import models +from app.api import bp +from app.api.errors import error_response +from app.api.auth import token_permission_required +from app.api.tools import get_etu_from_etudid_or_nip_or_ine +from app.models import FormSemestre, FormSemestreEtape +from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json +from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud +from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_pvjury import formsemestre_pvjury + + +@bp.route("/formsemestre/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def formsemestre(formsemestre_id: int): + """ + Retourne l'information sur le formsemestre correspondant au formsemestre_id + + formsemestre_id : l'id d'un formsemestre + + Exemple de résultat : + { + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [] + } + + """ + # Récupération de tous les formsemestres + formsemetre = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() + + # Mise en forme des données + data = formsemetre.to_dict() + + return jsonify(data) + + +@bp.route("/formsemestre/apo/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def formsemestre_apo(etape_apo: str): + """ + Retourne les informations sur les formsemestres + + etape_apo : l'id d'une étape apogée + """ + formsemestres = FormSemestre.query.filter( + FormSemestreEtape.etape_apo == etape_apo, + FormSemestreEtape.formsemestre_id == FormSemestre.id, + ) + + return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) + + +@bp.route( + "/formsemestre//departements//etudiant/etudid//bulletin", + methods=["GET"], +) +@bp.route( + "/formsemestre//departements//etudiant/nip//bulletin", + methods=["GET"], +) +@bp.route( + "/formsemestre//departements//etudiant/ine//bulletin", + methods=["GET"], +) +@token_permission_required(Permission.APIView) +def etudiant_bulletin( + formsemestre_id, + dept, + etudid: int = None, + nip: int = None, + ine: int = None, +): + """ + Retourne le bulletin de note d'un étudiant + + formsemestre_id : l'id d'un formsemestre + etudid : l'etudid d'un étudiant + nip : le code nip d'un étudiant + ine : le code ine d'un étudiant + + Exemple de résultat : + { + "etudid":1, + "formsemestre_id":1, + "date":"2022-04-27T10:44:47.448094", + "publie":true, + "etapes":[ + + ], + "etudiant":{ + "etudid":1, + "code_nip":"1", + "code_ine":"1", + "nom":"COSTA", + "prenom":"Sacha", + "civilite":"", + "photo_url":"/ScoDoc/TAPI/Scolarite/get_photo_image?etudid=1&size=small", + "email":"SACHA.COSTA@example.com", + "emailperso":"", + "sexe":"" + }, + "note":{ + "value":"10.60", + "min":"-", + "max":"-", + "moy":"-" + }, + "rang":{ + "value":"10", + "ninscrits":16 + }, + "rang_group":[ + { + "group_type":"TD", + "group_name":"", + "value":"", + "ninscrits":"" + } + ], + "note_max":{ + "value":20 + }, + "bonus_sport_culture":{ + "value":0.0 + }, + "ue":[ + { + "id":1, + "numero":"1", + "acronyme":"RT1.1", + "titre":"Administrer les r\u00e9seaux et l\u2019Internet", + "note":{ + "value":"08.50", + "min":"06.00", + "max":"16.50", + "moy":"11.31" + }, + "rang":"12", + "effectif":16, + "ects":"12", + "code_apogee":"", + "module":[ + { + "id":1, + "code":"R101", + "coefficient":1.0, + "numero":10, + "titre":"Initiation aux r\u00e9seaux informatiques", + "abbrev":"Init aux r\u00e9seaux informatiques", + "note":{ + "value":"12.00", + "moy":"-", + "max":"-", + "min":"-", + "nb_notes":"-", + "nb_missing":"-", + "nb_valid_evals":"-" + }, + "code_apogee":"", + "evaluation":[ + { + "jour":"2022-04-20", + "heure_debut":"08:00:00", + "heure_fin":"09:00:00", + "coefficient":1.0, + "evaluation_type":0, + "evaluation_id":1, + "description":"eval1", + "note":"12.00" + } + ] + }, + ... + ] + } + ], + "ue_capitalisee":[], + "absences":{ + "nbabs":2, + "nbabsjust":1 + }, + "appreciation":[] + } + """ + # Fonction utilisée : app.scodoc.sco_bulletins_json.make_json_formsemestre_bulletinetud() + + try: + app.set_sco_dept(dept) + except: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel.\n " + "Veilliez vérifier que le nom de département est valide", + ) + if etudid is None: + # Récupération de l'étudiant + try: + etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + etudid = etu.etudid + except AttributeError: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel.\n " + "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + ) + + data = make_json_formsemestre_bulletinetud(formsemestre_id, etudid) + + return data + + +@bp.route("/formsemestre//bulletins", methods=["GET"]) +@token_permission_required(Permission.APIView) +def bulletins(formsemestre_id: int): + """ + Retourne les bulletins d'un formsemestre donné + + formsemestre_id : l'id d'un formesemestre + + Exemple de résultat : + [ + { + "version": "0", + "type": "BUT", + "date": "2022-04-27T07:18:16.450634Z", + "publie": true, + "etudiant": { + "civilite": "X", + "code_ine": "1", + "code_nip": "1", + "date_naissance": "", + "email": "SACHA.COSTA@example.com", + "emailperso": "", + "etudid": 1, + "nom": "COSTA", + "prenom": "SACHA", + "nomprenom": "Sacha COSTA", + "lieu_naissance": "", + "dept_naissance": "", + "nationalite": "", + "boursier": "", + "fiche_url": "/ScoDoc/TAPI/Scolarite/ficheEtud?etudid=1", + "photo_url": "/ScoDoc/TAPI/Scolarite/get_photo_image?etudid=1&size=small", + "id": 1, + "codepostaldomicile": "", + "paysdomicile": "", + "telephonemobile": "", + "typeadresse": "domicile", + "domicile": "", + "villedomicile": "", + "telephone": "", + "fax": "", + "description": "" + }, + "formation": { + "id": 1, + "acronyme": "BUT R&T", + "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", + "titre": "BUT R&T" + }, + "formsemestre_id": 1, + "etat_inscription": "I", + "options": { + "show_abs": true, + "show_abs_modules": false, + "show_ects": true, + "show_codemodules": false, + "show_matieres": false, + "show_rangs": true, + "show_ue_rangs": true, + "show_mod_rangs": true, + "show_moypromo": false, + "show_minmax": false, + "show_minmax_mod": false, + "show_minmax_eval": false, + "show_coef": true, + "show_ue_cap_details": false, + "show_ue_cap_current": true, + "show_temporary": true, + "temporary_txt": "Provisoire", + "show_uevalid": true, + "show_date_inscr": true + }, + "ressources": { + "R101": { + "id": 1, + "titre": "Initiation aux r\u00e9seaux informatiques", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1", + "moyenne": {}, + "evaluations": [ + { + "id": 1, + "description": "eval1", + "date": "2022-04-20", + "heure_debut": "08:00", + "heure_fin": "09:00", + "coef": "01.00", + "poids": { + "RT1.1": 1.0, + }, + "note": { + "value": "12.00", + "min": "00.00", + "max": "18.00", + "moy": "10.88" + }, + "url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1" + } + ] + }, + }, + "saes": { + "SAE11": { + "id": 2, + "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2", + "moyenne": {}, + "evaluations": [] + }, + }, + "ues": { + "RT1.1": { + "id": 1, + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "numero": 1, + "type": 0, + "color": "#B80004", + "competence": null, + "moyenne": { + "value": "08.50", + "min": "06.00", + "max": "16.50", + "moy": "11.31", + "rang": "12", + "total": 16 + }, + "bonus": "00.00", + "malus": "00.00", + "capitalise": null, + "ressources": { + "R101": { + "id": 1, + "coef": 12.0, + "moyenne": "12.00" + }, + }, + "saes": { + "SAE11": { + "id": 2, + "coef": 16.0, + "moyenne": "~" + }, + }, + "ECTS": { + "acquis": 0.0, + "total": 12.0 + } + }, + "semestre": { + "etapes": [], + "date_debut": "2021-09-01", + "date_fin": "2022-08-31", + "annee_universitaire": "2021 - 2022", + "numero": 1, + "inscription": "", + "groupes": [], + "absences": { + "injustifie": 1, + "total": 2 + }, + "ECTS": { + "acquis": 0, + "total": 30.0 + }, + "notes": { + "value": "10.60", + "min": "02.40", + "moy": "11.05", + "max": "17.40" + }, + "rang": { + "value": "10", + "total": 16 + } + } + }, + ... + ] + """ + # Fonction utilisée : app.scodoc.sco_bulletins.get_formsemestre_bulletin_etud_json() + + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() + + app.set_sco_dept(dept.acronym) + + etuds = formsemestre.etuds + + data = [] + for etu in etuds: + bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu) + data.append(bul_etu.json) + + return jsonify(data) + + +@bp.route("/formsemestre//jury", methods=["GET"]) +@token_permission_required(Permission.APIView) +def jury(formsemestre_id: int): + """ + Retourne le récapitulatif des décisions jury + + formsemestre_id : l'id d'un formsemestre + + Exemple de résultat : + + """ + # Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury() + + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() + + app.set_sco_dept(dept.acronym) + + data = formsemestre_pvjury(formsemestre_id) + + # try: + # # Utilisation de la fonction formsemestre_pvjury + # data = formsemestre_pvjury(formsemestre_id) + # except AttributeError: + # return error_response( + # 409, + # message="La requête ne peut être traitée en l’état actuel. \n" + # "Veillez vérifier la conformité du 'formation_id'", + # ) + + return jsonify(data) diff --git a/app/api/jury.py b/app/api/jury.py new file mode 100644 index 00000000..98228077 --- /dev/null +++ b/app/api/jury.py @@ -0,0 +1,41 @@ +#################################################### Jury ############################################################# +from flask import jsonify + +from app import models +from app.api import bp +from app.api.errors import error_response +from app.api.auth import token_permission_required +from app.scodoc.sco_prepajury import feuille_preparation_jury +from app.scodoc.sco_pvjury import formsemestre_pvjury + + +@bp.route("/jury/formsemestre//preparation_jury", methods=["GET"]) +# @token_permission_required(Permission.?) +def jury_preparation(formsemestre_id: int): + """ + Retourne la feuille de préparation du jury + + formsemestre_id : l'id d'un formsemestre + """ + # Fonction utilisée : app.scodoc.sco_prepajury.feuille_preparation_jury() + + # Utilisation de la fonction feuille_preparation_jury + prepa_jury = feuille_preparation_jury(formsemestre_id) + + return error_response(501, message="Not implemented") + + +@bp.route("/jury/formsemestre//decisions_jury", methods=["GET"]) +# @token_permission_required(Permission.?) +def jury_decisions(formsemestre_id: int): + """ + Retourne les décisions du jury suivant un formsemestre donné + + formsemestre_id : l'id d'un formsemestre + """ + # Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury() + + # Utilisation de la fonction formsemestre_pvjury + decision_jury = formsemestre_pvjury(formsemestre_id) + + return error_response(501, message="Not implemented") diff --git a/app/api/logos.py b/app/api/logos.py index e32f6595..663ef602 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -36,13 +36,15 @@ from app.api import bp from app.api import requested_format from app.api.auth import token_auth from app.api.errors import error_response +from app.decorators import permission_required from app.models import Departement from app.scodoc.sco_logos import list_logos, find_logo +from app.api.auth import token_auth, token_permission_required from app.scodoc.sco_permissions import Permission @bp.route("/logos", methods=["GET"]) -@token_auth.login_required +@token_permission_required(Permission.APIView) def api_get_glob_logos(): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): return error_response(401, message="accès interdit") @@ -54,7 +56,7 @@ def api_get_glob_logos(): @bp.route("/logos/", methods=["GET"]) -@token_auth.login_required +@token_permission_required(Permission.APIView) def api_get_glob_logo(logoname): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): return error_response(401, message="accès interdit") @@ -70,7 +72,7 @@ def api_get_glob_logo(logoname): @bp.route("/departements//logos", methods=["GET"]) -@token_auth.login_required +@token_permission_required(Permission.APIView) def api_get_local_logos(departement): dept_id = Departement.from_acronym(departement).id if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): @@ -80,7 +82,7 @@ def api_get_local_logos(departement): @bp.route("/departements//logos/", methods=["GET"]) -@token_auth.login_required +@token_permission_required(Permission.APIView) def api_get_local_logo(departement, logoname): # format = requested_format("jpg", ['png', 'jpg']) XXX ? dept_id = Departement.from_acronym(departement).id diff --git a/app/api/partitions.py b/app/api/partitions.py new file mode 100644 index 00000000..2af2f7cf --- /dev/null +++ b/app/api/partitions.py @@ -0,0 +1,150 @@ +############################################### Partitions ############################################################ +from flask import jsonify + +from app import models +from app.api import bp + +from app.api.errors import error_response +from app.api.auth import token_permission_required +from app.scodoc.sco_groups import get_group_members, setGroups, get_partitions_list +from app.scodoc.sco_permissions import Permission + + +@bp.route("/partitions/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def partition(formsemestre_id: int): + """ + Retourne la liste de toutes les partitions d'un formsemestre + + formsemestre_id : l'id d'un formsemestre + + Exemple de résultat : + [ + { + "partition_id": 2, + "id": 2, + "formsemestre_id": 1, + "partition_name": "TD", + "numero": 1, + "bul_show_rank": false, + "show_in_lists": true + }, + { + "partition_id": 1, + "id": 1, + "formsemestre_id": 1, + "partition_name": null, + "numero": 0, + "bul_show_rank": false, + "show_in_lists": true + } + ] + """ + # # Récupération de toutes les partitions + # partitions = models.Partition.query.filter_by(id=formsemestre_id) + # + # # Mise en forme des données + # data = [partition.to_dict() for partition in partitions] + + data = get_partitions_list(formsemestre_id) + + return jsonify(data) + + +@bp.route("/partitions/groups/", methods=["GET"]) +@bp.route("/partitions/groups//etat/", methods=["GET"]) +@token_permission_required(Permission.APIView) +def etud_in_group(group_id: int, etat=None): + """ + Retourne la liste des étudiants dans un groupe + + group_id : l'id d'un groupe + etat : état de l'inscription + + Exemple de résultat : + [ + { + "etudid": 10, + "id": 10, + "dept_id": 1, + "nom": "BOUTET", + "prenom": "Marguerite", + "nom_usuel": "", + "civilite": "F", + "date_naissance": null, + "lieu_naissance": null, + "dept_naissance": null, + "nationalite": null, + "statut": null, + "boursier": null, + "photo_filename": null, + "code_nip": "10", + "code_ine": "10", + "scodoc7_id": null, + "email": "MARGUERITE.BOUTET@example.com", + "emailperso": null, + "domicile": null, + "codepostaldomicile": null, + "villedomicile": null, + "paysdomicile": null, + "telephone": null, + "telephonemobile": null, + "fax": null, + "typeadresse": "domicile", + "description": null, + "group_id": 1, + "etat": "I", + "civilite_str": "Mme", + "nom_disp": "BOUTET", + "nomprenom": "Mme Marguerite BOUTET", + "ne": "e", + "email_default": "MARGUERITE.BOUTET@example.com" + }, + ... + ] + """ + # Fonction utilisée : app.scodoc.sco_groups.get_group_members() + + if etat is None: + data = get_group_members(group_id) + else: + data = get_group_members(group_id, etat) + + if len(data) == 0: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel. \n" + "Aucun groupe ne correspond au 'group_id' renseigné", + ) + + return jsonify(data) + + +@bp.route( + "/partitions/set_groups?partition_id=&groups_lists=&" + "groups_to_create=&groups_to_delete=", + methods=["POST"], +) +@token_permission_required(Permission.APIEtudChangeGroups) +def set_groups( + partition_id: int, groups_lists: int, groups_to_delete: int, groups_to_create: int +): + """ + Set les groups + + partition_id : l'id d'une partition + groups_lists : membres de chaque groupe existant + groups_ti_delete : les groupes à supprimer + groups_to_create : les groupes à créer + """ + # Fonction utilisée : app.scodoc.sco_groups.setGroups() + try: + # Utilisation de la fonction setGroups + setGroups(partition_id, groups_lists, groups_to_create, groups_to_delete) + return error_response(200, message="Groups set") + except ValueError: + return error_response( + 409, + message="La requête ne peut être traitée en l’état actuel. \n" + "Veillez vérifier la conformité des éléments passé en paramètres", + ) diff --git a/app/api/remiser.py b/app/api/remiser.py new file mode 100644 index 00000000..156a4c5f --- /dev/null +++ b/app/api/remiser.py @@ -0,0 +1,253 @@ +# @bp.route("/etudiants", methods=["GET"]) +# @token_permission_required(Permission.APIView) +# def etudiants(): +# """ +# Retourne la liste de tous les étudiants +# +# Exemple de résultat : +# { +# "civilite": "X", +# "code_ine": null, +# "code_nip": null, +# "date_naissance": null, +# "email": null, +# "emailperso": null, +# "etudid": 18, +# "nom": "MOREL", +# "prenom": "JACQUES" +# }, +# { +# "civilite": "X", +# "code_ine": null, +# "code_nip": null, +# "date_naissance": null, +# "email": null, +# "emailperso": null, +# "etudid": 19, +# "nom": "FOURNIER", +# "prenom": "ANNE" +# }, +# ... +# """ +# # Récupération de tous les étudiants +# etu = models.Identite.query.all() +# +# # Mise en forme des données +# data = [d.to_dict_bul(include_urls=False) for d in etu] +# +# return jsonify(data) + + +# @bp.route( +# "/evaluations/eval_set_notes?eval_id=&etudid=¬e=", +# methods=["POST"], +# ) +# @bp.route( +# "/evaluations/eval_set_notes?eval_id=&nip=¬e=", +# methods=["POST"], +# ) +# @bp.route( +# "/evaluations/eval_set_notes?eval_id=&ine=¬e=", +# methods=["POST"], +# ) +# @token_permission_required(Permission.APIEditAllNotes) +# def evaluation_set_notes( +# eval_id: int, note: float, etudid: int = None, nip: int = None, ine: int = None +# ): +# """ +# Set les notes d'une évaluation pour un étudiant donnée +# +# eval_id : l'id d'une évaluation +# note : la note à attribuer +# etudid : l'etudid d'un étudiant +# nip : le code nip d'un étudiant +# ine : le code ine d'un étudiant +# """ +# # Fonction utilisée : app.scodoc.sco_saisie_notes.notes_add() +# +# # Qu'est ce qu'un user ??? +# # notes_add() +# return error_response(501, message="Not implemented") + + +# ### Inutil en définitif ### +# @bp.route( +# "/absences/abs_signale?etudid=&date=&matin=&justif=" +# "&description=", +# methods=["POST"], +# ) +# @bp.route( +# "/absences/abs_signale?nip=&date=&matin=&justif=" +# "&description=", +# methods=["POST"], +# ) +# @bp.route( +# "/absences/abs_signale?ine=&date=&matin=&justif=" +# "&description=", +# methods=["POST"], +# ) +# @bp.route( +# "/absences/abs_signale?ine=&date=&matin=&justif=" +# "&description=&moduleimpl_id=", +# methods=["POST"], +# ) +# @token_permission_required(Permission.APIAbsChange) +# def abs_signale( +# date: datetime, +# matin: bool, +# justif: bool, +# etudid: int = None, +# nip: int = None, +# ine: int = None, ### Inutil en définitif +# description: str = None, +# moduleimpl_id: int = None, +# ): +# """ +# Permet d'ajouter une absence en base +# +# date : la date de l'absence +# matin : True ou False +# justif : True ou False +# etudid : l'etudid d'un étudiant +# nip: le code nip d'un étudiant +# ine : le code ine d'un étudiant +# description : description possible à ajouter sur l'absence +# moduleimpl_id : l'id d'un moduleimpl +# """ +# # Fonctions utilisées : app.scodoc.sco_abs.add_absence() et app.scodoc.sco_abs.add_justif() +# +# if etudid is None: +# # Récupération de l'étudiant +# try: +# etu = get_etu_from_request(etudid, nip, ine) +# etudid = etu.etudid +# except AttributeError: +# return error_response( +# 409, +# message="La requête ne peut être traitée en l’état actuel.\n " +# "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", +# ) +# try: +# # Utilisation de la fonction add_absence +# add_absence(etudid, date, matin, justif, description, moduleimpl_id) +# if justif == True: +# # Utilisation de la fonction add_justif +# add_justif(etudid, date, matin, description) +# except ValueError: +# return error_response( +# 409, message="La requête ne peut être traitée en l’état actuel" +# ) +# @bp.route( +# "/absences/abs_annule_justif?etudid=&jour=&matin=", +# methods=["POST"], +# ) +# @bp.route( +# "/absences/abs_annule_justif?nip=&jour=&matin=", +# methods=["POST"], +# ) +# @bp.route( +# "/absences/abs_annule_justif?ine=&jour=&matin=", +# methods=["POST"], +# ) +# @token_permission_required(Permission.APIAbsChange) +# def abs_annule_justif( +# jour: datetime, matin: str, etudid: int = None, nip: int = None, ine: int = None +# ): +# """ +# Retourne un html + +# jour : la date de l'absence a annulé +# matin : True ou False +# etudid : l'etudid d'un étudiant +# nip: le code nip d'un étudiant +# ine : le code ine d'un étudiant +# """ +# # Fonction utilisée : app.scodoc.sco_abs.annule_justif() + +# if etudid is None: +# # Récupération de l'étudiant +# try: +# etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) +# etudid = etu.etudid +# except AttributeError: +# return error_response( +# 409, +# message="La requête ne peut être traitée en l’état actuel.\n " +# "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", +# ) +# try: +# # Utilisation de la fonction annule_justif +# annule_justif(etudid, jour, matin) +# except ValueError: +# return error_response( +# 409, +# message="La requête ne peut être traitée en l’état actuel.\n " +# "Veilliez vérifier que le 'jour' et le 'matin' sont valides", +# ) + +# return error_response(200, message="OK") + +# @bp.route( +# "/jury/set_decision/etudid?etudid=&formsemestre_id=" +# "&jury=&devenir=&assiduite=", +# methods=["POST"], +# ) +# @bp.route( +# "/jury/set_decision/nip?etudid=&formsemestre_id=" +# "&jury=&devenir=&assiduite=", +# methods=["POST"], +# ) +# @bp.route( +# "/jury/set_decision/ine?etudid=&formsemestre_id=" +# "&jury=&devenir=&assiduite=", +# methods=["POST"], +# ) +# # @token_permission_required(Permission.) +# def set_decision_jury( +# formsemestre_id: int, +# decision_jury: str, +# devenir_jury: str, +# assiduite: bool, +# etudid: int = None, +# nip: int = None, +# ine: int = None, +# ): +# """ +# Attribuer la décision du jury et le devenir à un etudiant +# +# formsemestre_id : l'id d'un formsemestre +# decision_jury : la décision du jury +# devenir_jury : le devenir du jury +# assiduite : True ou False +# etudid : l'etudid d'un étudiant +# nip: le code nip d'un étudiant +# ine : le code ine d'un étudiant +# """ +# return error_response(501, message="Not implemented") +# +# +# @bp.route( +# "/jury/etudid//formsemestre//annule_decision", +# methods=["DELETE"], +# ) +# @bp.route( +# "/jury/nip//formsemestre//annule_decision", +# methods=["DELETE"], +# ) +# @bp.route( +# "/jury/ine//formsemestre//annule_decision", +# methods=["DELETE"], +# ) +# # @token_permission_required(Permission.) +# def annule_decision_jury( +# formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None +# ): +# """ +# Supprime la déciosion du jury pour un étudiant donné +# +# formsemestre_id : l'id d'un formsemestre +# etudid : l'etudid d'un étudiant +# nip: le code nip d'un étudiant +# ine : le code ine d'un étudiant +# """ +# return error_response(501, message="Not implemented") diff --git a/app/api/sco_api.py b/app/api/sco_api.py index c8b20f91..8440aa55 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -50,442 +50,103 @@ from app.api.errors import error_response from app import models from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import ApcReferentielCompetences +from app.scodoc.sco_abs import ( + annule_absence, + annule_justif, + add_absence, + add_justif, + list_abs_date, +) +from app.scodoc.sco_bulletins import formsemestre_bulletinetud_dict +from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud +from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes +from app.scodoc.sco_formations import formation_export +from app.scodoc.sco_formsemestre_inscriptions import ( + do_formsemestre_inscription_listinscrits, +) +from app.scodoc.sco_groups import setGroups, get_etud_groups, get_group_members +from app.scodoc.sco_logos import list_logos, find_logo, _list_dept_logos +from app.scodoc.sco_moduleimpl import moduleimpl_list from app.scodoc.sco_permissions import Permission -@bp.route("/list_depts", methods=["GET"]) -@token_auth.login_required -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 Observateur 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_urls=False) for e in query]) - - -######################## Departements ################################## - - -@bp.route("/departements", methods=["GET"]) -@token_auth.login_required -def departements(): - """ - Liste des ids de départements - """ - depts = models.Departement.query.filter_by(visible=True).all() - data = [d.id for d in depts] - return jsonify(data) - - -@bp.route("/departements//etudiants/liste/", methods=["GET"]) -@token_auth.login_required -def liste_etudiants(dept, *args, sem_id): # XXX TODO A REVOIR - """ - Liste des étudiants d'un département - """ - # Test si le sem_id à été renseigné ou non - if sem_id is not None: - # Récupération du/des depts - list_depts = models.Departement.query.filter( - models.Departement.acronym == dept, - models.FormSemestre.semestre_id == sem_id, - ) - list_etuds = [] - for dept in list_depts: - # Récupération des étudiants d'un département - x = models.Identite.query.filter(models.Identite.dept_id == dept.getId()) - for y in x: - # Ajout des étudiants dans la liste global - list_etuds.append(y) - else: - list_depts = models.Departement.query.filter( - models.Departement.acronym == dept, - models.FormSemestre.semestre_id == models.Departement.formsemestres, - ) - list_etuds = [] - for dept in list_depts: - x = models.Identite.query.filter(models.Identite.dept_id == dept.getId()) - for y in x: - list_etuds.append(y) - - data = [d.to_dict() for d in list_etuds] - # return jsonify(data) - return error_response(501, message="Not implemented") - - -@bp.route("/departements//semestres_actifs", methods=["GET"]) -@token_auth.login_required -def liste_semestres_actifs(dept): # TODO : changer nom - """ - Liste des semestres actifs d'un départements donné - """ - # Récupération de l'id du dept - dept_id = models.Departement.query.filter(models.Departement.acronym == dept) - # Puis ici récupération du FormSemestre correspondant - depts_actifs = models.FormSemestre.query.filter_by( - etat=True, - dept_id=dept_id, - ) - data = [da.to_dict() for da in depts_actifs] - - # return jsonify(data) - return error_response(501, message="Not implemented") - - -@bp.route("/referentiel_competences/") -@token_auth.login_required -def referentiel_competences(referentiel_competence_id): - """ - Le référentiel de compétences - """ - ref = ApcReferentielCompetences.query.get_or_404(referentiel_competence_id) - return jsonify(ref.to_dict()) - - -####################### Etudiants ################################## - - -@bp.route("/etudiant/", methods=["GET"]) -@token_auth.login_required -def etudiant(etudid): - """ - Un dictionnaire avec les informations de l'étudiant correspondant à l'id passé en paramètres. - """ - etud: Identite = Identite.query.get_or_404(etudid) - return jsonify(etud.to_dict_bul()) - - -@bp.route("/etudiant//semestre//bulletin", methods=["GET"]) -@token_auth.login_required -def etudiant_bulletin_semestre(etudid, sem_id): - """ - Le bulletin d'un étudiant en fonction de son id et d'un semestre donné - """ - # return jsonify(models.BulAppreciations.query.filter_by(etudid=etudid, formsemestre_id=sem_id)) - return error_response(501, message="Not implemented") - - -@bp.route( - "/formsemestre//departements//etudiant/nip//releve", - methods=["GET"], -) -@bp.route( - "/formsemestre//departements//etudiant/id//releve", - methods=["GET"], -) -@bp.route( - "/formsemestre//departements//etudiant/ine//releve", - methods=["GET"], -) -@token_auth.login_required -def etudiant_bulletin(formsemestre_id, dept, etudid, format="json", *args, size): - """ - Un bulletin de note - """ - formsemestres = models.FormSemestre.query.filter_by(id=formsemestre_id) - depts = models.Departement.query.filter_by(acronym=dept) - etud = "" - - data = [] - if args[0] == "short": - pass - elif args[0] == "selectevals": - pass - elif args[0] == "long": - pass - else: - return "erreur" - - # return jsonify(data) - return error_response(501, message="Not implemented") - - -@bp.route( - "/etudiant//semestre//groups", methods=["GET"] -) -@token_auth.login_required -def etudiant_groups(etudid: int, formsemestre_id: int): - """ - Liste des groupes auxquels appartient l'étudiant dans le semestre indiqué - """ - semestre = models.FormSemestre.query.filter_by(id=formsemestre_id) - etudiant = models.Identite.query.filter_by(id=etudid) - - groups = models.Partition.query.filter( - models.Partition.formsemestre_id == semestre, - models.GroupDescr.etudiants == etudiant, - ) - data = [d.to_dict() for d in groups] - # return jsonify(data) - - return error_response(501, message="Not implemented") - - -#######################" Programmes de formations ######################### - - -@bp.route("/formations", methods=["GET"]) -@bp.route("/formations/", methods=["GET"]) -@token_auth.login_required -def formations(formation_id: int): - """ - Liste des formations - """ - formations = models.Formation.query.filter_by(id=formation_id) - data = [d.to_dict() for d in formations] - # return jsonify(data) - - return error_response(501, message="Not implemented") - - -@bp.route("/formations/formation_export/", methods=["GET"]) -@token_auth.login_required -def formation_export(formation_id: int, export_ids=False): - """ - La formation, avec UE, matières, modules - """ - return error_response(501, message="Not implemented") - - -###################### UE ####################################### - - -@bp.route( - "/departements//formations/programme/", methods=["GET"] -) -@token_auth.login_required -def eus(dept: str, sem_id: int): - """ - Liste des UES, ressources et SAE d'un semestre - """ - return error_response(501, message="Not implemented") - - -######## Semestres de formation ############### - - -@bp.route("/formations/formsemestre/", methods=["GET"]) -@bp.route("/formations/apo/", methods=["GET"]) -@token_auth.login_required -def formsemestre( - id: int, -): - """ - Information sur les formsemestres - """ - return error_response(501, message="Not implemented") - - -############ Modules de formation ############## - - -@bp.route("/formations/moduleimpl/", methods=["GET"]) -@bp.route( - "/formations/moduleimpl//formsemestre/", - methods=["GET"], -) -@token_auth.login_required -def moduleimpl(id: int): - """ - Liste de moduleimpl - """ - return error_response(501, message="Not implemented") - - -########### Groupes et partitions ############### - - -@bp.route("/partitions/", methods=["GET"]) -@token_auth.login_required -def partition(formsemestre_id: int): - """ - La liste de toutes les partitions d'un formsemestre - """ - partitions = models.Partition.query.filter_by(id=formsemestre_id) - data = [d.to_dict() for d in partitions] - - # return jsonify(data) - return error_response(501, message="Not implemented") - - -@bp.route( - "/partitions/formsemestre//groups/group_ids?with_codes=&all_groups=&etat=", - methods=["GET"], -) -@token_auth.login_required -def groups(formsemestre_id: int, group_ids: int): - """ - Liste des étudiants dans un groupe - """ - return error_response(501, message="Not implemented") - - -@bp.route( - "/partitions/set_groups?partition_id=&groups=&groups_to_delete=&groups_to_create=", - methods=["POST"], -) -@token_auth.login_required -def set_groups( - partition_id: int, groups: int, groups_to_delete: int, groups_to_create: int -): - """ - Set les groups - """ - return error_response(501, message="Not implemented") - - -####### Bulletins de notes ########### - - -@bp.route("/evaluations/", methods=["GET"]) -@token_auth.login_required -def evaluations(moduleimpl_id: int): - """ - Liste des évaluations à partir de l'id d'un moduleimpl - """ - evals = models.Evaluation.query.filter_by(id=moduleimpl_id) - data = [d.to_dict() for d in evals] - - # return jsonify(data) - return error_response(501, message="Not implemented") - - -@bp.route("/evaluations/eval_notes/", methods=["GET"]) -@token_auth.login_required -def evaluation_notes(evaluation_id: int): - """ - Liste des notes à partir de l'id d'une évaluation donnée - """ - evals = models.Evaluation.query.filter_by(id=evaluation_id) - notes = evals.get_notes() - - data = [d.to_dict() for d in notes] - # return jsonify(data) - return error_response(501, message="Not implemented") - - -@bp.route( - "/evaluations/eval_set_notes?eval_id=&etudid=¬e=", - methods=["POST"], -) -@token_auth.login_required -def evaluation_set_notes(eval_id: int, etudid: int, note: float): - """ - Set les notes d'une évaluation pour un étudiant donnée - """ - return error_response(501, message="Not implemented") - - -############## Absences ############# - - -@bp.route("/absences/", methods=["GET"]) -@bp.route("/absences//abs_just_only", methods=["GET"]) -def absences(etudid: int): - """ - Liste des absences d'un étudiant donnée - """ - abs = models.Absence.query.filter_by(id=etudid) - - data = [d.to_dict() for d in abs] - - # return jsonify(data) - return error_response(501, message="Not implemented") - - -@bp.route("/absences/abs_signale", methods=["POST"]) -@token_auth.login_required -def abs_signale(): - """ - Retourne un html - """ - return error_response(501, message="Not implemented") - - -@bp.route("/absences/abs_annule", methods=["POST"]) -@token_auth.login_required -def abs_annule(): - """ - Retourne un html - """ - return error_response(501, message="Not implemented") - - -@bp.route("/absences/abs_annule_justif", methods=["POST"]) -@token_auth.login_required -def abs_annule_justif(): - """ - Retourne un html - """ - return error_response(501, message="Not implemented") - - -@bp.route( - "/absences/abs_group_etat/?group_ids=&date_debut=date_debut&date_fin=date_fin", - methods=["GET"], -) -@token_auth.login_required -def abs_groupe_etat( - group_ids: int, date_debut, date_fin, with_boursier=True, format="html" -): - """ - Liste des absences d'un ou plusieurs groupes entre deux dates - """ - return error_response(501, message="Not implemented") - - -################ Logos ################ - - -@bp.route("/logos", methods=["GET"]) -@token_auth.login_required -def liste_logos(format="json"): - """ - Liste des logos définis pour le site scodoc. - """ - return error_response(501, message="Not implemented") - - -@bp.route("/logos/", methods=["GET"]) -@token_auth.login_required -def recup_logo_global(nom: str): - """ - Retourne l'image au format png ou jpg - """ - return error_response(501, message="Not implemented") - - -@bp.route("/departements//logos", methods=["GET"]) -@token_auth.login_required -def logo_dept(dept: str): - """ - Liste des logos définis pour le département visé. - """ - return error_response(501, message="Not implemented") - - -@bp.route("/departement//logos/", methods=["GET"]) -@token_auth.login_required -def recup_logo_dept_global(dept: str, nom: str): - """ - L'image format png ou jpg - """ - return error_response(501, message="Not implemented") +# ###################################################### Logos ########################################################## +# +# # XXX TODO voir get_logo déjà existant dans app/views/scodoc.py +# +# @bp.route("/logos", methods=["GET"]) +# def liste_logos(format="json"): +# """ +# Liste des logos définis pour le site scodoc. +# """ +# # fonction to use : list_logos() +# # try: +# # res = list_logos() +# # except ValueError: +# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") +# # +# # if res is None: +# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") +# # +# # return res +# +# +# +# @bp.route("/logos/", methods=["GET"]) +# def recup_logo_global(logo_name: str): +# """ +# Retourne l'image au format png ou jpg +# +# logo_name : le nom du logo rechercher +# """ +# # fonction to use find_logo +# # try: +# # res = find_logo(logo_name) +# # except ValueError: +# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") +# # +# # if res is None: +# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") +# # +# # return res +# +# +# @bp.route("/departements//logos", methods=["GET"]) +# def logo_dept(dept: str): +# """ +# Liste des logos définis pour le département visé. +# +# dept : l'id d'un département +# """ +# # fonction to use: _list_dept_logos +# # dept_id = models.Departement.query.filter_by(acronym=dept).first() +# # try: +# # res = _list_dept_logos(dept_id.id) +# # except ValueError: +# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") +# # +# # if res is None: +# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") +# # +# # return res +# +# +# @bp.route("/departement//logos/", methods=["GET"]) +# def recup_logo_dept_global(dept: str, logo_name: str): +# """ +# L'image format png ou jpg +# +# dept : l'id d'un département +# logo_name : le nom du logo rechercher +# """ +# # fonction to use find_logo +# # dept_id = models.Departement.query.filter_by(acronym=dept).first() +# # try: +# # res = find_logo(logo_name, dept_id.id) +# # except ValueError: +# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") +# # +# # if res is None: +# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") +# # +# # return res diff --git a/app/api/test_api.py b/app/api/test_api.py new file mode 100644 index 00000000..821e3a01 --- /dev/null +++ b/app/api/test_api.py @@ -0,0 +1,444 @@ +################################################## Tests ############################################################## + + +# XXX OBSOLETE ??? XXX + +import requests +import os + +from app import models +from app.api import bp, requested_format +from app.api.auth import token_auth +from app.api.errors import error_response + +SCODOC_USER = "test" +SCODOC_PASSWORD = "test" +SCODOC_URL = "http://192.168.1.12:5000" +CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) + +HEADERS = None + + +def get_token(): + """ + Permet de set le token dans le header + """ + global HEADERS + global SCODOC_USER + global SCODOC_PASSWORD + + r0 = requests.post( + SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD) + ) + token = r0.json()["token"] + HEADERS = {"Authorization": f"Bearer {token}"} + + +DEPT = None +FORMSEMESTRE = None +ETU = None + + +@bp.route("/test_dept", methods=["GET"]) +def get_departement(): + """ + Permet de tester departements() mais également de set un département dans DEPT pour la suite des tests + """ + + get_token() + + global HEADERS + global CHECK_CERTIFICATE + global SCODOC_USER + global SCODOC_PASSWORD + + # print(HEADERS) + # departements + r = requests.get( + SCODOC_URL + "/ScoDoc/api/departements", + headers=HEADERS, + verify=CHECK_CERTIFICATE, + ) + + if r.status_code == 200: + dept_id = r.json()[0] + # print(dept_id) + + dept = models.Departement.query.filter_by(id=dept_id).first() + dept = dept.to_dict() + + fields = ["id", "acronym", "description", "visible", "date_creation"] + + for field in dept: + if field not in fields: + return error_response(501, field + " field missing") + + global DEPT + DEPT = dept + + return error_response(200, "OK") + + return error_response(409, "La requête ne peut être traitée en l’état actuel") + + +@bp.route("/test_formsemestre", methods=["GET"]) +def get_formsemestre(): + """ + Permet de tester liste_semestres_courant() mais également de set un formsemestre dans FORMSEMESTRE + pour la suite des tests + """ + get_departement() + + global DEPT + dept_acronym = DEPT["acronym"] + + # liste_semestres_courant + r = requests.get( + SCODOC_URL + "/ScoDoc/api/departements/" + dept_acronym + "/semestres_courants", + auth=(SCODOC_USER, SCODOC_PASSWORD), + ) + + if r.status_code == 200: + formsemestre = r.json()[0] + print(r.json()[0]) + + fields = [ + "gestion_semestrielle", + "titre", + "scodoc7_id", + "date_debut", + "bul_bgcolor", + "date_fin", + "resp_can_edit", + "dept_id", + "etat", + "resp_can_change_ens", + "id", + "modalite", + "ens_can_edit_eval", + "formation_id", + "gestion_compensation", + "elt_sem_apo", + "semestre_id", + "bul_hide_xml", + "elt_annee_apo", + "block_moyennes", + "formsemestre_id", + "titre_num", + "date_debut_iso", + "date_fin_iso", + "responsables", + ] + + for field in formsemestre: + if field not in fields: + return error_response(501, field + " field missing") + + global FORMSEMESTRE + FORMSEMESTRE = formsemestre + + return error_response(200, "OK") + + return error_response(409, "La requête ne peut être traitée en l’état actuel") + + +@bp.route("/test_etu", methods=["GET"]) +def get_etudiant(): + """ + Permet de tester etudiants() mais également de set un etudiant dans ETU pour la suite des tests + """ + + # etudiants + r = requests.get( + SCODOC_URL + "/ScoDoc/api/etudiants/courant", + auth=(SCODOC_USER, SCODOC_PASSWORD), + ) + + if r.status_code == 200: + etu = r.json()[0] + + fields = [ + "civilite", + "code_ine", + "code_nip", + "date_naissance", + "email", + "emailperso", + "etudid", + "nom", + "prenom", + ] + + for field in etu: + if field not in fields: + return error_response(501, field + " field missing") + + global ETU + ETU = etu + print(etu) + + return error_response(200, "OK") + + return error_response(409, "La requête ne peut être traitée en l’état actuel") + + +############################################### Departements ########################################################## + + +@bp.route("/test_liste_etudiants") +def test_departements_liste_etudiants(): + """ + Test la route liste_etudiants + """ + # Set un département et un formsemestre pour les tests + get_departement() + get_formsemestre() + + global DEPT + global FORMSEMESTRE + + # Set les fields à vérifier + fields = [ + "civilite", + "code_ine", + "code_nip", + "date_naissance", + "email", + "emailperso", + "etudid", + "nom", + "prenom", + ] + + # liste_etudiants (sans formsemestre) + r1 = requests.get( + SCODOC_URL + "/ScoDoc/api/departements/" + DEPT["acronym"] + "/etudiants/liste", + auth=(SCODOC_USER, SCODOC_PASSWORD), + ) + + if r1.status_code == 200: # Si la requête est "OK" + # On récupère la liste des étudiants + etudiants = r1.json() + + # Vérification que tous les étudiants ont bien tous les bons champs + for etu in etudiants: + for field in etu: + if field not in fields: + return error_response(501, field + " field missing") + + # liste_etudiants (avec formsemestre) + r2 = requests.get( + SCODOC_URL + + "/ScoDoc/api/departements/" + + DEPT["acronym"] + + "/etudiants/liste/" + + str(FORMSEMESTRE["formsemestre_id"]), + auth=(SCODOC_USER, SCODOC_PASSWORD), + ) + + if r2.status_code == 200: # Si la requête est "OK" + # On récupère la liste des étudiants + etudiants = r2.json() + + # Vérification que tous les étudiants ont bien tous les bons champs + for etu in etudiants: + for field in etu: + if field not in fields: + return error_response(501, field + " field missing") + + return error_response(200, "OK") + + return error_response(409, "La requête ne peut être traitée en l’état actuel") + + +@bp.route("/test_referenciel_competences") +def test_departements_referenciel_competences(): + """ + Test la route referenciel_competences + """ + get_departement() + get_formsemestre() + + global DEPT + global FORMSEMESTRE + + # referenciel_competences + r = requests.post( + SCODOC_URL + + "/ScoDoc/api/departements/" + + DEPT["acronym"] + + "/formations/" + + FORMSEMESTRE["formation_id"] + + "/referentiel_competences", + auth=(SCODOC_USER, SCODOC_PASSWORD), + ) + + +@bp.route("/test_liste_semestre_index") +def test_departements_semestre_index(): + """ + Test la route semestre_index + """ + # semestre_index + r5 = requests.post( + SCODOC_URL + + "/ScoDoc/api/departements/" + + DEPT["acronym"] + + "/formsemestre/" + + FORMSEMESTRE["formation_id"] + + "/programme", + auth=(SCODOC_USER, SCODOC_PASSWORD), + ) + + +#################################################### Etudiants ######################################################## + + +def test_routes_etudiants(): + """ + Test les routes de la partie Etudiants + """ + # etudiants + r1 = requests.get( + SCODOC_URL + "/ScoDoc/api/etudiants", auth=(SCODOC_USER, SCODOC_PASSWORD) + ) + + # etudiants_courant + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # etudiant + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # etudiant_formsemestres + r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # etudiant_bulletin_semestre + r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # etudiant_groups + r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + +def test_routes_formation(): + """ + Test les routes de la partie Formation + """ + # formations + r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # formations_by_id + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # formation_export_by_formation_id + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # formsemestre_apo + r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # moduleimpls + r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # moduleimpls_sem + r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + +def test_routes_formsemestres(): + """ + Test les routes de la partie Formsemestres + """ + # formsemestre + r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # etudiant_bulletin + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # bulletins + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # jury + r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + +def test_routes_partitions(): + """ + Test les routes de la partie Partitions + """ + # partition + r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # etud_in_group + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # set_groups + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + +def test_routes_evaluations(): + """ + Test les routes de la partie Evaluations + """ + # evaluations + r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # evaluation_notes + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # evaluation_set_notes + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + +def test_routes_jury(): + """ + Test les routes de la partie Jury + """ + # jury_preparation + r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # jury_decisions + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # set_decision_jury + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # annule_decision_jury + r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + +def test_routes_absences(): + """ + Test les routes de la partie Absences + """ + # absences + r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # absences_justify + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # abs_signale + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # abs_annule + r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # abs_annule_justif + r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # abs_groupe_etat + r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + +def test_routes_logos(): + """ + Test les routes de la partie Logos + """ + # liste_logos + r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # recup_logo_global + r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # logo_dept + r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) + + # recup_logo_dept_global + r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) diff --git a/app/api/tools.py b/app/api/tools.py new file mode 100644 index 00000000..35b07cf2 --- /dev/null +++ b/app/api/tools.py @@ -0,0 +1,22 @@ +from app import models + + +def get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine): + """ + Fonction qui retourne un etudiant en fonction de l'etudid, code nip et code ine rentré en paramètres + + etudid : None ou un int etudid + nip : None ou un int code_nip + ine : None ou un int code_ine + + Exemple de résultat: + """ + if etudid is None: + if nip is None: # si ine + etud = models.Identite.query.filter_by(code_ine=str(ine)).first() + else: # si nip + etud = models.Identite.query.filter_by(code_nip=str(nip)).first() + else: # si etudid + etud = models.Identite.query.filter_by(id=etudid).first() + + return etud diff --git a/app/entreprises/__init__.py b/app/entreprises/__init__.py index b211670b..7f100d51 100644 --- a/app/entreprises/__init__.py +++ b/app/entreprises/__init__.py @@ -39,4 +39,12 @@ def get_dept_acronym(id): return dept.acronym +@bp.app_template_filter() +def get_civilité(civ): + if civ == "H": + return "Monsieur" + else: + return "Madame" + + from app.entreprises import routes diff --git a/app/entreprises/app_relations_entreprises.py b/app/entreprises/app_relations_entreprises.py index e3c0d1da..95f1264c 100644 --- a/app/entreprises/app_relations_entreprises.py +++ b/app/entreprises/app_relations_entreprises.py @@ -131,38 +131,46 @@ def send_email_notifications_entreprise(subject, entreprise: Entreprise): def verif_correspondant_data(correspondant_data): """ Verifie les données d'une ligne Excel (correspondant) - correspondant_data[0]: nom - correspondant_data[1]: prenom - correspondant_data[2]: telephone - correspondant_data[3]: mail - correspondant_data[4]: poste - correspondant_data[5]: service - correspondant_data[6]: entreprise_id + correspondant_data[0]: civilite + correspondant_data[1]: nom + correspondant_data[2]: prenom + correspondant_data[3]: telephone + correspondant_data[4]: mail + correspondant_data[5]: poste + correspondant_data[6]: service + correspondant_data[7]: origine + correspondant_data[8]: notes + correspondant_data[9]: entreprise_siret """ # champs obligatoires if ( correspondant_data[0].strip() == "" or correspondant_data[1].strip() == "" - or correspondant_data[6].strip() == "" + or correspondant_data[2].strip() == "" + or correspondant_data[9].strip() == "" ): return False + # civilite entre H ou F + if correspondant_data[0].strip() not in ["H", "F"]: + return False + # entreprise_id existant - entreprise = Entreprise.query.filter_by(siret=correspondant_data[6].strip()).first() + entreprise = Entreprise.query.filter_by(siret=correspondant_data[9].strip()).first() if entreprise is None: return False # correspondant possède le meme nom et prénom dans la meme entreprise correspondant = EntrepriseCorrespondant.query.filter_by( - nom=correspondant_data[0].strip(), - prenom=correspondant_data[1].strip(), + nom=correspondant_data[1].strip(), + prenom=correspondant_data[2].strip(), entreprise_id=entreprise.id, ).first() if correspondant is not None: return False if ( - correspondant_data[2].strip() == "" and correspondant_data[3].strip() == "" + correspondant_data[3].strip() == "" and correspondant_data[4].strip() == "" ): # 1 moyen de contact return False @@ -182,7 +190,7 @@ def verif_entreprise_data(entreprise_data): if data.strip() == "": return False if EntreprisePreferences.get_check_siret(): - siret = entreprise_data[0].replace(" ", "") # vérification sur le siret + siret = entreprise_data[0].strip().replace(" ", "") # vérification sur le siret if re.match("^\d{14}$", siret) is None: return False try: diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index 654b2618..a1619007 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -80,6 +80,11 @@ class EntrepriseCreationForm(FlaskForm): ville = _build_string_field("Ville de l'entreprise (*)") pays = _build_string_field("Pays de l'entreprise", required=False) + civilite = SelectField( + "Civilité du correspondant", + choices=[("M", "Monsieur"), ("F", "Madame")], + validators=[DataRequired(message=CHAMP_REQUIS)], + ) nom_correspondant = _build_string_field("Nom du correspondant", required=False) prenom_correspondant = _build_string_field( "Prénom du correspondant", required=False @@ -91,6 +96,8 @@ class EntrepriseCreationForm(FlaskForm): ) poste = _build_string_field("Poste du correspondant", required=False) service = _build_string_field("Service du correspondant", required=False) + origine = _build_string_field("Origine du correspondant", required=False) + notes = _build_string_field("Notes sur le correspondant", required=False) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) @@ -106,6 +113,8 @@ class EntrepriseCreationForm(FlaskForm): or self.mail.data.strip() or self.poste.data.strip() or self.service.data.strip() + or self.origine.data.strip() + or self.notes.data.strip() ): if not self.nom_correspondant.data.strip(): self.nom_correspondant.errors.append("Ce champ est requis") @@ -114,19 +123,16 @@ class EntrepriseCreationForm(FlaskForm): self.prenom_correspondant.errors.append("Ce champ est requis") validate = False if not self.telephone.data.strip() and not self.mail.data.strip(): - self.telephone.errors.append( - "Saisir un moyen de contact (mail ou téléphone)" - ) - self.mail.errors.append( - "Saisir un moyen de contact (mail ou téléphone)" - ) + msg = "Saisir un moyen de contact (mail ou téléphone)" + self.telephone.errors.append(msg) + self.mail.errors.append(msg) validate = False return validate def validate_siret(self, siret): if EntreprisePreferences.get_check_siret(): - siret_data = siret.data.replace(" ", "") + siret_data = siret.data.strip().replace(" ", "") self.siret.data = siret_data if re.match("^\d{14}$", siret_data) is None: raise ValidationError("Format incorrect") @@ -164,6 +170,15 @@ class EntrepriseModificationForm(FlaskForm): } +class SiteCreationForm(FlaskForm): + nom = _build_string_field("Nom du site (*)") + adresse = _build_string_field("Adresse (*)") + codepostal = _build_string_field("Code postal (*)") + ville = _build_string_field("Ville (*)") + pays = _build_string_field("Pays", required=False) + submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) + + class MultiCheckboxField(SelectMultipleField): widget = ListWidget(prefix_label=False) option_widget = CheckboxInput() @@ -184,11 +199,11 @@ class OffreCreationForm(FlaskForm): "Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) duree = _build_string_field("Durée (*)") - depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) + depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int) expiration_date = DateField("Date expiration", validators=[Optional()]) correspondant = SelectField("Correspondant à contacté", validators=[Optional()]) fichier = FileField( - "Fichier (*)", + "Fichier", validators=[ Optional(), FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"), @@ -237,7 +252,7 @@ class OffreModificationForm(FlaskForm): "Missions (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) duree = _build_string_field("Durée (*)") - depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) + depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int) expiration_date = DateField("Date expiration", validators=[Optional()]) correspondant = SelectField("Correspondant à contacté", validators=[Optional()]) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) @@ -269,6 +284,12 @@ class OffreModificationForm(FlaskForm): class CorrespondantCreationForm(FlaskForm): + civilite = SelectField( + "Civilité (*)", + choices=[("H", "Monsieur"), ("F", "Madame")], + validators=[DataRequired(message=CHAMP_REQUIS)], + render_kw={"class": "form-control"}, + ) nom = _build_string_field("Nom (*)", render_kw={"class": "form-control"}) prenom = _build_string_field("Prénom (*)", render_kw={"class": "form-control"}) telephone = _build_string_field( @@ -285,14 +306,12 @@ class CorrespondantCreationForm(FlaskForm): service = _build_string_field( "Service", required=False, render_kw={"class": "form-control"} ) - # depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) - - # def __init__(self, *args, **kwargs): - # super().__init__(*args, **kwargs) - - # self.depts.choices = [ - # (dept.id, dept.acronym) for dept in Departement.query.all() - # ] + origine = _build_string_field( + "Origine", required=False, render_kw={"class": "form-control"} + ) + notes = _build_string_field( + "Notes", required=False, render_kw={"class": "form-control"} + ) def validate(self): validate = True @@ -300,10 +319,9 @@ class CorrespondantCreationForm(FlaskForm): validate = False if not self.telephone.data and not self.mail.data: - self.telephone.errors.append( - "Saisir un moyen de contact (mail ou téléphone)" - ) - self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)") + msg = "Saisir un moyen de contact (mail ou téléphone)" + self.telephone.errors.append(msg) + self.mail.errors.append(msg) validate = False return validate @@ -321,7 +339,7 @@ class CorrespondantsCreationForm(FlaskForm): correspondant_list = [] for entry in self.correspondants.entries: - if entry.nom.data.strip() and entry.prenom.data.strip(): + if entry.nom.data and entry.prenom.data: if ( entry.nom.data.strip(), entry.prenom.data.strip(), @@ -351,6 +369,11 @@ class CorrespondantsCreationForm(FlaskForm): class CorrespondantModificationForm(FlaskForm): hidden_correspondant_id = HiddenField() hidden_entreprise_id = HiddenField() + civilite = SelectField( + "Civilité (*)", + choices=[("H", "Monsieur"), ("F", "Madame")], + validators=[DataRequired(message=CHAMP_REQUIS)], + ) nom = _build_string_field("Nom (*)") prenom = _build_string_field("Prénom (*)") telephone = _build_string_field("Téléphone (*)", required=False) @@ -360,16 +383,10 @@ class CorrespondantModificationForm(FlaskForm): ) poste = _build_string_field("Poste", required=False) service = _build_string_field("Service", required=False) - # depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) + origine = _build_string_field("Origine", required=False) + notes = _build_string_field("Notes", required=False) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) - # def __init__(self, *args, **kwargs): - # super().__init__(*args, **kwargs) - - # self.depts.choices = [ - # (dept.id, dept.acronym) for dept in Departement.query.all() - # ] - def validate(self): validate = True if not FlaskForm.validate(self): @@ -387,10 +404,9 @@ class CorrespondantModificationForm(FlaskForm): validate = False if not self.telephone.data and not self.mail.data: - self.telephone.errors.append( - "Saisir un moyen de contact (mail ou téléphone)" - ) - self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)") + msg = "Saisir un moyen de contact (mail ou téléphone)" + self.telephone.errors.append(msg) + self.mail.errors.append(msg) validate = False return validate @@ -593,6 +609,11 @@ class SuppressionConfirmationForm(FlaskForm): submit = SubmitField("Supprimer", render_kw=SUBMIT_MARGE) +class DesactivationConfirmationForm(FlaskForm): + notes_active = TextAreaField("Notes sur la désactivation", validators=[Optional()]) + submit = SubmitField("Désactiver", render_kw=SUBMIT_MARGE) + + class ValidationConfirmationForm(FlaskForm): submit = SubmitField("Valider", render_kw=SUBMIT_MARGE) diff --git a/app/entreprises/models.py b/app/entreprises/models.py index 90fcf335..4a318d24 100644 --- a/app/entreprises/models.py +++ b/app/entreprises/models.py @@ -9,14 +9,18 @@ class Entreprise(db.Model): adresse = db.Column(db.Text) codepostal = db.Column(db.Text) ville = db.Column(db.Text) - pays = db.Column(db.Text, default="FRANCE") + pays = db.Column(db.Text) visible = db.Column(db.Boolean, default=False) - correspondants = db.relationship( - "EntrepriseCorrespondant", + active = db.Column(db.Boolean, default=True) + notes_active = db.Column(db.Text) + + sites = db.relationship( + "EntrepriseSite", backref="entreprise", lazy="dynamic", cascade="all, delete-orphan", ) + offres = db.relationship( "EntrepriseOffre", backref="entreprise", @@ -35,13 +39,24 @@ class Entreprise(db.Model): } -# class EntrepriseSite(db.Model): -# __tablename__ = "are_sites" -# id = db.Column(db.Integer, primary_key=True) -# entreprise_id = db.Column( -# db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade") -# ) -# nom = db.Column(db.Text) +class EntrepriseSite(db.Model): + __tablename__ = "are_sites" + id = db.Column(db.Integer, primary_key=True) + entreprise_id = db.Column( + db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade") + ) + nom = db.Column(db.Text) + adresse = db.Column(db.Text) + codepostal = db.Column(db.Text) + ville = db.Column(db.Text) + pays = db.Column(db.Text) + + correspondants = db.relationship( + "EntrepriseCorrespondant", + backref="site", + lazy="dynamic", + cascade="all, delete-orphan", + ) class EntrepriseCorrespondant(db.Model): @@ -50,23 +65,29 @@ class EntrepriseCorrespondant(db.Model): entreprise_id = db.Column( db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade") ) - # site_id = db.Column(db.Integer, db.ForeignKey("are_sites.id", ondelete="cascade")) + site_id = db.Column(db.Integer, db.ForeignKey("are_sites.id", ondelete="cascade")) + civilite = db.Column(db.String(1)) nom = db.Column(db.Text) prenom = db.Column(db.Text) telephone = db.Column(db.Text) mail = db.Column(db.Text) poste = db.Column(db.Text) service = db.Column(db.Text) + origine = db.Column(db.Text) + notes = db.Column(db.Text) def to_dict(self): entreprise = Entreprise.query.filter_by(id=self.entreprise_id).first() return { + "civilite": self.civilite, "nom": self.nom, "prenom": self.prenom, "telephone": self.telephone, "mail": self.mail, "poste": self.poste, "service": self.service, + "origine": self.origine, + "notes": self.notes, "entreprise_siret": entreprise.siret, } @@ -161,15 +182,6 @@ class EntrepriseOffreDepartement(db.Model): dept_id = db.Column(db.Integer, db.ForeignKey("departement.id", ondelete="cascade")) -# class EntrepriseCorrespondantDepartement(db.Model): -# __tablename__ = "are_correspondant_departement" -# id = db.Column(db.Integer, primary_key=True) -# correspondant_id = db.Column( -# db.Integer, db.ForeignKey("are_correspondants.id", ondelete="cascade") -# ) -# dept_id = db.Column(db.Integer, db.ForeignKey("departement.id", ondelete="cascade")) - - class EntreprisePreferences(db.Model): __tablename__ = "are_preferences" id = db.Column(db.Integer, primary_key=True) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index 7fe2e727..b815b229 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -13,8 +13,10 @@ from app.decorators import permission_required from app.entreprises import LOGS_LEN from app.entreprises.forms import ( CorrespondantsCreationForm, + DesactivationConfirmationForm, EntrepriseCreationForm, EntrepriseModificationForm, + SiteCreationForm, SuppressionConfirmationForm, OffreCreationForm, OffreModificationForm, @@ -36,6 +38,7 @@ from app.entreprises.models import ( EntrepriseCorrespondant, EntrepriseLog, EntrepriseContact, + EntrepriseSite, EntrepriseStageApprentissage, EntrepriseEnvoiOffre, EntrepriseOffreDepartement, @@ -59,7 +62,7 @@ def index(): """ Permet d'afficher une page avec la liste des entreprises (visible) et une liste des dernières opérations """ - entreprises = Entreprise.query.filter_by(visible=True) + entreprises = Entreprise.query.filter_by(visible=True, active=True) logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() return render_template( "entreprises/entreprises.html", @@ -101,7 +104,7 @@ def validation(): @bp.route("/correspondants", methods=["GET"]) -@permission_required(Permission.RelationsEntreprisesView) +@permission_required(Permission.RelationsEntreprisesCorrespondants) def correspondants(): """ Permet d'afficher une page avec la liste des correspondants des entreprises visibles et une liste des dernières opérations @@ -109,7 +112,7 @@ def correspondants(): correspondants = ( db.session.query(EntrepriseCorrespondant, Entreprise) .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id) - .filter_by(visible=True) + .filter_by(visible=True, active=True) ) logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() return render_template( @@ -129,9 +132,9 @@ def fiche_entreprise(id): La fiche entreprise comporte les informations de l'entreprise, les correspondants de l'entreprise et les offres de l'entreprise. """ - entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( - description=f"fiche entreprise {id} inconnue" - ) + entreprise = Entreprise.query.filter_by( + id=id, visible=True, active=True + ).first_or_404(description=f"fiche entreprise {id} inconnue") offres_with_files = [] depts = are.get_depts() for offre in entreprise.offres: @@ -145,7 +148,7 @@ def fiche_entreprise(id): offre_with_files = are.get_offre_files_and_depts(offre, depts) if offre_with_files is not None: offres_with_files.append(offre_with_files) - correspondants = entreprise.correspondants[:] + sites = entreprise.sites[:] logs = ( EntrepriseLog.query.order_by(EntrepriseLog.date.desc()) .filter_by(object=id) @@ -163,7 +166,7 @@ def fiche_entreprise(id): "entreprises/fiche_entreprise.html", title="Fiche entreprise", entreprise=entreprise, - correspondants=correspondants, + sites=sites, offres=offres_with_files, logs=logs, stages_apprentissages=stages_apprentissages, @@ -294,16 +297,31 @@ def add_entreprise(): ) db.session.add(entreprise) db.session.commit() + db.session.refresh(entreprise) + site = EntrepriseSite( + entreprise_id=entreprise.id, + nom=form.nom_entreprise.data.strip(), + adresse=form.adresse.data.strip(), + codepostal=form.codepostal.data.strip(), + ville=form.ville.data.strip(), + pays=form.pays.data.strip() if form.pays.data.strip() else "FRANCE", + ) + db.session.add(site) + db.session.commit() if form.nom_correspondant.data.strip(): - db.session.refresh(entreprise) + db.session.refresh(site) correspondant = EntrepriseCorrespondant( entreprise_id=entreprise.id, + site_id=site.id, + civilite=form.civilite.data, nom=form.nom_correspondant.data.strip(), prenom=form.prenom_correspondant.data.strip(), telephone=form.telephone.data.strip(), mail=form.mail.data.strip(), poste=form.poste.data.strip(), service=form.service.data.strip(), + origine=form.origine.data.strip(), + notes=form.notes.data.strip(), ) db.session.add(correspondant) if current_user.has_permission(Permission.RelationsEntreprisesValidate, None): @@ -404,39 +422,27 @@ def edit_entreprise(id): ) -@bp.route("/fiche_entreprise/delete_entreprise/", methods=["GET", "POST"]) +@bp.route("/fiche_entreprise/desactiver/", methods=["GET", "POST"]) @permission_required(Permission.RelationsEntreprisesChange) -def delete_entreprise(id): +def fiche_entreprise_desactiver(id): """ - Permet de supprimer une entreprise de la base avec un formulaire de confirmation + Permet de désactiver une entreprise """ entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( description=f"entreprise {id} inconnue" ) - form = SuppressionConfirmationForm() + form = DesactivationConfirmationForm() if form.validate_on_submit(): - db.session.delete(entreprise) - # supprime les fichiers attachés aux offres - path = os.path.join( - Config.SCODOC_VAR_DIR, - "entreprises", - f"{entreprise.id}", - ) - if os.path.isdir(path): - shutil.rmtree(path) - log = EntrepriseLog( - authenticated_user=current_user.user_name, - object=entreprise.id, - text=f"Suppression de la fiche entreprise ({entreprise.nom})", - ) - db.session.add(log) + entreprise.notes_active = form.notes_active.data.strip() + entreprise.active = False db.session.commit() - flash("L'entreprise a été supprimé de la liste.") + flash("L'entreprise a été désactivé.") return redirect(url_for("entreprises.index")) return render_template( - "entreprises/delete_confirmation.html", - title="Supression entreprise", + "entreprises/confirmation_form.html", + title="Désactiver entreprise", form=form, + info_message="Cliquez sur le bouton Désactiver pour confirmer la désactivation", ) @@ -489,9 +495,10 @@ def delete_validation_entreprise(id): flash("L'entreprise a été supprimé de la liste des entreprise à valider.") return redirect(url_for("entreprises.validation")) return render_template( - "entreprises/delete_confirmation.html", + "entreprises/confirmation_form.html", title="Supression entreprise", form=form, + info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression", ) @@ -645,9 +652,10 @@ def delete_offre(id): flash("L'offre a été supprimé de la fiche entreprise.") return redirect(url_for("entreprises.fiche_entreprise", id=entreprise_id)) return render_template( - "entreprises/delete_confirmation.html", + "entreprises/confirmation_form.html", title="Supression offre", form=form, + info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression", ) @@ -683,26 +691,66 @@ def expired(id): return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise_id)) -@bp.route("/fiche_entreprise//add_correspondant", methods=["GET", "POST"]) +@bp.route( + "/fiche_entreprise//add_site", + methods=["GET", "POST"], +) @permission_required(Permission.RelationsEntreprisesChange) -def add_correspondant(id): +def add_site(id): + entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( + description=f"entreprise {id} inconnue" + ) + form = SiteCreationForm() + if form.validate_on_submit(): + site = EntrepriseSite( + entreprise_id=entreprise.id, + nom=form.nom.data.strip(), + adresse=form.adresse.data.strip(), + codepostal=form.codepostal.data.strip(), + ville=form.ville.data.strip(), + pays=form.pays.data.strip() if form.pays.data.strip() else "FRANCE", + ) + db.session.add(site) + db.session.commit() + flash("Le site a été créé et ajouté à la fiche entreprise") + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) + return render_template( + "entreprises/form.html", + title="Ajout site", + form=form, + ) + + +@bp.route( + "/fiche_entreprise//add_correspondant/", + methods=["GET", "POST"], +) +@permission_required(Permission.RelationsEntreprisesChange) +def add_correspondant(id_entreprise, id_site): """ Permet d'ajouter un correspondant a une entreprise """ - entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( - description=f"entreprise {id} inconnue" + entreprise = Entreprise.query.filter_by( + id=id_entreprise, visible=True + ).first_or_404(description=f"entreprise {id_entreprise} inconnue") + site = EntrepriseSite.query.filter_by(id=id_site).first_or_404( + description=f"site {id_site} inconnue" ) form = CorrespondantsCreationForm(hidden_entreprise_id=entreprise.id) if form.validate_on_submit(): for correspondant_entry in form.correspondants.entries: correspondant = EntrepriseCorrespondant( entreprise_id=entreprise.id, + site_id=site.id, + civilite=correspondant_entry.civilite.data, nom=correspondant_entry.nom.data.strip(), prenom=correspondant_entry.prenom.data.strip(), telephone=correspondant_entry.telephone.data.strip(), mail=correspondant_entry.mail.data.strip(), poste=correspondant_entry.poste.data.strip(), service=correspondant_entry.service.data.strip(), + origine=correspondant_entry.origine.data.strip(), + notes=correspondant_entry.notes.data.strip(), ) log = EntrepriseLog( authenticated_user=current_user.user_name, @@ -735,12 +783,15 @@ def edit_correspondant(id): hidden_correspondant_id=correspondant.id, ) if form.validate_on_submit(): + correspondant.civilite = form.civilite.data correspondant.nom = form.nom.data.strip() correspondant.prenom = form.prenom.data.strip() correspondant.telephone = form.telephone.data.strip() correspondant.mail = form.mail.data.strip() correspondant.poste = form.poste.data.strip() correspondant.service = form.service.data.strip() + correspondant.origine = form.origine.data.strip() + correspondant.notes = form.notes.data.strip() log = EntrepriseLog( authenticated_user=current_user.user_name, object=correspondant.entreprise_id, @@ -750,15 +801,18 @@ def edit_correspondant(id): db.session.commit() flash("Le correspondant a été modifié.") return redirect( - url_for("entreprises.fiche_entreprise", id=correspondant.entreprise.id) + url_for("entreprises.fiche_entreprise", id=correspondant.entreprise_id) ) elif request.method == "GET": + form.civilite.data = correspondant.civilite form.nom.data = correspondant.nom form.prenom.data = correspondant.prenom form.telephone.data = correspondant.telephone form.mail.data = correspondant.mail form.poste.data = correspondant.poste form.service.data = correspondant.service + form.origine.data = correspondant.origine + form.notes.data = correspondant.notes return render_template( "entreprises/form.html", title="Modification correspondant", @@ -790,9 +844,10 @@ def delete_correspondant(id): url_for("entreprises.fiche_entreprise", id=correspondant.entreprise_id) ) return render_template( - "entreprises/delete_confirmation.html", + "entreprises/confirmation_form.html", title="Supression correspondant", form=form, + info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression", ) @@ -1016,9 +1071,10 @@ def delete_stage_apprentissage(id): ) ) return render_template( - "entreprises/delete_confirmation.html", + "entreprises/confirmation_form.html", title="Supression stage/apprentissage", form=form, + info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression", ) @@ -1243,12 +1299,15 @@ def export_correspondants(): ) if correspondants: keys = [ + "civilite", "nom", "prenom", "telephone", "mail", "poste", "service", + "origine", + "notes", "entreprise_siret", ] titles = keys[:] @@ -1272,12 +1331,15 @@ def get_import_correspondants_file_sample(): Permet de récupérer un fichier exemple vide pour pouvoir importer des correspondants """ keys = [ + "civilite", "nom", "prenom", "telephone", "mail", "poste", "service", + "origine", + "notes", "entreprise_siret", ] titles = keys[:] @@ -1306,12 +1368,15 @@ def import_correspondants(): correspondant_list = [] ligne = 0 titles = [ + "civilite", "nom", "prenom", "telephone", "mail", "poste", "service", + "origine", + "notes", "entreprise_siret", ] if data[1][0] != titles: @@ -1326,29 +1391,32 @@ def import_correspondants(): if ( are.verif_correspondant_data(correspondant_data) and ( - correspondant_data[0].strip(), correspondant_data[1].strip(), - correspondant_data[6].strip(), + correspondant_data[2].strip(), + correspondant_data[9].strip(), ) not in correspondant_list ): correspondant_list.append( ( - correspondant_data[0].strip(), correspondant_data[1].strip(), - correspondant_data[6].strip(), + correspondant_data[2].strip(), + correspondant_data[9].strip(), ) ) entreprise = Entreprise.query.filter_by( - siret=correspondant_data[6].strip() + siret=correspondant_data[9].strip() ).first() correspondant = EntrepriseCorrespondant( - nom=correspondant_data[0].strip(), - prenom=correspondant_data[1].strip(), - telephone=correspondant_data[2].strip(), - mail=correspondant_data[3].strip(), - poste=correspondant_data[4].strip(), - service=correspondant_data[5].strip(), + civilite=correspondant_data[0].strip(), + nom=correspondant_data[1].strip(), + prenom=correspondant_data[2].strip(), + telephone=correspondant_data[3].strip(), + mail=correspondant_data[4].strip(), + poste=correspondant_data[5].strip(), + service=correspondant_data[6].strip(), + origine=correspondant_data[7].strip(), + notes=correspondant_data[8].strip(), entreprise_id=entreprise.id, ) correspondants_import.append(correspondant) @@ -1480,9 +1548,10 @@ def delete_offre_file(offre_id, filedir): url_for("entreprises.fiche_entreprise", id=offre.entreprise_id) ) return render_template( - "entreprises/delete_confirmation.html", + "entreprises/confirmation_form.html", title="Suppression fichier d'une offre", form=form, + info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression", ) diff --git a/app/models/absences.py b/app/models/absences.py index bbff2c5c..830d46f9 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -27,6 +27,20 @@ class Absence(db.Model): # XXX TODO: contrainte ajoutée: vérifier suppression du module # (mettre à NULL sans supprimer) + def to_dict(self): + data = { + "id": self.id, + "etudid": self.etudid, + "jour": self.jour, + "estabs": self.estabs, + "estjust": self.estjust, + "matin": self.matin, + "description": self.description, + "entry_date": self.entry_date, + "moduleimpl_id": self.moduleimpl_id, + } + return data + class AbsenceNotification(db.Model): """Notification d'absence émise""" diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index 5662df3a..f771ff08 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -303,12 +303,14 @@ def YearTable( return "\n".join(T) -def list_abs_in_range(etudid, debut, fin, matin=None, moduleimpl_id=None, cursor=None): +def list_abs_in_range( + etudid, debut=None, fin=None, matin=None, moduleimpl_id=None, cursor=None +): """Liste des absences entre deux dates. Args: etudid: - debut: string iso date ("2020-03-12") + debut: string iso date ("2020-03-12") ou None end: string iso date ("2020-03-12") matin: None, True, False moduleimpl_id: restreint le comptage aux absences dans ce module @@ -335,9 +337,13 @@ def list_abs_in_range(etudid, debut, fin, matin=None, moduleimpl_id=None, cursor AND A.ESTABS""" + ismatin + modul - + """ + + ( + "" + if debut is None + else """ AND A.JOUR BETWEEN %(debut)s AND %(fin)s - """, + """ + ), { "etudid": etudid, "debut": debut, @@ -412,22 +418,31 @@ WHERE A.ETUDID = %(etudid)s return res -def list_abs_date(etudid, beg_date, end_date): +def list_abs_date(etudid, beg_date=None, end_date=None): """Liste des absences et justifs entre deux dates (inclues).""" + print("On rentre") cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT jour, matin, estabs, estjust, description FROM ABSENCES A - WHERE A.ETUDID = %(etudid)s + + req = """SELECT jour, matin, estabs, estjust, description + FROM ABSENCES A + WHERE A.ETUDID = %(etudid)s""" + ( + "" + if beg_date is None + else """ AND A.jour >= %(beg_date)s AND A.jour <= %(end_date)s - """, + """ + ) + cursor.execute( + req, vars(), ) - Abs = cursor.dictfetchall() + + absences = cursor.dictfetchall() # remove duplicates A = {} # { (jour, matin) : abs } - for a in Abs: + for a in absences: jour, matin = a["jour"], a["matin"] if (jour, matin) in A: # garde toujours la description diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index c72b3ff5..512caa8a 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -47,6 +47,13 @@ _SCO_PERMISSIONS = ( ), (1 << 25, "RelationsEntreprisesSend", "Envoyer des offres"), (1 << 26, "RelationsEntreprisesValidate", "Valide les entreprises"), + (1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"), + # 27 à 39 ... réservé pour "entreprises" + # Api scodoc9 + (1 << 40, "APIView", "Voir"), + (1 << 41, "APIEtudChangeGroups", "Modifier les groupes"), + (1 << 42, "APIEditAllNotes", "Modifier toutes les notes"), + (1 << 43, "APIAbsChange", "Saisir des absences"), ) diff --git a/app/scodoc/sco_roles_default.py b/app/scodoc/sco_roles_default.py index 9529b031..69e237fd 100644 --- a/app/scodoc/sco_roles_default.py +++ b/app/scodoc/sco_roles_default.py @@ -53,6 +53,7 @@ SCO_ROLES_DEFAULTS = { p.ScoUsersAdmin, p.ScoUsersView, p.ScoView, + p.APIView, ), # RespPE est le responsable poursuites d'études # il peut ajouter des tags sur les formations: @@ -62,7 +63,11 @@ SCO_ROLES_DEFAULTS = { # ObservateurEntreprise est un observateur de l'application entreprise "ObservateurEntreprise": (p.RelationsEntreprisesView,), # UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification) - "UtilisateurEntreprise": (p.RelationsEntreprisesView, p.RelationsEntreprisesChange), + "UtilisateurEntreprise": ( + p.RelationsEntreprisesView, + p.RelationsEntreprisesChange, + p.RelationsEntreprisesCorrespondants, + ), # AdminEntreprise est un admin de l'application entreprise (toutes les actions possibles de l'application) "AdminEntreprise": ( p.RelationsEntreprisesView, @@ -70,7 +75,10 @@ SCO_ROLES_DEFAULTS = { p.RelationsEntreprisesExport, p.RelationsEntreprisesSend, p.RelationsEntreprisesValidate, + p.RelationsEntreprisesCorrespondants, ), + # LecteurAPI peut utiliser l'API en lecture + "LecteurAPI": (p.APIView,), # Super Admin est un root: création/suppression de départements # _tous_ les droits # Afin d'avoir tous les droits, il ne doit pas être asscoié à un département @@ -85,4 +93,5 @@ ROLES_ATTRIBUABLES_SCODOC = ( "ObservateurEntreprise", "UtilisateurEntreprise", "AdminEntreprise", + "LecteurAPI", ) diff --git a/app/static/css/entreprises.css b/app/static/css/entreprises.css index 1f4e5f23..0ede866c 100644 --- a/app/static/css/entreprises.css +++ b/app/static/css/entreprises.css @@ -40,6 +40,11 @@ *background-color: #151515; } +.btn-remove { + margin-top: 5px; + margin-bottom: 5px; +} + .fiche-entreprise .btn { margin-top: 5px; margin-bottom: 5px; @@ -54,23 +59,24 @@ margin-bottom: -5px; } -.entreprise, .correspondant, .offre { +.entreprise, .correspondant, .offre, .site{ border: solid 2px; border-radius: 10px; padding: 10px; margin-bottom: 10px; + margin-top: 10px; } -.correspondants-et-offres { +.sites-et-offres { display: flex; justify-content: space-between; } -.correspondants-et-offres > div { +.sites-et-offres > div { flex: 1 0 0; } -.correspondants-et-offres > div:nth-child(2) { +.sites-et-offres > div:nth-child(2) { margin-left: 20px; } diff --git a/app/templates/entreprises/_correspondant.html b/app/templates/entreprises/_correspondant.html index 0def6b89..d8c00846 100644 --- a/app/templates/entreprises/_correspondant.html +++ b/app/templates/entreprises/_correspondant.html @@ -1,6 +1,7 @@ {# -*- mode: jinja-html -*- #}
+ Civilité : {{ correspondant.civilite|get_civilité }}
Nom : {{ correspondant.nom }}
Prénom : {{ correspondant.prenom }}
{% if correspondant.telephone %} @@ -15,6 +16,12 @@ {% if correspondant.service %} Service : {{ correspondant.service }}
{% endif %} + {% if correspondant.origine %} + Origine : {{ correspondant.origine }}
+ {% endif %} + {% if correspondant.notes %} + Notes : {{ correspondant.notes }}
+ {% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} diff --git a/app/templates/entreprises/_offre.html b/app/templates/entreprises/_offre.html index 56541178..c799b6f4 100644 --- a/app/templates/entreprises/_offre.html +++ b/app/templates/entreprises/_offre.html @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #}
-
+
Ajouté le {{ offre[0].date_ajout.strftime('%d/%m/%y') }} à {{ offre[0].date_ajout.strftime('%Hh%M') }}
Intitulé : {{ offre[0].intitule }}
Description : {{ offre[0].description }}
diff --git a/app/templates/entreprises/ajout_correspondants.html b/app/templates/entreprises/ajout_correspondants.html index f7d5e5b1..18070b7f 100644 --- a/app/templates/entreprises/ajout_correspondants.html +++ b/app/templates/entreprises/ajout_correspondants.html @@ -35,6 +35,14 @@
{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/delete_confirmation.html b/app/templates/entreprises/confirmation_form.html similarity index 75% rename from app/templates/entreprises/delete_confirmation.html rename to app/templates/entreprises/confirmation_form.html index abd59c47..fb873788 100644 --- a/app/templates/entreprises/delete_confirmation.html +++ b/app/templates/entreprises/confirmation_form.html @@ -5,7 +5,7 @@ {% block app_content %}

{{ title }}


-
Cliquez sur le bouton Supprimer pour confirmer votre supression
+
{{ info_message }}

diff --git a/app/templates/entreprises/entreprises.html b/app/templates/entreprises/entreprises.html index d870e594..f6d08cec 100644 --- a/app/templates/entreprises/entreprises.html +++ b/app/templates/entreprises/entreprises.html @@ -67,7 +67,7 @@
diff --git a/app/templates/entreprises/envoi_offre_form.html b/app/templates/entreprises/envoi_offre_form.html index ca5ba131..6ead6477 100644 --- a/app/templates/entreprises/envoi_offre_form.html +++ b/app/templates/entreprises/envoi_offre_form.html @@ -36,6 +36,14 @@
{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/fiche_entreprise.html b/app/templates/entreprises/fiche_entreprise.html index b8d4b123..327a3bf7 100644 --- a/app/templates/entreprises/fiche_entreprise.html +++ b/app/templates/entreprises/fiche_entreprise.html @@ -37,24 +37,38 @@
- {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
+ {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} Modifier - Supprimer + Désactiver + Ajouter site Ajouter offre - Ajouter correspondant {% endif %} Liste contacts Voir les offres expirées -
+
-
- {% if correspondants %} +
+ {% if sites %}
-

Correspondants

- {% for correspondant in correspondants %} - {% include 'entreprises/_correspondant.html' %} +

Sites

+ {% for site in sites %} +
+ Nom : {{ site.nom }}
+ Adresse : {{ site.adresse }}
+ Code postal : {{ site.codepostal }}
+ Ville : {{ site.ville }}
+ Pays : {{ site.pays }} + {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} +
Ajouter correspondant + {% endif %} + {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesCorrespondants, None) %} + {% for correspondant in site.correspondants %} + {% include 'entreprises/_correspondant.html' %} + {% endfor %} + {% endif %} +
{% endfor %}
{% endif %} @@ -63,7 +77,7 @@

Offres - Voir les offres expirées

{% for offre in offres %} - {% include 'entreprises/_offre.html' %} + {% include 'entreprises/_offre.html' %} {% endfor %}
{% endif %} @@ -71,20 +85,20 @@
- {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} - Ajouter stage ou apprentissage - {% endif %}

Liste des stages et apprentissages réalisés au sein de l'entreprise

+ {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} + Ajouter stage ou apprentissage + {% endif %} - - - - - - - + + + + + + + {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} {% endif %} diff --git a/app/templates/entreprises/form.html b/app/templates/entreprises/form.html index fe51a97b..067f4a50 100644 --- a/app/templates/entreprises/form.html +++ b/app/templates/entreprises/form.html @@ -23,15 +23,15 @@
Date débutDate finDuréeTypeÉtudiantFormationNotesDate débutDate finDuréeTypeÉtudiantFormationNotesAction