* src/Plugins.py, src/PluginMgr.py: Move to ReportUtils.
svn: r6133
This commit is contained in:
@@ -11,7 +11,9 @@ pkgdata_PYTHON = \
|
||||
_ReportOptions.py\
|
||||
_Report.py\
|
||||
_ReportUtils.py\
|
||||
_Tool.py
|
||||
_Tool.py\
|
||||
_PluginMgr.py\
|
||||
_Plugins.py
|
||||
|
||||
pkgpyexecdir = @pkgpyexecdir@/PluginUtils
|
||||
pkgpythondir = @pkgpythondir@/PluginUtils
|
||||
|
470
src/PluginUtils/_PluginMgr.py
Normal file
470
src/PluginUtils/_PluginMgr.py
Normal file
@@ -0,0 +1,470 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2000-2005 Donald N. Allingham
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# $Id$
|
||||
|
||||
"""
|
||||
The core of the GRAMPS plugin system. This module provides tasks to load
|
||||
plugins from specfied directories, build menus for the different categories,
|
||||
and provide dialog to select and execute plugins.
|
||||
|
||||
Plugins are divided into several categories. This are: reports, tools,
|
||||
importers, exporters, and document generators.
|
||||
"""
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Standard Python modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import Errors
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Global lists
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
report_list = []
|
||||
tool_list = []
|
||||
import_list = []
|
||||
export_list = []
|
||||
expect_list = []
|
||||
attempt_list = []
|
||||
loaddir_list = []
|
||||
textdoc_list = []
|
||||
bookdoc_list = []
|
||||
drawdoc_list = []
|
||||
failmsg_list = []
|
||||
bkitems_list = []
|
||||
cl_list = []
|
||||
cli_tool_list = []
|
||||
|
||||
_success_list = []
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Default relationship calculator
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import Relationship
|
||||
_relcalc_class = Relationship.RelationshipCalculator
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
_unavailable = _("No description was provided"),
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# load_plugins
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def load_plugins(direct):
|
||||
"""Searches the specified directory, and attempts to load any python
|
||||
modules that it finds, adding name to the attempt_lists list. If the module
|
||||
successfully loads, it is added to the _success_list list. Each plugin is
|
||||
responsible for registering itself in the correct manner. No attempt
|
||||
is done in this routine to register the tasks."""
|
||||
|
||||
global _success_list,attempt_list,loaddir_list,failmsg_list
|
||||
|
||||
# if the directory does not exist, do nothing
|
||||
if not os.path.isdir(direct):
|
||||
return True
|
||||
|
||||
# if the path has not already been loaded, save it in the loaddir_list
|
||||
# list for use on reloading
|
||||
|
||||
if direct not in loaddir_list:
|
||||
loaddir_list.append(direct)
|
||||
|
||||
# add the directory to the python search path
|
||||
sys.path.append(direct)
|
||||
|
||||
pymod = re.compile(r"^(.*)\.py$")
|
||||
|
||||
# loop through each file in the directory, looking for files that
|
||||
# have a .py extention, and attempt to load the file. If it succeeds,
|
||||
# add it to the _success_list list. If it fails, add it to the _failure
|
||||
# list
|
||||
|
||||
for filename in os.listdir(direct):
|
||||
name = os.path.split(filename)
|
||||
match = pymod.match(name[1])
|
||||
if not match:
|
||||
continue
|
||||
attempt_list.append(filename)
|
||||
plugin = match.groups()[0]
|
||||
try:
|
||||
a = __import__(plugin)
|
||||
_success_list.append(a)
|
||||
except Errors.PluginError, msg:
|
||||
expect_list.append((filename,str(msg)))
|
||||
except:
|
||||
failmsg_list.append((filename,sys.exc_info()))
|
||||
|
||||
if len(expect_list)+len(failmsg_list):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Plugin registering
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def register_export(exportData,title,description='',config=None,filename=''):
|
||||
"""
|
||||
Register an export filter, taking the task, file filter,
|
||||
and the list of patterns for the filename matching.
|
||||
"""
|
||||
if description and filename:
|
||||
del_index = -1
|
||||
for i in range(0,len(export_list)):
|
||||
if export_list[i][1] == title:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del export_list[del_index]
|
||||
|
||||
export_list.append((exportData,title,description,config,filename))
|
||||
|
||||
def register_import(task, ffilter, mime=None, native_format=0, format_name=""):
|
||||
"""Register an import filter, taking the task and file filter"""
|
||||
if mime:
|
||||
del_index = -1
|
||||
for i in range(0,len(import_list)):
|
||||
if import_list[i][2] == mime:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del import_list[del_index]
|
||||
|
||||
import_list.append((task, ffilter, mime, native_format, format_name))
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Tool registration
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def register_tool(
|
||||
name,
|
||||
category,
|
||||
tool_class,
|
||||
options_class,
|
||||
modes,
|
||||
translated_name,
|
||||
status=_("Unknown"),
|
||||
description=_unavailable,
|
||||
author_name=_("Unknown"),
|
||||
author_email=_("Unknown"),
|
||||
unsupported=False
|
||||
):
|
||||
"""
|
||||
Register a tool with the plugin system.
|
||||
|
||||
This function should be used to register tool in GUI and/or
|
||||
command-line mode. The low-level functions (starting with '_')
|
||||
should not be used on their own. Instead, this functino will call
|
||||
them as needed.
|
||||
"""
|
||||
|
||||
import _Tool
|
||||
(junk,gui_task) = divmod(modes,2**_Tool.MODE_GUI)
|
||||
if gui_task:
|
||||
_register_gui_tool(tool_class,options_class,translated_name,
|
||||
name,category,description,
|
||||
status,author_name,author_email,unsupported)
|
||||
|
||||
(junk,cli_task) = divmod(modes-gui_task,2**_Tool.MODE_CLI)
|
||||
if cli_task:
|
||||
_register_cli_tool(name,category,tool_class,options_class,
|
||||
translated_name,unsupported)
|
||||
|
||||
def _register_gui_tool(tool_class,options_class,translated_name,
|
||||
name,category,
|
||||
description=_unavailable,
|
||||
status=_("Unknown"),
|
||||
author_name=_("Unknown"),
|
||||
author_email=_("Unknown"),
|
||||
unsupported=False):
|
||||
del_index = -1
|
||||
for i in range(0,len(tool_list)):
|
||||
val = tool_list[i]
|
||||
if val[4] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del tool_list[del_index]
|
||||
tool_list.append((tool_class,options_class,translated_name,
|
||||
category,name,description,status,
|
||||
author_name,author_email,unsupported))
|
||||
|
||||
def _register_cli_tool(name,category,tool_class,options_class,
|
||||
translated_name,unsupported=False):
|
||||
del_index = -1
|
||||
for i in range(0,len(cli_tool_list)):
|
||||
val = cli_tool_list[i]
|
||||
if val[0] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del cli_tool_list[del_index]
|
||||
cli_tool_list.append((name,category,tool_class,options_class,
|
||||
translated_name,unsupported))
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Report registration
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def register_report(
|
||||
name,
|
||||
category,
|
||||
report_class,
|
||||
options_class,
|
||||
modes,
|
||||
translated_name,
|
||||
status=_("Unknown"),
|
||||
description=_unavailable,
|
||||
author_name=_("Unknown"),
|
||||
author_email=_("Unknown"),
|
||||
unsupported=False
|
||||
):
|
||||
"""
|
||||
Registers report for all possible flavors.
|
||||
|
||||
This function should be used to register report as a stand-alone,
|
||||
book item, or command-line flavor in any combination of those.
|
||||
The low-level functions (starting with '_') should not be used
|
||||
on their own. Instead, this function will call them as needed.
|
||||
"""
|
||||
import _Report
|
||||
(junk,standalone_task) = divmod(modes,2**_Report.MODE_GUI)
|
||||
if standalone_task:
|
||||
_register_standalone(report_class,options_class,translated_name,
|
||||
name,category,description,
|
||||
status,author_name,author_email,unsupported)
|
||||
|
||||
(junk,book_item_task) = divmod(modes-standalone_task,2**_Report.MODE_BKI)
|
||||
if book_item_task:
|
||||
book_item_category = _Report.book_categories[category]
|
||||
register_book_item(translated_name,book_item_category,
|
||||
report_class,options_class,name,unsupported)
|
||||
|
||||
(junk,command_line_task) = divmod(modes-standalone_task-book_item_task,
|
||||
2**_Report.MODE_CLI)
|
||||
if command_line_task:
|
||||
_register_cl_report(name,category,report_class,options_class,
|
||||
translated_name,unsupported)
|
||||
|
||||
def _register_standalone(report_class, options_class, translated_name,
|
||||
name, category,
|
||||
description=_unavailable,
|
||||
status=_("Unknown"),
|
||||
author_name=_("Unknown"),
|
||||
author_email=_("Unknown"),
|
||||
unsupported=False
|
||||
):
|
||||
"""Register a report with the plugin system"""
|
||||
|
||||
from PluginUtils import Report
|
||||
|
||||
del_index = -1
|
||||
for i in range(0,len(report_list)):
|
||||
val = report_list[i]
|
||||
if val[4] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del report_list[del_index]
|
||||
|
||||
report_list.append((report_class, options_class, translated_name,
|
||||
category, name, description, status,
|
||||
author_name, author_email, unsupported))
|
||||
|
||||
def register_book_item(translated_name, category, report_class,
|
||||
option_class, name, unsupported):
|
||||
"""Register a book item"""
|
||||
|
||||
del_index = -1
|
||||
for i in range(0,len(bkitems_list)):
|
||||
val = bkitems_list[i]
|
||||
if val[4] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del bkitems_list[del_index]
|
||||
|
||||
bkitems_list.append((translated_name, category, report_class,
|
||||
option_class, name, unsupported))
|
||||
|
||||
def _register_cl_report(name,category,report_class,options_class,
|
||||
translated_name,unsupported):
|
||||
del_index = -1
|
||||
for i in range(0,len(cl_list)):
|
||||
val = cl_list[i]
|
||||
if val[0] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del cl_list[del_index]
|
||||
cl_list.append((name,category,report_class,options_class,
|
||||
translated_name,unsupported))
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Text document generator registration
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def register_text_doc(name,classref, table, paper, style, ext,
|
||||
print_report_label=None,clname=''):
|
||||
"""Register a text document generator"""
|
||||
del_index = -1
|
||||
for i in range(0,len(textdoc_list)):
|
||||
val = textdoc_list[i]
|
||||
if val[0] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del textdoc_list[del_index]
|
||||
|
||||
if not clname:
|
||||
clname = ext[1:]
|
||||
|
||||
textdoc_list.append(
|
||||
(name, classref, table, paper, style,
|
||||
ext, print_report_label, clname))
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Book document generator registration
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def register_book_doc(name,classref, table, paper, style, ext, clname=''):
|
||||
"""Register a text document generator"""
|
||||
del_index = -1
|
||||
for i in range(0,len(bookdoc_list)):
|
||||
val = bookdoc_list[i]
|
||||
if val[0] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del bookdoc_list[del_index]
|
||||
|
||||
if not clname:
|
||||
clname = ext[1:]
|
||||
bookdoc_list.append((name,classref,table,paper,style,ext,None,clname))
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Drawing document generator registration
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def register_draw_doc(name,classref,paper,style, ext,
|
||||
print_report_label=None,clname=''):
|
||||
"""Register a drawing document generator"""
|
||||
del_index = -1
|
||||
for i in range(0,len(drawdoc_list)):
|
||||
val = drawdoc_list[i]
|
||||
if val[0] == name:
|
||||
del_index = i
|
||||
if del_index != -1:
|
||||
del drawdoc_list[del_index]
|
||||
if not clname:
|
||||
clname = ext[1:]
|
||||
drawdoc_list.append((name, classref, paper,style, ext,
|
||||
print_report_label, clname))
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Remove plugins whose reloading failed from the already-registered lists
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def purge_failed(failed_list,export_list,import_list,tool_list,cli_tool_list,
|
||||
report_list,bkitems_list,cl_list,textdoc_list,bookdoc_list,
|
||||
drawdoc_list):
|
||||
failed_module_names = [
|
||||
os.path.splitext(os.path.basename(filename))[0]
|
||||
for filename,junk in failed_list
|
||||
]
|
||||
|
||||
export_list = [ item for item in export_list
|
||||
if item[0].__module__ not in failed_module_names ]
|
||||
import_list = [ item for item in import_list
|
||||
if item[0].__module__ not in failed_module_names ]
|
||||
tool_list = [ item for item in tool_list
|
||||
if item[0].__module__ not in failed_module_names ]
|
||||
cli_tool_list = [ item for item in cli_tool_list
|
||||
if item[2].__module__ not in failed_module_names ]
|
||||
report_list = [ item for item in report_list
|
||||
if item[0].__module__ not in failed_module_names ]
|
||||
bkitems_list = [ item for item in bkitems_list
|
||||
if item[2].__module__ not in failed_module_names ]
|
||||
cl_list = [ item for item in cl_list
|
||||
if item[2].__module__ not in failed_module_names ]
|
||||
textdoc_list = [ item for item in textdoc_list
|
||||
if item[1].__module__ not in failed_module_names ]
|
||||
bookdoc_list = [ item for item in bookdoc_list
|
||||
if item[1].__module__ not in failed_module_names ]
|
||||
drawdoc_list = [ item for item in drawdoc_list
|
||||
if item[1].__module__ not in failed_module_names ]
|
||||
|
||||
# For some funky reason this module's global variables
|
||||
# are not seen inside this function. But they are seen
|
||||
# from other modules, so we pass them back and forth.
|
||||
# Sucks, but I don't know why this happens :-(
|
||||
return (export_list,import_list,tool_list,cli_tool_list,
|
||||
report_list,bkitems_list,cl_list,textdoc_list,bookdoc_list,
|
||||
drawdoc_list)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Relationship calculator registration
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def register_relcalc(relclass, languages):
|
||||
"""Register a relationshp calculator"""
|
||||
global _relcalc_class
|
||||
|
||||
try:
|
||||
if os.environ["LANG"] in languages:
|
||||
_relcalc_class = relclass
|
||||
except:
|
||||
pass
|
||||
|
||||
def relationship_class(db):
|
||||
global _relcalc_class
|
||||
return _relcalc_class(db)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Image attributes
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
_image_attributes = []
|
||||
def register_image_attribute(name):
|
||||
if name not in _image_attributes:
|
||||
_image_attributes.append(name)
|
||||
|
||||
def get_image_attributes():
|
||||
return _image_attributes
|
575
src/PluginUtils/_Plugins.py
Normal file
575
src/PluginUtils/_Plugins.py
Normal file
@@ -0,0 +1,575 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2000-2005 Donald N. Allingham
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# $Id$
|
||||
|
||||
"""
|
||||
The core of the GRAMPS plugin system. This module provides tasks to load
|
||||
plugins from specfied directories, build menus for the different categories,
|
||||
and provide dialog to select and execute plugins.
|
||||
|
||||
Plugins are divided into several categories. This are: reports, tools,
|
||||
importers, exporters, and document generators.
|
||||
"""
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GTK libraries
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import gobject
|
||||
import gtk
|
||||
import gtk.glade
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Standard Python modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import traceback
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import const
|
||||
import Utils
|
||||
import Config
|
||||
import Errors
|
||||
import _Report
|
||||
import _Tool
|
||||
import _PluginMgr
|
||||
import GrampsDisplay
|
||||
import DisplayState
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
REPORTS = 0
|
||||
TOOLS = 1
|
||||
UNSUPPORTED = _("Unsupported")
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# PluginDialog interface class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
class PluginDialog(DisplayState.ManagedWindow):
|
||||
"""Displays the dialog box that allows the user to select the
|
||||
report that is desired."""
|
||||
|
||||
def __init__(self,state, uistate, track, item_list,categories,msg,label=None,
|
||||
button_label=None,tool_tip=None,content=REPORTS):
|
||||
"""Display the dialog box, and build up the list of available
|
||||
reports. This is used to build the selection tree on the left
|
||||
hand side of the dailog box."""
|
||||
|
||||
self.active = state.active
|
||||
self.imap = {}
|
||||
self.msg = msg
|
||||
self.content = content
|
||||
|
||||
DisplayState.ManagedWindow.__init__(self, uistate, [], None)
|
||||
|
||||
self.state = state
|
||||
self.uistate = uistate
|
||||
|
||||
self.dialog = gtk.glade.XML(const.plugins_glade,"report","gramps")
|
||||
self.dialog.signal_autoconnect({
|
||||
"on_report_apply_clicked" : self.on_apply_clicked,
|
||||
"destroy_passed_object" : self.close,
|
||||
"on_delete_event" : self.on_delete_event,
|
||||
})
|
||||
|
||||
self.tree = self.dialog.get_widget("tree")
|
||||
self.window = self.dialog.get_widget("report")
|
||||
self.title = self.dialog.get_widget("title")
|
||||
|
||||
Utils.set_titles(self.window, self.title, msg )
|
||||
|
||||
self.store = gtk.TreeStore(gobject.TYPE_STRING)
|
||||
self.selection = self.tree.get_selection()
|
||||
self.selection.connect('changed', self.on_node_selected)
|
||||
col = gtk.TreeViewColumn('',gtk.CellRendererText(),text=0)
|
||||
self.tree.append_column(col)
|
||||
self.tree.set_model(self.store)
|
||||
|
||||
self.description = self.dialog.get_widget("description")
|
||||
if label:
|
||||
self.description.set_text(label)
|
||||
self.status = self.dialog.get_widget("report_status")
|
||||
|
||||
Utils.set_title_label(self.dialog,msg)
|
||||
|
||||
self.author_name = self.dialog.get_widget("author_name")
|
||||
self.author_email = self.dialog.get_widget("author_email")
|
||||
|
||||
self.statbox = self.dialog.get_widget("statbox")
|
||||
|
||||
self.apply_button = self.dialog.get_widget("apply")
|
||||
if button_label:
|
||||
self.apply_button.set_label(button_label)
|
||||
else:
|
||||
self.apply_button.set_label(_("_Apply"))
|
||||
self.apply_button.set_use_underline(True)
|
||||
if tool_tip:
|
||||
try:
|
||||
tt = gtk.tooltips_data_get(self.apply_button)
|
||||
if tt:
|
||||
tt[0].set_tip(self.apply_button,tool_tip)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
self.item = None
|
||||
self.build_plugin_tree(item_list,categories)
|
||||
self.window.show()
|
||||
|
||||
def on_delete_event(self,obj,b):
|
||||
pass
|
||||
|
||||
def close(self,ok=0):
|
||||
self.window.destroy()
|
||||
|
||||
def on_apply_clicked(self,obj):
|
||||
"""Execute the selected report"""
|
||||
|
||||
(item_class,options_class,title,category,name) = self.item
|
||||
if self.content == REPORTS:
|
||||
_Report.report(self.state.db,self.state.active,
|
||||
item_class,options_class,title,name,category)
|
||||
else:
|
||||
_Tool.gui_tool(self.state.db,self.state.active,
|
||||
item_class,options_class,title,name,category,
|
||||
self.state.db.request_rebuild,self.parent)
|
||||
|
||||
def on_node_selected(self,obj):
|
||||
"""Updates the informational display on the right hand side of
|
||||
the dialog box with the description of the selected report"""
|
||||
|
||||
store,node = self.selection.get_selected()
|
||||
if node:
|
||||
path = store.get_path(node)
|
||||
if not node or not self.imap.has_key(path):
|
||||
self.statbox.hide()
|
||||
return
|
||||
self.statbox.show()
|
||||
data = self.imap[path]
|
||||
|
||||
(report_class,options_class,title,category,name,
|
||||
doc,status,author,email,unsupported) = data
|
||||
self.description.set_text(doc)
|
||||
if unsupported:
|
||||
status = UNSUPPORTED
|
||||
self.status.set_text(status)
|
||||
self.title.set_text('<span weight="bold" size="larger">%s</span>' % title)
|
||||
self.title.set_use_markup(1)
|
||||
self.author_name.set_text(author)
|
||||
self.author_email.set_text(email)
|
||||
self.item = (report_class,options_class,title,category,name)
|
||||
|
||||
def build_plugin_tree(self,item_list,categories):
|
||||
"""Populates a GtkTree with each menu item assocated with a entry
|
||||
in the lists. The list must consist of a tuples with the following
|
||||
format:
|
||||
|
||||
(item_class,options_class,title,category,name,
|
||||
doc,status,author,email)
|
||||
|
||||
Items in the same category are grouped under the same submenu.
|
||||
The categories must be dicts from integer to string.
|
||||
"""
|
||||
|
||||
ilist = []
|
||||
|
||||
# build the tree items and group together based on the category name
|
||||
item_hash = {}
|
||||
for plugin in item_list:
|
||||
if plugin[9]:
|
||||
category = UNSUPPORTED
|
||||
else:
|
||||
category = categories[plugin[3]]
|
||||
if item_hash.has_key(category):
|
||||
item_hash[category].append(plugin)
|
||||
else:
|
||||
item_hash[category] = [plugin]
|
||||
|
||||
# add a submenu for each category, and populate it with the
|
||||
# GtkTreeItems that are associated with it.
|
||||
key_list = [ item for item in item_hash.keys() if item != UNSUPPORTED]
|
||||
key_list.sort()
|
||||
key_list.reverse()
|
||||
|
||||
prev = None
|
||||
if item_hash.has_key(UNSUPPORTED):
|
||||
key = UNSUPPORTED
|
||||
data = item_hash[key]
|
||||
node = self.store.insert_after(None,prev)
|
||||
self.store.set(node,0,key)
|
||||
next = None
|
||||
data.sort(lambda x,y: cmp(x[2],y[2]))
|
||||
for item in data:
|
||||
next = self.store.insert_after(node,next)
|
||||
ilist.append((next,item))
|
||||
self.store.set(next,0,item[2])
|
||||
for key in key_list:
|
||||
data = item_hash[key]
|
||||
node = self.store.insert_after(None,prev)
|
||||
self.store.set(node,0,key)
|
||||
next = None
|
||||
data.sort(lambda x,y: cmp(x[2],y[2]))
|
||||
for item in data:
|
||||
next = self.store.insert_after(node,next)
|
||||
ilist.append((next,item))
|
||||
self.store.set(next,0,item[2])
|
||||
for next,tab in ilist:
|
||||
path = self.store.get_path(next)
|
||||
self.imap[path] = tab
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# ReportPlugins interface class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class ReportPlugins(PluginDialog):
|
||||
"""Displays the dialog box that allows the user to select the
|
||||
report that is desired."""
|
||||
|
||||
def __init__(self,dbstate,uistate,track):
|
||||
"""Display the dialog box, and build up the list of available
|
||||
reports. This is used to build the selection tree on the left
|
||||
hand side of the dailog box."""
|
||||
PluginDialog.__init__(
|
||||
self,
|
||||
dbstate,
|
||||
uistate,
|
||||
track,
|
||||
_PluginMgr.report_list,
|
||||
_Report.standalone_categories,
|
||||
_("Report Selection"),
|
||||
_("Select a report from those available on the left."),
|
||||
_("_Generate"), _("Generate selected report"),
|
||||
REPORTS)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# ToolPlugins interface class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class ToolPlugins(PluginDialog):
|
||||
"""Displays the dialog box that allows the user to select the tool
|
||||
that is desired."""
|
||||
|
||||
def __init__(self,dbstate,uistate,track):
|
||||
"""Display the dialog box, and build up the list of available
|
||||
reports. This is used to build the selection tree on the left
|
||||
hand side of the dailog box."""
|
||||
|
||||
PluginDialog.__init__(
|
||||
self,
|
||||
dbstate,
|
||||
uistate,
|
||||
track,
|
||||
_PluginMgr.tool_list,
|
||||
_Tool.tool_categories,
|
||||
_("Tool Selection"),
|
||||
_("Select a tool from those available on the left."),
|
||||
_("_Run"),
|
||||
_("Run selected tool"),
|
||||
TOOLS)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# PluginStatus
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
class PluginStatus(DisplayState.ManagedWindow):
|
||||
"""Displays a dialog showing the status of loaded plugins"""
|
||||
|
||||
def __init__(self,state,uistate,track):
|
||||
|
||||
import cStringIO
|
||||
DisplayState.ManagedWindow.__init__(self, uistate, [], None)
|
||||
|
||||
self.state = state
|
||||
self.uistate = uistate
|
||||
|
||||
self.glade = gtk.glade.XML(const.pluginsFile,"plugstat","gramps")
|
||||
self.window = self.glade.get_widget("plugstat")
|
||||
self.window.set_title("%s - GRAMPS" % _('Plugin status'))
|
||||
window = self.glade.get_widget("text")
|
||||
self.pop_button = self.glade.get_widget("pop_button")
|
||||
self.pop_button.set_active(Config.get_pop_plugin_status())
|
||||
self.pop_button.connect('toggled',
|
||||
lambda obj: Config.save_pop_plugin_status(self.pop_button.get_active()))
|
||||
Config.client.notify_add("/apps/gramps/behavior/pop-plugin-status",
|
||||
self.pop_button_update)
|
||||
self.glade.signal_autoconnect({
|
||||
'on_close_clicked' : self.close,
|
||||
'on_help_clicked' : self.help,
|
||||
'on_plugstat_delete_event' : self.on_delete,
|
||||
})
|
||||
|
||||
info = cStringIO.StringIO()
|
||||
|
||||
if len(_PluginMgr.expect_list) + len(_PluginMgr.failmsg_list) == 0:
|
||||
window.get_buffer().set_text(_('All modules were successfully loaded.'))
|
||||
else:
|
||||
info.write(_("The following modules could not be loaded:"))
|
||||
info.write("\n\n")
|
||||
|
||||
for (filename,msg) in _PluginMgr.expect_list:
|
||||
info.write("%s: %s\n\n" % (filename,msg))
|
||||
|
||||
for (filename,msgs) in _PluginMgr.failmsg_list:
|
||||
error = str(msgs[0])
|
||||
if error[0:11] == "exceptions.":
|
||||
error = error[11:]
|
||||
info.write("%s: %s\n" % (filename,error) )
|
||||
traceback.print_exception(msgs[0],msgs[1],msgs[2],None,info)
|
||||
info.write('\n')
|
||||
info.seek(0)
|
||||
window.get_buffer().set_text(info.read())
|
||||
|
||||
def on_delete(self,obj1,obj2):
|
||||
pass
|
||||
|
||||
def close(self,obj):
|
||||
self.window.destroy()
|
||||
|
||||
def help(self,obj):
|
||||
"""Display the GRAMPS manual"""
|
||||
GrampsDisplay.help('gramps-getting-started')
|
||||
|
||||
def pop_button_update(self, client,cnxn_id,entry,data):
|
||||
self.pop_button.set_active(Config.get_pop_plugin_status())
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Building pulldown menus
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
def build_tools_menu(top_menu,callback):
|
||||
build_plugin_menu(_PluginMgr.tool_list,
|
||||
_Tool.tool_categories,
|
||||
_Tool.gui_tool,
|
||||
top_menu,callback)
|
||||
|
||||
def build_report_menu(top_menu,callback):
|
||||
build_plugin_menu(_PluginMgr.report_list,
|
||||
_Report.standalone_categories,
|
||||
_Report.report,
|
||||
top_menu,callback)
|
||||
|
||||
def build_plugin_menu(item_list,categories,func,top_menu,callback):
|
||||
menu = gtk.Menu()
|
||||
menu.show()
|
||||
|
||||
hash_data = {}
|
||||
for item in item_list:
|
||||
if item[9]:
|
||||
category = UNSUPPORTED
|
||||
else:
|
||||
category = categories[item[3]]
|
||||
if hash_data.has_key(category):
|
||||
hash_data[category].append(
|
||||
(item[0],item[1],item[2],item[4],item[3]))
|
||||
else:
|
||||
hash_data[category] = [
|
||||
(item[0],item[1],item[2],item[4],item[3])]
|
||||
|
||||
# Sort categories, skipping the unsupported
|
||||
catlist = [item for item in hash_data.keys() if item != UNSUPPORTED]
|
||||
catlist.sort()
|
||||
for key in catlist:
|
||||
entry = gtk.MenuItem(key)
|
||||
entry.show()
|
||||
menu.append(entry)
|
||||
submenu = gtk.Menu()
|
||||
submenu.show()
|
||||
entry.set_submenu(submenu)
|
||||
lst = hash_data[key]
|
||||
lst.sort(by_menu_name)
|
||||
for name in lst:
|
||||
subentry = gtk.MenuItem("%s..." % name[2])
|
||||
subentry.show()
|
||||
subentry.connect("activate",callback,func,
|
||||
name[0],name[1],name[2],name[3],name[4])
|
||||
submenu.append(subentry)
|
||||
|
||||
# If there are any unsupported items we add separator
|
||||
# and the unsupported category at the end of the menu
|
||||
if hash_data.has_key(UNSUPPORTED):
|
||||
entry = gtk.MenuItem(None)
|
||||
entry.show()
|
||||
menu.append(entry)
|
||||
key = UNSUPPORTED
|
||||
entry = gtk.MenuItem(key)
|
||||
entry.show()
|
||||
menu.append(entry)
|
||||
submenu = gtk.Menu()
|
||||
submenu.show()
|
||||
entry.set_submenu(submenu)
|
||||
lst = hash_data[key]
|
||||
lst.sort(by_menu_name)
|
||||
for name in lst:
|
||||
subentry = gtk.MenuItem("%s..." % name[2])
|
||||
subentry.show()
|
||||
subentry.connect("activate",callback,func,
|
||||
name[0],name[1],name[2],name[3],name[4])
|
||||
submenu.append(subentry)
|
||||
|
||||
top_menu.set_submenu(menu)
|
||||
|
||||
def by_menu_name(a,b):
|
||||
return cmp(a[2],b[2])
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Reload plugins
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class Reload(_Tool.Tool):
|
||||
def __init__(self,db,person,options_class,name,callback=None,parent=None):
|
||||
_Tool.Tool.__init__(self,db,person,options_class,name)
|
||||
|
||||
"""
|
||||
Treated as a callback, causes all plugins to get reloaded.
|
||||
This is useful when writing and debugging a plugin.
|
||||
"""
|
||||
|
||||
pymod = re.compile(r"^(.*)\.py$")
|
||||
|
||||
oldfailmsg = _PluginMgr.failmsg_list[:]
|
||||
_PluginMgr.failmsg_list = []
|
||||
|
||||
# attempt to reload all plugins that have succeeded in the past
|
||||
for plugin in _PluginMgr._success_list:
|
||||
filename = os.path.basename(plugin.__file__)
|
||||
filename = filename.replace('pyc','py')
|
||||
filename = filename.replace('pyo','py')
|
||||
try:
|
||||
reload(plugin)
|
||||
except:
|
||||
_PluginMgr.failmsg_list.append((filename,sys.exc_info()))
|
||||
|
||||
# Remove previously good plugins that are now bad
|
||||
# from the registered lists
|
||||
(_PluginMgr.export_list,
|
||||
_PluginMgr.import_list,
|
||||
_PluginMgr.tool_list,
|
||||
_PluginMgr.cli_tool_list,
|
||||
_PluginMgr.report_list,
|
||||
_PluginMgr.bkitems_list,
|
||||
_PluginMgr.cl_list,
|
||||
_PluginMgr.textdoc_list,
|
||||
_PluginMgr.bookdoc_list,
|
||||
_PluginMgr.drawdoc_list) = _PluginMgr.purge_failed(
|
||||
_PluginMgr.failmsg_list,
|
||||
_PluginMgr.export_list,
|
||||
_PluginMgr.import_list,
|
||||
_PluginMgr.tool_list,
|
||||
_PluginMgr.cli_tool_list,
|
||||
_PluginMgr.report_list,
|
||||
_PluginMgr.bkitems_list,
|
||||
_PluginMgr.cl_list,
|
||||
_PluginMgr.textdoc_list,
|
||||
_PluginMgr.bookdoc_list,
|
||||
_PluginMgr.drawdoc_list)
|
||||
|
||||
# attempt to load the plugins that have failed in the past
|
||||
for (filename,message) in oldfailmsg:
|
||||
name = os.path.split(filename)
|
||||
match = pymod.match(name[1])
|
||||
if not match:
|
||||
continue
|
||||
_PluginMgr.attempt_list.append(filename)
|
||||
plugin = match.groups()[0]
|
||||
try:
|
||||
# For some strange reason second importing of a failed plugin
|
||||
# results in success. Then reload reveals the actual error.
|
||||
# Looks like a bug in Python.
|
||||
a = __import__(plugin)
|
||||
reload(a)
|
||||
_PluginMgr._success_list.append(a)
|
||||
except:
|
||||
_PluginMgr.failmsg_list.append((filename,sys.exc_info()))
|
||||
|
||||
# attempt to load any new files found
|
||||
for directory in _PluginMgr.loaddir_list:
|
||||
for filename in os.listdir(directory):
|
||||
name = os.path.split(filename)
|
||||
match = pymod.match(name[1])
|
||||
if not match:
|
||||
continue
|
||||
if filename in _PluginMgr.attempt_list:
|
||||
continue
|
||||
_PluginMgr.attempt_list.append(filename)
|
||||
plugin = match.groups()[0]
|
||||
try:
|
||||
a = __import__(plugin)
|
||||
if a not in _PluginMgr._success_list:
|
||||
_PluginMgr._success_list.append(a)
|
||||
except:
|
||||
_PluginMgr.failmsg_list.append((filename,sys.exc_info()))
|
||||
|
||||
if Config.get_pop_plugin_status() and len(_PluginMgr.failmsg_list):
|
||||
PluginStatus()
|
||||
else:
|
||||
global status_up
|
||||
if status_up:
|
||||
status_up.close(None)
|
||||
status_up = None
|
||||
|
||||
# Re-generate tool and report menus
|
||||
parent.build_plugin_menus(rebuild=True)
|
||||
|
||||
class ReloadOptions(_Tool.ToolOptions):
|
||||
"""
|
||||
Defines options and provides handling interface.
|
||||
"""
|
||||
|
||||
def __init__(self,name,person_id=None):
|
||||
_Tool.ToolOptions.__init__(self,name,person_id)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Register the plugin reloading tool
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
if __debug__:
|
||||
_PluginMgr.register_tool(
|
||||
name = 'reload',
|
||||
category = _Tool.TOOL_DEBUG,
|
||||
tool_class = Reload,
|
||||
options_class = ReloadOptions,
|
||||
modes = _Tool.MODE_GUI,
|
||||
translated_name = _("Reload plugins"),
|
||||
description=_("Attempt to reload plugins. "
|
||||
"Note: This tool itself is not reloaded!"),
|
||||
)
|
@@ -52,7 +52,7 @@ import gtk
|
||||
#-------------------------------------------------------------------------
|
||||
import const
|
||||
import Utils
|
||||
import PluginMgr
|
||||
import _PluginMgr
|
||||
import BaseDoc
|
||||
from _StyleEditor import StyleListDisplay
|
||||
import Config
|
||||
@@ -1769,27 +1769,27 @@ class CommandLineReport:
|
||||
self.options_help['of'].append(os.path.expanduser("~/whatever_name"))
|
||||
|
||||
if self.category == CATEGORY_TEXT:
|
||||
for item in PluginMgr.textdoc_list:
|
||||
for item in _PluginMgr.textdoc_list:
|
||||
if item[7] == self.options_dict['off']:
|
||||
self.format = item[1]
|
||||
self.options_help['off'].append(
|
||||
[ item[7] for item in PluginMgr.textdoc_list ]
|
||||
[ item[7] for item in _PluginMgr.textdoc_list ]
|
||||
)
|
||||
self.options_help['off'].append(False)
|
||||
elif self.category == CATEGORY_DRAW:
|
||||
for item in PluginMgr.drawdoc_list:
|
||||
for item in _PluginMgr.drawdoc_list:
|
||||
if item[6] == self.options_dict['off']:
|
||||
self.format = item[1]
|
||||
self.options_help['off'].append(
|
||||
[ item[6] for item in PluginMgr.drawdoc_list ]
|
||||
[ item[6] for item in _PluginMgr.drawdoc_list ]
|
||||
)
|
||||
self.options_help['off'].append(False)
|
||||
elif self.category == CATEGORY_BOOK:
|
||||
for item in PluginMgr.bookdoc_list:
|
||||
for item in _PluginMgr.bookdoc_list:
|
||||
if item[6] == self.options_dict['off']:
|
||||
self.format = item[1]
|
||||
self.options_help['off'].append(
|
||||
[ item[6] for item in PluginMgr.bookdoc_list ]
|
||||
[ item[6] for item in _PluginMgr.bookdoc_list ]
|
||||
)
|
||||
self.options_help['off'].append(False)
|
||||
else:
|
||||
@@ -1956,9 +1956,9 @@ class GrampsTextFormatComboBox(gtk.ComboBox):
|
||||
|
||||
out_pref = Config.get_output_preference()
|
||||
index = 0
|
||||
PluginMgr.textdoc_list.sort()
|
||||
_PluginMgr.textdoc_list.sort()
|
||||
active_index = 0
|
||||
for item in PluginMgr.textdoc_list:
|
||||
for item in _PluginMgr.textdoc_list:
|
||||
if tables and item[2] == 0:
|
||||
continue
|
||||
name = item[0]
|
||||
@@ -1973,25 +1973,25 @@ class GrampsTextFormatComboBox(gtk.ComboBox):
|
||||
self.set_active(active_index)
|
||||
|
||||
def get_label(self):
|
||||
return PluginMgr.textdoc_list[self.get_active()][0]
|
||||
return _PluginMgr.textdoc_list[self.get_active()][0]
|
||||
|
||||
def get_reference(self):
|
||||
return PluginMgr.textdoc_list[self.get_active()][1]
|
||||
return _PluginMgr.textdoc_list[self.get_active()][1]
|
||||
|
||||
def get_paper(self):
|
||||
return PluginMgr.textdoc_list[self.get_active()][3]
|
||||
return _PluginMgr.textdoc_list[self.get_active()][3]
|
||||
|
||||
def get_styles(self):
|
||||
return PluginMgr.textdoc_list[self.get_active()][4]
|
||||
return _PluginMgr.textdoc_list[self.get_active()][4]
|
||||
|
||||
def get_ext(self):
|
||||
return PluginMgr.textdoc_list[self.get_active()][5]
|
||||
return _PluginMgr.textdoc_list[self.get_active()][5]
|
||||
|
||||
def get_printable(self):
|
||||
return PluginMgr.textdoc_list[self.get_active()][6]
|
||||
return _PluginMgr.textdoc_list[self.get_active()][6]
|
||||
|
||||
def get_clname(self):
|
||||
return PluginMgr.textdoc_list[self.get_active()][7]
|
||||
return _PluginMgr.textdoc_list[self.get_active()][7]
|
||||
|
||||
class GrampsDrawFormatComboBox(gtk.ComboBox):
|
||||
|
||||
@@ -2004,9 +2004,9 @@ class GrampsDrawFormatComboBox(gtk.ComboBox):
|
||||
|
||||
out_pref = Config.get_output_preference()
|
||||
index = 0
|
||||
PluginMgr.drawdoc_list.sort()
|
||||
_PluginMgr.drawdoc_list.sort()
|
||||
active_index = 0
|
||||
for item in PluginMgr.drawdoc_list:
|
||||
for item in _PluginMgr.drawdoc_list:
|
||||
if tables and item[2] == 0:
|
||||
continue
|
||||
name = item[0]
|
||||
@@ -2021,25 +2021,25 @@ class GrampsDrawFormatComboBox(gtk.ComboBox):
|
||||
self.set_active(active_index)
|
||||
|
||||
def get_reference(self):
|
||||
return PluginMgr.drawdoc_list[self.get_active()][1]
|
||||
return _PluginMgr.drawdoc_list[self.get_active()][1]
|
||||
|
||||
def get_label(self):
|
||||
return PluginMgr.drawdoc_list[self.get_active()][0]
|
||||
return _PluginMgr.drawdoc_list[self.get_active()][0]
|
||||
|
||||
def get_paper(self):
|
||||
return PluginMgr.drawdoc_list[self.get_active()][2]
|
||||
return _PluginMgr.drawdoc_list[self.get_active()][2]
|
||||
|
||||
def get_styles(self):
|
||||
return PluginMgr.drawdoc_list[self.get_active()][3]
|
||||
return _PluginMgr.drawdoc_list[self.get_active()][3]
|
||||
|
||||
def get_ext(self):
|
||||
return PluginMgr.drawdoc_list[self.get_active()][4]
|
||||
return _PluginMgr.drawdoc_list[self.get_active()][4]
|
||||
|
||||
def get_printable(self):
|
||||
return PluginMgr.drawdoc_list[self.get_active()][5]
|
||||
return _PluginMgr.drawdoc_list[self.get_active()][5]
|
||||
|
||||
def get_clname(self):
|
||||
return PluginMgr.drawdoc_list[self.get_active()][6]
|
||||
return _PluginMgr.drawdoc_list[self.get_active()][6]
|
||||
|
||||
class GrampsBookFormatComboBox(gtk.ComboBox):
|
||||
|
||||
@@ -2052,10 +2052,10 @@ class GrampsBookFormatComboBox(gtk.ComboBox):
|
||||
|
||||
out_pref = Config.get_output_preference()
|
||||
index = 0
|
||||
PluginMgr.drawdoc_list.sort()
|
||||
_PluginMgr.drawdoc_list.sort()
|
||||
active_index = 0
|
||||
self.data = []
|
||||
for item in PluginMgr.bookdoc_list:
|
||||
for item in _PluginMgr.bookdoc_list:
|
||||
if tables and item[2] == 0:
|
||||
continue
|
||||
self.data.append(item)
|
||||
|
@@ -20,7 +20,16 @@
|
||||
|
||||
# $Id: Report.py 6044 2006-03-03 00:10:52Z rshura $
|
||||
|
||||
from _PluginMgr import \
|
||||
register_export, register_import, \
|
||||
register_tool, register_report, \
|
||||
register_relcalc, relationship_class, \
|
||||
textdoc_list, drawdoc_list, bookdoc_list, \
|
||||
bkitems_list, cl_list, cli_tool_list, \
|
||||
load_plugins
|
||||
|
||||
import _Report as Report
|
||||
import _ReportOptions as ReportOptions
|
||||
import _ReportUtils as ReportUtils
|
||||
import _Tool as Tool
|
||||
import _Plugins as Plugins
|
||||
|
Reference in New Issue
Block a user