forked from openlp/openlp
openlyrics fixes, add meta data to songs, opensong import fix, signals for openlyrics export
This commit is contained in:
commit
e7b1f88e53
@ -27,11 +27,10 @@
|
|||||||
The song export function for OpenLP.
|
The song export function for OpenLP.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
from openlp.core.lib import Receiver, SettingsManager, translate
|
from openlp.core.lib import Receiver, translate
|
||||||
from openlp.core.ui import criticalErrorMessageBox
|
from openlp.core.ui import criticalErrorMessageBox
|
||||||
from openlp.core.ui.wizard import OpenLPWizard
|
from openlp.core.ui.wizard import OpenLPWizard
|
||||||
from openlp.plugins.songs.lib.db import Song
|
from openlp.plugins.songs.lib.db import Song
|
||||||
@ -59,6 +58,16 @@ class SongExportForm(OpenLPWizard):
|
|||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
OpenLPWizard.__init__(self, parent, plugin, u'songExportWizard',
|
OpenLPWizard.__init__(self, parent, plugin, u'songExportWizard',
|
||||||
u':/wizards/wizard_importsong.bmp')
|
u':/wizards/wizard_importsong.bmp')
|
||||||
|
self.stop_export_flag = False
|
||||||
|
QtCore.QObject.connect(Receiver.get_receiver(),
|
||||||
|
QtCore.SIGNAL(u'openlp_stop_wizard'), self.stop_export)
|
||||||
|
|
||||||
|
def stop_export(self):
|
||||||
|
"""
|
||||||
|
Sets the flag for exporters to stop their export
|
||||||
|
"""
|
||||||
|
log.debug(u'Stopping songs export')
|
||||||
|
self.stop_export_flag = True
|
||||||
|
|
||||||
def setupUi(self, image):
|
def setupUi(self, image):
|
||||||
"""
|
"""
|
||||||
@ -70,25 +79,22 @@ class SongExportForm(OpenLPWizard):
|
|||||||
"""
|
"""
|
||||||
Song wizard specific initialisation.
|
Song wizard specific initialisation.
|
||||||
"""
|
"""
|
||||||
songs = self.plugin.manager.get_all_objects(Song)
|
pass
|
||||||
for song in songs:
|
|
||||||
author_list = u''
|
|
||||||
for author in song.authors:
|
|
||||||
if author_list != u'':
|
|
||||||
author_list = author_list + u', '
|
|
||||||
author_list = author_list + author.display_name
|
|
||||||
song_title = unicode(song.title)
|
|
||||||
song_detail = u'%s (%s)' % (song_title, author_list)
|
|
||||||
song_name = QtGui.QListWidgetItem(song_detail)
|
|
||||||
song_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(song))
|
|
||||||
self.availableListWidget.addItem(song_name)
|
|
||||||
self.availableListWidget.selectAll()
|
|
||||||
|
|
||||||
def customSignals(self):
|
def customSignals(self):
|
||||||
"""
|
"""
|
||||||
Song wizard specific signals.
|
Song wizard specific signals.
|
||||||
"""
|
"""
|
||||||
pass
|
QtCore.QObject.connect(self.addSelected,
|
||||||
|
QtCore.SIGNAL(u'clicked()'), self.onAddSelectedClicked)
|
||||||
|
QtCore.QObject.connect(self.removeSelected,
|
||||||
|
QtCore.SIGNAL(u'clicked()'), self.onRemoveSelectedClicked)
|
||||||
|
QtCore.QObject.connect(self.availableListWidget,
|
||||||
|
QtCore.SIGNAL(u'itemDoubleClicked(QListWidgetItem *)'),
|
||||||
|
self.onAvailableListItemDoubleClicked)
|
||||||
|
QtCore.QObject.connect(self.selectedListWidget,
|
||||||
|
QtCore.SIGNAL(u'itemDoubleClicked(QListWidgetItem *)'),
|
||||||
|
self.onSelectedListItemDoubleClicked)
|
||||||
|
|
||||||
def addCustomPages(self):
|
def addCustomPages(self):
|
||||||
"""
|
"""
|
||||||
@ -105,6 +111,11 @@ class SongExportForm(OpenLPWizard):
|
|||||||
self.verticalLayout.setObjectName(u'verticalLayout')
|
self.verticalLayout.setObjectName(u'verticalLayout')
|
||||||
self.availableListWidget = QtGui.QListWidget(self.availableGroupBox)
|
self.availableListWidget = QtGui.QListWidget(self.availableGroupBox)
|
||||||
self.availableListWidget.setObjectName(u'availableListWidget')
|
self.availableListWidget.setObjectName(u'availableListWidget')
|
||||||
|
self.availableListWidget.setSelectionMode(
|
||||||
|
QtGui.QAbstractItemView.ExtendedSelection)
|
||||||
|
self.availableListWidget.setSizePolicy(
|
||||||
|
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||||
|
self.availableListWidget.setSortingEnabled(True)
|
||||||
self.verticalLayout.addWidget(self.availableListWidget)
|
self.verticalLayout.addWidget(self.availableListWidget)
|
||||||
self.sourceLayout.addWidget(self.availableGroupBox)
|
self.sourceLayout.addWidget(self.availableGroupBox)
|
||||||
self.selectionWidget = QtGui.QWidget(self.sourcePage)
|
self.selectionWidget = QtGui.QWidget(self.sourcePage)
|
||||||
@ -138,9 +149,15 @@ class SongExportForm(OpenLPWizard):
|
|||||||
self.verticalLayout.setObjectName(u'verticalLayout')
|
self.verticalLayout.setObjectName(u'verticalLayout')
|
||||||
self.selectedListWidget = QtGui.QListWidget(self.selectedGroupBox)
|
self.selectedListWidget = QtGui.QListWidget(self.selectedGroupBox)
|
||||||
self.selectedListWidget.setObjectName(u'selectedListWidget')
|
self.selectedListWidget.setObjectName(u'selectedListWidget')
|
||||||
|
self.selectedListWidget.setSelectionMode(
|
||||||
|
QtGui.QAbstractItemView.ExtendedSelection)
|
||||||
|
self.selectedListWidget.setSizePolicy(
|
||||||
|
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||||
|
self.selectedListWidget.setSortingEnabled(True)
|
||||||
self.verticalLayout.addWidget(self.selectedListWidget)
|
self.verticalLayout.addWidget(self.selectedListWidget)
|
||||||
self.sourceLayout.addWidget(self.selectedGroupBox)
|
self.sourceLayout.addWidget(self.selectedGroupBox)
|
||||||
self.addPage(self.sourcePage)
|
self.addPage(self.sourcePage)
|
||||||
|
#TODO: Add save dialog
|
||||||
|
|
||||||
def retranslateUi(self):
|
def retranslateUi(self):
|
||||||
"""
|
"""
|
||||||
@ -158,10 +175,10 @@ class SongExportForm(OpenLPWizard):
|
|||||||
'format. You can import these songs in all lyrics projection '
|
'format. You can import these songs in all lyrics projection '
|
||||||
'software, which supports OpenLyrics.'))
|
'software, which supports OpenLyrics.'))
|
||||||
self.sourcePage.setTitle(
|
self.sourcePage.setTitle(
|
||||||
translate('SongsPlugin.ExportWizardForm', 'Select Emport Source'))
|
translate('SongsPlugin.ExportWizardForm', 'Select Songs'))
|
||||||
self.sourcePage.setSubTitle(
|
self.sourcePage.setSubTitle(
|
||||||
translate('SongsPlugin.ExportWizardForm',
|
translate('SongsPlugin.ExportWizardForm',
|
||||||
'Select the export format, and where to export from.'))
|
'Select the songs, you want to export.'))
|
||||||
|
|
||||||
self.progressPage.setTitle(
|
self.progressPage.setTitle(
|
||||||
translate('SongsPlugin.ExportWizardForm', 'Exporting'))
|
translate('SongsPlugin.ExportWizardForm', 'Exporting'))
|
||||||
@ -187,10 +204,35 @@ class SongExportForm(OpenLPWizard):
|
|||||||
Validate the current page before moving on to the next page.
|
Validate the current page before moving on to the next page.
|
||||||
"""
|
"""
|
||||||
if self.currentPage() == self.welcomePage:
|
if self.currentPage() == self.welcomePage:
|
||||||
|
Receiver.send_message(u'cursor_busy')
|
||||||
|
songs = self.plugin.manager.get_all_objects(Song)
|
||||||
|
for song in songs:
|
||||||
|
author_list = u''
|
||||||
|
for author in song.authors:
|
||||||
|
if author_list != u'':
|
||||||
|
author_list = author_list + u', '
|
||||||
|
author_list = author_list + author.display_name
|
||||||
|
song_title = unicode(song.title)
|
||||||
|
song_detail = u'%s (%s)' % (song_title, author_list)
|
||||||
|
song_name = QtGui.QListWidgetItem(song_detail)
|
||||||
|
song_name.setData(QtCore.Qt.UserRole, QtCore.QVariant(song))
|
||||||
|
self.availableListWidget.addItem(song_name)
|
||||||
|
self.availableListWidget.selectAll()
|
||||||
|
Receiver.send_message(u'cursor_normal')
|
||||||
return True
|
return True
|
||||||
elif self.currentPage() == self.sourcePage:
|
elif self.currentPage() == self.sourcePage:
|
||||||
|
self.selectedListWidget.selectAll()
|
||||||
|
if not self.selectedListWidget.selectedItems():
|
||||||
|
criticalErrorMessageBox(
|
||||||
|
translate('SongsPlugin.ExportWizardForm',
|
||||||
|
'No Song Selected'),
|
||||||
|
translate('SongsPlugin.ImportWizardForm',
|
||||||
|
'You need to add at least one Song to export.'))
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
elif self.currentPage() == self.progressPage:
|
elif self.currentPage() == self.progressPage:
|
||||||
|
self.availableListWidget.clear()
|
||||||
|
self.selectedListWidget.clear()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def registerFields(self):
|
def registerFields(self):
|
||||||
@ -222,8 +264,9 @@ class SongExportForm(OpenLPWizard):
|
|||||||
class, and then runs the ``do_export`` method of the exporter to do
|
class, and then runs the ``do_export`` method of the exporter to do
|
||||||
the actual exporting.
|
the actual exporting.
|
||||||
"""
|
"""
|
||||||
exporter = OpenLyricsExport(self.plugin.manager,
|
songs = [item.data(QtCore.Qt.UserRole).toPyObject()
|
||||||
self.plugin.manager.get_all_objects(Song), u'/tmp/')
|
for item in self.selectedListWidget.selectedItems()]
|
||||||
|
exporter = OpenLyricsExport(self, songs, u'/tmp/')
|
||||||
if exporter.do_export():
|
if exporter.do_export():
|
||||||
self.progressLabel.setText(
|
self.progressLabel.setText(
|
||||||
translate('SongsPlugin.SongExportForm', 'Finished export.'))
|
translate('SongsPlugin.SongExportForm', 'Finished export.'))
|
||||||
@ -231,3 +274,47 @@ class SongExportForm(OpenLPWizard):
|
|||||||
self.progressLabel.setText(
|
self.progressLabel.setText(
|
||||||
translate('SongsPlugin.SongExportForm',
|
translate('SongsPlugin.SongExportForm',
|
||||||
'Your song export failed.'))
|
'Your song export failed.'))
|
||||||
|
|
||||||
|
def onAddSelectedClicked(self):
|
||||||
|
"""
|
||||||
|
Removes the selected items from the list of available songs and add them
|
||||||
|
to the list of selected songs.
|
||||||
|
"""
|
||||||
|
items = self.availableListWidget.selectedItems()
|
||||||
|
# Save a list with tuples which consist of the item row, and the item.
|
||||||
|
items = [(self.availableListWidget.row(item), item) for item in items]
|
||||||
|
items.sort(reverse=True)
|
||||||
|
for item in items:
|
||||||
|
self.availableListWidget.takeItem(item[0])
|
||||||
|
self.selectedListWidget.addItem(item[1])
|
||||||
|
|
||||||
|
def onRemoveSelectedClicked(self):
|
||||||
|
"""
|
||||||
|
Removes the selected items from the list of selected songs and add them
|
||||||
|
back to the list of available songs.
|
||||||
|
"""
|
||||||
|
items = self.selectedListWidget.selectedItems()
|
||||||
|
# Save a list with tuples which consist of the item row, and the item.
|
||||||
|
items = [(self.selectedListWidget.row(item), item) for item in items]
|
||||||
|
items.sort(reverse=True)
|
||||||
|
for item in items:
|
||||||
|
self.selectedListWidget.takeItem(item[0])
|
||||||
|
self.availableListWidget.addItem(item[1])
|
||||||
|
|
||||||
|
def onAvailableListItemDoubleClicked(self, item):
|
||||||
|
"""
|
||||||
|
Adds the double clicked item to the list of selected songs and removes
|
||||||
|
it from the list of availables songs.
|
||||||
|
"""
|
||||||
|
row = self.availableListWidget.row(item)
|
||||||
|
self.availableListWidget.takeItem(row)
|
||||||
|
self.selectedListWidget.addItem(item)
|
||||||
|
|
||||||
|
def onSelectedListItemDoubleClicked(self, item):
|
||||||
|
"""
|
||||||
|
Adds the double clicked item back to the list of available songs and
|
||||||
|
removes it from the list of selected songs.
|
||||||
|
"""
|
||||||
|
row = self.selectedListWidget.row(item)
|
||||||
|
self.selectedListWidget.takeItem(row)
|
||||||
|
self.availableListWidget.addItem(item)
|
||||||
|
@ -27,13 +27,12 @@
|
|||||||
The :mod:`openlyricsexport` module provides the functionality for exporting
|
The :mod:`openlyricsexport` module provides the functionality for exporting
|
||||||
songs from the database.
|
songs from the database.
|
||||||
"""
|
"""
|
||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from lxml import etree, objectify
|
from lxml import etree, objectify
|
||||||
|
|
||||||
from openlp.core.lib import translate
|
from openlp.core.lib import Receiver, translate
|
||||||
from openlp.plugins.songs.lib import OpenLyrics
|
from openlp.plugins.songs.lib import OpenLyrics
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -42,42 +41,34 @@ class OpenLyricsExport(object):
|
|||||||
"""
|
"""
|
||||||
This provides the Openlyrics export.
|
This provides the Openlyrics export.
|
||||||
"""
|
"""
|
||||||
def __init__(self, master_manager, song_ids, save_path):
|
def __init__(self, parent, songs, save_path):
|
||||||
"""
|
"""
|
||||||
Initialise the export.
|
Initialise the export.
|
||||||
"""
|
"""
|
||||||
log.debug(u'initialise OpenLyricsExport')
|
log.debug(u'initialise OpenLyricsExport')
|
||||||
self.master_manager = master_manager
|
self.parent = parent
|
||||||
self.songs = song_ids
|
self.manager = parent.plugin.manager
|
||||||
|
self.songs = songs
|
||||||
self.save_path = save_path
|
self.save_path = save_path
|
||||||
|
|
||||||
def do_export(self):
|
def do_export(self):
|
||||||
"""
|
"""
|
||||||
Export the songs.
|
Export the songs.
|
||||||
"""
|
"""
|
||||||
openLyrics = OpenLyrics(self.master_manager)
|
openLyrics = OpenLyrics(self.manager)
|
||||||
# self.export_wizard.exportProgressBar.setMaximum(len(songs))
|
self.parent.progressBar.setMaximum(len(self.songs))
|
||||||
for song in self.songs:
|
for song in self.songs:
|
||||||
# if self.stop_export_flag:
|
Receiver.send_message(u'openlp_process_events')
|
||||||
# return False
|
if self.parent.stop_export_flag:
|
||||||
# self.export_wizard.incrementProgressBar(unicode(translate(
|
return False
|
||||||
# 'SongsPlugin.OpenLyricsExport', 'Exporting %s...')) %
|
self.parent.incrementProgressBar(unicode(translate(
|
||||||
# song.title)
|
'SongsPlugin.OpenLyricsExport', 'Exporting %s...')) %
|
||||||
|
song.title)
|
||||||
# Check if path exists. If not, create the directories!
|
# Check if path exists. If not, create the directories!
|
||||||
# What do we do with songs with the same title? I do not want to
|
# What do we do with songs with the same title? I do not want to
|
||||||
# overwrite them!
|
# overwrite them!
|
||||||
path = os.path.join(self.save_path, song.title + u'.xml')
|
path = os.path.join(self.save_path, song.title + u'.xml')
|
||||||
# Convert the song object to an unicode string.
|
|
||||||
xml = openLyrics.song_to_xml(song)
|
xml = openLyrics.song_to_xml(song)
|
||||||
song_xml = objectify.fromstring(xml)
|
|
||||||
# Append the necessary meta data to the song.
|
|
||||||
# (Maybe move this to the xml module?
|
|
||||||
song_xml.set(u'version', OpenLyrics.IMPLEMENTED_VERSION)
|
|
||||||
song_xml.set(u'createdIn', u'OpenLP 1.9.4') # Use variable
|
|
||||||
song_xml.set(u'modifiedIn', u'OpenLP 1.9.4') # Use variable
|
|
||||||
song_xml.set(u'modifiedDate',
|
|
||||||
datetime.datetime.now().strftime(u'%Y-%m-%dT%H:%M:%S'))
|
|
||||||
xml = etree.tostring(song_xml)
|
|
||||||
tree = etree.ElementTree(etree.fromstring(xml))
|
tree = etree.ElementTree(etree.fromstring(xml))
|
||||||
tree.write(path, encoding=u'utf-8', xml_declaration=True,
|
tree.write(path, encoding=u'utf-8', xml_declaration=True,
|
||||||
pretty_print=True)
|
pretty_print=True)
|
||||||
|
@ -39,6 +39,7 @@ log = logging.getLogger(__name__)
|
|||||||
class OpenSongImportError(Exception):
|
class OpenSongImportError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
#TODO: Use lxml for parsing and make sure we use methods of "SongImport" .
|
||||||
class OpenSongImport(SongImport):
|
class OpenSongImport(SongImport):
|
||||||
"""
|
"""
|
||||||
Import songs exported from OpenSong
|
Import songs exported from OpenSong
|
||||||
@ -279,7 +280,7 @@ class OpenSongImport(SongImport):
|
|||||||
for num in versenums:
|
for num in versenums:
|
||||||
versetag = u'%s%s' % (our_verse_type, num)
|
versetag = u'%s%s' % (our_verse_type, num)
|
||||||
lines = u'\n'.join(verses[versetype][num])
|
lines = u'\n'.join(verses[versetype][num])
|
||||||
self.verses.append([versetag, lines])
|
self.add_verse(lines, versetag)
|
||||||
# Keep track of what we have for error checking later
|
# Keep track of what we have for error checking later
|
||||||
versetags[versetag] = 1
|
versetags[versetag] = 1
|
||||||
# now figure out the presentation order
|
# now figure out the presentation order
|
||||||
@ -295,6 +296,8 @@ class OpenSongImport(SongImport):
|
|||||||
else:
|
else:
|
||||||
log.warn(u'No verse order available for %s, skipping.',
|
log.warn(u'No verse order available for %s, skipping.',
|
||||||
self.title)
|
self.title)
|
||||||
|
# TODO: make sure that the default order list will be overwritten, if
|
||||||
|
# the songs provides its own order list.
|
||||||
for tag in order:
|
for tag in order:
|
||||||
if tag[0].isdigit():
|
if tag[0].isdigit():
|
||||||
# Assume it's a verse if it has no prefix
|
# Assume it's a verse if it has no prefix
|
||||||
|
@ -60,6 +60,7 @@ The XML of `OpenLyrics <http://openlyrics.info/>`_ songs is of the format::
|
|||||||
</song>
|
</song>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -207,7 +208,8 @@ class OpenLyrics(object):
|
|||||||
This property is not supported.
|
This property is not supported.
|
||||||
|
|
||||||
*<verse name="v1a" lang="he" translit="en">*
|
*<verse name="v1a" lang="he" translit="en">*
|
||||||
The attribute *translit* is not supported.
|
The attribute *translit* is not supported. Note, the attribute *lang* is
|
||||||
|
considered, but there is not further functionality implemented yet.
|
||||||
|
|
||||||
*<verseOrder>*
|
*<verseOrder>*
|
||||||
OpenLP supports this property.
|
OpenLP supports this property.
|
||||||
@ -222,8 +224,14 @@ class OpenLyrics(object):
|
|||||||
"""
|
"""
|
||||||
sxml = SongXML()
|
sxml = SongXML()
|
||||||
verse_list = sxml.get_verses(song.lyrics)
|
verse_list = sxml.get_verses(song.lyrics)
|
||||||
song_xml = objectify.fromstring(
|
song_xml = objectify.fromstring(u'<song/>')
|
||||||
u'<song version="0.7" createdIn="OpenLP 2.0"/>')
|
# Append the necessary meta data to the song.
|
||||||
|
song_xml.set(u'xmlns', u'http://openlyrics.info/namespace/2009/song')
|
||||||
|
song_xml.set(u'version', OpenLyrics.IMPLEMENTED_VERSION)
|
||||||
|
song_xml.set(u'createdIn', u'OpenLP 1.9.4') # Use variable
|
||||||
|
song_xml.set(u'modifiedIn', u'OpenLP 1.9.4') # Use variable
|
||||||
|
song_xml.set(u'modifiedDate',
|
||||||
|
datetime.datetime.now().strftime(u'%Y-%m-%dT%H:%M:%S'))
|
||||||
properties = etree.SubElement(song_xml, u'properties')
|
properties = etree.SubElement(song_xml, u'properties')
|
||||||
titles = etree.SubElement(properties, u'titles')
|
titles = etree.SubElement(properties, u'titles')
|
||||||
self._add_text_to_element(u'title', titles, song.title.strip())
|
self._add_text_to_element(u'title', titles, song.title.strip())
|
||||||
@ -237,7 +245,7 @@ class OpenLyrics(object):
|
|||||||
self._add_text_to_element(u'copyright', properties, song.copyright)
|
self._add_text_to_element(u'copyright', properties, song.copyright)
|
||||||
if song.verse_order:
|
if song.verse_order:
|
||||||
self._add_text_to_element(
|
self._add_text_to_element(
|
||||||
u'verseOrder', properties, song.verse_order)
|
u'verseOrder', properties, song.verse_order.lower())
|
||||||
if song.ccli_number:
|
if song.ccli_number:
|
||||||
self._add_text_to_element(u'ccliNo', properties, song.ccli_number)
|
self._add_text_to_element(u'ccliNo', properties, song.ccli_number)
|
||||||
if song.authors:
|
if song.authors:
|
||||||
@ -450,7 +458,7 @@ class OpenLyrics(object):
|
|||||||
text += u'\n'
|
text += u'\n'
|
||||||
text += u'\n'.join([unicode(line) for line in lines.line])
|
text += u'\n'.join([unicode(line) for line in lines.line])
|
||||||
verse_name = self._get(verse, u'name')
|
verse_name = self._get(verse, u'name')
|
||||||
verse_type = unicode(VerseType.to_string(verse_name[0]))[0]
|
verse_type = unicode(VerseType.to_string(verse_name[0]))
|
||||||
verse_number = re.compile(u'[a-zA-Z]*').sub(u'', verse_name)
|
verse_number = re.compile(u'[a-zA-Z]*').sub(u'', verse_name)
|
||||||
verse_part = re.compile(u'[0-9]*').sub(u'', verse_name[1:])
|
verse_part = re.compile(u'[0-9]*').sub(u'', verse_name[1:])
|
||||||
# OpenLyrics allows e. g. "c", but we need "c1".
|
# OpenLyrics allows e. g. "c", but we need "c1".
|
||||||
|
Loading…
Reference in New Issue
Block a user