mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement choose library action
This commit is contained in:
parent
0ad6ab164f
commit
12a4797a6f
82
src/calibre/gui2/dialogs/choose_library.py
Normal file
82
src/calibre/gui2/dialogs/choose_library.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt4.Qt import QDialog
|
||||||
|
|
||||||
|
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
|
||||||
|
from calibre.gui2 import error_dialog, choose_dir
|
||||||
|
from calibre.constants import filesystem_encoding
|
||||||
|
from calibre import isbytestring, patheq
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.gui2.wizard import move_library
|
||||||
|
|
||||||
|
class ChooseLibrary(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
|
def __init__(self, db, callback, parent):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.setupUi(self)
|
||||||
|
self.db = db
|
||||||
|
self.new_db = None
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
lp = db.library_path
|
||||||
|
if isbytestring(lp):
|
||||||
|
lp = lp.decode(filesystem_encoding)
|
||||||
|
loc = unicode(self.old_location.text()).format(lp)
|
||||||
|
self.old_location.setText(loc)
|
||||||
|
self.browse_button.clicked.connect(self.choose_loc)
|
||||||
|
|
||||||
|
def choose_loc(self, *args):
|
||||||
|
loc = choose_dir(self, 'choose library location',
|
||||||
|
_('Choose location for calibre library'))
|
||||||
|
if loc is not None:
|
||||||
|
self.location.setText(loc)
|
||||||
|
|
||||||
|
def check_action(self, ac, loc):
|
||||||
|
exists = self.db.exists_at(loc)
|
||||||
|
if patheq(loc, self.db.library_path):
|
||||||
|
error_dialog(self, _('Same as current'),
|
||||||
|
_('The location %s contains the current calibre'
|
||||||
|
' library')%loc, show=True)
|
||||||
|
return False
|
||||||
|
empty = not os.listdir(loc)
|
||||||
|
if ac == 'existing' and not exists:
|
||||||
|
error_dialog(self, _('No existing library found'),
|
||||||
|
_('There is no existing calibre library at %s')%loc,
|
||||||
|
show=True)
|
||||||
|
return False
|
||||||
|
if ac in ('new', 'move') and not empty:
|
||||||
|
error_dialog(self, _('Not empty'),
|
||||||
|
_('The folder %s is not empty. Please choose an empty'
|
||||||
|
' folder')%loc,
|
||||||
|
show=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def perform_action(self, ac, loc):
|
||||||
|
if ac in ('new', 'existing'):
|
||||||
|
prefs['library_path'] = loc
|
||||||
|
self.callback(loc)
|
||||||
|
else:
|
||||||
|
move_library(self.db.library_path, loc, self.parent(),
|
||||||
|
self.callback)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
action = 'move'
|
||||||
|
if self.existing_library.isChecked():
|
||||||
|
action = 'existing'
|
||||||
|
elif self.empty_library.isChecked():
|
||||||
|
action = 'new'
|
||||||
|
loc = os.path.abspath(unicode(self.location.text()).strip())
|
||||||
|
if not loc or not os.path.exists(loc) or not self.check_action(action,
|
||||||
|
loc):
|
||||||
|
return
|
||||||
|
QDialog.accept(self)
|
||||||
|
self.perform_action(action, loc)
|
171
src/calibre/gui2/dialogs/choose_library.ui
Normal file
171
src/calibre/gui2/dialogs/choose_library.ui
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>602</width>
|
||||||
|
<height>245</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Choose your calibre library</string>
|
||||||
|
</property>
|
||||||
|
<property name="windowIcon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0" colspan="3">
|
||||||
|
<widget class="QLabel" name="old_location">
|
||||||
|
<property name="text">
|
||||||
|
<string>Your calibre library is currently located at {0}</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>New &Location:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>location</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLineEdit" name="location">
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0" colspan="3">
|
||||||
|
<widget class="QRadioButton" name="existing_library">
|
||||||
|
<property name="text">
|
||||||
|
<string>Use &existing library at the new location</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0" colspan="2">
|
||||||
|
<widget class="QRadioButton" name="empty_library">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Create an empty library at the new location</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0" colspan="2">
|
||||||
|
<widget class="QRadioButton" name="move_library">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Move current library to new location</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="1">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="1">
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<spacer name="verticalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QToolButton" name="browse_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -14,7 +14,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
|||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||||
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
from calibre.gui2 import error_dialog, config, gprefs, \
|
||||||
open_url, open_local_file, \
|
open_url, open_local_file, \
|
||||||
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
||||||
warning_dialog, ResizableDialog, question_dialog
|
warning_dialog, ResizableDialog, question_dialog
|
||||||
@ -343,9 +343,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.model = library_view.model()
|
self.model = library_view.model()
|
||||||
self.db = self.model.db
|
self.db = self.model.db
|
||||||
self.server = server
|
self.server = server
|
||||||
path = prefs['library_path']
|
|
||||||
self.location.setText(path if path else '')
|
|
||||||
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
|
|
||||||
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
|
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
|
||||||
|
|
||||||
input_map = prefs['input_format_order']
|
input_map = prefs['input_format_order']
|
||||||
@ -808,12 +805,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
d = CheckIntegrity(self.db, self)
|
d = CheckIntegrity(self.db, self)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def browse(self):
|
|
||||||
dir = choose_dir(self, 'database location dialog',
|
|
||||||
_('Select location for books'))
|
|
||||||
if dir:
|
|
||||||
self.location.setText(dir)
|
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
mcs = unicode(self.max_cover_size.text()).strip()
|
mcs = unicode(self.max_cover_size.text()).strip()
|
||||||
if not re.match(r'\d+x\d+', mcs):
|
if not re.match(r'\d+x\d+', mcs):
|
||||||
@ -834,7 +825,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
||||||
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
||||||
prefs['network_timeout'] = int(self.timeout.value())
|
prefs['network_timeout'] = int(self.timeout.value())
|
||||||
path = unicode(self.location.text())
|
|
||||||
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
|
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
|
||||||
prefs['input_format_order'] = input_cols
|
prefs['input_format_order'] = input_cols
|
||||||
|
|
||||||
@ -875,24 +865,13 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
|
val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
|
||||||
config['gui_layout'] = unicode(val)
|
config['gui_layout'] = unicode(val)
|
||||||
|
|
||||||
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
if must_restart:
|
||||||
d = error_dialog(self, _('Invalid database location'),
|
warning_dialog(self, _('Must restart'),
|
||||||
_('Invalid database location ')+path+
|
_('The changes you made require that Calibre be '
|
||||||
_('<br>Must be a directory.'))
|
'restarted. Please restart as soon as practical.'),
|
||||||
d.exec_()
|
show=True, show_copy_button=False)
|
||||||
elif not os.access(path, os.W_OK):
|
self.parent.must_restart_before_config = True
|
||||||
d = error_dialog(self, _('Invalid database location'),
|
QDialog.accept(self)
|
||||||
_('Invalid database location.<br>Cannot write to ')+path)
|
|
||||||
d.exec_()
|
|
||||||
else:
|
|
||||||
self.database_location = os.path.abspath(path)
|
|
||||||
if must_restart:
|
|
||||||
warning_dialog(self, _('Must restart'),
|
|
||||||
_('The changes you made require that Calibre be '
|
|
||||||
'restarted. Please restart as soon as practical.'),
|
|
||||||
show=True, show_copy_button=False)
|
|
||||||
self.parent.must_restart_before_config = True
|
|
||||||
QDialog.accept(self)
|
|
||||||
|
|
||||||
class VacThread(QThread):
|
class VacThread(QThread):
|
||||||
|
|
||||||
|
@ -113,50 +113,6 @@
|
|||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="page_3">
|
<widget class="QWidget" name="page_3">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
|
||||||
<layout class="QVBoxLayout" name="_2">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="maximumSize">
|
|
||||||
<size>
|
|
||||||
<width>16777215</width>
|
|
||||||
<height>70</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>&Location of ebooks (The ebooks are stored in folders sorted by author and metadata is stored in the file metadata.db)</string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>location</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="_3">
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="location"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="browse_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Browse for the new database location</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/mimetypes/dir.svg</normaloff>:/images/mimetypes/dir.svg</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="new_version_notification">
|
<widget class="QCheckBox" name="new_version_notification">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -329,6 +329,7 @@ class MainWindowMixin(object):
|
|||||||
self.tool_bar = ToolBar(all_actions, self.donate_button,
|
self.tool_bar = ToolBar(all_actions, self.donate_button,
|
||||||
self.location_manager, self)
|
self.location_manager, self)
|
||||||
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
||||||
|
self.tool_bar.choose_action.triggered.connect(self.choose_library)
|
||||||
|
|
||||||
l = self.centralwidget.layout()
|
l = self.centralwidget.layout()
|
||||||
l.addWidget(self.search_bar)
|
l.addWidget(self.search_bar)
|
||||||
@ -337,6 +338,12 @@ class MainWindowMixin(object):
|
|||||||
def read_toolbar_settings(self):
|
def read_toolbar_settings(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def choose_library(self, *args):
|
||||||
|
from calibre.gui2.dialogs.choose_library import ChooseLibrary
|
||||||
|
db = self.library_view.model().db
|
||||||
|
c = ChooseLibrary(db, self.library_moved, self)
|
||||||
|
c.exec_()
|
||||||
|
|
||||||
def setup_actions(self): # {{{
|
def setup_actions(self): # {{{
|
||||||
all_actions = []
|
all_actions = []
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from PyQt4.Qt import Qt, SIGNAL, QTimer, \
|
|||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
QMessageBox, QHelpEvent
|
QMessageBox, QHelpEvent
|
||||||
|
|
||||||
from calibre import prints, patheq
|
from calibre import prints
|
||||||
from calibre.constants import __appname__, isosx
|
from calibre.constants import __appname__, isosx
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
@ -27,7 +27,6 @@ from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
|||||||
gprefs, max_available_height, config, info_dialog
|
gprefs, max_available_height, config, info_dialog
|
||||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre.gui2.wizard import move_library
|
|
||||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
from calibre.gui2.dialogs.scheduler import Scheduler
|
||||||
from calibre.gui2.update import UpdateMixin
|
from calibre.gui2.update import UpdateMixin
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
@ -389,10 +388,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
self.create_device_menu()
|
self.create_device_menu()
|
||||||
self.set_device_menu_items_state(bool(self.device_connected))
|
self.set_device_menu_items_state(bool(self.device_connected))
|
||||||
if not patheq(self.library_path, d.database_location):
|
|
||||||
newloc = d.database_location
|
|
||||||
move_library(self.library_path, newloc, self,
|
|
||||||
self.library_moved)
|
|
||||||
|
|
||||||
def library_moved(self, newloc):
|
def library_moved(self, newloc):
|
||||||
if newloc is None: return
|
if newloc is None: return
|
||||||
|
@ -116,6 +116,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
# missing functions
|
# missing functions
|
||||||
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter')
|
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exists_at(cls, path):
|
||||||
|
return path and os.path.exists(os.path.join(path, 'metadata.db'))
|
||||||
|
|
||||||
def __init__(self, library_path, row_factory=False):
|
def __init__(self, library_path, row_factory=False):
|
||||||
self.field_metadata = FieldMetadata()
|
self.field_metadata = FieldMetadata()
|
||||||
if not os.path.exists(library_path):
|
if not os.path.exists(library_path):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user