Merge branch 'master' of git://git.assembla.com/fpdb-eric
This commit is contained in:
commit
b52ceb3f90
|
@ -32,6 +32,7 @@ class Absolute(HandHistoryConverter):
|
|||
filetype = "text"
|
||||
codepage = "cp1252"
|
||||
siteid = 8
|
||||
HORSEHand = False
|
||||
|
||||
# Static regexes
|
||||
re_SplitHands = re.compile(r"\n\n\n+")
|
||||
|
@ -41,9 +42,11 @@ class Absolute(HandHistoryConverter):
|
|||
#Seat 6 - FETS63 ($0.75 in chips)
|
||||
#Board [10s 5d Kh Qh 8c]
|
||||
|
||||
re_GameInfo = re.compile(ur"^Stage #([0-9]+): (?P<GAME>Holdem|)(?: \(1 on 1\)|)? (?P<LIMIT>No Limit|Pot Limit|Normal) (?P<CURRENCY>\$| €|)(?P<SB>[.0-9]+)/?(?:\$| €|)(?P<BB>[.0-9]+)?", re.MULTILINE)
|
||||
re_GameInfo = re.compile(ur"^Stage #([0-9]+): (?P<GAME>Holdem|HORSE)(?: \(1 on 1\)|)? ?(?P<LIMIT>No Limit|Pot Limit|Normal|)? ?(?P<CURRENCY>\$| €|)(?P<SB>[.0-9]+)/?(?:\$| €|)(?P<BB>[.0-9]+)?", re.MULTILINE)
|
||||
re_HorseGameInfo = re.compile(ur"^Game Type: (?P<LIMIT>Limit) (?P<GAME>Holdem)", re.MULTILINE)
|
||||
# TODO: can set max seats via (1 on 1) to a known 2 ..
|
||||
re_HandInfo = re.compile(ur"^Stage #(?P<HID>[0-9]+): .*(?P<DATETIME>\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d).*\nTable: (?P<TABLE>.*) \(Real Money\)", re.MULTILINE)
|
||||
re_HandInfo = re.compile(ur"^Stage #(?P<HID>[0-9]+): .*(?P<DATETIME>\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d).*\n(Table: (?P<TABLE>.*) \(Real Money\))?", re.MULTILINE)
|
||||
re_TableFromFilename = re.compile(ur".*IHH([0-9]+) (?P<TABLE>.*) -") # on HORSE STUD games, the table name isn't in the hand info!
|
||||
re_Button = re.compile(ur"Seat #(?P<BUTTON>[0-9]) is the ?[dead]* dealer$", re.MULTILINE) # TODO: that's not the right way to match for "dead" dealer is it?
|
||||
re_PlayerInfo = re.compile(ur"^Seat (?P<SEAT>[0-9]) - (?P<PNAME>.*) \((?:\$| €|)(?P<CASH>[0-9]*[.0-9]+) in chips\)", re.MULTILINE)
|
||||
re_Board = re.compile(ur"\[(?P<CARDS>[^\]]*)\]? *$", re.MULTILINE)
|
||||
|
@ -113,7 +116,7 @@ or None if we fail to get the info """
|
|||
mg = m.groupdict()
|
||||
|
||||
# translations from captured groups to our info strings
|
||||
limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Normal':'fl' }
|
||||
limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Normal':'fl', 'Limit':'fl'}
|
||||
games = { # base, category
|
||||
"Holdem" : ('hold','holdem'),
|
||||
'Omaha' : ('hold','omahahi'),
|
||||
|
@ -121,10 +124,22 @@ or None if we fail to get the info """
|
|||
'7 Card Stud' : ('stud','studhi')
|
||||
}
|
||||
currencies = { u' €':'EUR', '$':'USD', '':'T$' }
|
||||
if 'LIMIT' in mg:
|
||||
info['limitType'] = limits[mg['LIMIT']]
|
||||
if 'GAME' in mg and mg['GAME'] == "HORSE": # if we're a HORSE game, the game type is on the next line
|
||||
self.HORSEHand = True
|
||||
m = self.re_HorseGameInfo.search(handText)
|
||||
if not m:
|
||||
return None # it's a HORSE game and we don't understand the game type
|
||||
temp = m.groupdict()
|
||||
#print "AP HORSE processing"
|
||||
if 'GAME' not in temp or 'LIMIT' not in temp:
|
||||
return None # sort of understood it but not really
|
||||
#print "temp=", temp
|
||||
mg['GAME'] = temp['GAME']
|
||||
mg['LIMIT'] = temp['LIMIT']
|
||||
if 'GAME' in mg:
|
||||
(info['base'], info['category']) = games[mg['GAME']]
|
||||
if 'LIMIT' in mg:
|
||||
info['limitType'] = limits[mg['LIMIT']]
|
||||
if 'SB' in mg:
|
||||
info['sb'] = mg['SB']
|
||||
else:
|
||||
|
@ -153,9 +168,15 @@ or None if we fail to get the info """
|
|||
return None
|
||||
logging.debug("HID %s, Table %s" % (m.group('HID'), m.group('TABLE')))
|
||||
hand.handid = m.group('HID')
|
||||
if m.group('TABLE'):
|
||||
hand.tablename = m.group('TABLE')
|
||||
else:
|
||||
t = self.re_TableFromFilename.search(self.in_path)
|
||||
hand.tablename = t.group('TABLE')
|
||||
hand.maxseats = 6 # assume 6-max unless we have proof it's a larger/smaller game, since absolute doesn't give seat max info
|
||||
|
||||
# TODO: (1-on-1) does have that info in the game type line
|
||||
if self.HORSEHand:
|
||||
hand.maxseats = 8
|
||||
hand.starttime = datetime.datetime.strptime(m.group('DATETIME'), "%Y-%m-%d %H:%M:%S")
|
||||
return
|
||||
|
||||
|
|
|
@ -583,8 +583,15 @@ Map the tuple self.gametype onto the pokerstars string describing it
|
|||
else: # non-mixed cash games
|
||||
gs = gs + " %s (%s) - " % (self.getGameTypeAsString(), self.getStakesAsString())
|
||||
|
||||
return gs + datetime.datetime.strftime(self.starttime,'%Y/%m/%d %H:%M:%S ET')
|
||||
|
||||
try:
|
||||
timestr = datetime.datetime.strftime(self.starttime, '%Y/%m/%d %H:%M:%S ET')
|
||||
except TypeError:
|
||||
print "*** ERROR - HAND: calling writeGameLine with unexpected STARTTIME value, expecting datetime.date object, received:", self.starttime
|
||||
print "*** Make sure your HandHistoryConverter is setting hand.starttime properly!"
|
||||
print "*** Game String:", gs
|
||||
return gs
|
||||
else:
|
||||
return gs + timestr
|
||||
|
||||
def writeTableLine(self):
|
||||
table_string = "Table \'%s\' %s-max" % (self.tablename, self.maxseats)
|
||||
|
|
180
pyfpdb/Hud.py
180
pyfpdb/Hud.py
|
@ -81,6 +81,10 @@ class Hud:
|
|||
(font, font_size) = config.get_default_font(self.table.site)
|
||||
self.colors = config.get_default_colors(self.table.site)
|
||||
|
||||
self.backgroundcolor = gtk.gdk.color_parse(self.colors['hudbgcolor'])
|
||||
self.foregroundcolor = gtk.gdk.color_parse(self.colors['hudfgcolor'])
|
||||
|
||||
|
||||
if font == None:
|
||||
font = "Sans"
|
||||
if font_size == None:
|
||||
|
@ -102,84 +106,65 @@ class Hud:
|
|||
def create_mw(self):
|
||||
|
||||
# Set up a main window for this this instance of the HUD
|
||||
self.main_window = gtk.Window()
|
||||
self.main_window.set_gravity(gtk.gdk.GRAVITY_STATIC)
|
||||
self.main_window.set_title("%s FPDBHUD" % (self.table.name))
|
||||
self.main_window.set_decorated(False)
|
||||
self.main_window.set_opacity(self.colors["hudopacity"])
|
||||
self.main_window.set_focus_on_map(False)
|
||||
win = gtk.Window()
|
||||
win.set_gravity(gtk.gdk.GRAVITY_STATIC)
|
||||
win.set_title("%s FPDBHUD" % (self.table.name))
|
||||
win.set_decorated(False)
|
||||
win.set_opacity(self.colors["hudopacity"])
|
||||
|
||||
self.ebox = gtk.EventBox()
|
||||
self.label = gtk.Label("FPDB Menu (Right Click)\nLeft-drag to move")
|
||||
eventbox = gtk.EventBox()
|
||||
label = gtk.Label("FPDB Menu - Right click\nLeft-Drag to Move")
|
||||
|
||||
self.backgroundcolor = gtk.gdk.color_parse(self.colors['hudbgcolor'])
|
||||
self.foregroundcolor = gtk.gdk.color_parse(self.colors['hudfgcolor'])
|
||||
win.add(eventbox)
|
||||
eventbox.add(label)
|
||||
|
||||
self.label.modify_bg(gtk.STATE_NORMAL, self.backgroundcolor)
|
||||
self.label.modify_fg(gtk.STATE_NORMAL, self.foregroundcolor)
|
||||
label.modify_bg(gtk.STATE_NORMAL, self.backgroundcolor)
|
||||
label.modify_fg(gtk.STATE_NORMAL, self.foregroundcolor)
|
||||
|
||||
self.main_window.add(self.ebox)
|
||||
self.ebox.add(self.label)
|
||||
|
||||
self.ebox.modify_bg(gtk.STATE_NORMAL, self.backgroundcolor)
|
||||
self.ebox.modify_fg(gtk.STATE_NORMAL, self.foregroundcolor)
|
||||
eventbox.modify_bg(gtk.STATE_NORMAL, self.backgroundcolor)
|
||||
eventbox.modify_fg(gtk.STATE_NORMAL, self.foregroundcolor)
|
||||
|
||||
self.main_window = win
|
||||
self.main_window.move(self.table.x, self.table.y)
|
||||
|
||||
# A popup menu for the main window
|
||||
self.menu = gtk.Menu()
|
||||
self.item1 = gtk.MenuItem('Kill this HUD')
|
||||
self.menu.append(self.item1)
|
||||
menu = gtk.Menu()
|
||||
|
||||
killitem = gtk.MenuItem('Kill This HUD')
|
||||
menu.append(killitem)
|
||||
if self.parent != None:
|
||||
self.item1.connect("activate", self.parent.kill_hud, self.table_name)
|
||||
self.item1.show()
|
||||
killitem.connect("activate", self.parent.kill_hud, self.table_name)
|
||||
|
||||
self.item2 = gtk.MenuItem('Save Layout')
|
||||
self.menu.append(self.item2)
|
||||
self.item2.connect("activate", self.save_layout)
|
||||
self.item2.show()
|
||||
saveitem = gtk.MenuItem('Save HUD Layout')
|
||||
menu.append(saveitem)
|
||||
saveitem.connect("activate", self.save_layout)
|
||||
|
||||
self.item3 = gtk.MenuItem('Reposition Stats')
|
||||
self.menu.append(self.item3)
|
||||
self.item3.connect("activate", self.reposition_windows)
|
||||
self.item3.show()
|
||||
repositem = gtk.MenuItem('Reposition StatWindows')
|
||||
menu.append(repositem)
|
||||
repositem.connect("activate", self.reposition_windows)
|
||||
|
||||
self.item4 = gtk.MenuItem('Debug Stat Windows')
|
||||
self.menu.append(self.item4)
|
||||
self.item4.connect("activate", self.debug_stat_windows)
|
||||
self.item4.show()
|
||||
debugitem = gtk.MenuItem('Debug StatWindows')
|
||||
menu.append(debugitem)
|
||||
debugitem.connect("activate", self.debug_stat_windows)
|
||||
|
||||
self.item5 = gtk.MenuItem('Set max seats')
|
||||
self.menu.append(self.item5)
|
||||
self.item5.show()
|
||||
self.maxSeatsMenu = gtk.Menu()
|
||||
self.item5.set_submenu(self.maxSeatsMenu)
|
||||
item5 = gtk.MenuItem('Set max seats')
|
||||
menu.append(item5)
|
||||
maxSeatsMenu = gtk.Menu()
|
||||
item5.set_submenu(maxSeatsMenu)
|
||||
for i in range(2, 11, 1):
|
||||
item = gtk.MenuItem('%d-max' % i)
|
||||
item.ms = i
|
||||
self.maxSeatsMenu.append(item)
|
||||
maxSeatsMenu.append(item)
|
||||
item.connect("activate", self.change_max_seats)
|
||||
item.show()
|
||||
setattr(self, 'maxSeatsMenuItem%d' % (i-1), item)
|
||||
|
||||
eventbox.connect_object("button-press-event", self.on_button_press, menu)
|
||||
|
||||
|
||||
self.ebox.connect_object("button-press-event", self.on_button_press, self.menu)
|
||||
|
||||
self.main_window.show_all()
|
||||
self.mw_created = True
|
||||
|
||||
# TODO: fold all uses of this type of 'topify' code into a single function, if the differences between the versions don't
|
||||
# create adverse effects?
|
||||
|
||||
if os.name == 'nt':
|
||||
self.label = label
|
||||
menu.show_all()
|
||||
self.main_window.show_all()
|
||||
self.topify_window(self.main_window)
|
||||
else:
|
||||
self.main_window.parentgdkhandle = gtk.gdk.window_foreign_new(int(self.table.number)) # gets a gdk handle for poker client
|
||||
self.main_window.gdkhandle = gtk.gdk.window_foreign_new(self.main_window.window.xid) # gets a gdk handle for the hud table window
|
||||
self.main_window.gdkhandle.set_transient_for(self.main_window.parentgdkhandle) #
|
||||
|
||||
self.update_table_position()
|
||||
|
||||
def change_max_seats(self, widget):
|
||||
if self.max != widget.ms:
|
||||
|
@ -199,8 +184,8 @@ class Hud:
|
|||
self.parent.kill_hud(self, self.table.name)
|
||||
return False
|
||||
# anyone know how to do this in unix, or better yet, trap the X11 error that is triggered when executing the get_origin() for a closed window?
|
||||
|
||||
(x, y) = self.main_window.parentgdkhandle.get_origin()
|
||||
if self.table.gdkhandle is not None:
|
||||
(x, y) = self.table.gdkhandle.get_origin()
|
||||
if self.table.x != x or self.table.y != y:
|
||||
self.table.x = x
|
||||
self.table.y = y
|
||||
|
@ -374,30 +359,15 @@ class Hud:
|
|||
Stats.do_tip(window.e_box[r][c], tip)
|
||||
|
||||
def topify_window(self, window):
|
||||
# """Set the specified gtk window to stayontop in MS Windows."""
|
||||
#
|
||||
# def windowEnumerationHandler(hwnd, resultList):
|
||||
# '''Callback for win32gui.EnumWindows() to generate list of window handles.'''
|
||||
# resultList.append((hwnd, win32gui.GetWindowText(hwnd)))
|
||||
# unique_name = 'unique name for finding this window'
|
||||
# real_name = window.get_title()
|
||||
# window.set_title(unique_name)
|
||||
# tl_windows = []
|
||||
# win32gui.EnumWindows(windowEnumerationHandler, tl_windows)
|
||||
#
|
||||
# for w in tl_windows:
|
||||
# if w[1] == unique_name:
|
||||
self.main_window.parentgdkhandle = gtk.gdk.window_foreign_new(long(self.table.number))
|
||||
# self.main_window.gdkhandle = gtk.gdk.window_foreign_new(w[0])
|
||||
self.main_window.gdkhandle = self.main_window.window
|
||||
self.main_window.gdkhandle.set_transient_for(self.main_window.parentgdkhandle)
|
||||
window.set_focus_on_map(False)
|
||||
window.set_accept_focus(False)
|
||||
|
||||
style = win32gui.GetWindowLong(self.table.number, win32con.GWL_EXSTYLE)
|
||||
style |= win32con.WS_CLIPCHILDREN
|
||||
win32gui.SetWindowLong(self.table.number, win32con.GWL_EXSTYLE, style)
|
||||
# break
|
||||
if not self.table.gdkhandle:
|
||||
self.table.gdkhandle = gtk.gdk.window_foreign_new(int(self.table.number)) # gtk handle to poker window
|
||||
# window.window.reparent(self.table.gdkhandle, 0, 0)
|
||||
window.window.set_transient_for(self.table.gdkhandle)
|
||||
# window.present()
|
||||
|
||||
# window.set_title(real_name)
|
||||
|
||||
class Stat_Window:
|
||||
|
||||
|
@ -419,6 +389,7 @@ class Stat_Window:
|
|||
|
||||
if event.button == 1: # left button event
|
||||
# TODO: make position saving save sizes as well?
|
||||
self.window.show_all()
|
||||
if event.state & gtk.gdk.SHIFT_MASK:
|
||||
self.window.begin_resize_drag(gtk.gdk.WINDOW_EDGE_SOUTH_EAST, event.button, int(event.x_root), int(event.y_root), event.time)
|
||||
else:
|
||||
|
@ -462,7 +433,6 @@ class Stat_Window:
|
|||
|
||||
self.window.set_title("%s" % seat)
|
||||
self.window.set_property("skip-taskbar-hint", True)
|
||||
self.window.set_transient_for(parent.main_window)
|
||||
self.window.set_focus_on_map(False)
|
||||
|
||||
grid = gtk.Table(rows = game.rows, columns = game.cols, homogeneous = False)
|
||||
|
@ -514,11 +484,26 @@ class Stat_Window:
|
|||
self.window.connect("focus-in-event", self.noop)
|
||||
self.window.connect("focus-out-event", self.noop)
|
||||
self.window.connect("button_press_event", self.button_press_cb)
|
||||
self.window.set_focus_on_map(False)
|
||||
self.window.set_accept_focus(False)
|
||||
|
||||
|
||||
self.window.move(self.x, self.y)
|
||||
self.window.show_all() # window must be mapped before it has a gdkwindow so we can attach it to the table window.. i hate gtk.
|
||||
self.topify_window(self.window)
|
||||
|
||||
self.window.hide()
|
||||
|
||||
def topify_window(self, window):
|
||||
window.set_focus_on_map(False)
|
||||
window.set_accept_focus(False)
|
||||
|
||||
if not self.table.gdkhandle:
|
||||
self.table.gdkhandle = gtk.gdk.window_foreign_new(int(self.table.number)) # gtk handle to poker window
|
||||
# window.window.reparent(self.table.gdkhandle, 0, 0)
|
||||
window.window.set_transient_for(self.table.gdkhandle)
|
||||
# window.present()
|
||||
|
||||
def destroy(*args): # call back for terminating the main eventloop
|
||||
gtk.main_quit()
|
||||
|
||||
|
@ -534,6 +519,8 @@ class Popup_window:
|
|||
self.window.set_gravity(gtk.gdk.GRAVITY_STATIC)
|
||||
self.window.set_title("popup")
|
||||
self.window.set_property("skip-taskbar-hint", True)
|
||||
self.window.set_focus_on_map(False)
|
||||
self.window.set_accept_focus(False)
|
||||
self.window.set_transient_for(parent.get_toplevel())
|
||||
|
||||
self.window.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
|
||||
|
@ -599,9 +586,6 @@ class Popup_window:
|
|||
|
||||
self.window.set_transient_for(stat_window.window)
|
||||
|
||||
# if os.name == 'nt':
|
||||
# self.topify_window(self.window)
|
||||
|
||||
def button_press_cb(self, widget, event, *args):
|
||||
# This handles all callbacks from button presses on the event boxes in
|
||||
# the popup windows. There is a bit of an ugly kludge to separate single-
|
||||
|
@ -630,27 +614,15 @@ class Popup_window:
|
|||
top.move(x, y)
|
||||
|
||||
def topify_window(self, window):
|
||||
"""Set the specified gtk window to stayontop in MS Windows."""
|
||||
window.set_focus_on_map(False)
|
||||
window.set_accept_focus(False)
|
||||
|
||||
# def windowEnumerationHandler(hwnd, resultList):
|
||||
# '''Callback for win32gui.EnumWindows() to generate list of window handles.'''
|
||||
# resultList.append((hwnd, win32gui.GetWindowText(hwnd)))
|
||||
if not self.table.gdkhandle:
|
||||
self.table.gdkhandle = gtk.gdk.window_foreign_new(int(self.table.number)) # gtk handle to poker window
|
||||
# window.window.reparent(self.table.gdkhandle, 0, 0)
|
||||
window.window.set_transient_for(self.table.gdkhandle)
|
||||
# window.present()
|
||||
|
||||
# unique_name = 'unique name for finding this window'
|
||||
# real_name = window.get_title()
|
||||
# window.set_title(unique_name)
|
||||
# tl_windows = []
|
||||
# win32gui.EnumWindows(windowEnumerationHandler, tl_windows)
|
||||
|
||||
# for w in tl_windows:
|
||||
# if w[1] == unique_name:
|
||||
# window.set_transient_for(self.parent.window)
|
||||
style = win32gui.GetWindowLong(self.parent.table.number, win32con.GWL_EXSTYLE)
|
||||
style |= win32con.WS_CLIPCHILDREN
|
||||
win32gui.SetWindowLong(self.parent.table.number, win32con.GWL_EXSTYLE, style)
|
||||
# break
|
||||
|
||||
# window.set_title(real_name)
|
||||
|
||||
if __name__== "__main__":
|
||||
main_window = gtk.Window()
|
||||
|
@ -661,7 +633,7 @@ if __name__== "__main__":
|
|||
|
||||
c = Configuration.Config()
|
||||
#tables = Tables.discover(c)
|
||||
t = Tables.discover_table_by_name(c, "Patriot Dr")
|
||||
t = Tables.discover_table_by_name(c, "Motorway")
|
||||
if t is None:
|
||||
print "Table not found."
|
||||
db = Database.Database(c, 'fpdb', 'holdem')
|
||||
|
|
|
@ -69,6 +69,7 @@ class Table_Window:
|
|||
if 'site' in info: self.site = info['site']
|
||||
if 'title' in info: self.title = info['title']
|
||||
if 'name' in info: self.name = info['name']
|
||||
self.gdkhandle = None
|
||||
|
||||
def __str__(self):
|
||||
# __str__ method for testing
|
||||
|
|
54
pyfpdb/py2exe_setup.py
Normal file
54
pyfpdb/py2exe_setup.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""setup.py
|
||||
|
||||
Py2exe script for fpdb.
|
||||
"""
|
||||
# Copyright 2009, Ray E. Barker
|
||||
#
|
||||
# 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
|
||||
|
||||
########################################################################
|
||||
|
||||
#TODO: change GuiAutoImport so that it knows to start HUD_main.exe, when appropriate
|
||||
# include the lib needed to handle png files in mucked
|
||||
# get rid of all the uneeded libraries (e.g., pyQT)
|
||||
# think about an installer
|
||||
|
||||
from distutils.core import setup
|
||||
import py2exe
|
||||
|
||||
setup(
|
||||
name = 'fpdb',
|
||||
description = 'Free Poker DataBase',
|
||||
version = '0.12',
|
||||
|
||||
console = [ {'script': 'fpdb.py', },
|
||||
{'script': 'HUD_main.py', }
|
||||
],
|
||||
|
||||
options = {'py2exe': {
|
||||
'packages' :'encodings',
|
||||
'includes' : 'cairo, pango, pangocairo, atk, gobject, PokerStarsToFpdb',
|
||||
'excludes' : '_tkagg, _agg2, cocoaagg, fltkagg',
|
||||
'dll_excludes': 'libglade-2.0-0.dll',
|
||||
}
|
||||
},
|
||||
|
||||
data_files = ['HUD_config.xml',
|
||||
'Cards01.png'
|
||||
]
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user