346 lines
11 KiB
Python
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
|
|
|
|
from sco_utils import *
|
|
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 ALPHANUM_EXP.match(term.decode(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 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()
|
|
"""
|