2007-01-29 Benny Malengier <benny.malengier@ugent.be>
* src/DataViews/_PersonView.py: numlock handling * src/PageView.py: numlock handling * src/Filters/_SearchBar.py: numblock handling * src/Filters/SideBar/_SidebarFilter.py: numblock handling * src/DataViews/_PersonView.py: refactoring * src/DisplayModels/_PeopleModel.py: refactoring svn: r8013
This commit is contained in:
parent
f6c7d5b2a1
commit
00bafcca5b
@ -1,4 +1,12 @@
|
||||
2007-01-29 Benny Malengier <benny.malengier@ugent.be>
|
||||
* src/DataViews/_PersonView.py: numlock handling
|
||||
* src/PageView.py: numlock handling
|
||||
* src/Filters/_SearchBar.py: numblock handling
|
||||
* src/Filters/SideBar/_SidebarFilter.py: numblock handling
|
||||
|
||||
2007-01-29 Don Allingham <don@gramps-project.org>
|
||||
* src/DataViews/_PersonView.py: refactoring
|
||||
* src/DisplayModels/_PeopleModel.py: refactoring
|
||||
* src/DisplayModels/_BaseModel.py: Refactor out node/path maps
|
||||
* various: remove sets import
|
||||
* NEWS: update
|
||||
|
@ -742,8 +742,8 @@ class PersonView(PageView.PersonNavView):
|
||||
if not self.model.is_visable(node):
|
||||
continue
|
||||
|
||||
if (not self.model.sname_sub.has_key(top) or
|
||||
len(self.model.sname_sub[top]) == 1):
|
||||
if (not self.model.mapper.has_top_node(top) or
|
||||
self.model.mapper.number_of_children(top) == 1):
|
||||
path = self.model.on_get_path(top)
|
||||
pnode = self.model.get_iter(path)
|
||||
self.model.row_inserted(path, pnode)
|
||||
@ -754,7 +754,7 @@ class PersonView(PageView.PersonNavView):
|
||||
self.dirty = True
|
||||
|
||||
def func(self, tree, path, ex_list):
|
||||
ex_list.append(self.model.top_path2iter[path[0]])
|
||||
ex_list.append(self.model.mapper.top_path2iter[path[0]])
|
||||
|
||||
def person_removed(self, handle_list):
|
||||
if not self.model:
|
||||
@ -765,7 +765,7 @@ class PersonView(PageView.PersonNavView):
|
||||
|
||||
self.build_tree(handle_list)
|
||||
for i in expand:
|
||||
path = self.model.top_iter2path.get(i)
|
||||
path = self.model.mapper.top_iter2path.get(i)
|
||||
if path:
|
||||
self.tree.expand_row(path, False)
|
||||
|
||||
@ -777,7 +777,7 @@ class PersonView(PageView.PersonNavView):
|
||||
for node in handle_list:
|
||||
person = self.dbstate.db.get_person_from_handle(node)
|
||||
try:
|
||||
oldpath = self.model.iter2path[node]
|
||||
oldpath = self.model.mapper.iter2path[node]
|
||||
except:
|
||||
return
|
||||
pathval = self.model.on_get_path(node)
|
||||
@ -800,7 +800,7 @@ class PersonView(PageView.PersonNavView):
|
||||
self.model.calculate_data()
|
||||
|
||||
# find the path of the person in the new data build
|
||||
newpath = self.model.temp_iter2path[node]
|
||||
newpath = self.model.mapper.temp_iter2path[node]
|
||||
|
||||
# if paths same, just issue row changed signal
|
||||
|
||||
@ -850,7 +850,7 @@ class PersonView(PageView.PersonNavView):
|
||||
return False
|
||||
|
||||
def key_press(self,obj,event):
|
||||
if not event.state:
|
||||
if not event.state or event.state in (gtk.gdk.MOD2_MASK,):
|
||||
if event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
|
||||
if self.dbstate.active:
|
||||
self.edit(obj)
|
||||
|
@ -70,30 +70,145 @@ from Lru import LRU
|
||||
|
||||
_CACHE_SIZE = 250
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# python 2.3 has a bug in the unicode sorting using locale.strcoll. Seems
|
||||
# to have a buffer overrun. We can convince it to do the right thing by
|
||||
# forcing the string to be nul terminated, sorting, then stripping off the
|
||||
# nul.
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
if sys.version_info[0:2] == (2, 3):
|
||||
def locale_sort(mylist):
|
||||
"""
|
||||
Sort version to get around a python2.3 bug with unicode strings
|
||||
"""
|
||||
mylist = [ value + "\x00" for value in mylist ]
|
||||
mylist.sort(locale.strcoll)
|
||||
return [ value[:-1] for value in mylist ]
|
||||
else:
|
||||
def locale_sort(mylist):
|
||||
"""
|
||||
Normal sort routine
|
||||
"""
|
||||
mylist.sort(locale.strcoll)
|
||||
return mylist
|
||||
l = [ (locale.strxfrm(x),x) for x in mylist ]
|
||||
l.sort()
|
||||
return [ x[1] for x in l ]
|
||||
|
||||
class NodeTreeMap:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.sortnames = {}
|
||||
self.temp_top_path2iter = []
|
||||
|
||||
self.iter2path = {}
|
||||
self.path2iter = {}
|
||||
self.sname_sub = {}
|
||||
|
||||
self.temp_iter2path = {}
|
||||
self.temp_path2iter = {}
|
||||
self.temp_sname_sub = {}
|
||||
|
||||
def clear_sort_names(self):
|
||||
self.sortnames = {}
|
||||
|
||||
def clear_temp_data(self):
|
||||
self.temp_iter2path = {}
|
||||
self.temp_path2iter = {}
|
||||
self.temp_sname_sub = {}
|
||||
|
||||
def build_toplevel(self):
|
||||
self.temp_top_path2iter = locale_sort(self.temp_sname_sub.keys())
|
||||
for name in self.temp_top_path2iter:
|
||||
self.build_sub_entry(name)
|
||||
|
||||
def get_group_names(self):
|
||||
return self.temp_top_path2iter
|
||||
|
||||
def assign_sort_name(self, handle, sorted_name, group_name):
|
||||
self.sortnames[handle] = sorted_name
|
||||
try:
|
||||
self.temp_sname_sub[group_name].append(handle)
|
||||
except:
|
||||
self.temp_sname_sub[group_name] = [handle]
|
||||
|
||||
def assign_data(self):
|
||||
self.top_path2iter = self.temp_top_path2iter
|
||||
self.iter2path = self.temp_iter2path
|
||||
self.path2iter = self.temp_path2iter
|
||||
self.sname_sub = self.temp_sname_sub
|
||||
self.top_iter2path = {}
|
||||
i = 0
|
||||
for item in self.top_path2iter:
|
||||
self.top_iter2path[item] = i
|
||||
i+=1
|
||||
|
||||
def get_path(self, node):
|
||||
try:
|
||||
return (self.top_iter2path[node], )
|
||||
except:
|
||||
(surname, index) = self.iter2path[node]
|
||||
return (self.top_iter2path[surname], index)
|
||||
|
||||
def has_entry(self, handle):
|
||||
return self.iter2path.has_key(handle)
|
||||
|
||||
def get_iter(self, path):
|
||||
try:
|
||||
if len(path)==1: # Top Level
|
||||
return self.top_path2iter[path[0]]
|
||||
else: # Sublevel
|
||||
surname = self.top_path2iter[path[0]]
|
||||
return self.path2iter[(surname, path[1])]
|
||||
except:
|
||||
return None
|
||||
|
||||
def has_top_node(self, node):
|
||||
return self.sname_sub.has_key(node)
|
||||
|
||||
def find_next_node(self, node):
|
||||
try:
|
||||
path = self.top_iter2path[node]
|
||||
if path+1 == len(self.top_path2iter):
|
||||
return None
|
||||
return self.top_path2iter[path+1]
|
||||
except:
|
||||
(surname, val) = self.iter2path[node]
|
||||
return self.path2iter.get((surname, val+1))
|
||||
|
||||
def first_child(self, node):
|
||||
if node == None:
|
||||
return self.top_path2iter[0]
|
||||
else:
|
||||
return self.path2iter.get((node, 0))
|
||||
|
||||
def has_child(self, node):
|
||||
if node == None:
|
||||
return len(self.sname_sub)
|
||||
if self.sname_sub.has_key(node) and len(self.sname_sub[node]) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def number_of_children(self, node):
|
||||
if node == None:
|
||||
return len(self.sname_sub)
|
||||
try:
|
||||
return len(self.sname_sub[node])
|
||||
except:
|
||||
return 0
|
||||
|
||||
def get_nth_child(self, node, n):
|
||||
try:
|
||||
if node == None:
|
||||
return self.top_path2iter[n]
|
||||
try:
|
||||
return self.path2iter[(node, n)]
|
||||
except:
|
||||
return None
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def get_parent_of(self, node):
|
||||
path = self.iter2path.get(node)
|
||||
if path:
|
||||
return path[0]
|
||||
return None
|
||||
|
||||
def build_sub_entry(self, name):
|
||||
slist = [ (locale.strxfrm(self.sortnames[x]), x) \
|
||||
for x in self.temp_sname_sub[name] ]
|
||||
slist.sort()
|
||||
|
||||
val = 0
|
||||
for (junk, person_handle) in slist:
|
||||
tpl = (name, val)
|
||||
self.temp_iter2path[person_handle] = tpl
|
||||
self.temp_path2iter[tpl] = person_handle
|
||||
val += 1
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
@ -122,7 +237,6 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
_CHANGE_COL = 17
|
||||
_MARKER_COL = 18
|
||||
|
||||
|
||||
_GENDER = [ _(u'female'), _(u'male'), _(u'unknown') ]
|
||||
|
||||
# dynamic calculation of column indices, for use by various Views
|
||||
@ -153,13 +267,10 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
self.todo_color = Config.get(Config.TODO_COLOR)
|
||||
self.custom_color = Config.get(Config.CUSTOM_MARKER_COLOR)
|
||||
|
||||
self.sortnames = {}
|
||||
self.marker_color_column = 10
|
||||
self.tooltip_column = 11
|
||||
self.temp_top_path2iter = []
|
||||
self.iter2path = {}
|
||||
self.path2iter = {}
|
||||
self.sname_sub = {}
|
||||
|
||||
self.mapper = NodeTreeMap()
|
||||
|
||||
if filter_info and filter_info != (1, (0, u'', False)):
|
||||
if filter_info[0] == PeopleModel.GENERIC:
|
||||
@ -204,11 +315,12 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
self.current_filter = data_filter
|
||||
|
||||
def _build_search_sub(self,dfilter, skip):
|
||||
self.sortnames = {}
|
||||
|
||||
ngn = NameDisplay.displayer.name_grouping_data
|
||||
nsn = NameDisplay.displayer.raw_sorted_name
|
||||
|
||||
self.mapper.clear_sort_names()
|
||||
|
||||
cursor = self.db.get_person_cursor()
|
||||
node = cursor.first()
|
||||
|
||||
@ -216,17 +328,15 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
handle, d = node
|
||||
if not (handle in skip or (dfilter and not dfilter.match(handle))):
|
||||
name_data = d[PeopleModel._NAME_COL]
|
||||
sn = ngn(self.db, name_data)
|
||||
self.sortnames[handle] = nsn(name_data)
|
||||
try:
|
||||
self.temp_sname_sub[sn].append(handle)
|
||||
except:
|
||||
self.temp_sname_sub[sn] = [handle]
|
||||
|
||||
group_name = ngn(self.db, name_data)
|
||||
sorted_name = nsn(name_data)
|
||||
|
||||
self.mapper.assign_sort_name(handle, sorted_name, group_name)
|
||||
node = cursor.next()
|
||||
cursor.close()
|
||||
|
||||
def _build_filter_sub(self,dfilter, skip):
|
||||
self.sortnames = {}
|
||||
|
||||
ngn = NameDisplay.displayer.name_grouping_data
|
||||
nsn = NameDisplay.displayer.raw_sorted_name
|
||||
@ -236,16 +346,17 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
else:
|
||||
handle_list = self.db.get_person_handles()
|
||||
|
||||
self.mapper.clear_sort_names()
|
||||
|
||||
for handle in handle_list:
|
||||
d = self.db.get_raw_person_data(handle)
|
||||
if not (handle in skip or (dfilter and not dfilter.match(handle))):
|
||||
name_data = d[PeopleModel._NAME_COL]
|
||||
sn = ngn(self.db, name_data)
|
||||
self.sortnames[handle] = nsn(name_data)
|
||||
try:
|
||||
self.temp_sname_sub[sn].append(handle)
|
||||
except:
|
||||
self.temp_sname_sub[sn] = [handle]
|
||||
|
||||
group_name = ngn(self.db, name_data)
|
||||
sorted_name = nsn(name_data)
|
||||
|
||||
self.mapper.assign_sort_name(handle, sorted_name, group_name)
|
||||
|
||||
def calculate_data(self, dfilter=None, skip=[]):
|
||||
"""
|
||||
@ -259,18 +370,16 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
|
||||
if dfilter:
|
||||
self.dfilter = dfilter
|
||||
self.temp_iter2path = {}
|
||||
self.temp_path2iter = {}
|
||||
self.temp_sname_sub = {}
|
||||
|
||||
self.mapper.clear_temp_data()
|
||||
|
||||
if not self.db.is_open():
|
||||
return
|
||||
|
||||
self._build_data(dfilter, skip)
|
||||
|
||||
self.temp_top_path2iter = locale_sort(self.temp_sname_sub.keys())
|
||||
for name in self.temp_top_path2iter:
|
||||
self.build_sub_entry(name)
|
||||
self.mapper.build_toplevel()
|
||||
|
||||
self.in_build = False
|
||||
|
||||
def clear_cache(self):
|
||||
@ -280,27 +389,10 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
self.lru_ddate = LRU(_CACHE_SIZE)
|
||||
|
||||
def build_sub_entry(self, name):
|
||||
slist = [ (locale.strxfrm(self.sortnames[x]), x) \
|
||||
for x in self.temp_sname_sub[name] ]
|
||||
slist.sort()
|
||||
|
||||
val = 0
|
||||
for (junk, person_handle) in slist:
|
||||
tpl = (name, val)
|
||||
self.temp_iter2path[person_handle] = tpl
|
||||
self.temp_path2iter[tpl] = person_handle
|
||||
val += 1
|
||||
self.mapper.build_sub_entry(name)
|
||||
|
||||
def assign_data(self):
|
||||
self.top_path2iter = self.temp_top_path2iter
|
||||
self.iter2path = self.temp_iter2path
|
||||
self.path2iter = self.temp_path2iter
|
||||
self.sname_sub = self.temp_sname_sub
|
||||
self.top_iter2path = {}
|
||||
i = 0
|
||||
for item in self.top_path2iter:
|
||||
self.top_iter2path[item] = i
|
||||
i+=1
|
||||
self.mapper.assign_data()
|
||||
|
||||
def on_get_flags(self):
|
||||
'''returns the GtkTreeModelFlags for this particular type of model'''
|
||||
@ -312,31 +404,20 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
def on_get_path(self, node):
|
||||
'''returns the tree path (a tuple of indices at the various
|
||||
levels) for a particular node.'''
|
||||
try:
|
||||
return (self.top_iter2path[node], )
|
||||
except:
|
||||
(surname, index) = self.iter2path[node]
|
||||
return (self.top_iter2path[surname], index)
|
||||
return self.mapper.get_path(node)
|
||||
|
||||
def is_visable(self, handle):
|
||||
return self.iter2path.has_key(handle)
|
||||
return self.mapper.has_entry(handle)
|
||||
|
||||
def on_get_column_type(self, index):
|
||||
return PeopleModel.COLUMN_DEFS[index][PeopleModel.COLUMN_DEF_TYPE]
|
||||
|
||||
def on_get_iter(self, path):
|
||||
try:
|
||||
if len(path)==1: # Top Level
|
||||
return self.top_path2iter[path[0]]
|
||||
else: # Sublevel
|
||||
surname = self.top_path2iter[path[0]]
|
||||
return self.path2iter[(surname, path[1])]
|
||||
except:
|
||||
return None
|
||||
return self.mapper.get_iter(path)
|
||||
|
||||
def on_get_value(self, node, col):
|
||||
# test for header or data row-type
|
||||
if self.sname_sub.has_key(node):
|
||||
if self.mapper.has_top_node(node):
|
||||
# Header rows dont get the foreground color set
|
||||
if col == self.marker_color_column:
|
||||
return None
|
||||
@ -364,55 +445,25 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
|
||||
def on_iter_next(self, node):
|
||||
'''returns the next node at this level of the tree'''
|
||||
try:
|
||||
path = self.top_iter2path[node]
|
||||
if path+1 == len(self.top_path2iter):
|
||||
return None
|
||||
return self.top_path2iter[path+1]
|
||||
except:
|
||||
(surname, val) = self.iter2path[node]
|
||||
return self.path2iter.get((surname, val+1))
|
||||
return self.mapper.find_next_node(node)
|
||||
|
||||
def on_iter_children(self, node):
|
||||
"""Return the first child of the node"""
|
||||
if node == None:
|
||||
return self.top_path2iter[0]
|
||||
else:
|
||||
return self.path2iter.get((node, 0))
|
||||
return self.mapper.first_child(node)
|
||||
|
||||
def on_iter_has_child(self, node):
|
||||
'''returns true if this node has children'''
|
||||
if node == None:
|
||||
return len(self.sname_sub)
|
||||
if self.sname_sub.has_key(node) and len(self.sname_sub[node]) > 0:
|
||||
return True
|
||||
return False
|
||||
return self.mapper.has_child(node)
|
||||
|
||||
def on_iter_n_children(self, node):
|
||||
if node == None:
|
||||
return len(self.sname_sub)
|
||||
try:
|
||||
return len(self.sname_sub[node])
|
||||
except:
|
||||
return 0
|
||||
return self.mapper.number_of_children(node)
|
||||
|
||||
def on_iter_nth_child(self, node, n):
|
||||
try:
|
||||
if node == None:
|
||||
return self.top_path2iter[n]
|
||||
try:
|
||||
return self.path2iter[(node, n)]
|
||||
except:
|
||||
return None
|
||||
except IndexError:
|
||||
return None
|
||||
return self.mapper.get_nth_child(node, n)
|
||||
|
||||
def on_iter_parent(self, node):
|
||||
'''returns the parent of this node'''
|
||||
path = self.iter2path.get(node)
|
||||
if path:
|
||||
return path[0]
|
||||
return None
|
||||
return self.mapper.get_parent_of(node)
|
||||
|
||||
def column_sort_name(self, data, node):
|
||||
n = Name()
|
||||
@ -489,7 +540,7 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
if (etype in [EventType.BAPTISM, EventType.CHRISTEN]
|
||||
and er.get_role() == EventRoleType.PRIMARY
|
||||
and date_str != ""):
|
||||
return "<i>" + cgi.escape(date_str) + "</i>"
|
||||
return "<i>%s</i>" % cgi.escape(date_str)
|
||||
|
||||
return u""
|
||||
|
||||
@ -524,8 +575,8 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
date_str = DateHandler.get_date(event)
|
||||
if (etype in [EventType.BURIAL, EventType.CREMATION]
|
||||
and er.get_role() == EventRoleType.PRIMARY
|
||||
and date_str != ""):
|
||||
return "<i>" + cgi.escape(date_str) + "</i>"
|
||||
and date_str):
|
||||
return "<i>%s</i>" % cgi.escape(date_str)
|
||||
|
||||
return u""
|
||||
|
||||
@ -542,7 +593,7 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
if place_handle:
|
||||
place = self.db.get_place_from_handle(place_handle)
|
||||
place_title = place.get_title()
|
||||
if place_title != "":
|
||||
if place_title:
|
||||
return cgi.escape(place_title)
|
||||
except:
|
||||
return u''
|
||||
@ -558,8 +609,8 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
if place_handle:
|
||||
place = self.db.get_place_from_handle(place_handle)
|
||||
place_title = place.get_title()
|
||||
if place_title != "":
|
||||
return "<i>" + cgi.escape(place_title) + "</i>"
|
||||
if place_title:
|
||||
return "<i>%s</i>" % cgi.escape(place_title)
|
||||
|
||||
return u""
|
||||
|
||||
@ -576,7 +627,7 @@ class PeopleModel(gtk.GenericTreeModel):
|
||||
if place_handle:
|
||||
place = self.db.get_place_from_handle(place_handle)
|
||||
place_title = place.get_title()
|
||||
if place_title != "":
|
||||
if place_title:
|
||||
return cgi.escape(place_title)
|
||||
except:
|
||||
return u''
|
||||
|
@ -25,6 +25,7 @@ import gtk
|
||||
import GrampsWidgets
|
||||
|
||||
_RETURN = gtk.gdk.keyval_from_name("Return")
|
||||
_KP_ENTER = gtk.gdk.keyval_from_name("KP_Enter")
|
||||
|
||||
class SidebarFilter:
|
||||
|
||||
@ -99,7 +100,8 @@ class SidebarFilter:
|
||||
self.tooltips.set_tip(widget, tooltip)
|
||||
|
||||
def key_press(self, obj, event):
|
||||
if event.keyval == _RETURN and not event.state:
|
||||
if not event.state or event.state in (gtk.gdk.MOD2_MASK,):
|
||||
if event.keyval in (_RETURN, _KP_ENTER):
|
||||
self.clicked(obj)
|
||||
return False
|
||||
|
||||
|
@ -33,6 +33,7 @@ import gtk
|
||||
from gettext import gettext as _
|
||||
|
||||
_RETURN = gtk.gdk.keyval_from_name("Return")
|
||||
_KP_ENTER = gtk.gdk.keyval_from_name("KP_Enter")
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
@ -114,7 +115,8 @@ class SearchBar:
|
||||
self.clear_button.set_sensitive(True)
|
||||
|
||||
def key_press(self, obj, event):
|
||||
if event.keyval == _RETURN and not event.state:
|
||||
if not event.state or event.state in (gtk.gdk.MOD2_MASK,):
|
||||
if event.keyval in (_RETURN, _KP_ENTER):
|
||||
self.filter_button.set_sensitive(False)
|
||||
self.clear_button.set_sensitive(True)
|
||||
self.apply_filter()
|
||||
|
@ -174,8 +174,9 @@ class PageView:
|
||||
pass
|
||||
|
||||
def key_press(self,obj,event):
|
||||
ret_key = gtk.gdk.keyval_from_name("Return")
|
||||
if event.keyval == ret_key and not event.state:
|
||||
#act if no modifier, and allow Num Lock as MOD2_MASK
|
||||
if not event.state or event.state in (gtk.gdk.MOD2_MASK,):
|
||||
if event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
|
||||
self.edit(obj)
|
||||
return True
|
||||
return False
|
||||
@ -837,8 +838,8 @@ class ListView(BookMarkView):
|
||||
return False
|
||||
|
||||
def key_press(self,obj,event):
|
||||
ret_key = gtk.gdk.keyval_from_name("Return")
|
||||
if event.keyval == ret_key and not event.state:
|
||||
if not event.state or event.state in (gtk.gdk.MOD2_MASK,):
|
||||
if event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
|
||||
self.edit(obj)
|
||||
return True
|
||||
return False
|
||||
|
Loading…
Reference in New Issue
Block a user