# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 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 types import sco_utils as scu import notesdb as ndb from notes_log import log from sco_exceptions import ScoValueError, AccessDenied from sco_permissions import ScoEditFormationTags, ScoChangeFormation # Opérations à implementer: # + 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 class ScoTag: """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, context, title, object_id=""): """Load tag, or create if does not exist""" self.context = context self.title = title.strip() if not self.title: raise ScoValueError("invalid empty tag") r = ndb.SimpleDictFetch( context, "SELECT * FROM " + self.tag_table + " WHERE title = %(title)s", {"title": self.title}, ) if r: self.tag_id = r[0]["tag_id"] else: # Create new tag: log("creating new tag: %s" % self.title) cnx = context.GetDBConnexion() oid = ndb.DBInsertDict( cnx, self.tag_table, {"title": self.title}, commit=True ) self.tag_id = ndb.SimpleDictFetch( context, "SELECT tag_id FROM " + self.tag_table + " WHERE oid=%(oid)s", {"oid": oid}, )[0]["tag_id"] if object_id: self.tag_object(object_id) def __repr__(self): # debug return '' % self.title def delete(self): """Delete this tag. This object should not be used after this call ! """ args = {"tag_id": self.tag_id} ndb.SimpleQuery( self.context, "DELETE FROM " + self.tag_table + " t WHERE t.tag_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( self.context, "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 = self.context.GetDBConnexion() ndb.DBInsertDict(cnx, self.assoc_table, args, commit=True) 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( self.context, "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( self.context, """SELECT * FROM notes_modules_tags mt WHERE tag_id = %(tag_id)s """, args, ) if not r: # tag no more used, delete ndb.SimpleQuery( self.context, """DELETE FROM notes_tags t WHERE t.tag_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( self.context, "SELECT " + self.obj_colname + " FROM " + self.assoc_table + " WHERE tag_id = %(tag_id)s", args, ) else: args["formation_code"] = formation_code r = ndb.SimpleDictFetch( self.context, """SELECT mt.module_id FROM notes_modules_tags mt, notes_modules m, notes_formations f WHERE mt.tag_id = %(tag_id)s AND m.module_id = mt.module_id AND m.formation_id = f.formation_id AND f.formation_code = %(formation_code)s """, args, ) return [x["module_id"] for x in r] # API def module_tag_search(context, term, REQUEST=None): """List all used tag names (for auto-completion)""" # restrict charset to avoid injections if not scu.ALPHANUM_EXP.match(term.decode(scu.SCO_ENCODING)): data = [] else: r = ndb.SimpleDictFetch( context, "SELECT title FROM notes_tags WHERE title LIKE %(term)s", {"term": term + "%"}, ) data = [x["title"] for x in r] return scu.sendJSON(REQUEST, data) def module_tag_list(context, module_id=""): """les noms de tags associés à ce module""" r = ndb.SimpleDictFetch( context, """SELECT t.title FROM notes_modules_tags mt, notes_tags t WHERE mt.tag_id = t.tag_id AND mt.module_id = %(module_id)s """, {"module_id": module_id}, ) return [x["title"] for x in r] def module_tag_set(context, module_id="", taglist=[], REQUEST=None): """taglist may either be: a string with tag names separated by commas ("un;deux") or a list of strings (["un", "deux"]) """ # We check permission here to allow old Admins (withn only ScoChangeFormation perm) if REQUEST: # called from Web authuser = REQUEST.AUTHENTICATED_USER tag_editable = authuser.has_permission( ScoEditFormationTags, context ) or authuser.has_permission(ScoChangeFormation, context) if not tag_editable: raise AccessDenied("Modification des tags impossible pour %s" % authuser) # if not taglist: taglist = [] elif type(taglist) == types.StringType: taglist = taglist.split(",") taglist = [t.strip() for t in taglist] log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist)) # Sanity check: Mod = context.do_module_list(args={"module_id": module_id}) if not Mod: raise ScoValueError("invalid module !") newtags = set(taglist) oldtags = set(module_tag_list(context, module_id)) to_del = oldtags - newtags to_add = newtags - oldtags # should be atomic, but it's not. for tagname in to_add: t = ModuleTag(context, tagname, object_id=module_id) for tagname in to_del: t = ModuleTag(context, tagname) t.remove_tag_from_object(module_id) def get_etud_tagged_modules(context, etudid, tagname): """Liste d'infos sur les modules de ce semestre avec ce tag. Cherche dans tous les semestres dans lesquel l'étudiant est ou a été inscrit. Construit la liste des modules avec le tag donné par tagname """ etud = context.getEtudInfo(etudid=etudid, filled=True)[0] R = [] for sem in etud["sems"]: nt = context._getNotesCache().get_NotesTable(context, sem["formsemestre_id"]) modimpls = nt.get_modimpls() for modimpl in modimpls: tags = module_tag_list(context, module_id=modimpl["module_id"]) if tagname in tags: moy = nt.get_etud_mod_moy( modimpl["moduleimpl_id"], etudid ) # ou NI si non inscrit R.append( { "sem": sem, "moy": moy, # valeur réelle, ou NI (non inscrit au module ou NA0 (pas de note) "moduleimpl": modimpl, "tags": tags, } ) return R def split_tagname_coeff(tag, separateur=":"): """Découpe un tag saisi par un utilisateur pour en extraire un tagname (chaine de caractère correspondant au tag) et un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":"). Renvoie le résultat sous la forme d'une liste [tagname, pond] où pond est un float Auteur: CB """ if separateur in tag: temp = tag.split(":") try: pond = float(temp[1]) return [temp[0], pond] except: return [tag, 1.0] # renvoie tout le tag si le découpage à échouer else: # initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag return [tag, 1.0] """Tests: from debug import * from sco_tag_module import * context = go_dept(app, 'RT').Notes t = ModuleTag(context, 'essai') t.tag_module('totoro') # error (module invalide) t.tag_module('MOD21460') t.delete() # detruit tag et assoc t = ModuleTag(context, 'essai2') t.tag_module('MOD21460') t.tag_module('MOD21464') t.list_modules() t.list_modules(formation_code='ccc') # empty list t.list_modules(formation_code='FCOD2') Un essai de get_etud_tagged_modules: from debug import * from sco_tag_module import * context = go_dept(app, 'GEA').Notes etudid='GEAEID80687' etud = context.getEtudInfo(etudid=etudid, filled=True)[0] sem = etud['sems'][0] [ tm['moy'] for tm in get_etud_tagged_modules(context, etudid, 'allo') ] # si besoin après modif par le Web: # context._inval_cache() """