diff --git a/app/api/__init__.py b/app/api/__init__.py index 6c2e493ffb3d94980b06397b22b0821db83d049d..5acb090966a62b055f54c6583dc02a124ea239a8 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -122,6 +122,7 @@ from app.api import ( justificatifs, logos, moduleimpl, + operations, partitions, semset, users, diff --git a/app/api/operations.py b/app/api/operations.py new file mode 100644 index 0000000000000000000000000000000000000000..51ba5c6189d79421ba41602d7faf1ef676907c9e --- /dev/null +++ b/app/api/operations.py @@ -0,0 +1,100 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" + ScoDoc 9 API : liste opérations effectuées par un utilisateur + + CATEGORY + -------- + Operations +""" + +from flask import url_for +from flask_json import as_json +from flask_login import login_required + +import app +from app import db +from app.api import api_bp as bp, api_web_bp +from app.api import api_permission_required as permission_required +from app.decorators import scodoc +from app.models import NotesNotes +from app.scodoc.sco_permissions import Permission +from app.scodoc import sco_utils as scu + +MAX_QUERY_LENGTH = 10000 + + +@bp.route("/operations/user/<int:uid>/notes") +@api_web_bp.route("/operations/user/<int:uid>/notes") +@login_required +@scodoc +@permission_required(Permission.ScoView) +@as_json +def operations_user_notes(uid: int): + """Liste les opérations de saisie de notes effectuées par utilisateur. + + QUERY + ----- + start: indice de début de la liste + length: nombre d'éléments à retourner + draw: numéro de la requête (pour pagination, renvoyé tel quel) + order[dir]: desc ou asc + search[value]: chaîne à chercher (dans évaluation et étudiant) + PARAMS + ----- + uid: l'id de l'utilisateur + """ + start = int(app.request.args.get("start", 0)) + length = min(int(app.request.args.get("length", 10)), MAX_QUERY_LENGTH) + order = app.request.args.get("order[dir]", "desc") + draw = int(app.request.args.get("draw", 1)) + search = app.request.args.get("search[value]", "") + query = db.session.query(NotesNotes).filter(NotesNotes.uid == uid) + if order == "asc": + query = query.order_by(NotesNotes.date.asc()) + else: + query = query.order_by(NotesNotes.date.desc()) + + # Pour l'efficacité, limite si pas de recherche en python + limited_query = query.offset(start).limit(length) if not search else query + + data = [] + for note in limited_query: + obj = { + "date": note.date.isoformat(), + "date_dmy": note.date.strftime(scu.DATEATIME_FMT), + "operation": "Saisie de note", + "value": scu.fmt_note(note.value), + "id": note.id, + "uid": note.uid, + "etudiant": note.etudiant.to_dict_short(), + "etudiant_link": note.etudiant.html_link_fiche(), + "evaluation": note.evaluation.to_dict_api(), + "evaluation_link": f"""<a href="{ + url_for('notes.evaluation_listenotes', + scodoc_dept=note.evaluation.moduleimpl.formsemestre.departement.acronym, + evaluation_id=note.evaluation_id) + }">{note.evaluation.descr()}</a>""", + } + if search: + search = search.lower() + if ( + search not in note.etudiant.nomprenom.lower() + and search not in note.evaluation.descr().lower() + and search not in obj["date_dmy"] + ): + continue # skip + + data.append(obj) + + result = data[start : start + length] if search else data + return { + "draw": draw, + "recordsTotal": query.count(), # unfiltered + "recordsFiltered": len(data) if search else query.count(), + "data": result, + } diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 6ef71ff2f64112dabd81251e1b27d46cc2ca2b29..6574047ee4df7a862931f026b523f00b0e52148b 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -121,6 +121,12 @@ class Identite(models.ScoDocModel): cascade="all, delete-orphan", lazy="dynamic", ) + notes = db.relationship( + "NotesNotes", + backref="etudiant", + cascade="all, delete-orphan", + lazy="dynamic", + ) # Relations avec les assiduites et les justificatifs assiduites = db.relationship( "Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete" diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 06db8075d228ac534884905f558833a5f0da4204..cf998a56a429633b4a54808cf34976bfefda326b 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -58,6 +58,12 @@ class Evaluation(models.ScoDocModel): # ordre de presentation (par défaut, le plus petit numero # est la plus ancienne eval): numero = db.Column(db.Integer, nullable=False, default=0) + notes = db.relationship( + "NotesNotes", + backref="evaluation", + cascade="all, delete-orphan", + lazy="dynamic", + ) ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) _sco_dept_relations = ("ModuleImpl", "FormSemestre") # accès au dept_id @@ -380,6 +386,13 @@ class Evaluation(models.ScoDocModel): return f"""du {self.date_debut.strftime('%d/%m/%Y')} à { _h(self.date_debut)} au {self.date_fin.strftime('%d/%m/%Y')} à {_h(self.date_fin)}""" + def descr(self) -> str: + "Description de l'évaluation pour affichage (avec module et semestre)" + return f"""{self.description} {self.descr_date()} en { + self.moduleimpl.module.titre_str()} du { + self.moduleimpl.formsemestre.titre_formation(with_sem_idx=True) + }""" + def heure_debut(self) -> str: """L'heure de début (sans la date), en ISO. Chaine vide si non renseignée.""" diff --git a/app/templates/user_board/user_board.j2 b/app/templates/user_board/user_board.j2 index 5f5dac7ab5e33576b2ae3d4bfe06285f6016a75d..2832f95180ac2c6afdbcf2cc9f32bb11b9ac0e29 100644 --- a/app/templates/user_board/user_board.j2 +++ b/app/templates/user_board/user_board.j2 @@ -1,8 +1,10 @@ {# Tableau de bord utilisateur #} {% extends "base.j2" %} - -{% block app_content %} +{% block styles %} +{{super()}} +<link type="text/css" rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.css" /> +<link rel="stylesheet" type="text/css" href="{{scu.STATIC_DIR}}/DataTables/datatables.min.css" /> <style> .ub-formsemestres { display: flex; @@ -60,7 +62,14 @@ color: black; text-decoration: none; } + +div.scobox.saisies-notes { + background-color: rgb(243, 255, 255); +} </style> +{% endblock %} + +{% block app_content %} <div class="tab-content"> <h2>{{user.get_nomcomplet()}}</h2> @@ -105,6 +114,61 @@ {% endfor %} </div> {% endfor %} + + + <div class="scobox saisies-notes"> + <div class="scobox-title"> + Dernières saisies de notes par {{user.get_prenomnom()}} + </div> + <table id="saisies-notes" class="display" style="width:100%"> + <thead> + <tr> + <th>Date</th> + <th>Évaluation</th> + <th>Étudiant</th> + <th>Note</th> + </tr> + </thead> + <tbody> + <!-- Data will be loaded dynamically via JavaScript --> + </tbody> + </table> </div> + +</div> + + + {% endblock app_content %} + + +{% block scripts %} + {{ super() }} + <script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script> + <script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script> + <script> + $(document).ready(function() { + $('#saisies-notes').DataTable({ + "processing": true, + "serverSide": true, + "ajax": { + "url": "{{ url_for('apiweb.operations_user_notes', + scodoc_dept=current_user.dept or g.scodoc_dept or fallback_dept.acronym, + uid=user.id) }}", + "type": "GET" + }, + "columns": [ + { "data": "date_dmy", "orderable": false }, + { "data": "evaluation_link", "orderable": false }, + { "data": "etudiant_link", "orderable": false }, + { "data": "value", "orderable": false } + ], + "language": { + search: "Chercher (date, titre, étudiant) :", // Change the "Search:" label + lengthMenu: "Show _MENU_ records per page" + } + }); + }); + </script> +{% endblock %} diff --git a/app/views/user_board.py b/app/views/user_board.py index fe3489e4106dcc19ca9b5c5e28fefc4a5133eb7e..30d51c3ea8dd6a54be69152806c27942c3eb7de6 100644 --- a/app/views/user_board.py +++ b/app/views/user_board.py @@ -14,6 +14,7 @@ from app.decorators import ( permission_required, ) from app.models import Departement, FormSemestre +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc import sco_preferences from app.views import scodoc_bp as bp @@ -25,6 +26,9 @@ from app.views import ScoData @login_required def user_board(user_name: str): """Tableau de bord utilisateur: liens vers ses objets""" + fallback_dept = db.session.query(Departement).first() + if not fallback_dept: + raise ScoValueError("Aucun département existant") user = User.query.filter_by(user_name=user_name).first_or_404() ( formsemestres_by_dept, @@ -47,6 +51,7 @@ def user_board(user_name: str): "user_board/user_board.j2", depts=depts, dept_names=dept_names_sorted, + fallback_dept=fallback_dept, formsemestres_by_dept=formsemestres_by_dept, modimpls_by_formsemestre=modimpls_by_formsemestre, sco=ScoData(), diff --git a/sco_version.py b/sco_version.py index 83b81513ea7f6e5ebb69690c006a818455526d6b..2e58c9f14c108151ecee3cdec4a408db4ece747e 100644 --- a/sco_version.py +++ b/sco_version.py @@ -3,7 +3,7 @@ "Infos sur version ScoDoc" -SCOVERSION = "9.7.37" +SCOVERSION = "9.7.38" SCONAME = "ScoDoc"