333 lines
11 KiB
Python
333 lines
11 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
#
|
|
##############################################################################
|
|
|
|
"""Gestion des tags sur les modules
|
|
|
|
Implementation expérimentale (Jul. 2016) pour grouper les modules sur
|
|
les avis de poursuites d'études.
|
|
|
|
|
|
Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html
|
|
"""
|
|
import http
|
|
import re
|
|
|
|
from flask import g
|
|
|
|
from app import db, log
|
|
from app.models import Formation, NotesTag
|
|
from app.scodoc import sco_edit_module
|
|
import app.scodoc.sco_utils as scu
|
|
import app.scodoc.notesdb as ndb
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
|
|
# Opérations implementées:
|
|
# + liste des modules des formations de code donné (formation_code) avec ce tag
|
|
# + liste de tous les noms de tag
|
|
# + tag pour un nom
|
|
# + creer un tag (nom)
|
|
# + lier un tag à un module
|
|
# + enlever un tag d'un module (si c'est le dernier, supprimer le tag lui même)
|
|
#
|
|
# API publiée:
|
|
# module_tag_list_all -> tous les noms d etags (pour l'autocomplete)
|
|
# module_tag_list( module_id ) -> les noms de tags associés à ce module
|
|
# module_tag_set( module_id, taglist ) -> modifie les tags
|
|
|
|
|
|
# NOTA: ancien code, n'utile pas de modèles SQLAlchemy
|
|
class ScoTag(object):
|
|
"""Generic tags for ScoDoc"""
|
|
|
|
# must be overloaded:
|
|
tag_table = None # table (tag_id, title)
|
|
assoc_table = None # table (tag_id, object_id)
|
|
obj_colname = None # column name for object_id in assoc_table
|
|
|
|
def __init__(self, title, object_id=""):
|
|
"""Load tag, or create if does not exist"""
|
|
self.title = title.strip()
|
|
if not self.title:
|
|
raise ScoValueError("invalid empty tag")
|
|
r = ndb.SimpleDictFetch(
|
|
"SELECT id as tag_id, * FROM "
|
|
+ self.tag_table
|
|
+ " WHERE dept_id=%(dept_id)s AND title = %(title)s",
|
|
{"dept_id": g.scodoc_dept_id, "title": self.title},
|
|
)
|
|
if r:
|
|
self.tag_id = r[0]["tag_id"]
|
|
else:
|
|
# Create new tag:
|
|
# log("creating new tag: %s" % self.title)
|
|
cnx = ndb.GetDBConnexion()
|
|
self.tag_id = ndb.DBInsertDict(
|
|
cnx,
|
|
self.tag_table,
|
|
{"title": self.title, "dept_id": g.scodoc_dept_id},
|
|
commit=True,
|
|
)
|
|
if object_id:
|
|
self.tag_object(object_id)
|
|
|
|
def __repr__(self): # debug
|
|
return '<tag "%s">' % self.title
|
|
|
|
@classmethod
|
|
def check_tag_title(cls, title: str) -> bool:
|
|
""" "true si le tag est acceptable: longueur max (32), syntaxe
|
|
un_tag ou un_tag:78
|
|
"""
|
|
if not title or len(title) > 32:
|
|
return False
|
|
if re.match(r"^[\w0-9\-_$!?+=,&\.]*(:[0-9]*)?$", title):
|
|
return True
|
|
return False
|
|
|
|
def delete(self):
|
|
"""Delete this tag.
|
|
This object should not be used after this call !
|
|
"""
|
|
args = {"tag_id": self.tag_id}
|
|
ndb.SimpleQuery(
|
|
"DELETE FROM " + self.tag_table + " t WHERE t.id = %(tag_id)s",
|
|
args,
|
|
)
|
|
|
|
def tag_object(self, object_id):
|
|
"""Associate tag to given object"""
|
|
args = {self.obj_colname: object_id, "tag_id": self.tag_id}
|
|
r = ndb.SimpleDictFetch(
|
|
"SELECT * FROM "
|
|
+ self.assoc_table
|
|
+ " a WHERE a."
|
|
+ self.obj_colname
|
|
+ " = %("
|
|
+ self.obj_colname
|
|
+ ")s AND a.tag_id = %(tag_id)s",
|
|
args,
|
|
)
|
|
if not r:
|
|
# log("tag %s with %s" % (object_id, self.title))
|
|
cnx = ndb.GetDBConnexion()
|
|
query = f"""INSERT INTO {self.assoc_table}
|
|
(tag_id, {self.obj_colname})
|
|
VALUES"""
|
|
ndb.SimpleQuery(
|
|
query + " (%(tag_id)s, %(object_id)s)",
|
|
{"tag_id": self.tag_id, "object_id": object_id},
|
|
)
|
|
cnx.commit()
|
|
|
|
def remove_tag_from_object(self, object_id):
|
|
"""Remove tag from module.
|
|
If no more modules tagged with this tag, delete it.
|
|
Return True if Tag still exists.
|
|
"""
|
|
# log("removing tag %s from %s" % (self.title, object_id))
|
|
args = {"object_id": object_id, "tag_id": self.tag_id}
|
|
ndb.SimpleQuery(
|
|
"DELETE FROM "
|
|
+ self.assoc_table
|
|
+ " a WHERE a."
|
|
+ self.obj_colname
|
|
+ " = %(object_id)s AND a.tag_id = %(tag_id)s",
|
|
args,
|
|
)
|
|
r = ndb.SimpleDictFetch(
|
|
"""SELECT * FROM notes_modules_tags mt WHERE tag_id = %(tag_id)s
|
|
""",
|
|
args,
|
|
)
|
|
if not r:
|
|
# tag no more used, delete
|
|
ndb.SimpleQuery(
|
|
"""DELETE FROM notes_tags t WHERE t.id = %(tag_id)s""",
|
|
args,
|
|
)
|
|
|
|
|
|
class ModuleTag(ScoTag):
|
|
"""Tags sur les modules dans les programmes pédagogiques"""
|
|
|
|
tag_table = "notes_tags" # table (tag_id, title)
|
|
assoc_table = "notes_modules_tags" # table (tag_id, object_id)
|
|
obj_colname = "module_id" # column name for object_id in assoc_table
|
|
|
|
def list_modules(self, formation_code=""):
|
|
"""Liste des modules des formations de code donné (formation_code) avec ce tag"""
|
|
args = {"tag_id": self.tag_id}
|
|
if not formation_code:
|
|
# tous les modules de toutes les formations !
|
|
r = ndb.SimpleDictFetch(
|
|
"SELECT id AS"
|
|
+ self.obj_colname
|
|
+ " FROM "
|
|
+ self.assoc_table
|
|
+ " WHERE tag_id = %(tag_id)s",
|
|
args,
|
|
)
|
|
else:
|
|
args["formation_code"] = formation_code
|
|
|
|
r = ndb.SimpleDictFetch(
|
|
"""SELECT mt.module_id
|
|
FROM notes_modules_tags mt, notes_modules m, notes_formations f
|
|
WHERE mt.tag_id = %(tag_id)s
|
|
AND m.id = mt.module_id
|
|
AND m.formation_id = f.id
|
|
AND f.formation_code = %(formation_code)s
|
|
""",
|
|
args,
|
|
)
|
|
return [x["module_id"] for x in r]
|
|
|
|
|
|
# API
|
|
|
|
|
|
# TODO placer dans la vraie API et ne plus utiliser sco_publish
|
|
def module_tag_search(term: str | int):
|
|
"""List all used tag names (for auto-completion)"""
|
|
term = "" if term is None else str(term)
|
|
# restrict charset to avoid injections
|
|
if not scu.ALPHANUM_EXP.match(term):
|
|
data = []
|
|
else:
|
|
r = ndb.SimpleDictFetch(
|
|
"SELECT title FROM notes_tags WHERE title LIKE %(term)s AND dept_id=%(dept_id)s",
|
|
{
|
|
"term": term + "%",
|
|
"dept_id": g.scodoc_dept_id,
|
|
},
|
|
)
|
|
data = [x["title"] for x in r]
|
|
|
|
return scu.sendJSON(data)
|
|
|
|
|
|
def module_tag_list(module_id="") -> list[str]:
|
|
"""les noms de tags associés à ce module"""
|
|
r = ndb.SimpleDictFetch(
|
|
"""SELECT t.title
|
|
FROM notes_modules_tags mt, notes_tags t
|
|
WHERE mt.tag_id = t.id
|
|
AND mt.module_id = %(module_id)s
|
|
""",
|
|
{"module_id": module_id},
|
|
)
|
|
return [x["title"] for x in r]
|
|
|
|
|
|
def module_tag_set(module_id="", taglist=None):
|
|
"""taglist may either be:
|
|
a string with tag names separated by commas ("un,deux")
|
|
or a list of strings (["un", "deux"])
|
|
Remplace les tags existants
|
|
"""
|
|
if not taglist:
|
|
taglist = []
|
|
elif isinstance(taglist, str):
|
|
taglist = taglist.split(",")
|
|
taglist = [t.strip() for t in taglist]
|
|
log(f"module_tag_set: module_id={module_id} taglist={taglist}")
|
|
# Check tags syntax
|
|
for tag in taglist:
|
|
if not ScoTag.check_tag_title(tag):
|
|
log(f"module_tag_set({module_id}): invalid tag title")
|
|
return scu.json_error(404, "invalid tag")
|
|
|
|
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
|
|
|
|
# Sanity check:
|
|
mod_dict = sco_edit_module.module_list(args={"module_id": module_id})
|
|
if not mod_dict:
|
|
raise ScoValueError("invalid module !")
|
|
|
|
newtags = set(taglist)
|
|
oldtags = set(module_tag_list(module_id))
|
|
to_del = oldtags - newtags
|
|
to_add = newtags - oldtags
|
|
|
|
# should be atomic, but it's not.
|
|
for tagname in to_add:
|
|
t = ModuleTag(tagname, object_id=module_id)
|
|
for tagname in to_del:
|
|
t = ModuleTag(tagname)
|
|
t.remove_tag_from_object(module_id)
|
|
|
|
return "", http.HTTPStatus.NO_CONTENT
|
|
|
|
|
|
def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]:
|
|
"""Découpage d'un tag, tel que saisi par un utilisateur dans le programme,
|
|
pour en extraire :
|
|
|
|
* son _nom de tag_ (tagname) (chaine de caractère correspondant au tag)
|
|
* un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":").
|
|
|
|
Args:
|
|
tag: La saisie utilisateur du tag dans le programme
|
|
separateur: Le séparateur des informations dans la saisie utilisateur
|
|
|
|
Return:
|
|
Tuple (tagname, coeff_de_ponderation) extrait de la saisie utilisateur
|
|
(avec coeff_de_ponderation=1.0 si non mentionné)
|
|
|
|
Author:
|
|
Cléo Baras
|
|
"""
|
|
if separateur in tag:
|
|
temp = tag.split(":")
|
|
try:
|
|
pond = float(temp[1])
|
|
return (temp[0], pond)
|
|
except (IndexError, ValueError, TypeError):
|
|
# Renvoie tout le tag si le découpage a échoué
|
|
return (tag, 1.0)
|
|
else:
|
|
# initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération
|
|
# n'ait indiqué dans le tag
|
|
return (tag, 1.0)
|
|
|
|
|
|
def formation_tag_modules_by_type(formation: Formation):
|
|
"""Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus'
|
|
Ne taggue pas les modules standards.
|
|
"""
|
|
tag_titles = {
|
|
m.type_abbrv() for m in formation.modules
|
|
} # usually {'res', 'mod', 'sae'}
|
|
tag_by_type = {
|
|
tag_title: NotesTag.get_or_create(title=tag_title, dept_id=formation.dept_id)
|
|
for tag_title in tag_titles
|
|
}
|
|
for module in formation.modules:
|
|
if module.module_type != scu.ModuleType.STANDARD:
|
|
module.add_tag(tag_by_type[module.type_abbrv()])
|
|
db.session.commit()
|