Merge trunk

This commit is contained in:
Ken Roberts 2017-06-01 15:36:38 -07:00
commit 9f8433a18e
24 changed files with 468 additions and 523 deletions

View File

@ -26,7 +26,6 @@ import os
import logging import logging
import json import json
from xml.dom.minidom import Document
from lxml import etree, objectify from lxml import etree, objectify
from openlp.core.common import AppLocation, de_hump from openlp.core.common import AppLocation, de_hump
@ -150,7 +149,7 @@ INTEGER_LIST = ['size', 'line_adjustment', 'x', 'height', 'y', 'width', 'shadow_
'horizontal_align', 'vertical_align', 'wrap_style'] 'horizontal_align', 'vertical_align', 'wrap_style']
class ThemeXML(object): class Theme(object):
""" """
A class to encapsulate the Theme XML. A class to encapsulate the Theme XML.
""" """
@ -195,184 +194,6 @@ class ThemeXML(object):
self.background_filename = self.background_filename.strip() self.background_filename = self.background_filename.strip()
self.background_filename = os.path.join(path, self.theme_name, self.background_filename) self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
def _new_document(self, name):
"""
Create a new theme XML document.
"""
self.theme_xml = Document()
self.theme = self.theme_xml.createElement('theme')
self.theme_xml.appendChild(self.theme)
self.theme.setAttribute('version', '2.0')
self.name = self.theme_xml.createElement('name')
text_node = self.theme_xml.createTextNode(name)
self.name.appendChild(text_node)
self.theme.appendChild(self.name)
def add_background_transparent(self):
"""
Add a transparent background.
"""
background = self.theme_xml.createElement('background')
background.setAttribute('type', 'transparent')
self.theme.appendChild(background)
def add_background_solid(self, bkcolor):
"""
Add a Solid background.
:param bkcolor: The color of the background.
"""
background = self.theme_xml.createElement('background')
background.setAttribute('type', 'solid')
self.theme.appendChild(background)
self.child_element(background, 'color', str(bkcolor))
def add_background_gradient(self, startcolor, endcolor, direction):
"""
Add a gradient background.
:param startcolor: The gradient's starting colour.
:param endcolor: The gradient's ending colour.
:param direction: The direction of the gradient.
"""
background = self.theme_xml.createElement('background')
background.setAttribute('type', 'gradient')
self.theme.appendChild(background)
# Create startColor element
self.child_element(background, 'startColor', str(startcolor))
# Create endColor element
self.child_element(background, 'endColor', str(endcolor))
# Create direction element
self.child_element(background, 'direction', str(direction))
def add_background_image(self, filename, border_color):
"""
Add a image background.
:param filename: The file name of the image.
:param border_color:
"""
background = self.theme_xml.createElement('background')
background.setAttribute('type', 'image')
self.theme.appendChild(background)
# Create Filename element
self.child_element(background, 'filename', filename)
# Create endColor element
self.child_element(background, 'borderColor', str(border_color))
def add_background_video(self, filename, border_color):
"""
Add a video background.
:param filename: The file name of the video.
:param border_color:
"""
background = self.theme_xml.createElement('background')
background.setAttribute('type', 'video')
self.theme.appendChild(background)
# Create Filename element
self.child_element(background, 'filename', filename)
# Create endColor element
self.child_element(background, 'borderColor', str(border_color))
def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
"""
Add a Font.
:param name: The name of the font.
:param color: The colour of the font.
:param size: The size of the font.
:param override: Whether or not to override the default positioning of the theme.
:param fonttype: The type of font, ``main`` or ``footer``. Defaults to ``main``.
:param bold:
:param italics: The weight of then font Defaults to 50 Normal
:param line_adjustment: Does the font render to italics Defaults to 0 Normal
:param xpos: The X position of the text block.
:param ypos: The Y position of the text block.
:param width: The width of the text block.
:param height: The height of the text block.
:param outline: Whether or not to show an outline.
:param outline_color: The colour of the outline.
:param outline_pixel: How big the Shadow is
:param shadow: Whether or not to show a shadow.
:param shadow_color: The colour of the shadow.
:param shadow_pixel: How big the Shadow is
"""
background = self.theme_xml.createElement('font')
background.setAttribute('type', fonttype)
self.theme.appendChild(background)
# Create Font name element
self.child_element(background, 'name', name)
# Create Font color element
self.child_element(background, 'color', str(color))
# Create Proportion name element
self.child_element(background, 'size', str(size))
# Create weight name element
self.child_element(background, 'bold', str(bold))
# Create italics name element
self.child_element(background, 'italics', str(italics))
# Create indentation name element
self.child_element(background, 'line_adjustment', str(line_adjustment))
# Create Location element
element = self.theme_xml.createElement('location')
element.setAttribute('override', str(override))
element.setAttribute('x', str(xpos))
element.setAttribute('y', str(ypos))
element.setAttribute('width', str(width))
element.setAttribute('height', str(height))
background.appendChild(element)
# Shadow
element = self.theme_xml.createElement('shadow')
element.setAttribute('shadowColor', str(shadow_color))
element.setAttribute('shadowSize', str(shadow_pixel))
value = self.theme_xml.createTextNode(str(shadow))
element.appendChild(value)
background.appendChild(element)
# Outline
element = self.theme_xml.createElement('outline')
element.setAttribute('outlineColor', str(outline_color))
element.setAttribute('outlineSize', str(outline_pixel))
value = self.theme_xml.createTextNode(str(outline))
element.appendChild(value)
background.appendChild(element)
def add_display(self, horizontal, vertical, transition):
"""
Add a Display options.
:param horizontal: The horizontal alignment of the text.
:param vertical: The vertical alignment of the text.
:param transition: Whether the slide transition is active.
"""
background = self.theme_xml.createElement('display')
self.theme.appendChild(background)
# Horizontal alignment
element = self.theme_xml.createElement('horizontalAlign')
value = self.theme_xml.createTextNode(str(horizontal))
element.appendChild(value)
background.appendChild(element)
# Vertical alignment
element = self.theme_xml.createElement('verticalAlign')
value = self.theme_xml.createTextNode(str(vertical))
element.appendChild(value)
background.appendChild(element)
# Slide Transition
element = self.theme_xml.createElement('slideTransition')
value = self.theme_xml.createTextNode(str(transition))
element.appendChild(value)
background.appendChild(element)
def child_element(self, element, tag, value):
"""
Generic child element creator.
"""
child = self.theme_xml.createElement(tag)
child.appendChild(self.theme_xml.createTextNode(value))
element.appendChild(child)
return child
def set_default_header_footer(self): def set_default_header_footer(self):
""" """
Set the header and footer size into the current primary screen. Set the header and footer size into the current primary screen.
@ -386,25 +207,24 @@ class ThemeXML(object):
self.font_footer_y = current_screen['size'].height() * 9 / 10 self.font_footer_y = current_screen['size'].height() * 9 / 10
self.font_footer_height = current_screen['size'].height() / 10 self.font_footer_height = current_screen['size'].height() / 10
def dump_xml(self): def load_theme(self, theme):
""" """
Dump the XML to file used for debugging Convert the JSON file and expand it.
"""
return self.theme_xml.toprettyxml(indent=' ')
def extract_xml(self): :param theme: the theme string
""" """
Print out the XML string. jsn = json.loads(theme)
""" self.expand_json(jsn)
self._build_xml_from_attrs()
return self.theme_xml.toxml('utf-8').decode('utf-8')
def extract_formatted_xml(self): def export_theme(self):
""" """
Pull out the XML string formatted for human consumption Loop through the fields and build a dictionary of them
""" """
self._build_xml_from_attrs() theme_data = {}
return self.theme_xml.toprettyxml(indent=' ', newl='\n', encoding='utf-8') for attr, value in self.__dict__.items():
theme_data["{attr}".format(attr=attr)] = value
return json.dumps(theme_data)
def parse(self, xml): def parse(self, xml):
""" """
@ -461,7 +281,8 @@ class ThemeXML(object):
if element.tag == 'name': if element.tag == 'name':
self._create_attr('theme', element.tag, element.text) self._create_attr('theme', element.tag, element.text)
def _translate_tags(self, master, element, value): @staticmethod
def _translate_tags(master, element, value):
""" """
Clean up XML removing and redefining tags Clean up XML removing and redefining tags
""" """
@ -514,71 +335,5 @@ class ThemeXML(object):
theme_strings = [] theme_strings = []
for key in dir(self): for key in dir(self):
if key[0:1] != '_': if key[0:1] != '_':
# TODO: Due to bound methods returned, I don't know how to write a proper test
theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key))) theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
return '\n'.join(theme_strings) return '\n'.join(theme_strings)
def _build_xml_from_attrs(self):
"""
Build the XML from the varables in the object
"""
self._new_document(self.theme_name)
if self.background_type == BackgroundType.to_string(BackgroundType.Solid):
self.add_background_solid(self.background_color)
elif self.background_type == BackgroundType.to_string(BackgroundType.Gradient):
self.add_background_gradient(
self.background_start_color,
self.background_end_color,
self.background_direction
)
elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
filename = os.path.split(self.background_filename)[1]
self.add_background_image(filename, self.background_border_color)
elif self.background_type == BackgroundType.to_string(BackgroundType.Video):
filename = os.path.split(self.background_filename)[1]
self.add_background_video(filename, self.background_border_color)
elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
self.add_background_transparent()
self.add_font(
self.font_main_name,
self.font_main_color,
self.font_main_size,
self.font_main_override, 'main',
self.font_main_bold,
self.font_main_italics,
self.font_main_line_adjustment,
self.font_main_x,
self.font_main_y,
self.font_main_width,
self.font_main_height,
self.font_main_outline,
self.font_main_outline_color,
self.font_main_outline_size,
self.font_main_shadow,
self.font_main_shadow_color,
self.font_main_shadow_size
)
self.add_font(
self.font_footer_name,
self.font_footer_color,
self.font_footer_size,
self.font_footer_override, 'footer',
self.font_footer_bold,
self.font_footer_italics,
0, # line adjustment
self.font_footer_x,
self.font_footer_y,
self.font_footer_width,
self.font_footer_height,
self.font_footer_outline,
self.font_footer_outline_color,
self.font_footer_outline_size,
self.font_footer_shadow,
self.font_footer_shadow_color,
self.font_footer_shadow_size
)
self.add_display(
self.display_horizontal_align,
self.display_vertical_align,
self.display_slide_transition
)

14
openlp/core/ui/lib/listwidgetwithdnd.py Normal file → Executable file
View File

@ -44,7 +44,6 @@ class ListWidgetWithDnD(QtWidgets.QListWidget):
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.locked = False
def activateDnD(self): def activateDnD(self):
""" """
@ -54,15 +53,13 @@ class ListWidgetWithDnD(QtWidgets.QListWidget):
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
def clear(self, search_while_typing=False, override_lock=False): def clear(self, search_while_typing=False):
""" """
Re-implement clear, so that we can customise feedback when using 'Search as you type' Re-implement clear, so that we can customise feedback when using 'Search as you type'
:param search_while_typing: True if we want to display the customised message :param search_while_typing: True if we want to display the customised message
:return: None :return: None
""" """
if self.locked and not override_lock:
return
if search_while_typing: if search_while_typing:
self.no_results_text = UiStrings().ShortResults self.no_results_text = UiStrings().ShortResults
else: else:
@ -128,6 +125,15 @@ class ListWidgetWithDnD(QtWidgets.QListWidget):
else: else:
event.ignore() event.ignore()
def allItems(self):
"""
An generator to list all the items in the widget
:return: a generator
"""
for row in range(self.count()):
yield self.item(row)
def paintEvent(self, event): def paintEvent(self, event):
""" """
Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty. Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty.

View File

@ -698,7 +698,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
translate('OpenLP.ServiceManager', translate('OpenLP.ServiceManager',
'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
else: else:
file_name, filter_uesd = QtWidgets.QFileDialog.getSaveFileName( file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
self.main_window, UiStrings().SaveService, path, self.main_window, UiStrings().SaveService, path,
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;')) translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
if not file_name: if not file_name:

View File

@ -22,6 +22,7 @@
""" """
The Theme Manager manages adding, deleteing and modifying of themes. The Theme Manager manages adding, deleteing and modifying of themes.
""" """
import json
import os import os
import zipfile import zipfile
import shutil import shutil
@ -33,7 +34,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settin
check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file
from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \ from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \
check_item_selected, create_thumb, validate_thumb check_item_selected, create_thumb, validate_thumb
from openlp.core.lib.theme import ThemeXML, BackgroundType from openlp.core.lib.theme import Theme, BackgroundType
from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.ui import FileRenameForm, ThemeForm from openlp.core.ui import FileRenameForm, ThemeForm
from openlp.core.ui.lib import OpenLPToolbar from openlp.core.ui.lib import OpenLPToolbar
@ -245,7 +246,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
their customisations. their customisations.
:param field: :param field:
""" """
theme = ThemeXML() theme = Theme()
theme.set_default_header_footer() theme.set_default_header_footer()
self.theme_form.theme = theme self.theme_form.theme = theme
self.theme_form.exec() self.theme_form.exec()
@ -378,11 +379,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.')) critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
return return
theme = item.data(QtCore.Qt.UserRole) theme = item.data(QtCore.Qt.UserRole)
path = QtWidgets.QFileDialog.getExistingDirectory(self, path, filter_used = \
translate('OpenLP.ThemeManager', QtWidgets.QFileDialog.getSaveFileName(self.main_window,
'Save Theme - ({name})').format(name=theme), translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
Settings().value(self.settings_section + format(name=theme),
'/last directory export')) Settings().value(self.settings_section + '/last directory export'),
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
self.application.set_busy_cursor() self.application.set_busy_cursor()
if path: if path:
Settings().setValue(self.settings_section + '/last directory export', path) Settings().setValue(self.settings_section + '/last directory export', path)
@ -393,13 +395,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
'Your theme has been successfully exported.')) 'Your theme has been successfully exported.'))
self.application.set_normal_cursor() self.application.set_normal_cursor()
def _export_theme(self, path, theme): def _export_theme(self, theme_path, theme):
""" """
Create the zipfile with the theme contents. Create the zipfile with the theme contents.
:param path: Location where the zip file will be placed :param theme_path: Location where the zip file will be placed
:param theme: The name of the theme to be exported :param theme: The name of the theme to be exported
""" """
theme_path = os.path.join(path, theme + '.otz')
theme_zip = None theme_zip = None
try: try:
theme_zip = zipfile.ZipFile(theme_path, 'w') theme_zip = zipfile.ZipFile(theme_path, 'w')
@ -452,7 +453,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
files = AppLocation.get_files(self.settings_section, '.png') files = AppLocation.get_files(self.settings_section, '.png')
# No themes have been found so create one # No themes have been found so create one
if not files: if not files:
theme = ThemeXML() theme = Theme()
theme.theme_name = UiStrings().Default theme.theme_name = UiStrings().Default
self._write_theme(theme, None, None) self._write_theme(theme, None, None)
Settings().setValue(self.settings_section + '/global theme', theme.theme_name) Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
@ -505,19 +506,27 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
def get_theme_data(self, theme_name): def get_theme_data(self, theme_name):
""" """
Returns a theme object from an XML file Returns a theme object from an XML or JSON file
:param theme_name: Name of the theme to load from file :param theme_name: Name of the theme to load from file
:return: The theme object. :return: The theme object.
""" """
self.log_debug('get theme data for theme {name}'.format(name=theme_name)) self.log_debug('get theme data for theme {name}'.format(name=theme_name))
xml_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml') theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json')
xml = get_text_file_string(xml_file) theme_data = get_text_file_string(theme_file)
if not xml: jsn = True
if not theme_data:
theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
theme_data = get_text_file_string(theme_file)
jsn = False
if not theme_data:
self.log_debug('No theme data - using default theme') self.log_debug('No theme data - using default theme')
return ThemeXML() return Theme()
else: else:
return self._create_theme_from_xml(xml, self.path) if jsn:
return self._create_theme_from_json(theme_data, self.path)
else:
return self._create_theme_from_xml(theme_data, self.path)
def over_write_message_box(self, theme_name): def over_write_message_box(self, theme_name):
""" """
@ -547,18 +556,28 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
out_file = None out_file = None
file_xml = None file_xml = None
abort_import = True abort_import = True
json_theme = False
theme_name = ""
try: try:
theme_zip = zipfile.ZipFile(file_name) theme_zip = zipfile.ZipFile(file_name)
xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml'] json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
if len(xml_file) != 1: if len(json_file) != 1:
self.log_error('Theme contains "{val:d}" XML files'.format(val=len(xml_file))) # TODO: remove XML handling at some point but would need a auto conversion to run first.
raise ValidationError xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot() if len(xml_file) != 1:
theme_version = xml_tree.get('version', default=None) self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
if not theme_version or float(theme_version) < 2.0: raise ValidationError
self.log_error('Theme version is less than 2.0') xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
raise ValidationError theme_version = xml_tree.get('version', default=None)
theme_name = xml_tree.find('name').text.strip() if not theme_version or float(theme_version) < 2.0:
self.log_error('Theme version is less than 2.0')
raise ValidationError
theme_name = xml_tree.find('name').text.strip()
else:
new_theme = Theme()
new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
theme_name = new_theme.theme_name
json_theme = True
theme_folder = os.path.join(directory, theme_name) theme_folder = os.path.join(directory, theme_name)
theme_exists = os.path.exists(theme_folder) theme_exists = os.path.exists(theme_folder)
if theme_exists and not self.over_write_message_box(theme_name): if theme_exists and not self.over_write_message_box(theme_name):
@ -574,7 +593,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
continue continue
full_name = os.path.join(directory, out_name) full_name = os.path.join(directory, out_name)
check_directory_exists(os.path.dirname(full_name)) check_directory_exists(os.path.dirname(full_name))
if os.path.splitext(name)[1].lower() == '.xml': if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
file_xml = str(theme_zip.read(name), 'utf-8') file_xml = str(theme_zip.read(name), 'utf-8')
out_file = open(full_name, 'w', encoding='utf-8') out_file = open(full_name, 'w', encoding='utf-8')
out_file.write(file_xml) out_file.write(file_xml)
@ -597,7 +616,10 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
if not abort_import: if not abort_import:
# As all files are closed, we can create the Theme. # As all files are closed, we can create the Theme.
if file_xml: if file_xml:
theme = self._create_theme_from_xml(file_xml, self.path) if json_theme:
theme = self._create_theme_from_json(file_xml, self.path)
else:
theme = self._create_theme_from_xml(file_xml, self.path)
self.generate_and_save_image(theme_name, theme) self.generate_and_save_image(theme_name, theme)
# Only show the error message, when IOError was not raised (in # Only show the error message, when IOError was not raised (in
# this case the error message has already been shown). # this case the error message has already been shown).
@ -646,16 +668,16 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
:param image_to: Where the Theme Image is to be saved to :param image_to: Where the Theme Image is to be saved to
""" """
name = theme.theme_name name = theme.theme_name
theme_pretty_xml = theme.extract_formatted_xml() theme_pretty = theme.export_theme()
theme_dir = os.path.join(self.path, name) theme_dir = os.path.join(self.path, name)
check_directory_exists(theme_dir) check_directory_exists(theme_dir)
theme_file = os.path.join(theme_dir, name + '.xml') theme_file = os.path.join(theme_dir, name + '.json')
if self.old_background_image and image_to != self.old_background_image: if self.old_background_image and image_to != self.old_background_image:
delete_file(self.old_background_image) delete_file(self.old_background_image)
out_file = None out_file = None
try: try:
out_file = open(theme_file, 'w', encoding='utf-8') out_file = open(theme_file, 'w', encoding='utf-8')
out_file.write(theme_pretty_xml.decode('utf-8')) out_file.write(theme_pretty)
except IOError: except IOError:
self.log_exception('Saving theme to file failed') self.log_exception('Saving theme to file failed')
finally: finally:
@ -717,7 +739,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
""" """
return os.path.join(self.path, theme + '.png') return os.path.join(self.path, theme + '.png')
def _create_theme_from_xml(self, theme_xml, image_path): @staticmethod
def _create_theme_from_xml(theme_xml, image_path):
""" """
Return a theme object using information parsed from XML Return a theme object using information parsed from XML
@ -725,11 +748,25 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
:param image_path: Where the theme image is stored :param image_path: Where the theme image is stored
:return: Theme data. :return: Theme data.
""" """
theme = ThemeXML() theme = Theme()
theme.parse(theme_xml) theme.parse(theme_xml)
theme.extend_image_filename(image_path) theme.extend_image_filename(image_path)
return theme return theme
@staticmethod
def _create_theme_from_json(theme_json, image_path):
"""
Return a theme object using information parsed from JSON
:param theme_json: The Theme data object.
:param image_path: Where the theme image is stored
:return: Theme data.
"""
theme = Theme()
theme.load_theme(theme_json)
theme.extend_image_filename(image_path)
return theme
def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True): def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True):
""" """
Check to see if theme has been selected and the destructive action is allowed. Check to see if theme has been selected and the destructive action is allowed.

View File

@ -341,10 +341,10 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
if not book_ref_id: if not book_ref_id:
book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection) book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection)
elif not bible.get_book_by_book_ref_id(book_ref_id): elif not bible.get_book_by_book_ref_id(book_ref_id):
return False return []
# We have not found the book so do not continue # We have not found the book so do not continue
if not book_ref_id: if not book_ref_id:
return False return []
ranges = match.group('ranges') ranges = match.group('ranges')
range_list = get_reference_match('range_separator').split(ranges) range_list = get_reference_match('range_separator').split(ranges)
ref_list = [] ref_list = []
@ -403,7 +403,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
return ref_list return ref_list
else: else:
log.debug('Invalid reference: {text}'.format(text=reference)) log.debug('Invalid reference: {text}'.format(text=reference))
return None return []
class SearchResults(object): class SearchResults(object):

View File

@ -158,6 +158,7 @@ class BibleDB(Manager):
self.get_name() self.get_name()
if 'path' in kwargs: if 'path' in kwargs:
self.path = kwargs['path'] self.path = kwargs['path']
self._is_web_bible = None
def get_name(self): def get_name(self):
""" """
@ -426,6 +427,18 @@ class BibleDB(Manager):
return 0 return 0
return count return count
@property
def is_web_bible(self):
"""
A read only property indicating if the bible is a 'web bible'
:return: If the bible is a web bible.
:rtype: bool
"""
if self._is_web_bible is None:
self._is_web_bible = bool(self.get_object(BibleMeta, 'download_source'))
return self._is_web_bible
def dump_bible(self): def dump_bible(self):
""" """
Utility debugging method to dump the contents of a bible. Utility debugging method to dump the contents of a bible.

View File

@ -142,8 +142,8 @@ class BibleManager(OpenLPMixin, RegistryProperties):
log.debug('Bible Name: "{name}"'.format(name=name)) log.debug('Bible Name: "{name}"'.format(name=name))
self.db_cache[name] = bible self.db_cache[name] = bible
# Look to see if lazy load bible exists and get create getter. # Look to see if lazy load bible exists and get create getter.
source = self.db_cache[name].get_object(BibleMeta, 'download_source') if self.db_cache[name].is_web_bible:
if source: source = self.db_cache[name].get_object(BibleMeta, 'download_source')
download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value
meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server') meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server')
web_bible = HTTPBible(self.parent, path=self.path, file=filename, download_source=source.value, web_bible = HTTPBible(self.parent, path=self.path, file=filename, download_source=source.value,
@ -278,7 +278,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
:param show_error: :param show_error:
""" """
if not bible or not ref_list: if not bible or not ref_list:
return None return []
return self.db_cache[bible].get_verses(ref_list, show_error) return self.db_cache[bible].get_verses(ref_list, show_error)
def get_language_selection(self, bible): def get_language_selection(self, bible):
@ -305,11 +305,17 @@ class BibleManager(OpenLPMixin, RegistryProperties):
""" """
Does a verse search for the given bible and text. Does a verse search for the given bible and text.
:param bible: The bible to search in (unicode). :param bible: The bible to search
:param second_bible: The second bible (unicode). We do not search in this bible. :type bible: str
:param text: The text to search for (unicode). :param text: The text to search for
:type text: str
:return: The search results if valid, or None if the search is invalid.
:rtype: None, list
""" """
log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text)) log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text))
if not text:
return None
# If no bibles are installed, message is given. # If no bibles are installed, message is given.
if not bible: if not bible:
self.main_window.information_message( self.main_window.information_message(
@ -317,8 +323,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
UiStrings().BibleNoBibles) UiStrings().BibleNoBibles)
return None return None
# Check if the bible or second_bible is a web bible. # Check if the bible or second_bible is a web bible.
web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source') if self.db_cache[bible].is_web_bible:
if web_bible:
# If either Bible is Web, cursor is reset to normal and message is given. # If either Bible is Web, cursor is reset to normal and message is given.
self.application.set_normal_cursor() self.application.set_normal_cursor()
self.main_window.information_message( self.main_window.information_message(
@ -328,41 +333,8 @@ class BibleManager(OpenLPMixin, RegistryProperties):
'This means that the currently selected Bible is a Web Bible.') 'This means that the currently selected Bible is a Web Bible.')
) )
return None return None
# Shorter than 3 char searches break OpenLP with very long search times, thus they are blocked.
if len(text) - text.count(' ') < 3:
return None
# Fetch the results from db. If no results are found, return None, no message is given for this. # Fetch the results from db. If no results are found, return None, no message is given for this.
elif text: if text:
return self.db_cache[bible].verse_search(text)
else:
return None
def verse_search_while_typing(self, bible, second_bible, text):
"""
Does a verse search for the given bible and text.
This is used during "Search while typing"
It's the same thing as the normal text search, but it does not show the web Bible error.
(It would result in the error popping every time a char is entered or removed)
It also does not have a minimum text len, this is set in mediaitem.py
:param bible: The bible to search in (unicode).
:param second_bible: The second bible (unicode). We do not search in this bible.
:param text: The text to search for (unicode).
"""
# If no bibles are installed, message is given.
if not bible:
return None
# Check if the bible or second_bible is a web bible.
web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source')
second_web_bible = ''
if second_bible:
second_web_bible = self.db_cache[second_bible].get_object(BibleMeta, 'download_source')
if web_bible or second_web_bible:
# If either Bible is Web, cursor is reset to normal and search ends w/o any message.
self.application.set_normal_cursor()
return None
# Fetch the results from db. If no results are found, return None, no message is given for this.
elif text:
return self.db_cache[bible].verse_search(text) return self.db_cache[bible].verse_search(text)
else: else:
return None return None

241
openlp/plugins/bibles/lib/mediaitem.py Normal file → Executable file
View File

@ -22,6 +22,7 @@
import logging import logging
import re import re
from enum import IntEnum, unique
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
@ -48,15 +49,45 @@ def get_reference_separators():
'list': get_reference_separator('sep_l_display')} 'list': get_reference_separator('sep_l_display')}
class BibleSearch(object): @unique
class BibleSearch(IntEnum):
""" """
Enumeration class for the different search methods for the "Search" tab. Enumeration class for the different search types for the "Search" tab.
""" """
Reference = 1 Reference = 1
Text = 2 Text = 2
Combined = 3 Combined = 3
@unique
class ResultsTab(IntEnum):
"""
Enumeration class for the different tabs for the results list.
"""
Saved = 0
Search = 1
@unique
class SearchStatus(IntEnum):
"""
Enumeration class for the different search methods.
"""
SearchButton = 0
SearchAsYouType = 1
NotEnoughText = 2
@unique
class SearchTabs(IntEnum):
"""
Enumeration class for the tabs on the media item.
"""
Search = 0
Select = 1
Options = 2
class BibleMediaItem(MediaManagerItem): class BibleMediaItem(MediaManagerItem):
""" """
This is the custom media manager item for Bibles. This is the custom media manager item for Bibles.
@ -73,11 +104,13 @@ class BibleMediaItem(MediaManagerItem):
:param kwargs: Keyword arguments to pass to the super method. (dict) :param kwargs: Keyword arguments to pass to the super method. (dict)
""" """
self.clear_icon = build_icon(':/bibles/bibles_search_clear.png') self.clear_icon = build_icon(':/bibles/bibles_search_clear.png')
self.lock_icon = build_icon(':/bibles/bibles_search_lock.png') self.save_results_icon = build_icon(':/bibles/bibles_save_results.png')
self.unlock_icon = build_icon(':/bibles/bibles_search_unlock.png')
self.sort_icon = build_icon(':/bibles/bibles_book_sort.png') self.sort_icon = build_icon(':/bibles/bibles_book_sort.png')
self.bible = None self.bible = None
self.second_bible = None self.second_bible = None
self.saved_results = []
self.current_results = []
self.search_status = SearchStatus.SearchButton
# TODO: Make more central and clean up after! # TODO: Make more central and clean up after!
self.search_timer = QtCore.QTimer() self.search_timer = QtCore.QTimer()
self.search_timer.setInterval(200) self.search_timer.setInterval(200)
@ -162,8 +195,10 @@ class BibleMediaItem(MediaManagerItem):
self.select_tab.setVisible(False) self.select_tab.setVisible(False)
self.page_layout.addWidget(self.select_tab) self.page_layout.addWidget(self.select_tab)
# General Search Opions # General Search Opions
self.options_widget = QtWidgets.QGroupBox(translate('BiblesPlugin.MediaItem', 'Options'), self) self.options_tab = QtWidgets.QWidget()
self.general_bible_layout = QtWidgets.QFormLayout(self.options_widget) self.options_tab.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.search_tab_bar.addTab(translate('BiblesPlugin.MediaItem', 'Options'))
self.general_bible_layout = QtWidgets.QFormLayout(self.options_tab)
self.version_combo_box = create_horizontal_adjusting_combo_box(self, 'version_combo_box') self.version_combo_box = create_horizontal_adjusting_combo_box(self, 'version_combo_box')
self.general_bible_layout.addRow('{version}:'.format(version=UiStrings().Version), self.version_combo_box) self.general_bible_layout.addRow('{version}:'.format(version=UiStrings().Version), self.version_combo_box)
self.second_combo_box = create_horizontal_adjusting_combo_box(self, 'second_combo_box') self.second_combo_box = create_horizontal_adjusting_combo_box(self, 'second_combo_box')
@ -171,20 +206,28 @@ class BibleMediaItem(MediaManagerItem):
self.style_combo_box = create_horizontal_adjusting_combo_box(self, 'style_combo_box') self.style_combo_box = create_horizontal_adjusting_combo_box(self, 'style_combo_box')
self.style_combo_box.addItems(['', '', '']) self.style_combo_box.addItems(['', '', ''])
self.general_bible_layout.addRow(UiStrings().LayoutStyle, self.style_combo_box) self.general_bible_layout.addRow(UiStrings().LayoutStyle, self.style_combo_box)
self.search_button_layout = QtWidgets.QHBoxLayout() self.options_tab.setVisible(False)
self.page_layout.addWidget(self.options_tab)
# This widget is the easier way to reset the spacing of search_button_layout. (Because page_layout has had its
# spacing set to 0)
self.search_button_widget = QtWidgets.QWidget()
self.search_button_layout = QtWidgets.QHBoxLayout(self.search_button_widget)
self.search_button_layout.addStretch() self.search_button_layout.addStretch()
# Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon. # Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon.
self.clear_button = QtWidgets.QToolButton(self) self.clear_button = QtWidgets.QPushButton()
self.clear_button.setIcon(self.clear_icon) self.clear_button.setIcon(self.clear_icon)
self.lock_button = QtWidgets.QToolButton(self) self.save_results_button = QtWidgets.QPushButton()
self.lock_button.setIcon(self.unlock_icon) self.save_results_button.setIcon(self.save_results_icon)
self.lock_button.setCheckable(True)
self.search_button_layout.addWidget(self.clear_button) self.search_button_layout.addWidget(self.clear_button)
self.search_button_layout.addWidget(self.lock_button) self.search_button_layout.addWidget(self.save_results_button)
self.search_button = QtWidgets.QPushButton(self) self.search_button = QtWidgets.QPushButton(self)
self.search_button_layout.addWidget(self.search_button) self.search_button_layout.addWidget(self.search_button)
self.general_bible_layout.addRow(self.search_button_layout) self.page_layout.addWidget(self.search_button_widget)
self.page_layout.addWidget(self.options_widget) self.results_view_tab = QtWidgets.QTabBar(self)
self.results_view_tab.addTab('')
self.results_view_tab.addTab('')
self.results_view_tab.setCurrentIndex(ResultsTab.Search)
self.page_layout.addWidget(self.results_view_tab)
def setupUi(self): def setupUi(self):
super().setupUi() super().setupUi()
@ -211,12 +254,15 @@ class BibleMediaItem(MediaManagerItem):
# Buttons # Buttons
self.book_order_button.toggled.connect(self.on_book_order_button_toggled) self.book_order_button.toggled.connect(self.on_book_order_button_toggled)
self.clear_button.clicked.connect(self.on_clear_button_clicked) self.clear_button.clicked.connect(self.on_clear_button_clicked)
self.lock_button.toggled.connect(self.on_lock_button_toggled) self.save_results_button.clicked.connect(self.on_save_results_button_clicked)
self.search_button.clicked.connect(self.on_search_button_clicked) self.search_button.clicked.connect(self.on_search_button_clicked)
# Other stuff # Other stuff
self.search_edit.returnPressed.connect(self.on_search_button_clicked) self.search_edit.returnPressed.connect(self.on_search_button_clicked)
self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed) self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed)
self.results_view_tab.currentChanged.connect(self.on_results_view_tab_current_changed)
self.search_edit.textChanged.connect(self.on_search_edit_text_changed) self.search_edit.textChanged.connect(self.on_search_edit_text_changed)
self.on_results_view_tab_total_update(ResultsTab.Saved)
self.on_results_view_tab_total_update(ResultsTab.Search)
def retranslateUi(self): def retranslateUi(self):
log.debug('retranslateUi') log.debug('retranslateUi')
@ -225,9 +271,9 @@ class BibleMediaItem(MediaManagerItem):
self.style_combo_box.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide) self.style_combo_box.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide)
self.style_combo_box.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine) self.style_combo_box.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine)
self.style_combo_box.setItemText(LayoutStyle.Continuous, UiStrings().Continuous) self.style_combo_box.setItemText(LayoutStyle.Continuous, UiStrings().Continuous)
self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.')) self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the results on the current tab.'))
self.lock_button.setToolTip( self.save_results_button.setToolTip(
translate('BiblesPlugin.MediaItem', 'Toggle to keep or clear the previous results.')) translate('BiblesPlugin.MediaItem', 'Add the search results to the saved list.'))
self.search_button.setText(UiStrings().Search) self.search_button.setText(UiStrings().Search)
def on_focus(self): def on_focus(self):
@ -241,8 +287,10 @@ class BibleMediaItem(MediaManagerItem):
if self.search_tab.isVisible(): if self.search_tab.isVisible():
self.search_edit.setFocus() self.search_edit.setFocus()
self.search_edit.selectAll() self.search_edit.selectAll()
else: if self.select_tab.isVisible():
self.select_book_combo_box.setFocus() self.select_book_combo_box.setFocus()
if self.options_tab.isVisible():
self.version_combo_box.setFocus()
def config_update(self): def config_update(self):
""" """
@ -415,14 +463,48 @@ class BibleMediaItem(MediaManagerItem):
""" """
Show the selected tab and set focus to it Show the selected tab and set focus to it
:param index: The tab selected (int) :param index: The tab selected
:type index: int
:return: None :return: None
""" """
search_tab = index == 0 if index == SearchTabs.Search or index == SearchTabs.Select:
self.search_tab.setVisible(search_tab) self.search_button.setEnabled(True)
self.select_tab.setVisible(not search_tab) else:
self.search_button.setEnabled(False)
self.search_tab.setVisible(index == SearchTabs.Search)
self.select_tab.setVisible(index == SearchTabs.Select)
self.options_tab.setVisible(index == SearchTabs.Options)
self.on_focus() self.on_focus()
def on_results_view_tab_current_changed(self, index):
"""
Update list_widget with the contents of the selected list
:param index: The index of the tab that has been changed to. (int)
:return: None
"""
if index == ResultsTab.Saved:
self.add_built_results_to_list_widget(self.saved_results)
elif index == ResultsTab.Search:
self.add_built_results_to_list_widget(self.current_results)
def on_results_view_tab_total_update(self, index):
"""
Update the result total count on the tab with the given index.
:param index: Index of the tab to update (int)
:return: None
"""
string = ''
count = 0
if index == ResultsTab.Saved:
string = translate('BiblesPlugin.MediaItem', 'Saved ({result_count})')
count = len(self.saved_results)
elif index == ResultsTab.Search:
string = translate('BiblesPlugin.MediaItem', 'Results ({result_count})')
count = len(self.current_results)
self.results_view_tab.setTabText(index, string.format(result_count=count))
def on_book_order_button_toggled(self, checked): def on_book_order_button_toggled(self, checked):
""" """
Change the sort order of the book names Change the sort order of the book names
@ -442,22 +524,25 @@ class BibleMediaItem(MediaManagerItem):
:return: None :return: None
""" """
self.list_view.clear() current_index = self.results_view_tab.currentIndex()
self.search_edit.clear() for item in self.list_view.selectedItems():
self.on_focus() self.list_view.takeItem(self.list_view.row(item))
results = [item.data(QtCore.Qt.UserRole) for item in self.list_view.allItems()]
if current_index == ResultsTab.Saved:
self.saved_results = results
elif current_index == ResultsTab.Search:
self.current_results = results
self.on_results_view_tab_total_update(current_index)
def on_lock_button_toggled(self, checked): def on_save_results_button_clicked(self):
""" """
Toggle the lock button, if Search tab is used, set focus to search field. Add the selected verses to the saved_results list.
:param checked: The state of the toggle button. (bool)
:return: None :return: None
""" """
self.list_view.locked = checked for verse in self.list_view.selectedItems():
if checked: self.saved_results.append(verse.data(QtCore.Qt.UserRole))
self.sender().setIcon(self.lock_icon) self.on_results_view_tab_total_update(ResultsTab.Saved)
else:
self.sender().setIcon(self.unlock_icon)
def on_style_combo_box_index_changed(self, index): def on_style_combo_box_index_changed(self, index):
""" """
@ -490,16 +575,17 @@ class BibleMediaItem(MediaManagerItem):
:return: None :return: None
""" """
new_selection = self.second_combo_box.currentData() new_selection = self.second_combo_box.currentData()
if self.list_view.count(): if self.saved_results:
# Exclusive or (^) the new and previous selections to detect if the user has switched between single and # Exclusive or (^) the new and previous selections to detect if the user has switched between single and
# dual bible mode # dual bible mode
if (new_selection is None) ^ (self.second_bible is None): if (new_selection is None) ^ (self.second_bible is None):
if critical_error_message_box( if critical_error_message_box(
message=translate('BiblesPlugin.MediaItem', message=translate('BiblesPlugin.MediaItem',
'OpenLP cannot combine single and dual Bible verse search results. ' 'OpenLP cannot combine single and dual Bible verse search results. '
'Do you want to clear your search results and start a new search?'), 'Do you want to clear your saved results?'),
parent=self, question=True) == QtWidgets.QMessageBox.Yes: parent=self, question=True) == QtWidgets.QMessageBox.Yes:
self.list_view.clear(override_lock=True) self.saved_results = []
self.on_results_view_tab_total_update(ResultsTab.Saved)
else: else:
self.second_combo_box.setCurrentIndex(self.second_combo_box.findData(self.second_bible)) self.second_combo_box.setCurrentIndex(self.second_combo_box.findData(self.second_bible))
return return
@ -525,7 +611,8 @@ class BibleMediaItem(MediaManagerItem):
log.warning('Not enough chapters in %s', book_ref_id) log.warning('Not enough chapters in %s', book_ref_id)
critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.')) critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.'))
else: else:
self.search_button.setEnabled(True) if self.select_tab.isVisible():
self.search_button.setEnabled(True)
self.adjust_combo_box(1, self.chapter_count, self.from_chapter) self.adjust_combo_box(1, self.chapter_count, self.from_chapter)
self.adjust_combo_box(1, self.chapter_count, self.to_chapter) self.adjust_combo_box(1, self.chapter_count, self.to_chapter)
self.adjust_combo_box(1, verse_count, self.from_verse) self.adjust_combo_box(1, verse_count, self.from_verse)
@ -602,6 +689,8 @@ class BibleMediaItem(MediaManagerItem):
:return: None :return: None
""" """
self.search_timer.stop()
self.search_status = SearchStatus.SearchButton
if not self.bible: if not self.bible:
self.main_window.information_message(UiStrings().BibleNoBiblesTitle, UiStrings().BibleNoBibles) self.main_window.information_message(UiStrings().BibleNoBiblesTitle, UiStrings().BibleNoBibles)
return return
@ -613,6 +702,7 @@ class BibleMediaItem(MediaManagerItem):
elif self.select_tab.isVisible(): elif self.select_tab.isVisible():
self.select_search() self.select_search()
self.search_button.setEnabled(True) self.search_button.setEnabled(True)
self.results_view_tab.setCurrentIndex(ResultsTab.Search)
self.application.set_normal_cursor() self.application.set_normal_cursor()
def select_search(self): def select_search(self):
@ -636,18 +726,21 @@ class BibleMediaItem(MediaManagerItem):
:return: None :return: None
""" """
self.search_results = []
verse_refs = self.plugin.manager.parse_ref(self.bible.name, search_text) verse_refs = self.plugin.manager.parse_ref(self.bible.name, search_text)
self.search_results = self.plugin.manager.get_verses(self.bible.name, verse_refs, True) self.search_results = self.plugin.manager.get_verses(self.bible.name, verse_refs, True)
if self.second_bible and self.search_results: if self.second_bible and self.search_results:
self.search_results = self.plugin.manager.get_verses(self.second_bible.name, verse_refs, True) self.search_results = self.plugin.manager.get_verses(self.second_bible.name, verse_refs, True)
self.display_results() self.display_results()
def on_text_search(self, text, search_while_type=False): def on_text_search(self, text):
""" """
We are doing a 'Text Search'. We are doing a 'Text Search'.
This search is called on def text_search by 'Search' Text and Combined Searches. This search is called on def text_search by 'Search' Text and Combined Searches.
""" """
self.search_results = self.plugin.manager.verse_search(self.bible.name, text) self.search_results = self.plugin.manager.verse_search(self.bible.name, text)
if self.search_results is None:
return
if self.second_bible and self.search_results: if self.second_bible and self.search_results:
filtered_search_results = [] filtered_search_results = []
not_found_count = 0 not_found_count = 0
@ -663,7 +756,7 @@ class BibleMediaItem(MediaManagerItem):
verse=verse.verse, bible_name=self.second_bible.name)) verse=verse.verse, bible_name=self.second_bible.name))
not_found_count += 1 not_found_count += 1
self.search_results = filtered_search_results self.search_results = filtered_search_results
if not_found_count != 0 and not search_while_type: if not_found_count != 0 and self.search_status == SearchStatus.SearchButton:
self.main_window.information_message( self.main_window.information_message(
translate('BiblesPlugin.MediaItem', 'Verses not found'), translate('BiblesPlugin.MediaItem', 'Verses not found'),
translate('BiblesPlugin.MediaItem', translate('BiblesPlugin.MediaItem',
@ -673,22 +766,23 @@ class BibleMediaItem(MediaManagerItem):
).format(second_name=self.second_bible.name, name=self.bible.name, count=not_found_count)) ).format(second_name=self.second_bible.name, name=self.bible.name, count=not_found_count))
self.display_results() self.display_results()
def text_search(self, search_while_type=False): def text_search(self):
""" """
This triggers the proper 'Search' search based on which search type is used. This triggers the proper 'Search' search based on which search type is used.
"Eg. "Reference Search", "Text Search" or "Combined search". "Eg. "Reference Search", "Text Search" or "Combined search".
""" """
self.search_results = []
log.debug('text_search called') log.debug('text_search called')
text = self.search_edit.text() text = self.search_edit.text()
if text == '': if text == '':
self.list_view.clear() self.display_results()
return return
self.list_view.clear(search_while_typing=search_while_type) self.on_results_view_tab_total_update(ResultsTab.Search)
if self.search_edit.current_search_type() == BibleSearch.Reference: if self.search_edit.current_search_type() == BibleSearch.Reference:
if get_reference_match('full').match(text): if get_reference_match('full').match(text):
# Valid reference found. Do reference search. # Valid reference found. Do reference search.
self.text_reference_search(text) self.text_reference_search(text)
elif not search_while_type: elif self.search_status == SearchStatus.SearchButton:
self.main_window.information_message( self.main_window.information_message(
translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'), translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'),
translate('BiblesPlugin.BibleManager', translate('BiblesPlugin.BibleManager',
@ -700,10 +794,12 @@ class BibleMediaItem(MediaManagerItem):
self.text_reference_search(text) self.text_reference_search(text)
else: else:
# It can only be a 'Combined' search without a valid reference, or a 'Text' search # It can only be a 'Combined' search without a valid reference, or a 'Text' search
if search_while_type: if self.search_status == SearchStatus.SearchAsYouType:
if len(text) > 8 and VALID_TEXT_SEARCH.search(text): if len(text) <= 8:
self.on_text_search(text, True) self.search_status = SearchStatus.NotEnoughText
elif VALID_TEXT_SEARCH.search(text): self.display_results()
return
if VALID_TEXT_SEARCH.search(text):
self.on_text_search(text) self.on_text_search(text)
def on_search_edit_text_changed(self): def on_search_edit_text_changed(self):
@ -713,9 +809,12 @@ class BibleMediaItem(MediaManagerItem):
:return: None :return: None
""" """
if Settings().value('bibles/is search while typing enabled'): if not Settings().value('bibles/is search while typing enabled') or \
if not self.search_timer.isActive(): not self.bible or self.bible.is_web_bible or \
self.search_timer.start() (self.second_bible and self.bible.is_web_bible):
return
if not self.search_timer.isActive():
self.search_timer.start()
def on_search_timer_timeout(self): def on_search_timer_timeout(self):
""" """
@ -724,7 +823,9 @@ class BibleMediaItem(MediaManagerItem):
:return: None :return: None
""" """
self.text_search(True) self.search_status = SearchStatus.SearchAsYouType
self.text_search()
self.results_view_tab.setCurrentIndex(ResultsTab.Search)
def display_results(self): def display_results(self):
""" """
@ -732,14 +833,16 @@ class BibleMediaItem(MediaManagerItem):
:return: None :return: None
""" """
self.list_view.clear() self.current_results = self.build_display_results(self.bible, self.second_bible, self.search_results)
if self.search_results:
items = self.build_display_results(self.bible, self.second_bible, self.search_results)
for item in items:
self.list_view.addItem(item)
self.list_view.selectAll()
self.search_results = [] self.search_results = []
self.second_search_results = [] self.add_built_results_to_list_widget(self.current_results)
def add_built_results_to_list_widget(self, results):
self.list_view.clear(self.search_status == SearchStatus.NotEnoughText)
for item in self.build_list_widget_items(results):
self.list_view.addItem(item)
self.list_view.selectAll()
self.on_results_view_tab_total_update(ResultsTab.Search)
def build_display_results(self, bible, second_bible, search_results): def build_display_results(self, bible, second_bible, search_results):
""" """
@ -789,11 +892,18 @@ class BibleMediaItem(MediaManagerItem):
bible_text = '{book} {chapter:d}{sep}{verse:d} ({version}, {second_version})' bible_text = '{book} {chapter:d}{sep}{verse:d} ({version}, {second_version})'
else: else:
bible_text = '{book} {chapter:d}{sep}{verse:d} ({version})' bible_text = '{book} {chapter:d}{sep}{verse:d} ({version})'
bible_verse = QtWidgets.QListWidgetItem(bible_text.format(sep=verse_separator, **data)) data['item_title'] = bible_text.format(sep=verse_separator, **data)
bible_verse.setData(QtCore.Qt.UserRole, data) items.append(data)
items.append(bible_verse)
return items return items
def build_list_widget_items(self, items):
list_widget_items = []
for data in items:
bible_verse = QtWidgets.QListWidgetItem(data['item_title'])
bible_verse.setData(QtCore.Qt.UserRole, data)
list_widget_items.append(bible_verse)
return list_widget_items
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
context=ServiceItemContext.Service): context=ServiceItemContext.Service):
""" """
@ -897,6 +1007,8 @@ class BibleMediaItem(MediaManagerItem):
""" """
Search for some Bible verses (by reference). Search for some Bible verses (by reference).
""" """
if self.bible is None:
return []
reference = self.plugin.manager.parse_ref(self.bible.name, string) reference = self.plugin.manager.parse_ref(self.bible.name, string)
search_results = self.plugin.manager.get_verses(self.bible.name, reference, showError) search_results = self.plugin.manager.get_verses(self.bible.name, reference, showError)
if search_results: if search_results:
@ -908,6 +1020,9 @@ class BibleMediaItem(MediaManagerItem):
""" """
Create a media item from an item id. Create a media item from an item id.
""" """
if self.bible is None:
return []
reference = self.plugin.manager.parse_ref(self.bible.name, item_id) reference = self.plugin.manager.parse_ref(self.bible.name, item_id)
search_results = self.plugin.manager.get_verses(self.bible.name, reference, False) search_results = self.plugin.manager.get_verses(self.bible.name, reference, False)
return self.build_display_results(self.bible, None, search_results) items = self.build_display_results(self.bible, None, search_results)
return self.build_list_widget_items(items)

View File

@ -23,7 +23,6 @@
The :mod:`songshowplus` module provides the functionality for importing SongShow Plus songs into the OpenLP The :mod:`songshowplus` module provides the functionality for importing SongShow Plus songs into the OpenLP
database. database.
""" """
import chardet
import os import os
import logging import logging
import re import re
@ -226,6 +225,7 @@ class SongShowPlusImport(SongImport):
def decode(self, data): def decode(self, data):
try: try:
return str(data, chardet.detect(data)['encoding']) # Don't question this, it works...
return data.decode('utf-8').encode('cp1251').decode('cp1251')
except: except:
return str(data, retrieve_windows_encoding()) return data.decode(retrieve_windows_encoding())

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 B

View File

@ -34,8 +34,7 @@
<file>bibles_search_text.png</file> <file>bibles_search_text.png</file>
<file>bibles_search_reference.png</file> <file>bibles_search_reference.png</file>
<file>bibles_search_clear.png</file> <file>bibles_search_clear.png</file>
<file>bibles_search_unlock.png</file> <file>bibles_save_results.png</file>
<file>bibles_search_lock.png</file>
</qresource> </qresource>
<qresource prefix="plugins"> <qresource prefix="plugins">
<file>plugin_alerts.png</file> <file>plugin_alerts.png</file>
@ -144,7 +143,6 @@
</qresource> </qresource>
<qresource prefix="remote"> <qresource prefix="remote">
<file>network_server.png</file> <file>network_server.png</file>
<file>network_ssl.png</file>
<file>network_auth.png</file> <file>network_auth.png</file>
</qresource> </qresource>
<qresource prefix="songusage"> <qresource prefix="songusage">
@ -188,4 +186,4 @@
<file>android_app_qr.png</file> <file>android_app_qr.png</file>
<file>ios_app_qr.png</file> <file>ios_app_qr.png</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -30,7 +30,7 @@ from PyQt5 import QtCore
from openlp.core.common import Registry from openlp.core.common import Registry
from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
from openlp.core.lib.renderer import words_split, get_start_tags from openlp.core.lib.renderer import words_split, get_start_tags
from openlp.core.lib.theme import ThemeXML from openlp.core.lib.theme import Theme
SCREEN = { SCREEN = {
@ -189,7 +189,7 @@ class TestRenderer(TestCase):
# GIVEN: test object and data # GIVEN: test object and data
mock_lyrics_css.return_value = ' FORMAT CSS; ' mock_lyrics_css.return_value = ' FORMAT CSS; '
mock_outline_css.return_value = ' OUTLINE CSS; ' mock_outline_css.return_value = ' OUTLINE CSS; '
theme_data = ThemeXML() theme_data = Theme()
theme_data.font_main_name = 'Arial' theme_data.font_main_name = 'Arial'
theme_data.font_main_size = 20 theme_data.font_main_size = 20
theme_data.font_main_color = '#FFFFFF' theme_data.font_main_color = '#FFFFFF'

View File

@ -25,36 +25,30 @@ Package to test the openlp.core.lib.theme package.
from unittest import TestCase from unittest import TestCase
import os import os
from openlp.core.lib.theme import ThemeXML from openlp.core.lib.theme import Theme
class TestThemeXML(TestCase): class TestTheme(TestCase):
""" """
Test the ThemeXML class Test the Theme class
""" """
def test_new_theme(self): def test_new_theme(self):
""" """
Test the ThemeXML constructor Test the Theme constructor
""" """
# GIVEN: The ThemeXML class # GIVEN: The Theme class
# WHEN: A theme object is created # WHEN: A theme object is created
default_theme = ThemeXML() default_theme = Theme()
# THEN: The default values should be correct # THEN: The default values should be correct
self.assertEqual('#000000', default_theme.background_border_color, self.check_theme(default_theme)
'background_border_color should be "#000000"')
self.assertEqual('solid', default_theme.background_type, 'background_type should be "solid"')
self.assertEqual(0, default_theme.display_vertical_align, 'display_vertical_align should be 0')
self.assertEqual('Arial', default_theme.font_footer_name, 'font_footer_name should be "Arial"')
self.assertFalse(default_theme.font_main_bold, 'font_main_bold should be False')
self.assertEqual(47, len(default_theme.__dict__), 'The theme should have 47 attributes')
def test_expand_json(self): def test_expand_json(self):
""" """
Test the expand_json method Test the expand_json method
""" """
# GIVEN: A ThemeXML object and some JSON to "expand" # GIVEN: A Theme object and some JSON to "expand"
theme = ThemeXML() theme = Theme()
theme_json = { theme_json = {
'background': { 'background': {
'border_color': '#000000', 'border_color': '#000000',
@ -73,31 +67,48 @@ class TestThemeXML(TestCase):
} }
} }
# WHEN: ThemeXML.expand_json() is run # WHEN: Theme.expand_json() is run
theme.expand_json(theme_json) theme.expand_json(theme_json)
# THEN: The attributes should be set on the object # THEN: The attributes should be set on the object
self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"') self.check_theme(theme)
self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
def test_extend_image_filename(self): def test_extend_image_filename(self):
""" """
Test the extend_image_filename method Test the extend_image_filename method
""" """
# GIVEN: A theme object # GIVEN: A theme object
theme = ThemeXML() theme = Theme()
theme.theme_name = 'MyBeautifulTheme ' theme.theme_name = 'MyBeautifulTheme '
theme.background_filename = ' video.mp4' theme.background_filename = ' video.mp4'
theme.background_type = 'video' theme.background_type = 'video'
path = os.path.expanduser('~') path = os.path.expanduser('~')
# WHEN: ThemeXML.extend_image_filename is run # WHEN: Theme.extend_image_filename is run
theme.extend_image_filename(path) theme.extend_image_filename(path)
# THEN: The filename of the background should be correct # THEN: The filename of the background should be correct
expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4') expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')
self.assertEqual(expected_filename, theme.background_filename) self.assertEqual(expected_filename, theme.background_filename)
self.assertEqual('MyBeautifulTheme', theme.theme_name) self.assertEqual('MyBeautifulTheme', theme.theme_name)
def test_save_retrieve(self):
"""
Load a dummy theme, save it and reload it
"""
# GIVEN: The default Theme class
# WHEN: A theme object is created
default_theme = Theme()
# THEN: The default values should be correct
save_theme_json = default_theme.export_theme()
lt = Theme()
lt.load_theme(save_theme_json)
self.check_theme(lt)
def check_theme(self, theme):
self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
self.assertEqual(47, len(theme.__dict__), 'The theme should have 47 attributes')

View File

@ -63,7 +63,7 @@ class TestThemeManager(TestCase):
mocked_zipfile_init.return_value = None mocked_zipfile_init.return_value = None
# WHEN: The theme is exported # WHEN: The theme is exported
theme_manager._export_theme(os.path.join('some', 'path'), 'Default') theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default')
# THEN: The zipfile should be created at the given path # THEN: The zipfile should be created at the given path
mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
@ -126,8 +126,9 @@ class TestThemeManager(TestCase):
theme_manager.path = '' theme_manager.path = ''
mocked_theme = MagicMock() mocked_theme = MagicMock()
mocked_theme.theme_name = 'themename' mocked_theme.theme_name = 'themename'
mocked_theme.extract_formatted_xml = MagicMock() mocked_theme.filename = "filename"
mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() # mocked_theme.extract_formatted_xml = MagicMock()
# mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
# WHEN: Calling _write_theme with path to different images # WHEN: Calling _write_theme with path to different images
file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg') file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
@ -148,14 +149,13 @@ class TestThemeManager(TestCase):
theme_manager.path = self.temp_folder theme_manager.path = self.temp_folder
mocked_theme = MagicMock() mocked_theme = MagicMock()
mocked_theme.theme_name = 'theme 愛 name' mocked_theme.theme_name = 'theme 愛 name'
mocked_theme.extract_formatted_xml = MagicMock() mocked_theme.export_theme.return_value = "{}"
mocked_theme.extract_formatted_xml.return_value = 'fake theme 愛 XML'.encode()
# WHEN: Calling _write_theme with a theme with a name with special characters in it # WHEN: Calling _write_theme with a theme with a name with special characters in it
theme_manager._write_theme(mocked_theme, None, None) theme_manager._write_theme(mocked_theme, None, None)
# THEN: It should have been created # THEN: It should have been created
self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.xml')), self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')),
'Theme with special characters should have been created!') 'Theme with special characters should have been created!')
def test_over_write_message_box_yes(self): def test_over_write_message_box_yes(self):

View File

@ -23,6 +23,7 @@
This module contains tests for the openlp.core.lib.listwidgetwithdnd module This module contains tests for the openlp.core.lib.listwidgetwithdnd module
""" """
from unittest import TestCase from unittest import TestCase
from types import GeneratorType
from openlp.core.common.uistrings import UiStrings from openlp.core.common.uistrings import UiStrings
from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
@ -33,37 +34,6 @@ class TestListWidgetWithDnD(TestCase):
""" """
Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class
""" """
def test_clear_locked(self):
"""
Test the clear method the list is 'locked'
"""
with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
# GIVEN: An instance of ListWidgetWithDnD
widget = ListWidgetWithDnD()
# WHEN: The list is 'locked' and clear has been called
widget.locked = True
widget.clear()
# THEN: The super method should not have been called (i.e. The list not cleared)
self.assertFalse(mocked_clear_super_method.called)
def test_clear_overide_locked(self):
"""
Test the clear method the list is 'locked', but clear is called with 'override_lock' set to True
"""
with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
# GIVEN: An instance of ListWidgetWithDnD
widget = ListWidgetWithDnD()
# WHEN: The list is 'locked' and clear has been called with override_lock se to True
widget.locked = True
widget.clear(override_lock=True)
# THEN: The super method should have been called (i.e. The list is cleared regardless whether it is locked
# or not)
mocked_clear_super_method.assert_called_once_with()
def test_clear(self): def test_clear(self):
""" """
Test the clear method when called without any arguments. Test the clear method when called without any arguments.
@ -90,6 +60,38 @@ class TestListWidgetWithDnD(TestCase):
# THEN: The results text should be the 'short results' text. # THEN: The results text should be the 'short results' text.
self.assertEqual(widget.no_results_text, UiStrings().ShortResults) self.assertEqual(widget.no_results_text, UiStrings().ShortResults)
def test_all_items_no_list_items(self):
"""
Test allItems when there are no items in the list widget
"""
# GIVEN: An instance of ListWidgetWithDnD
widget = ListWidgetWithDnD()
with patch.object(widget, 'count', return_value=0), \
patch.object(widget, 'item', side_effect=lambda x: [][x]):
# WHEN: Calling allItems
result = widget.allItems()
# THEN: An instance of a Generator object should be returned. The generator should not yeild any results
self.assertIsInstance(result, GeneratorType)
self.assertEqual(list(result), [])
def test_all_items_list_items(self):
"""
Test allItems when the list widget contains some items.
"""
# GIVEN: An instance of ListWidgetWithDnD
widget = ListWidgetWithDnD()
with patch.object(widget, 'count', return_value=2), \
patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]):
# WHEN: Calling allItems
result = widget.allItems()
# THEN: An instance of a Generator object should be returned. The generator should not yeild any results
self.assertIsInstance(result, GeneratorType)
self.assertEqual(list(result), [5, 3])
def test_paint_event(self): def test_paint_event(self):
""" """
Test the paintEvent method when the list is not empty Test the paintEvent method when the list is not empty

View File

@ -31,7 +31,8 @@ from tests.helpers.testmixin import TestMixin
from openlp.core.common import Registry from openlp.core.common import Registry
from openlp.core.lib import MediaManagerItem from openlp.core.lib import MediaManagerItem
from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, get_reference_separators, VALID_TEXT_SEARCH from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, ResultsTab, SearchStatus, \
get_reference_separators, VALID_TEXT_SEARCH
class TestBibleMediaItemModulefunctions(TestCase): class TestBibleMediaItemModulefunctions(TestCase):
@ -143,6 +144,7 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item = BibleMediaItem(None, self.mocked_plugin) self.media_item = BibleMediaItem(None, self.mocked_plugin)
self.media_item.settings_section = 'bibles' self.media_item.settings_section = 'bibles'
self.media_item.results_view_tab = MagicMock()
self.mocked_book_1 = MagicMock(**{'get_name.return_value': 'Book 1', 'book_reference_id': 1}) self.mocked_book_1 = MagicMock(**{'get_name.return_value': 'Book 1', 'book_reference_id': 1})
self.mocked_book_2 = MagicMock(**{'get_name.return_value': 'Book 2', 'book_reference_id': 2}) self.mocked_book_2 = MagicMock(**{'get_name.return_value': 'Book 2', 'book_reference_id': 2})
@ -658,56 +660,65 @@ class TestMediaItem(TestCase, TestMixin):
# THEN: The select_book_combo_box model sort should have been reset # THEN: The select_book_combo_box model sort should have been reset
self.media_item.select_book_combo_box.model().sort.assert_called_once_with(-1) self.media_item.select_book_combo_box.model().sort.assert_called_once_with(-1)
def test_on_clear_button_clicked(self): def test_on_clear_button_clicked_saved_tab(self):
""" """
Test on_clear_button_clicked Test on_clear_button_clicked when the saved tab is selected
"""
# GIVEN: An instance of :class:`MediaManagerItem` and mocked out saved_tab and select_tab and a mocked out
# list_view and search_edit
self.media_item.list_view = MagicMock()
self.media_item.search_edit = MagicMock()
self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Saved})
self.media_item.saved_results = ['Some', 'Results']
with patch.object(self.media_item, 'on_focus'):
# WHEN: Calling on_clear_button_clicked
self.media_item.on_clear_button_clicked()
# THEN: The list_view should be cleared
self.assertEqual(self.media_item.saved_results, [])
self.media_item.list_view.clear.assert_called_once_with()
def test_on_clear_button_clicked_search_tab(self):
"""
Test on_clear_button_clicked when the search tab is selected
""" """
# GIVEN: An instance of :class:`MediaManagerItem` and mocked out search_tab and select_tab and a mocked out # GIVEN: An instance of :class:`MediaManagerItem` and mocked out search_tab and select_tab and a mocked out
# list_view and search_edit # list_view and search_edit
self.media_item.list_view = MagicMock() self.media_item.list_view = MagicMock()
self.media_item.search_edit = MagicMock() self.media_item.search_edit = MagicMock()
self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Search})
self.media_item.current_results = ['Some', 'Results']
with patch.object(self.media_item, 'on_focus'): with patch.object(self.media_item, 'on_focus'):
# WHEN: Calling on_clear_button_clicked # WHEN: Calling on_clear_button_clicked
self.media_item.on_clear_button_clicked() self.media_item.on_clear_button_clicked()
# THEN: The list_view and the search_edit should be cleared # THEN: The list_view and the search_edit should be cleared
self.assertEqual(self.media_item.current_results, [])
self.media_item.list_view.clear.assert_called_once_with() self.media_item.list_view.clear.assert_called_once_with()
self.media_item.search_edit.clear.assert_called_once_with() self.media_item.search_edit.clear.assert_called_once_with()
def test_on_lock_button_toggled_search_tab_lock_icon(self): def test_on_save_results_button_clicked(self):
""" """
Test that "on_lock_button_toggled" toggles the lock properly. Test that "on_save_results_button_clicked" saves the results.
""" """
# GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view # GIVEN: An instance of :class:`MediaManagerItem` and a mocked list_view
self.media_item.list_view = MagicMock() result_1 = MagicMock(**{'data.return_value': 'R1'})
self.media_item.lock_icon = 'lock icon' result_2 = MagicMock(**{'data.return_value': 'R2'})
mocked_sender_instance = MagicMock() result_3 = MagicMock(**{'data.return_value': 'R3'})
with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance): self.media_item.list_view = MagicMock(**{'selectedItems.return_value': [result_1, result_2, result_3]})
# WHEN: When the lock_button is checked with patch.object(self.media_item, 'on_results_view_tab_total_update') as \
self.media_item.on_lock_button_toggled(True) mocked_on_results_view_tab_total_update:
# THEN: list_view should be 'locked' and the lock icon set # WHEN: When the save_results_button is clicked
self.assertTrue(self.media_item.list_view.locked) self.media_item.on_save_results_button_clicked()
mocked_sender_instance.setIcon.assert_called_once_with('lock icon')
def test_on_lock_button_toggled_unlock_icon(self): # THEN: The selected results in the list_view should be added to the 'saved_results' list. And the saved_tab
""" # total should be updated.
Test that "on_lock_button_toggled" toggles the lock properly. self.assertEqual(self.media_item.saved_results, ['R1', 'R2', 'R3'])
""" mocked_on_results_view_tab_total_update.assert_called_once_with(ResultsTab.Saved)
# GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view
self.media_item.list_view = MagicMock()
self.media_item.unlock_icon = 'unlock icon'
mocked_sender_instance = MagicMock()
with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance):
# WHEN: When the lock_button is unchecked
self.media_item.on_lock_button_toggled(False)
# THEN: list_view should be 'unlocked' and the unlock icon set
self.assertFalse(self.media_item.list_view.locked)
mocked_sender_instance.setIcon.assert_called_once_with('unlock icon')
def test_on_style_combo_box_changed(self): def test_on_style_combo_box_changed(self):
""" """
@ -815,7 +826,9 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.list_view = MagicMock(**{'count.return_value': 5}) self.media_item.list_view = MagicMock(**{'count.return_value': 5})
self.media_item.style_combo_box = MagicMock() self.media_item.style_combo_box = MagicMock()
self.media_item.select_book_combo_box = MagicMock() self.media_item.select_book_combo_box = MagicMock()
self.media_item.search_results = ['list', 'of', 'results']
with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \ with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
patch.object(self.media_item, 'display_results'), \
patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box', patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box: return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
@ -825,9 +838,8 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': self.mocked_bible_1}) self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': self.mocked_bible_1})
self.media_item.on_second_combo_box_index_changed(5) self.media_item.on_second_combo_box_index_changed(5)
# THEN: The list_view should be cleared and the selected bible should be set as the current bible # THEN: The selected bible should be set as the current bible
self.assertTrue(mocked_critical_error_message_box.called) self.assertTrue(mocked_critical_error_message_box.called)
self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
self.media_item.style_combo_box.setEnabled.assert_called_once_with(False) self.media_item.style_combo_box.setEnabled.assert_called_once_with(False)
self.assertTrue(mocked_initialise_advanced_bible.called) self.assertTrue(mocked_initialise_advanced_bible.called)
self.assertEqual(self.media_item.second_bible, self.mocked_bible_1) self.assertEqual(self.media_item.second_bible, self.mocked_bible_1)
@ -841,7 +853,9 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.list_view = MagicMock(**{'count.return_value': 5}) self.media_item.list_view = MagicMock(**{'count.return_value': 5})
self.media_item.style_combo_box = MagicMock() self.media_item.style_combo_box = MagicMock()
self.media_item.select_book_combo_box = MagicMock() self.media_item.select_book_combo_box = MagicMock()
self.media_item.search_results = ['list', 'of', 'results']
with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \ with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
patch.object(self.media_item, 'display_results'), \
patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box', patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box: return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
# WHEN: The previously is a bible new selection is None and the user selects yes # WHEN: The previously is a bible new selection is None and the user selects yes
@ -850,9 +864,8 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': None}) self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': None})
self.media_item.on_second_combo_box_index_changed(0) self.media_item.on_second_combo_box_index_changed(0)
# THEN: The list_view should be cleared and the selected bible should be set as the current bible # THEN: The selected bible should be set as the current bible
self.assertTrue(mocked_critical_error_message_box.called) self.assertTrue(mocked_critical_error_message_box.called)
self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
self.media_item.style_combo_box.setEnabled.assert_called_once_with(True) self.media_item.style_combo_box.setEnabled.assert_called_once_with(True)
self.assertFalse(mocked_initialise_advanced_bible.called) self.assertFalse(mocked_initialise_advanced_bible.called)
self.assertEqual(self.media_item.second_bible, None) self.assertEqual(self.media_item.second_bible, None)
@ -1388,8 +1401,9 @@ class TestMediaItem(TestCase, TestMixin):
# WHEN: Calling on_search_timer_timeout # WHEN: Calling on_search_timer_timeout
self.media_item.on_search_timer_timeout() self.media_item.on_search_timer_timeout()
# THEN: The text_search method should have been called with True # THEN: The search_status should be set to SearchAsYouType and text_search should have been called
mocked_text_search.assert_called_once_with(True) self.assertEqual(self.media_item.search_status, SearchStatus().SearchAsYouType)
mocked_text_search.assert_called_once_with()
def test_display_results_no_results(self): def test_display_results_no_results(self):
""" """
@ -1407,7 +1421,6 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.display_results() self.media_item.display_results()
# THEN: No items should be added to the list # THEN: No items should be added to the list
self.media_item.list_view.clear.assert_called_once_with()
self.assertFalse(self.media_item.list_view.addItem.called) self.assertFalse(self.media_item.list_view.addItem.called)
def test_display_results_results(self): def test_display_results_results(self):
@ -1415,7 +1428,10 @@ class TestMediaItem(TestCase, TestMixin):
Test the display_results method when there are items to display Test the display_results method when there are items to display
""" """
# GIVEN: An instance of BibleMediaItem and a mocked build_display_results which returns a list of results # GIVEN: An instance of BibleMediaItem and a mocked build_display_results which returns a list of results
with patch.object(self.media_item, 'build_display_results', return_value=['list', 'items']): with patch.object(self.media_item, 'build_display_results', return_value=[
{'item_title': 'Title 1'}, {'item_title': 'Title 2'}]), \
patch.object(self.media_item, 'add_built_results_to_list_widget') as \
mocked_add_built_results_to_list_widget:
self.media_item.search_results = ['results'] self.media_item.search_results = ['results']
self.media_item.list_view = MagicMock() self.media_item.list_view = MagicMock()
@ -1423,5 +1439,5 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.display_results() self.media_item.display_results()
# THEN: addItem should have been with the display items # THEN: addItem should have been with the display items
self.media_item.list_view.clear.assert_called_once_with() mocked_add_built_results_to_list_widget.assert_called_once_with(
self.assertEqual(self.media_item.list_view.addItem.call_args_list, [call('list'), call('items')]) [{'item_title': 'Title 1'}, {'item_title': 'Title 2'}])

View File

@ -27,10 +27,15 @@ from unittest.mock import MagicMock
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import Settings
from openlp.plugins.songs.forms.editverseform import EditVerseForm from openlp.plugins.songs.forms.editverseform import EditVerseForm
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
__default_settings__ = {
'songs/enable chords': True,
}
class TestEditVerseForm(TestCase, TestMixin): class TestEditVerseForm(TestCase, TestMixin):
""" """
@ -40,9 +45,10 @@ class TestEditVerseForm(TestCase, TestMixin):
""" """
Set up the components need for all tests. Set up the components need for all tests.
""" """
self.edit_verse_form = EditVerseForm(None)
self.setup_application() self.setup_application()
self.build_settings() self.build_settings()
Settings().extend_default_settings(__default_settings__)
self.edit_verse_form = EditVerseForm(None)
QtCore.QLocale.setDefault(QtCore.QLocale('en_GB')) QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
def tearDown(self): def tearDown(self):

View File

@ -108,7 +108,7 @@ class TestBibleManager(TestCase, TestMixin):
# WHEN asking to parse the bible reference # WHEN asking to parse the bible reference
results = parse_reference('Raoul 1', self.manager.db_cache['tests'], MagicMock()) results = parse_reference('Raoul 1', self.manager.db_cache['tests'], MagicMock())
# THEN a verse array should be returned # THEN a verse array should be returned
self.assertEqual(False, results, "The bible Search should return False") self.assertEqual([], results, "The bible Search should return an empty list")
def test_parse_reference_five(self): def test_parse_reference_five(self):
""" """

View File

@ -27,12 +27,16 @@ from unittest.mock import MagicMock
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from openlp.core.common import Registry from openlp.core.common import Registry, Settings
from openlp.core.common.uistrings import UiStrings from openlp.core.common.uistrings import UiStrings
from openlp.plugins.songs.forms.editsongform import EditSongForm from openlp.plugins.songs.forms.editsongform import EditSongForm
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
__default_settings__ = {
'songs/enable chords': True,
}
class TestEditSongForm(TestCase, TestMixin): class TestEditSongForm(TestCase, TestMixin):
""" """
@ -48,12 +52,15 @@ class TestEditSongForm(TestCase, TestMixin):
self.main_window = QtWidgets.QMainWindow() self.main_window = QtWidgets.QMainWindow()
Registry().register('main_window', self.main_window) Registry().register('main_window', self.main_window)
Registry().register('theme_manager', MagicMock()) Registry().register('theme_manager', MagicMock())
self.build_settings()
Settings().extend_default_settings(__default_settings__)
self.form = EditSongForm(MagicMock(), self.main_window, MagicMock()) self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())
def tearDown(self): def tearDown(self):
""" """
Delete all the C++ objects at the end so that we don't have a segfault Delete all the C++ objects at the end so that we don't have a segfault
""" """
self.destroy_settings()
del self.form del self.form
del self.main_window del self.main_window

View File

@ -26,10 +26,14 @@ from unittest import TestCase
from PyQt5 import QtCore, QtTest, QtWidgets from PyQt5 import QtCore, QtTest, QtWidgets
from openlp.core.common import Registry from openlp.core.common import Registry, Settings
from openlp.plugins.songs.forms.editverseform import EditVerseForm from openlp.plugins.songs.forms.editverseform import EditVerseForm
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
__default_settings__ = {
'songs/enable chords': True,
}
class TestEditVerseForm(TestCase, TestMixin): class TestEditVerseForm(TestCase, TestMixin):
""" """
@ -44,12 +48,15 @@ class TestEditVerseForm(TestCase, TestMixin):
self.setup_application() self.setup_application()
self.main_window = QtWidgets.QMainWindow() self.main_window = QtWidgets.QMainWindow()
Registry().register('main_window', self.main_window) Registry().register('main_window', self.main_window)
self.build_settings()
Settings().extend_default_settings(__default_settings__)
self.form = EditVerseForm() self.form = EditVerseForm()
def tearDown(self): def tearDown(self):
""" """
Delete all the C++ objects at the end so that we don't have a segfault Delete all the C++ objects at the end so that we don't have a segfault
""" """
self.destroy_settings()
del self.form del self.form
del self.main_window del self.main_window

View File

@ -15,7 +15,7 @@
"v1" "v1"
], ],
[ [
"Did we in our own strength confide, our striving would be losing;\r\nWere not the right Man on our side, the Man of God’s own choosing:\r\nDost ask who that may be? Christ Jesus, it is He;\r\nLord Sabaoth, His Name, from age to age the same,\r\nAnd He must win the battle.\r\n", "Did we in our own strength confide, our striving would be losing;\r\nWere not the right Man on our side, the Man of Gods own choosing:\r\nDost ask who that may be? Christ Jesus, it is He;\r\nLord Sabaoth, His Name, from age to age the same,\r\nAnd He must win the battle.\r\n",
"v2" "v2"
], ],
[ [
@ -23,7 +23,7 @@
"v3" "v3"
], ],
[ [
"That word above all earthly powers, no thanks to them, abideth;\r\nThe Spirit and the gifts are ours through Him Who with us sideth:\r\nLet goods and kindred go, this mortal life also;\r\nThe body they may kill: God’s truth abideth still,\r\nHis kingdom is forever.\r\n", "That word above all earthly powers, no thanks to them, abideth;\r\nThe Spirit and the gifts are ours through Him Who with us sideth:\r\nLet goods and kindred go, this mortal life also;\r\nThe body they may kill: Gods truth abideth still,\r\nHis kingdom is forever.\r\n",
"v4" "v4"
] ]
] ]