ScoDoc/sco_tag_module.py

346 lines
11 KiB
Python

# -*- 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 '<tag "%s">' % 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()
"""