forked from openlp/openlp
head
This commit is contained in:
commit
c851fa84f0
@ -1 +1 @@
|
|||||||
2.1.0-bzr2141
|
2.1.0-bzr2234
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
The :mod:`lib` module contains most of the components and libraries that make
|
The :mod:`lib` module contains most of the components and libraries that make
|
||||||
OpenLP work.
|
OpenLP work.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import division
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -202,12 +203,13 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
|
|||||||
States if an icon should be build and returned from the thumb. Defaults to ``True``.
|
States if an icon should be build and returned from the thumb. Defaults to ``True``.
|
||||||
|
|
||||||
``size``
|
``size``
|
||||||
Allows to state a own size to use. Defaults to ``None``, which means that a default height of 88 is used.
|
Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default height of 88
|
||||||
|
is used.
|
||||||
"""
|
"""
|
||||||
ext = os.path.splitext(thumb_path)[1].lower()
|
ext = os.path.splitext(thumb_path)[1].lower()
|
||||||
reader = QtGui.QImageReader(image_path)
|
reader = QtGui.QImageReader(image_path)
|
||||||
if size is None:
|
if size is None:
|
||||||
ratio = float(reader.size().width()) / float(reader.size().height())
|
ratio = reader.size().width() / reader.size().height()
|
||||||
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
|
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
|
||||||
else:
|
else:
|
||||||
reader.setScaledSize(size)
|
reader.setScaledSize(size)
|
||||||
@ -260,8 +262,8 @@ def resize_image(image_path, width, height, background=u'#000000'):
|
|||||||
log.debug(u'resize_image - start')
|
log.debug(u'resize_image - start')
|
||||||
reader = QtGui.QImageReader(image_path)
|
reader = QtGui.QImageReader(image_path)
|
||||||
# The image's ratio.
|
# The image's ratio.
|
||||||
image_ratio = float(reader.size().width()) / float(reader.size().height())
|
image_ratio = reader.size().width() / reader.size().height()
|
||||||
resize_ratio = float(width) / float(height)
|
resize_ratio = width / height
|
||||||
# Figure out the size we want to resize the image to (keep aspect ratio).
|
# Figure out the size we want to resize the image to (keep aspect ratio).
|
||||||
if image_ratio == resize_ratio:
|
if image_ratio == resize_ratio:
|
||||||
size = QtCore.QSize(width, height)
|
size = QtCore.QSize(width, height)
|
||||||
@ -282,7 +284,7 @@ def resize_image(image_path, width, height, background=u'#000000'):
|
|||||||
new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied)
|
new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied)
|
||||||
painter = QtGui.QPainter(new_image)
|
painter = QtGui.QPainter(new_image)
|
||||||
painter.fillRect(new_image.rect(), QtGui.QColor(background))
|
painter.fillRect(new_image.rect(), QtGui.QColor(background))
|
||||||
painter.drawImage((width - real_width) / 2, (height - real_height) / 2, preview)
|
painter.drawImage((width - real_width) // 2, (height - real_height) // 2, preview)
|
||||||
return new_image
|
return new_image
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"""
|
"""
|
||||||
Provide additional functionality required by OpenLP from the inherited QDockWidget.
|
Provide additional functionality required by OpenLP from the inherited QDockWidget.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import division
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt4 import QtGui
|
from PyQt4 import QtGui
|
||||||
@ -55,7 +56,7 @@ class OpenLPDockWidget(QtGui.QDockWidget):
|
|||||||
self.setWindowIcon(build_icon(icon))
|
self.setWindowIcon(build_icon(icon))
|
||||||
# Sort out the minimum width.
|
# Sort out the minimum width.
|
||||||
screens = ScreenList()
|
screens = ScreenList()
|
||||||
main_window_docbars = screens.current[u'size'].width() / 5
|
main_window_docbars = screens.current[u'size'].width() // 5
|
||||||
if main_window_docbars > 300:
|
if main_window_docbars > 300:
|
||||||
self.setMinimumWidth(300)
|
self.setMinimumWidth(300)
|
||||||
else:
|
else:
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from __future__ import division
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt4 import QtWebKit
|
from PyQt4 import QtWebKit
|
||||||
@ -276,7 +276,7 @@ def build_background_css(item, width):
|
|||||||
``item``
|
``item``
|
||||||
Service Item containing theme and location information
|
Service Item containing theme and location information
|
||||||
"""
|
"""
|
||||||
width = int(width) / 2
|
width = int(width) // 2
|
||||||
theme = item.themedata
|
theme = item.themedata
|
||||||
background = u'background-color: black'
|
background = u'background-color: black'
|
||||||
if theme:
|
if theme:
|
||||||
|
@ -102,7 +102,6 @@ class MediaManagerItem(QtGui.QWidget):
|
|||||||
self.setupUi()
|
self.setupUi()
|
||||||
self.retranslateUi()
|
self.retranslateUi()
|
||||||
self.auto_select_id = -1
|
self.auto_select_id = -1
|
||||||
Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load)
|
|
||||||
# Need to use event as called across threads and UI is updated
|
# Need to use event as called across threads and UI is updated
|
||||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote)
|
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote)
|
||||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote)
|
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote)
|
||||||
@ -585,12 +584,15 @@ class MediaManagerItem(QtGui.QWidget):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def service_load(self, message):
|
def service_load(self, item):
|
||||||
"""
|
"""
|
||||||
Method to add processing when a service has been loaded and individual service items need to be processed by the
|
Method to add processing when a service has been loaded and individual service items need to be processed by the
|
||||||
plugins.
|
plugins.
|
||||||
|
|
||||||
|
``item``
|
||||||
|
The item to be processed and returned.
|
||||||
"""
|
"""
|
||||||
pass
|
return item
|
||||||
|
|
||||||
def check_search_result(self):
|
def check_search_result(self):
|
||||||
"""
|
"""
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from __future__ import division
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt4 import QtGui, QtCore, QtWebKit
|
from PyQt4 import QtGui, QtCore, QtWebKit
|
||||||
@ -327,7 +327,7 @@ class Renderer(object):
|
|||||||
screen_size = self.screens.current[u'size']
|
screen_size = self.screens.current[u'size']
|
||||||
self.width = screen_size.width()
|
self.width = screen_size.width()
|
||||||
self.height = screen_size.height()
|
self.height = screen_size.height()
|
||||||
self.screen_ratio = float(self.height) / float(self.width)
|
self.screen_ratio = self.height / self.width
|
||||||
log.debug(u'_calculate default %s, %f' % (screen_size, self.screen_ratio))
|
log.debug(u'_calculate default %s, %f' % (screen_size, self.screen_ratio))
|
||||||
# 90% is start of footer
|
# 90% is start of footer
|
||||||
self.footer_start = int(self.height * 0.90)
|
self.footer_start = int(self.height * 0.90)
|
||||||
@ -546,15 +546,15 @@ class Renderer(object):
|
|||||||
"""
|
"""
|
||||||
smallest_index = 0
|
smallest_index = 0
|
||||||
highest_index = len(html_list) - 1
|
highest_index = len(html_list) - 1
|
||||||
index = int(highest_index / 2)
|
index = highest_index // 2
|
||||||
while True:
|
while True:
|
||||||
if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
|
if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
|
||||||
# We know that it does not fit, so change/calculate the new index and highest_index accordingly.
|
# We know that it does not fit, so change/calculate the new index and highest_index accordingly.
|
||||||
highest_index = index
|
highest_index = index
|
||||||
index = int(index - (index - smallest_index) / 2)
|
index = index - (index - smallest_index) // 2
|
||||||
else:
|
else:
|
||||||
smallest_index = index
|
smallest_index = index
|
||||||
index = int(index + (highest_index - index) / 2)
|
index = index + (highest_index - index) // 2
|
||||||
# We found the number of words which will fit.
|
# We found the number of words which will fit.
|
||||||
if smallest_index == index or highest_index == index:
|
if smallest_index == index or highest_index == index:
|
||||||
index = smallest_index
|
index = smallest_index
|
||||||
@ -582,7 +582,7 @@ class Renderer(object):
|
|||||||
html_list[0] = html_tags + html_list[0]
|
html_list[0] = html_tags + html_list[0]
|
||||||
smallest_index = 0
|
smallest_index = 0
|
||||||
highest_index = len(html_list) - 1
|
highest_index = len(html_list) - 1
|
||||||
index = int(highest_index / 2)
|
index = highest_index // 2
|
||||||
return previous_html, previous_raw
|
return previous_html, previous_raw
|
||||||
|
|
||||||
def _text_fits_on_slide(self, text):
|
def _text_fits_on_slide(self, text):
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
The :mod:`screen` module provides management functionality for a machines'
|
The :mod:`screen` module provides management functionality for a machines'
|
||||||
displays.
|
displays.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import division
|
||||||
import logging
|
import logging
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
@ -232,8 +233,8 @@ class ScreenList(object):
|
|||||||
``window``
|
``window``
|
||||||
A QWidget we are finding the location of.
|
A QWidget we are finding the location of.
|
||||||
"""
|
"""
|
||||||
x = window.x() + (window.width() / 2)
|
x = window.x() + (window.width() // 2)
|
||||||
y = window.y() + (window.height() / 2)
|
y = window.y() + (window.height() // 2)
|
||||||
for screen in self.screen_list:
|
for screen in self.screen_list:
|
||||||
size = screen[u'size']
|
size = screen[u'size']
|
||||||
if x >= size.x() and x <= (size.x() + size.width()) and y >= size.y() and y <= (size.y() + size.height()):
|
if x >= size.x() and x <= (size.x() + size.width()) and y >= size.y() and y <= (size.y() + size.height()):
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from __future__ import division
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
from PyQt4 import QtCore, QtGui
|
||||||
@ -85,10 +85,10 @@ class SearchEdit(QtGui.QLineEdit):
|
|||||||
size = self.clear_button.size()
|
size = self.clear_button.size()
|
||||||
frame_width = self.style().pixelMetric(QtGui.QStyle.PM_DefaultFrameWidth)
|
frame_width = self.style().pixelMetric(QtGui.QStyle.PM_DefaultFrameWidth)
|
||||||
self.clear_button.move(self.rect().right() - frame_width - size.width(),
|
self.clear_button.move(self.rect().right() - frame_width - size.width(),
|
||||||
(self.rect().bottom() + 1 - size.height()) / 2)
|
(self.rect().bottom() + 1 - size.height()) // 2)
|
||||||
if hasattr(self, u'menu_button'):
|
if hasattr(self, u'menu_button'):
|
||||||
size = self.menu_button.size()
|
size = self.menu_button.size()
|
||||||
self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) / 2)
|
self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2)
|
||||||
|
|
||||||
def current_search_type(self):
|
def current_search_type(self):
|
||||||
"""
|
"""
|
||||||
|
@ -58,8 +58,7 @@ class ItemCapabilities(object):
|
|||||||
Provides an enumeration of a service item's capabilities
|
Provides an enumeration of a service item's capabilities
|
||||||
|
|
||||||
``CanPreview``
|
``CanPreview``
|
||||||
The capability to allow the ServiceManager to add to the preview
|
The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
|
||||||
tab when making the previous item live.
|
|
||||||
|
|
||||||
``CanEdit``
|
``CanEdit``
|
||||||
The capability to allow the ServiceManager to allow the item to be edited
|
The capability to allow the ServiceManager to allow the item to be edited
|
||||||
@ -71,8 +70,7 @@ class ItemCapabilities(object):
|
|||||||
Determines is the service_item needs a Media Player
|
Determines is the service_item needs a Media Player
|
||||||
|
|
||||||
``CanLoop``
|
``CanLoop``
|
||||||
The capability to allow the SlideController to allow the loop
|
The capability to allow the SlideController to allow the loop processing.
|
||||||
processing.
|
|
||||||
|
|
||||||
``CanAppend``
|
``CanAppend``
|
||||||
The capability to allow the ServiceManager to add leaves to the
|
The capability to allow the ServiceManager to add leaves to the
|
||||||
@ -82,22 +80,19 @@ class ItemCapabilities(object):
|
|||||||
The capability to remove lines breaks in the renderer
|
The capability to remove lines breaks in the renderer
|
||||||
|
|
||||||
``OnLoadUpdate``
|
``OnLoadUpdate``
|
||||||
The capability to update MediaManager when a service Item is
|
The capability to update MediaManager when a service Item is loaded.
|
||||||
loaded.
|
|
||||||
|
|
||||||
``AddIfNewItem``
|
``AddIfNewItem``
|
||||||
Not Used
|
Not Used
|
||||||
|
|
||||||
``ProvidesOwnDisplay``
|
``ProvidesOwnDisplay``
|
||||||
The capability to tell the SlideController the service Item has a
|
The capability to tell the SlideController the service Item has a different display.
|
||||||
different display.
|
|
||||||
|
|
||||||
``HasDetailedTitleDisplay``
|
``HasDetailedTitleDisplay``
|
||||||
ServiceItem provides a title
|
Being Removed and decommissioned.
|
||||||
|
|
||||||
``HasVariableStartTime``
|
``HasVariableStartTime``
|
||||||
The capability to tell the ServiceManager that a change to start
|
The capability to tell the ServiceManager that a change to start time is possible.
|
||||||
time is possible.
|
|
||||||
|
|
||||||
``CanSoftBreak``
|
``CanSoftBreak``
|
||||||
The capability to tell the renderer that Soft Break is allowed
|
The capability to tell the renderer that Soft Break is allowed
|
||||||
@ -149,7 +144,7 @@ class ServiceItem(object):
|
|||||||
if plugin:
|
if plugin:
|
||||||
self.name = plugin.name
|
self.name = plugin.name
|
||||||
self.title = u''
|
self.title = u''
|
||||||
self.shortname = u''
|
self.processor = None
|
||||||
self.audit = u''
|
self.audit = u''
|
||||||
self.items = []
|
self.items = []
|
||||||
self.iconic_representation = None
|
self.iconic_representation = None
|
||||||
@ -353,7 +348,8 @@ class ServiceItem(object):
|
|||||||
u'media_length': self.media_length,
|
u'media_length': self.media_length,
|
||||||
u'background_audio': self.background_audio,
|
u'background_audio': self.background_audio,
|
||||||
u'theme_overwritten': self.theme_overwritten,
|
u'theme_overwritten': self.theme_overwritten,
|
||||||
u'will_auto_start': self.will_auto_start
|
u'will_auto_start': self.will_auto_start,
|
||||||
|
u'processor': self.processor
|
||||||
}
|
}
|
||||||
service_data = []
|
service_data = []
|
||||||
if self.service_item_type == ServiceItemType.Text:
|
if self.service_item_type == ServiceItemType.Text:
|
||||||
@ -387,7 +383,6 @@ class ServiceItem(object):
|
|||||||
self.title = header[u'title']
|
self.title = header[u'title']
|
||||||
self.name = header[u'name']
|
self.name = header[u'name']
|
||||||
self.service_item_type = header[u'type']
|
self.service_item_type = header[u'type']
|
||||||
self.shortname = header[u'plugin']
|
|
||||||
self.theme = header[u'theme']
|
self.theme = header[u'theme']
|
||||||
self.add_icon(header[u'icon'])
|
self.add_icon(header[u'icon'])
|
||||||
self.raw_footer = header[u'footer']
|
self.raw_footer = header[u'footer']
|
||||||
@ -406,7 +401,13 @@ class ServiceItem(object):
|
|||||||
self.auto_play_slides_loop = header.get(u'auto_play_slides_loop', False)
|
self.auto_play_slides_loop = header.get(u'auto_play_slides_loop', False)
|
||||||
self.timed_slide_interval = header.get(u'timed_slide_interval', 0)
|
self.timed_slide_interval = header.get(u'timed_slide_interval', 0)
|
||||||
self.will_auto_start = header.get(u'will_auto_start', False)
|
self.will_auto_start = header.get(u'will_auto_start', False)
|
||||||
|
self.processor = header.get(u'processor', None)
|
||||||
self.has_original_files = True
|
self.has_original_files = True
|
||||||
|
#TODO Remove me in 2,3 build phase
|
||||||
|
if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay):
|
||||||
|
self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay)
|
||||||
|
self.processor = self.title
|
||||||
|
self.title = None
|
||||||
if u'background_audio' in header:
|
if u'background_audio' in header:
|
||||||
self.background_audio = []
|
self.background_audio = []
|
||||||
for filename in header[u'background_audio']:
|
for filename in header[u'background_audio']:
|
||||||
@ -429,6 +430,8 @@ class ServiceItem(object):
|
|||||||
self.add_from_image(text_image[u'path'], text_image[u'title'], background)
|
self.add_from_image(text_image[u'path'], text_image[u'title'], background)
|
||||||
elif self.service_item_type == ServiceItemType.Command:
|
elif self.service_item_type == ServiceItemType.Command:
|
||||||
for text_image in serviceitem[u'serviceitem'][u'data']:
|
for text_image in serviceitem[u'serviceitem'][u'data']:
|
||||||
|
if not self.title:
|
||||||
|
self.title = text_image[u'title']
|
||||||
if path:
|
if path:
|
||||||
self.has_original_files = False
|
self.has_original_files = False
|
||||||
self.add_from_command(path, text_image[u'title'], text_image[u'image'])
|
self.add_from_command(path, text_image[u'title'], text_image[u'image'])
|
||||||
@ -443,9 +446,7 @@ class ServiceItem(object):
|
|||||||
if self.is_text():
|
if self.is_text():
|
||||||
return self.title
|
return self.title
|
||||||
else:
|
else:
|
||||||
if ItemCapabilities.HasDetailedTitleDisplay in self.capabilities:
|
if len(self._raw_frames) > 1:
|
||||||
return self._raw_frames[0][u'title']
|
|
||||||
elif len(self._raw_frames) > 1:
|
|
||||||
return self.title
|
return self.title
|
||||||
else:
|
else:
|
||||||
return self._raw_frames[0][u'title']
|
return self._raw_frames[0][u'title']
|
||||||
|
@ -271,6 +271,7 @@ class Settings(QtCore.QSettings):
|
|||||||
u'shortcuts/songImportItem': [],
|
u'shortcuts/songImportItem': [],
|
||||||
u'shortcuts/themeScreen': [QtGui.QKeySequence(u'T')],
|
u'shortcuts/themeScreen': [QtGui.QKeySequence(u'T')],
|
||||||
u'shortcuts/toolsReindexItem': [],
|
u'shortcuts/toolsReindexItem': [],
|
||||||
|
u'shortcuts/toolsFindDuplicates': [],
|
||||||
u'shortcuts/toolsAlertItem': [QtGui.QKeySequence(u'F7')],
|
u'shortcuts/toolsAlertItem': [QtGui.QKeySequence(u'F7')],
|
||||||
u'shortcuts/toolsFirstTimeWizard': [],
|
u'shortcuts/toolsFirstTimeWizard': [],
|
||||||
u'shortcuts/toolsOpenDataFolder': [],
|
u'shortcuts/toolsOpenDataFolder': [],
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
The :mod:`~openlp.core.lib.settingstab` module contains the base SettingsTab class which plugins use for adding their
|
The :mod:`~openlp.core.lib.settingstab` module contains the base SettingsTab class which plugins use for adding their
|
||||||
own tab to the settings dialog.
|
own tab to the settings dialog.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
from PyQt4 import QtGui
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ class SettingsTab(QtGui.QWidget):
|
|||||||
QtGui.QWidget.resizeEvent(self, event)
|
QtGui.QWidget.resizeEvent(self, event)
|
||||||
width = self.width() - self.tab_layout.spacing() - \
|
width = self.width() - self.tab_layout.spacing() - \
|
||||||
self.tab_layout.contentsMargins().left() - self.tab_layout.contentsMargins().right()
|
self.tab_layout.contentsMargins().left() - self.tab_layout.contentsMargins().right()
|
||||||
left_width = min(width - self.right_column.minimumSizeHint().width(), width / 2)
|
left_width = min(width - self.right_column.minimumSizeHint().width(), width // 2)
|
||||||
left_width = max(left_width, self.left_column.minimumSizeHint().width())
|
left_width = max(left_width, self.left_column.minimumSizeHint().width())
|
||||||
self.left_column.setFixedWidth(left_width)
|
self.left_column.setFixedWidth(left_width)
|
||||||
|
|
||||||
|
173
openlp/core/ui/listpreviewwidget.py
Normal file
173
openlp/core/ui/listpreviewwidget.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2013 Raoul Snyman #
|
||||||
|
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
|
||||||
|
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||||
|
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||||
|
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||||
|
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||||
|
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||||
|
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller.
|
||||||
|
It is based on a QTableWidget but represents its contents in list form.
|
||||||
|
"""
|
||||||
|
from __future__ import division
|
||||||
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import ImageSource, Registry, ServiceItem
|
||||||
|
|
||||||
|
|
||||||
|
class ListPreviewWidget(QtGui.QTableWidget):
|
||||||
|
def __init__(self, parent, screen_ratio):
|
||||||
|
"""
|
||||||
|
Initializes the widget to default state.
|
||||||
|
An empty ServiceItem is used per default.
|
||||||
|
One needs to call replace_service_manager_item() to make this widget display something.
|
||||||
|
"""
|
||||||
|
super(QtGui.QTableWidget, self).__init__(parent)
|
||||||
|
# Set up the widget.
|
||||||
|
self.setColumnCount(1)
|
||||||
|
self.horizontalHeader().setVisible(False)
|
||||||
|
self.setColumnWidth(0, parent.width())
|
||||||
|
self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||||
|
self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||||
|
self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||||
|
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||||
|
self.setAlternatingRowColors(True)
|
||||||
|
# Initialize variables.
|
||||||
|
self.service_item = ServiceItem()
|
||||||
|
self.screen_ratio = screen_ratio
|
||||||
|
|
||||||
|
def resizeEvent(self, QResizeEvent):
|
||||||
|
"""
|
||||||
|
Overloaded method from QTableWidget. Will recalculate the layout.
|
||||||
|
"""
|
||||||
|
self.__recalculate_layout()
|
||||||
|
|
||||||
|
def __recalculate_layout(self):
|
||||||
|
"""
|
||||||
|
Recalculates the layout of the table widget. It will set height and width
|
||||||
|
of the table cells. QTableWidget does not adapt the cells to the widget size on its own.
|
||||||
|
"""
|
||||||
|
self.setColumnWidth(0, self.viewport().width())
|
||||||
|
if self.service_item:
|
||||||
|
# Sort out songs, bibles, etc.
|
||||||
|
if self.service_item.is_text():
|
||||||
|
self.resizeRowsToContents()
|
||||||
|
else:
|
||||||
|
# Sort out image heights.
|
||||||
|
for framenumber in range(len(self.service_item.get_frames())):
|
||||||
|
height = self.viewport().width() // self.screen_ratio
|
||||||
|
self.setRowHeight(framenumber, height)
|
||||||
|
|
||||||
|
def screen_size_changed(self, screen_ratio):
|
||||||
|
"""
|
||||||
|
To be called whenever the live screen size changes.
|
||||||
|
Because this makes a layout recalculation necessary.
|
||||||
|
"""
|
||||||
|
self.screen_ratio = screen_ratio
|
||||||
|
self.__recalculate_layout()
|
||||||
|
|
||||||
|
def replace_service_item(self, service_item, width, slideNumber):
|
||||||
|
"""
|
||||||
|
Replaces the current preview items with the ones in service_item.
|
||||||
|
Displays the given slide.
|
||||||
|
"""
|
||||||
|
self.service_item = service_item
|
||||||
|
self.clear()
|
||||||
|
self.setRowCount(0)
|
||||||
|
self.setColumnWidth(0, width)
|
||||||
|
row = 0
|
||||||
|
text = []
|
||||||
|
for framenumber, frame in enumerate(self.service_item.get_frames()):
|
||||||
|
self.setRowCount(self.slide_count() + 1)
|
||||||
|
item = QtGui.QTableWidgetItem()
|
||||||
|
slide_height = 0
|
||||||
|
if self.service_item.is_text():
|
||||||
|
if frame[u'verseTag']:
|
||||||
|
# These tags are already translated.
|
||||||
|
verse_def = frame[u'verseTag']
|
||||||
|
verse_def = u'%s%s' % (verse_def[0], verse_def[1:])
|
||||||
|
two_line_def = u'%s\n%s' % (verse_def[0], verse_def[1:])
|
||||||
|
row = two_line_def
|
||||||
|
else:
|
||||||
|
row += 1
|
||||||
|
item.setText(frame[u'text'])
|
||||||
|
else:
|
||||||
|
label = QtGui.QLabel()
|
||||||
|
label.setMargin(4)
|
||||||
|
if self.service_item.is_media():
|
||||||
|
label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
|
||||||
|
else:
|
||||||
|
label.setScaledContents(True)
|
||||||
|
if self.service_item.is_command():
|
||||||
|
label.setPixmap(QtGui.QPixmap(frame[u'image']))
|
||||||
|
else:
|
||||||
|
image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin)
|
||||||
|
label.setPixmap(QtGui.QPixmap.fromImage(image))
|
||||||
|
self.setCellWidget(framenumber, 0, label)
|
||||||
|
slide_height = width // self.screen_ratio
|
||||||
|
row += 1
|
||||||
|
text.append(unicode(row))
|
||||||
|
self.setItem(framenumber, 0, item)
|
||||||
|
if slide_height:
|
||||||
|
self.setRowHeight(framenumber, slide_height)
|
||||||
|
self.setVerticalHeaderLabels(text)
|
||||||
|
if self.service_item.is_text():
|
||||||
|
self.resizeRowsToContents()
|
||||||
|
self.setColumnWidth(0, self.viewport().width())
|
||||||
|
self.setFocus()
|
||||||
|
self.change_slide(slideNumber)
|
||||||
|
|
||||||
|
def change_slide(self, slide):
|
||||||
|
"""
|
||||||
|
Switches to the given row.
|
||||||
|
"""
|
||||||
|
if slide >= self.slide_count():
|
||||||
|
slide = self.slide_count() - 1
|
||||||
|
# Scroll to next item if possible.
|
||||||
|
if slide + 1 < self.slide_count():
|
||||||
|
self.scrollToItem(self.item(slide + 1, 0))
|
||||||
|
self.selectRow(slide)
|
||||||
|
|
||||||
|
def current_slide_number(self):
|
||||||
|
"""
|
||||||
|
Returns the position of the currently active item. Will return -1 if the widget is empty.
|
||||||
|
"""
|
||||||
|
return super(ListPreviewWidget, self).currentRow()
|
||||||
|
|
||||||
|
def slide_count(self):
|
||||||
|
"""
|
||||||
|
Returns the number of slides this widget holds.
|
||||||
|
"""
|
||||||
|
return super(ListPreviewWidget, self).rowCount()
|
||||||
|
|
||||||
|
def _get_image_manager(self):
|
||||||
|
"""
|
||||||
|
Adds the image manager to the class dynamically.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, u'_image_manager'):
|
||||||
|
self._image_manager = Registry().get(u'image_manager')
|
||||||
|
return self._image_manager
|
||||||
|
|
||||||
|
image_manager = property(_get_image_manager)
|
||||||
|
|
@ -35,6 +35,7 @@ Some of the code for this form is based on the examples at:
|
|||||||
* `http://html5demos.com/two-videos`_
|
* `http://html5demos.com/two-videos`_
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import division
|
||||||
import cgi
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
@ -207,8 +208,8 @@ class MainDisplay(Display):
|
|||||||
painter_image.begin(self.initial_fame)
|
painter_image.begin(self.initial_fame)
|
||||||
painter_image.fillRect(self.initial_fame.rect(), background_color)
|
painter_image.fillRect(self.initial_fame.rect(), background_color)
|
||||||
painter_image.drawImage(
|
painter_image.drawImage(
|
||||||
(self.screen[u'size'].width() - splash_image.width()) / 2,
|
(self.screen[u'size'].width() - splash_image.width()) // 2,
|
||||||
(self.screen[u'size'].height() - splash_image.height()) / 2,
|
(self.screen[u'size'].height() - splash_image.height()) // 2,
|
||||||
splash_image)
|
splash_image)
|
||||||
service_item = ServiceItem()
|
service_item = ServiceItem()
|
||||||
service_item.bg_image_bytes = image_to_byte(self.initial_fame)
|
service_item.bg_image_bytes = image_to_byte(self.initial_fame)
|
||||||
@ -268,7 +269,7 @@ class MainDisplay(Display):
|
|||||||
self.resize(self.width(), alert_height)
|
self.resize(self.width(), alert_height)
|
||||||
self.setVisible(True)
|
self.setVisible(True)
|
||||||
if location == AlertLocation.Middle:
|
if location == AlertLocation.Middle:
|
||||||
self.move(self.screen[u'size'].left(), (self.screen[u'size'].height() - alert_height) / 2)
|
self.move(self.screen[u'size'].left(), (self.screen[u'size'].height() - alert_height) // 2)
|
||||||
elif location == AlertLocation.Bottom:
|
elif location == AlertLocation.Bottom:
|
||||||
self.move(self.screen[u'size'].left(), self.screen[u'size'].height() - alert_height)
|
self.move(self.screen[u'size'].left(), self.screen[u'size'].height() - alert_height)
|
||||||
else:
|
else:
|
||||||
@ -287,7 +288,7 @@ class MainDisplay(Display):
|
|||||||
self.image(path)
|
self.image(path)
|
||||||
# Update the preview frame.
|
# Update the preview frame.
|
||||||
if self.is_live:
|
if self.is_live:
|
||||||
self.live_controller.updatePreview()
|
self.live_controller.update_preview()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def image(self, path):
|
def image(self, path):
|
||||||
|
@ -669,7 +669,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
|
|||||||
Check and display message if screen blank on setup.
|
Check and display message if screen blank on setup.
|
||||||
"""
|
"""
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
self.live_controller.mainDisplaySetBackground()
|
self.live_controller.main_display_set_background()
|
||||||
if settings.value(u'%s/screen blank' % self.general_settings_section):
|
if settings.value(u'%s/screen blank' % self.general_settings_section):
|
||||||
if settings.value(u'%s/blank warning' % self.general_settings_section):
|
if settings.value(u'%s/blank warning' % self.general_settings_section):
|
||||||
QtGui.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Main Display Blanked'),
|
QtGui.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Main Display Blanked'),
|
||||||
@ -779,8 +779,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
|
|||||||
"""
|
"""
|
||||||
We need to make sure, that the SlidePreview's size is correct.
|
We need to make sure, that the SlidePreview's size is correct.
|
||||||
"""
|
"""
|
||||||
self.preview_controller.previewSizeChanged()
|
self.preview_controller.preview_size_changed()
|
||||||
self.live_controller.previewSizeChanged()
|
self.live_controller.preview_size_changed()
|
||||||
|
|
||||||
def on_settings_shortcuts_item_clicked(self):
|
def on_settings_shortcuts_item_clicked(self):
|
||||||
"""
|
"""
|
||||||
@ -989,8 +989,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
|
|||||||
self.application.set_busy_cursor()
|
self.application.set_busy_cursor()
|
||||||
self.image_manager.update_display()
|
self.image_manager.update_display()
|
||||||
self.renderer.update_display()
|
self.renderer.update_display()
|
||||||
self.preview_controller.screenSizeChanged()
|
self.preview_controller.screen_size_changed()
|
||||||
self.live_controller.screenSizeChanged()
|
self.live_controller.screen_size_changed()
|
||||||
self.setFocus()
|
self.setFocus()
|
||||||
self.activateWindow()
|
self.activateWindow()
|
||||||
self.application.set_normal_cursor()
|
self.application.set_normal_cursor()
|
||||||
|
@ -466,8 +466,8 @@ class MediaController(object):
|
|||||||
The ServiceItem containing the details to be played.
|
The ServiceItem containing the details to be played.
|
||||||
"""
|
"""
|
||||||
used_players = get_media_players()[0]
|
used_players = get_media_players()[0]
|
||||||
if service_item.title != UiStrings().Automatic:
|
if service_item.processor != UiStrings().Automatic:
|
||||||
used_players = [service_item.title.lower()]
|
used_players = [service_item.processor.lower()]
|
||||||
if controller.media_info.file_info.isFile():
|
if controller.media_info.file_info.isFile():
|
||||||
suffix = u'*.%s' % controller.media_info.file_info.suffix().lower()
|
suffix = u'*.%s' % controller.media_info.file_info.suffix().lower()
|
||||||
for title in used_players:
|
for title in used_players:
|
||||||
|
@ -31,7 +31,7 @@ The actual plugin view form
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
from openlp.core.lib import PluginStatus, Registry, translate
|
from openlp.core.lib import PluginStatus, Registry, translate
|
||||||
from plugindialog import Ui_PluginViewDialog
|
from plugindialog import Ui_PluginViewDialog
|
||||||
|
@ -715,13 +715,10 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
|||||||
else:
|
else:
|
||||||
service_item.set_from_service(item, self.servicePath)
|
service_item.set_from_service(item, self.servicePath)
|
||||||
service_item.validate_item(self.suffixes)
|
service_item.validate_item(self.suffixes)
|
||||||
self.load_item_unique_identifier = 0
|
|
||||||
if service_item.is_capable(ItemCapabilities.OnLoadUpdate):
|
if service_item.is_capable(ItemCapabilities.OnLoadUpdate):
|
||||||
Registry().execute(u'%s_service_load' % service_item.name.lower(), service_item)
|
new_item = Registry().get(service_item.name).service_load(service_item)
|
||||||
# if the item has been processed
|
if new_item:
|
||||||
if service_item.unique_identifier == self.load_item_unique_identifier:
|
service_item = new_item
|
||||||
service_item.edit_id = int(self.load_item_edit_id)
|
|
||||||
service_item.temporary_edit = self.load_item_temporary
|
|
||||||
self.add_service_item(service_item, repaint=False)
|
self.add_service_item(service_item, repaint=False)
|
||||||
delete_file(p_file)
|
delete_file(p_file)
|
||||||
self.main_window.add_recent_file(file_name)
|
self.main_window.add_recent_file(file_name)
|
||||||
@ -1260,14 +1257,6 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
|||||||
self.repaint_service_list(-1, -1)
|
self.repaint_service_list(-1, -1)
|
||||||
self.application.set_normal_cursor()
|
self.application.set_normal_cursor()
|
||||||
|
|
||||||
def service_item_update(self, edit_id, unique_identifier, temporary=False):
|
|
||||||
"""
|
|
||||||
Triggered from plugins to update service items. Save the values as they will be used as part of the service load
|
|
||||||
"""
|
|
||||||
self.load_item_unique_identifier = unique_identifier
|
|
||||||
self.load_item_edit_id = int(edit_id)
|
|
||||||
self.load_item_temporary = str_to_bool(temporary)
|
|
||||||
|
|
||||||
def replace_service_item(self, newItem):
|
def replace_service_item(self, newItem):
|
||||||
"""
|
"""
|
||||||
Using the service item passed replace the one with the same edit id if found.
|
Using the service item passed replace the one with the same edit id if found.
|
||||||
@ -1278,7 +1267,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
|||||||
newItem.merge(item[u'service_item'])
|
newItem.merge(item[u'service_item'])
|
||||||
item[u'service_item'] = newItem
|
item[u'service_item'] = newItem
|
||||||
self.repaint_service_list(item_count + 1, 0)
|
self.repaint_service_list(item_count + 1, 0)
|
||||||
self.live_controller.replaceServiceManagerItem(newItem)
|
self.live_controller.replace_service_manager_item(newItem)
|
||||||
self.set_modified()
|
self.set_modified()
|
||||||
|
|
||||||
def add_service_item(self, item, rebuild=False, expand=None, replace=False, repaint=True, selected=False):
|
def add_service_item(self, item, rebuild=False, expand=None, replace=False, repaint=True, selected=False):
|
||||||
@ -1300,7 +1289,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
|||||||
item.merge(self.service_items[sitem][u'service_item'])
|
item.merge(self.service_items[sitem][u'service_item'])
|
||||||
self.service_items[sitem][u'service_item'] = item
|
self.service_items[sitem][u'service_item'] = item
|
||||||
self.repaint_service_list(sitem, child)
|
self.repaint_service_list(sitem, child)
|
||||||
self.live_controller.replaceServiceManagerItem(item)
|
self.live_controller.replace_service_manager_item(item)
|
||||||
else:
|
else:
|
||||||
item.render()
|
item.render()
|
||||||
# nothing selected for dnd
|
# nothing selected for dnd
|
||||||
@ -1323,7 +1312,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
|||||||
self.repaint_service_list(self.drop_position, -1)
|
self.repaint_service_list(self.drop_position, -1)
|
||||||
# if rebuilding list make sure live is fixed.
|
# if rebuilding list make sure live is fixed.
|
||||||
if rebuild:
|
if rebuild:
|
||||||
self.live_controller.replaceServiceManagerItem(item)
|
self.live_controller.replace_service_manager_item(item)
|
||||||
self.drop_position = 0
|
self.drop_position = 0
|
||||||
self.set_modified()
|
self.set_modified()
|
||||||
|
|
||||||
@ -1334,7 +1323,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
|||||||
self.application.set_busy_cursor()
|
self.application.set_busy_cursor()
|
||||||
item, child = self.find_service_item()
|
item, child = self.find_service_item()
|
||||||
if self.service_items[item][u'service_item'].is_valid:
|
if self.service_items[item][u'service_item'].is_valid:
|
||||||
self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], child)
|
self.preview_controller.add_service_manager_item(self.service_items[item][u'service_item'], child)
|
||||||
else:
|
else:
|
||||||
critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'),
|
critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'),
|
||||||
translate('OpenLP.ServiceManager',
|
translate('OpenLP.ServiceManager',
|
||||||
@ -1372,15 +1361,15 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
|||||||
child = row
|
child = row
|
||||||
self.application.set_busy_cursor()
|
self.application.set_busy_cursor()
|
||||||
if self.service_items[item][u'service_item'].is_valid:
|
if self.service_items[item][u'service_item'].is_valid:
|
||||||
self.live_controller.addServiceManagerItem(self.service_items[item][u'service_item'], child)
|
self.live_controller.add_service_manager_item(self.service_items[item][u'service_item'], child)
|
||||||
if Settings().value(self.main_window.general_settings_section + u'/auto preview'):
|
if Settings().value(self.main_window.general_settings_section + u'/auto preview'):
|
||||||
item += 1
|
item += 1
|
||||||
if self.service_items and item < len(self.service_items) and \
|
if self.service_items and item < len(self.service_items) and \
|
||||||
self.service_items[item][u'service_item'].is_capable(ItemCapabilities.CanPreview):
|
self.service_items[item][u'service_item'].is_capable(ItemCapabilities.CanPreview):
|
||||||
self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], 0)
|
self.preview_controller.add_service_manager_item(self.service_items[item][u'service_item'], 0)
|
||||||
next_item = self.service_manager_list.topLevelItem(item)
|
next_item = self.service_manager_list.topLevelItem(item)
|
||||||
self.service_manager_list.setCurrentItem(next_item)
|
self.service_manager_list.setCurrentItem(next_item)
|
||||||
self.live_controller.preview_list_widget.setFocus()
|
self.live_controller.preview_widget.setFocus()
|
||||||
else:
|
else:
|
||||||
critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'),
|
critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'),
|
||||||
translate('OpenLP.ServiceManager',
|
translate('OpenLP.ServiceManager',
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,8 @@
|
|||||||
"""
|
"""
|
||||||
The Themes configuration tab
|
The Themes configuration tab
|
||||||
"""
|
"""
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
from openlp.core.lib import Registry, Settings, SettingsTab, UiStrings, translate
|
from openlp.core.lib import Registry, Settings, SettingsTab, UiStrings, translate
|
||||||
@ -90,7 +92,7 @@ class ThemesTab(SettingsTab):
|
|||||||
self.global_level_label.setObjectName(u'global_level_label')
|
self.global_level_label.setObjectName(u'global_level_label')
|
||||||
self.level_layout.addRow(self.global_level_radio_button, self.global_level_label)
|
self.level_layout.addRow(self.global_level_radio_button, self.global_level_label)
|
||||||
label_top_margin = (self.song_level_radio_button.sizeHint().height() -
|
label_top_margin = (self.song_level_radio_button.sizeHint().height() -
|
||||||
self.song_level_label.sizeHint().height()) / 2
|
self.song_level_label.sizeHint().height()) // 2
|
||||||
for label in [self.song_level_label, self.service_level_label, self.global_level_label]:
|
for label in [self.song_level_label, self.service_level_label, self.global_level_label]:
|
||||||
rect = label.rect()
|
rect = label.rect()
|
||||||
rect.setTop(rect.top() + label_top_margin)
|
rect.setTop(rect.top() + label_top_margin)
|
||||||
|
@ -178,8 +178,8 @@ class Ui_ThemeWizard(object):
|
|||||||
self.lineSpacingLabel = QtGui.QLabel(self.mainAreaPage)
|
self.lineSpacingLabel = QtGui.QLabel(self.mainAreaPage)
|
||||||
self.lineSpacingLabel.setObjectName(u'LineSpacingLabel')
|
self.lineSpacingLabel.setObjectName(u'LineSpacingLabel')
|
||||||
self.lineSpacingSpinBox = QtGui.QSpinBox(self.mainAreaPage)
|
self.lineSpacingSpinBox = QtGui.QSpinBox(self.mainAreaPage)
|
||||||
self.lineSpacingSpinBox.setMinimum(-50)
|
self.lineSpacingSpinBox.setMinimum(-250)
|
||||||
self.lineSpacingSpinBox.setMaximum(50)
|
self.lineSpacingSpinBox.setMaximum(250)
|
||||||
self.lineSpacingSpinBox.setObjectName(u'LineSpacingSpinBox')
|
self.lineSpacingSpinBox.setObjectName(u'LineSpacingSpinBox')
|
||||||
self.mainAreaLayout.addRow(self.lineSpacingLabel, self.lineSpacingSpinBox)
|
self.mainAreaLayout.addRow(self.lineSpacingLabel, self.lineSpacingSpinBox)
|
||||||
self.outlineCheckBox = QtGui.QCheckBox(self.mainAreaPage)
|
self.outlineCheckBox = QtGui.QCheckBox(self.mainAreaPage)
|
||||||
|
@ -75,13 +75,30 @@ class OpenLPWizard(QtGui.QWizard):
|
|||||||
"""
|
"""
|
||||||
Generic OpenLP wizard to provide generic functionality and a unified look
|
Generic OpenLP wizard to provide generic functionality and a unified look
|
||||||
and feel.
|
and feel.
|
||||||
|
|
||||||
|
``parent``
|
||||||
|
The QWidget-derived parent of the wizard.
|
||||||
|
|
||||||
|
``plugin``
|
||||||
|
Plugin this wizard is part of. The plugin will be saved in the "plugin" variable.
|
||||||
|
The plugin will also be used as basis for the file dialog methods this class provides.
|
||||||
|
|
||||||
|
``name``
|
||||||
|
The object name this wizard should have.
|
||||||
|
|
||||||
|
``image``
|
||||||
|
The image to display on the "welcome" page of the wizard. Should be 163x350.
|
||||||
|
|
||||||
|
``add_progress_page``
|
||||||
|
Whether to add a progress page with a progressbar at the end of the wizard.
|
||||||
"""
|
"""
|
||||||
def __init__(self, parent, plugin, name, image):
|
def __init__(self, parent, plugin, name, image, add_progress_page=True):
|
||||||
"""
|
"""
|
||||||
Constructor
|
Constructor
|
||||||
"""
|
"""
|
||||||
QtGui.QWizard.__init__(self, parent)
|
QtGui.QWizard.__init__(self, parent)
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
|
self.with_progress_page = add_progress_page
|
||||||
self.setObjectName(name)
|
self.setObjectName(name)
|
||||||
self.open_icon = build_icon(u':/general/general_open.png')
|
self.open_icon = build_icon(u':/general/general_open.png')
|
||||||
self.delete_icon = build_icon(u':/general/general_delete.png')
|
self.delete_icon = build_icon(u':/general/general_delete.png')
|
||||||
@ -92,8 +109,9 @@ class OpenLPWizard(QtGui.QWizard):
|
|||||||
self.custom_init()
|
self.custom_init()
|
||||||
self.custom_signals()
|
self.custom_signals()
|
||||||
self.currentIdChanged.connect(self.on_current_id_changed)
|
self.currentIdChanged.connect(self.on_current_id_changed)
|
||||||
self.error_copy_to_button.clicked.connect(self.on_error_copy_to_button_clicked)
|
if self.with_progress_page:
|
||||||
self.error_save_to_button.clicked.connect(self.on_error_save_to_button_clicked)
|
self.error_copy_to_button.clicked.connect(self.on_error_copy_to_button_clicked)
|
||||||
|
self.error_save_to_button.clicked.connect(self.on_error_save_to_button_clicked)
|
||||||
|
|
||||||
def setupUi(self, image):
|
def setupUi(self, image):
|
||||||
"""
|
"""
|
||||||
@ -105,7 +123,8 @@ class OpenLPWizard(QtGui.QWizard):
|
|||||||
QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage)
|
QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage)
|
||||||
add_welcome_page(self, image)
|
add_welcome_page(self, image)
|
||||||
self.add_custom_pages()
|
self.add_custom_pages()
|
||||||
self.add_progress_page()
|
if self.with_progress_page:
|
||||||
|
self.add_progress_page()
|
||||||
self.retranslateUi()
|
self.retranslateUi()
|
||||||
|
|
||||||
def register_fields(self):
|
def register_fields(self):
|
||||||
@ -185,7 +204,7 @@ class OpenLPWizard(QtGui.QWizard):
|
|||||||
Stop the wizard on cancel button, close button or ESC key.
|
Stop the wizard on cancel button, close button or ESC key.
|
||||||
"""
|
"""
|
||||||
log.debug(u'Wizard cancelled by user.')
|
log.debug(u'Wizard cancelled by user.')
|
||||||
if self.currentPage() == self.progress_page:
|
if self.with_progress_page and self.currentPage() == self.progress_page:
|
||||||
Registry().execute(u'openlp_stop_wizard')
|
Registry().execute(u'openlp_stop_wizard')
|
||||||
self.done(QtGui.QDialog.Rejected)
|
self.done(QtGui.QDialog.Rejected)
|
||||||
|
|
||||||
@ -193,14 +212,14 @@ class OpenLPWizard(QtGui.QWizard):
|
|||||||
"""
|
"""
|
||||||
Perform necessary functions depending on which wizard page is active.
|
Perform necessary functions depending on which wizard page is active.
|
||||||
"""
|
"""
|
||||||
if self.page(pageId) == self.progress_page:
|
if self.with_progress_page and self.page(pageId) == self.progress_page:
|
||||||
self.pre_wizard()
|
self.pre_wizard()
|
||||||
self.performWizard()
|
self.performWizard()
|
||||||
self.post_wizard()
|
self.post_wizard()
|
||||||
else:
|
else:
|
||||||
self.custom_cage_changed(pageId)
|
self.custom_page_changed(pageId)
|
||||||
|
|
||||||
def custom_cage_changed(self, pageId):
|
def custom_page_changed(self, pageId):
|
||||||
"""
|
"""
|
||||||
Called when changing to a page other than the progress page
|
Called when changing to a page other than the progress page
|
||||||
"""
|
"""
|
||||||
|
@ -101,46 +101,38 @@ def get_application_version():
|
|||||||
if APPLICATION_VERSION:
|
if APPLICATION_VERSION:
|
||||||
return APPLICATION_VERSION
|
return APPLICATION_VERSION
|
||||||
if u'--dev-version' in sys.argv or u'-d' in sys.argv:
|
if u'--dev-version' in sys.argv or u'-d' in sys.argv:
|
||||||
# If we're running the dev version, let's use bzr to get the version.
|
# NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
|
||||||
try:
|
# there.
|
||||||
# If bzrlib is available, use it.
|
|
||||||
from bzrlib.branch import Branch
|
# Get the revision of this tree.
|
||||||
b = Branch.open_containing('.')[0]
|
bzr = Popen((u'bzr', u'revno'), stdout=PIPE)
|
||||||
b.lock_read()
|
tree_revision, error = bzr.communicate()
|
||||||
try:
|
code = bzr.wait()
|
||||||
# Get the branch's latest revision number.
|
if code != 0:
|
||||||
revno = b.revno()
|
raise Exception(u'Error running bzr log')
|
||||||
# Convert said revision number into a bzr revision id.
|
|
||||||
revision_id = b.dotted_revno_to_revision_id((revno,))
|
# Get all tags.
|
||||||
# Get a dict of tags, with the revision id as the key.
|
bzr = Popen((u'bzr', u'tags'), stdout=PIPE)
|
||||||
tags = b.tags.get_reverse_tag_dict()
|
output, error = bzr.communicate()
|
||||||
# Check if the latest
|
code = bzr.wait()
|
||||||
if revision_id in tags:
|
if code != 0:
|
||||||
full_version = u'%s' % tags[revision_id][0]
|
raise Exception(u'Error running bzr tags')
|
||||||
else:
|
tags = output.splitlines()
|
||||||
full_version = '%s-bzr%s' % (sorted(b.tags.get_tag_dict().keys())[-1], revno)
|
if not tags:
|
||||||
finally:
|
tag_version = u'0.0.0'
|
||||||
b.unlock()
|
tag_revision = u'0'
|
||||||
except:
|
else:
|
||||||
# Otherwise run the command line bzr client.
|
# Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
|
||||||
bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE)
|
# another series.
|
||||||
output, error = bzr.communicate()
|
tags = [tag for tag in tags if tag.split()[-1].strip() != u'?']
|
||||||
code = bzr.wait()
|
# Get the last tag and split it in a revision and tag name.
|
||||||
if code != 0:
|
tag_version, tag_revision = tags[-1].split()
|
||||||
raise Exception(u'Error running bzr tags')
|
# If they are equal, then this tree is tarball with the source for the release. We do not want the revision
|
||||||
lines = output.splitlines()
|
# number in the full version.
|
||||||
if not lines:
|
if tree_revision == tag_revision:
|
||||||
tag = u'0.0.0'
|
full_version = tag_version
|
||||||
revision = u'0'
|
else:
|
||||||
else:
|
full_version = u'%s-bzr%s' % (tag_version, tree_revision)
|
||||||
tag, revision = lines[-1].split()
|
|
||||||
bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE)
|
|
||||||
output, error = bzr.communicate()
|
|
||||||
code = bzr.wait()
|
|
||||||
if code != 0:
|
|
||||||
raise Exception(u'Error running bzr log')
|
|
||||||
latest = output.split(u':')[0]
|
|
||||||
full_version = latest == revision and tag or u'%s-bzr%s' % (tag, latest)
|
|
||||||
else:
|
else:
|
||||||
# We're not running the development version, let's use the file.
|
# We're not running the development version, let's use the file.
|
||||||
filepath = AppLocation.get_directory(AppLocation.VersionDir)
|
filepath = AppLocation.get_directory(AppLocation.VersionDir)
|
||||||
|
@ -511,7 +511,7 @@ class BibleImportForm(OpenLPWizard):
|
|||||||
name = bible[u'abbreviation']
|
name = bible[u'abbreviation']
|
||||||
self.web_bible_list[download_type][version] = name.strip()
|
self.web_bible_list[download_type][version] = name.strip()
|
||||||
|
|
||||||
def preWizard(self):
|
def pre_wizard(self):
|
||||||
"""
|
"""
|
||||||
Prepare the UI for the import.
|
Prepare the UI for the import.
|
||||||
"""
|
"""
|
||||||
|
@ -105,7 +105,7 @@ class BibleUpgradeForm(OpenLPWizard):
|
|||||||
Perform necessary functions depending on which wizard page is active.
|
Perform necessary functions depending on which wizard page is active.
|
||||||
"""
|
"""
|
||||||
if self.page(pageId) == self.progress_page:
|
if self.page(pageId) == self.progress_page:
|
||||||
self.preWizard()
|
self.pre_wizard()
|
||||||
self.performWizard()
|
self.performWizard()
|
||||||
self.post_wizard()
|
self.post_wizard()
|
||||||
elif self.page(pageId) == self.selectPage and not self.files:
|
elif self.page(pageId) == self.selectPage and not self.files:
|
||||||
@ -329,7 +329,7 @@ class BibleUpgradeForm(OpenLPWizard):
|
|||||||
self.cancel_button.setVisible(True)
|
self.cancel_button.setVisible(True)
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
|
|
||||||
def preWizard(self):
|
def pre_wizard(self):
|
||||||
"""
|
"""
|
||||||
Prepare the UI for the upgrade.
|
Prepare the UI for the upgrade.
|
||||||
"""
|
"""
|
||||||
|
@ -40,6 +40,7 @@ from openlp.plugins.custom.lib.db import CustomSlide
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CustomSearch(object):
|
class CustomSearch(object):
|
||||||
"""
|
"""
|
||||||
An enumeration for custom search methods.
|
An enumeration for custom search methods.
|
||||||
@ -214,7 +215,6 @@ class CustomMediaItem(MediaManagerItem):
|
|||||||
Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type())
|
Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type())
|
||||||
# Reload the list considering the new search type.
|
# Reload the list considering the new search type.
|
||||||
search_keywords = self.search_text_edit.displayText()
|
search_keywords = self.search_text_edit.displayText()
|
||||||
search_results = []
|
|
||||||
search_type = self.search_text_edit.current_search_type()
|
search_type = self.search_text_edit.current_search_type()
|
||||||
if search_type == CustomSearch.Titles:
|
if search_type == CustomSearch.Titles:
|
||||||
log.debug(u'Titles Search')
|
log.debug(u'Titles Search')
|
||||||
@ -252,7 +252,8 @@ class CustomMediaItem(MediaManagerItem):
|
|||||||
and_(CustomSlide.title == item.title, CustomSlide.theme_name == item.theme,
|
and_(CustomSlide.title == item.title, CustomSlide.theme_name == item.theme,
|
||||||
CustomSlide.credits == item.raw_footer[0][len(item.title) + 1:]))
|
CustomSlide.credits == item.raw_footer[0][len(item.title) + 1:]))
|
||||||
if custom:
|
if custom:
|
||||||
self.service_manager.service_item_update(custom.id, item.unique_identifier)
|
item.edit_id = custom.id
|
||||||
|
return item
|
||||||
else:
|
else:
|
||||||
if self.add_custom_from_service:
|
if self.add_custom_from_service:
|
||||||
self.create_from_service_item(item)
|
self.create_from_service_item(item)
|
||||||
@ -281,8 +282,6 @@ class CustomMediaItem(MediaManagerItem):
|
|||||||
custom.text = unicode(custom_xml.extract_xml(), u'utf-8')
|
custom.text = unicode(custom_xml.extract_xml(), u'utf-8')
|
||||||
self.plugin.manager.save_object(custom)
|
self.plugin.manager.save_object(custom)
|
||||||
self.on_search_text_button_clicked()
|
self.on_search_text_button_clicked()
|
||||||
if item.name.lower() == u'custom':
|
|
||||||
Registry().execute(u'service_item_update', u'%s:%s:%s' % (custom.id, item.unique_identifier, False))
|
|
||||||
|
|
||||||
def on_clear_text_button_click(self):
|
def on_clear_text_button_click(self):
|
||||||
"""
|
"""
|
||||||
|
@ -50,6 +50,7 @@ class ChooseGroupForm(QtGui.QDialog, Ui_ChooseGroupDialog):
|
|||||||
``selected_group``
|
``selected_group``
|
||||||
The ID of the group that should be selected by default when showing the dialog.
|
The ID of the group that should be selected by default when showing the dialog.
|
||||||
"""
|
"""
|
||||||
|
self.new_group_edit.clear()
|
||||||
if selected_group is not None:
|
if selected_group is not None:
|
||||||
for index in range(self.group_combobox.count()):
|
for index in range(self.group_combobox.count()):
|
||||||
if self.group_combobox.itemData(index) == selected_group:
|
if self.group_combobox.itemData(index) == selected_group:
|
||||||
|
@ -32,7 +32,6 @@ from PyQt4 import QtGui
|
|||||||
from openlp.core.lib import Registry, SettingsTab, Settings, UiStrings, translate
|
from openlp.core.lib import Registry, SettingsTab, Settings, UiStrings, translate
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ImageTab(SettingsTab):
|
class ImageTab(SettingsTab):
|
||||||
"""
|
"""
|
||||||
ImageTab is the images settings tab in the settings dialog.
|
ImageTab is the images settings tab in the settings dialog.
|
||||||
|
@ -391,6 +391,7 @@ class ImageMediaItem(MediaManagerItem):
|
|||||||
``initial_load``
|
``initial_load``
|
||||||
When set to False, the busy cursor and progressbar will be shown while loading images
|
When set to False, the busy cursor and progressbar will be shown while loading images
|
||||||
"""
|
"""
|
||||||
|
parent_group = None
|
||||||
if target_group is None:
|
if target_group is None:
|
||||||
# Find out if a group must be pre-selected
|
# Find out if a group must be pre-selected
|
||||||
preselect_group = None
|
preselect_group = None
|
||||||
@ -436,6 +437,8 @@ class ImageMediaItem(MediaManagerItem):
|
|||||||
parent_group.parent_id = 0
|
parent_group.parent_id = 0
|
||||||
parent_group.group_name = self.choose_group_form.new_group_edit.text()
|
parent_group.group_name = self.choose_group_form.new_group_edit.text()
|
||||||
self.manager.save_object(parent_group)
|
self.manager.save_object(parent_group)
|
||||||
|
self.fill_groups_combobox(self.choose_group_form.group_combobox)
|
||||||
|
self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
|
||||||
else:
|
else:
|
||||||
parent_group = target_group.data(0, QtCore.Qt.UserRole)
|
parent_group = target_group.data(0, QtCore.Qt.UserRole)
|
||||||
if isinstance(parent_group, ImageFilenames):
|
if isinstance(parent_group, ImageFilenames):
|
||||||
@ -470,7 +473,7 @@ class ImageMediaItem(MediaManagerItem):
|
|||||||
This boolean is set to True when the list in the interface should be reloaded after saving the new images
|
This boolean is set to True when the list in the interface should be reloaded after saving the new images
|
||||||
"""
|
"""
|
||||||
for filename in images_list:
|
for filename in images_list:
|
||||||
if type(filename) is not str and type(filename) is not unicode:
|
if not isinstance(filename, basestring):
|
||||||
continue
|
continue
|
||||||
log.debug(u'Adding new image: %s', filename)
|
log.debug(u'Adding new image: %s', filename)
|
||||||
imageFile = ImageFilenames()
|
imageFile = ImageFilenames()
|
||||||
@ -550,28 +553,25 @@ class ImageMediaItem(MediaManagerItem):
|
|||||||
service_item.add_capability(ItemCapabilities.CanAppend)
|
service_item.add_capability(ItemCapabilities.CanAppend)
|
||||||
# force a nonexistent theme
|
# force a nonexistent theme
|
||||||
service_item.theme = -1
|
service_item.theme = -1
|
||||||
missing_items = []
|
|
||||||
missing_items_filenames = []
|
missing_items_filenames = []
|
||||||
|
images_filenames = []
|
||||||
# Expand groups to images
|
# Expand groups to images
|
||||||
for bitem in items:
|
for bitem in items:
|
||||||
if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None:
|
if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None:
|
||||||
for index in range(0, bitem.childCount()):
|
for index in range(0, bitem.childCount()):
|
||||||
if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames):
|
if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames):
|
||||||
items.append(bitem.child(index))
|
images_filenames.append(bitem.child(index).data(0, QtCore.Qt.UserRole).filename)
|
||||||
items.remove(bitem)
|
elif isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames):
|
||||||
|
images_filenames.append(bitem.data(0, QtCore.Qt.UserRole).filename)
|
||||||
# Don't try to display empty groups
|
# Don't try to display empty groups
|
||||||
if not items:
|
if not images_filenames:
|
||||||
return False
|
return False
|
||||||
# Find missing files
|
# Find missing files
|
||||||
for bitem in items:
|
for filename in images_filenames:
|
||||||
filename = bitem.data(0, QtCore.Qt.UserRole).filename
|
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
missing_items.append(bitem)
|
|
||||||
missing_items_filenames.append(filename)
|
missing_items_filenames.append(filename)
|
||||||
for item in missing_items:
|
|
||||||
items.remove(item)
|
|
||||||
# We cannot continue, as all images do not exist.
|
# We cannot continue, as all images do not exist.
|
||||||
if not items:
|
if not images_filenames:
|
||||||
if not remote:
|
if not remote:
|
||||||
critical_error_message_box(
|
critical_error_message_box(
|
||||||
translate('ImagePlugin.MediaItem', 'Missing Image(s)'),
|
translate('ImagePlugin.MediaItem', 'Missing Image(s)'),
|
||||||
@ -579,15 +579,14 @@ class ImageMediaItem(MediaManagerItem):
|
|||||||
u'\n'.join(missing_items_filenames))
|
u'\n'.join(missing_items_filenames))
|
||||||
return False
|
return False
|
||||||
# We have missing as well as existing images. We ask what to do.
|
# We have missing as well as existing images. We ask what to do.
|
||||||
elif missing_items and QtGui.QMessageBox.question(self,
|
elif missing_items_filenames and QtGui.QMessageBox.question(self,
|
||||||
translate('ImagePlugin.MediaItem', 'Missing Image(s)'),
|
translate('ImagePlugin.MediaItem', 'Missing Image(s)'),
|
||||||
translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n'
|
translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n'
|
||||||
'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames),
|
'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames),
|
||||||
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No:
|
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No:
|
||||||
return False
|
return False
|
||||||
# Continue with the existing images.
|
# Continue with the existing images.
|
||||||
for bitem in items:
|
for filename in images_filenames:
|
||||||
filename = bitem.data(0, QtCore.Qt.UserRole).filename
|
|
||||||
name = os.path.split(filename)[1]
|
name = os.path.split(filename)[1]
|
||||||
service_item.add_from_image(filename, name, background)
|
service_item.add_from_image(filename, name, background)
|
||||||
return True
|
return True
|
||||||
|
@ -155,7 +155,7 @@ class MediaMediaItem(MediaManagerItem):
|
|||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
service_item = ServiceItem()
|
service_item = ServiceItem()
|
||||||
service_item.title = u'webkit'
|
service_item.title = u'webkit'
|
||||||
service_item.shortname = service_item.title
|
service_item.processor = u'webkit'
|
||||||
(path, name) = os.path.split(filename)
|
(path, name) = os.path.split(filename)
|
||||||
service_item.add_from_command(path, name,CLAPPERBOARD)
|
service_item.add_from_command(path, name,CLAPPERBOARD)
|
||||||
if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True):
|
if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True):
|
||||||
@ -185,9 +185,9 @@ class MediaMediaItem(MediaManagerItem):
|
|||||||
translate('MediaPlugin.MediaItem', 'Missing Media File'),
|
translate('MediaPlugin.MediaItem', 'Missing Media File'),
|
||||||
translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename)
|
translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename)
|
||||||
return False
|
return False
|
||||||
service_item.title = self.display_type_combo_box.currentText()
|
|
||||||
service_item.shortname = service_item.title
|
|
||||||
(path, name) = os.path.split(filename)
|
(path, name) = os.path.split(filename)
|
||||||
|
service_item.title = name
|
||||||
|
service_item.processor = self.display_type_combo_box.currentText()
|
||||||
service_item.add_from_command(path, name, CLAPPERBOARD)
|
service_item.add_from_command(path, name, CLAPPERBOARD)
|
||||||
# Only get start and end times if going to a service
|
# Only get start and end times if going to a service
|
||||||
if context == ServiceItemContext.Service:
|
if context == ServiceItemContext.Service:
|
||||||
@ -196,7 +196,6 @@ class MediaMediaItem(MediaManagerItem):
|
|||||||
return False
|
return False
|
||||||
service_item.add_capability(ItemCapabilities.CanAutoStartForLive)
|
service_item.add_capability(ItemCapabilities.CanAutoStartForLive)
|
||||||
service_item.add_capability(ItemCapabilities.RequiresMedia)
|
service_item.add_capability(ItemCapabilities.RequiresMedia)
|
||||||
service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay)
|
|
||||||
if Settings().value(self.settings_section + u'/media auto start') == QtCore.Qt.Checked:
|
if Settings().value(self.settings_section + u'/media auto start') == QtCore.Qt.Checked:
|
||||||
service_item.will_auto_start = True
|
service_item.will_auto_start = True
|
||||||
# force a non-existent theme
|
# force a non-existent theme
|
||||||
@ -260,8 +259,7 @@ class MediaMediaItem(MediaManagerItem):
|
|||||||
Settings().setValue(self.settings_section + u'/media files', self.get_file_list())
|
Settings().setValue(self.settings_section + u'/media files', self.get_file_list())
|
||||||
|
|
||||||
def load_list(self, media, target_group=None):
|
def load_list(self, media, target_group=None):
|
||||||
# Sort the media by its filename considering language specific
|
# Sort the media by its filename considering language specific characters.
|
||||||
# characters.
|
|
||||||
media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
|
media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
|
||||||
for track in media:
|
for track in media:
|
||||||
track_info = QtCore.QFileInfo(track)
|
track_info = QtCore.QFileInfo(track)
|
||||||
|
@ -244,22 +244,20 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
items = self.list_view.selectedItems()
|
items = self.list_view.selectedItems()
|
||||||
if len(items) > 1:
|
if len(items) > 1:
|
||||||
return False
|
return False
|
||||||
service_item.title = self.display_type_combo_box.currentText()
|
service_item.processor = self.display_type_combo_box.currentText()
|
||||||
service_item.shortname = self.display_type_combo_box.currentText()
|
|
||||||
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
||||||
service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay)
|
if not self.display_type_combo_box.currentText():
|
||||||
shortname = service_item.shortname
|
|
||||||
if not shortname:
|
|
||||||
return False
|
return False
|
||||||
for bitem in items:
|
for bitem in items:
|
||||||
filename = bitem.data(QtCore.Qt.UserRole)
|
filename = bitem.data(QtCore.Qt.UserRole)
|
||||||
|
(path, name) = os.path.split(filename)
|
||||||
|
service_item.title = name
|
||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
if shortname == self.Automatic:
|
if service_item.processor == self.Automatic:
|
||||||
service_item.shortname = self.findControllerByType(filename)
|
service_item.processor = self.findControllerByType(filename)
|
||||||
if not service_item.shortname:
|
if not service_item.processor:
|
||||||
return False
|
return False
|
||||||
controller = self.controllers[service_item.shortname]
|
controller = self.controllers[service_item.processor]
|
||||||
(path, name) = os.path.split(filename)
|
|
||||||
doc = controller.add_document(filename)
|
doc = controller.add_document(filename)
|
||||||
if doc.get_thumbnail_path(1, True) is None:
|
if doc.get_thumbnail_path(1, True) is None:
|
||||||
doc.load_presentation()
|
doc.load_presentation()
|
||||||
|
@ -36,6 +36,7 @@ from openlp.core.ui import HideMode
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Controller(object):
|
class Controller(object):
|
||||||
"""
|
"""
|
||||||
This is the Presentation listener who acts on events from the slide controller and passes the messages on the the
|
This is the Presentation listener who acts on events from the slide controller and passes the messages on the the
|
||||||
@ -314,7 +315,7 @@ class MessageListener(object):
|
|||||||
item = message[0]
|
item = message[0]
|
||||||
hide_mode = message[2]
|
hide_mode = message[2]
|
||||||
file = item.get_frame_path()
|
file = item.get_frame_path()
|
||||||
self.handler = item.title
|
self.handler = item.processor
|
||||||
if self.handler == self.media_item.Automatic:
|
if self.handler == self.media_item.Automatic:
|
||||||
self.handler = self.media_item.findControllerByType(file)
|
self.handler = self.media_item.findControllerByType(file)
|
||||||
if not self.handler:
|
if not self.handler:
|
||||||
|
@ -38,6 +38,7 @@ from openlp.core.utils import AppLocation
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PresentationDocument(object):
|
class PresentationDocument(object):
|
||||||
"""
|
"""
|
||||||
Base class for presentation documents to inherit from. Loads and closes the presentation as well as triggering the
|
Base class for presentation documents to inherit from. Loads and closes the presentation as well as triggering the
|
||||||
@ -322,7 +323,7 @@ class PresentationController(object):
|
|||||||
``supports``
|
``supports``
|
||||||
The primary native file types this application supports.
|
The primary native file types this application supports.
|
||||||
|
|
||||||
``alsosupports``
|
``also_supports``
|
||||||
Other file types the application can import, although not necessarily the first choice due to potential
|
Other file types the application can import, although not necessarily the first choice due to potential
|
||||||
incompatibilities.
|
incompatibilities.
|
||||||
|
|
||||||
@ -358,7 +359,7 @@ class PresentationController(object):
|
|||||||
Name of the application, to appear in the application
|
Name of the application, to appear in the application
|
||||||
"""
|
"""
|
||||||
self.supports = []
|
self.supports = []
|
||||||
self.alsosupports = []
|
self.also_supports = []
|
||||||
self.docs = []
|
self.docs = []
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -49,7 +49,7 @@ __default_settings__ = {
|
|||||||
u'presentations/Powerpoint': QtCore.Qt.Checked,
|
u'presentations/Powerpoint': QtCore.Qt.Checked,
|
||||||
u'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
|
u'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
|
||||||
u'presentations/presentations files': []
|
u'presentations/presentations files': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PresentationPlugin(Plugin):
|
class PresentationPlugin(Plugin):
|
||||||
|
39
openlp/plugins/remotes/html/live.css
Normal file
39
openlp/plugins/remotes/html/live.css
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2013 Raoul Snyman *
|
||||||
|
* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan *
|
||||||
|
* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, *
|
||||||
|
* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. *
|
||||||
|
* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, *
|
||||||
|
* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, *
|
||||||
|
* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, *
|
||||||
|
* Frode Woldsund, Martin Zibricky *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; version 2 of the License. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||||
|
* more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License along *
|
||||||
|
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||||
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||||
|
******************************************************************************/
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
41
openlp/plugins/remotes/html/live.html
Normal file
41
openlp/plugins/remotes/html/live.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<!--
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2013 Raoul Snyman #
|
||||||
|
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
|
||||||
|
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||||
|
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||||
|
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||||
|
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||||
|
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||||
|
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>${live_title}</title>
|
||||||
|
<link rel="stylesheet" href="/files/live.css" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
|
||||||
|
<script type="text/javascript" src="/files/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="/files/live.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img id="image" class="size"/>
|
||||||
|
</body>
|
||||||
|
</html>
|
52
openlp/plugins/remotes/html/live.js
Normal file
52
openlp/plugins/remotes/html/live.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2013 Raoul Snyman *
|
||||||
|
* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan *
|
||||||
|
* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, *
|
||||||
|
* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. *
|
||||||
|
* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, *
|
||||||
|
* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, *
|
||||||
|
* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, *
|
||||||
|
* Frode Woldsund, Martin Zibricky *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; version 2 of the License. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||||
|
* more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License along *
|
||||||
|
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||||
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||||
|
******************************************************************************/
|
||||||
|
window.OpenLP = {
|
||||||
|
loadSlide: function (event) {
|
||||||
|
$.getJSON(
|
||||||
|
"/live/image",
|
||||||
|
function (data, status) {
|
||||||
|
var img = document.getElementById('image');
|
||||||
|
img.src = data.results.slide_image;
|
||||||
|
img.style.display = 'block';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pollServer: function () {
|
||||||
|
$.getJSON(
|
||||||
|
"/live/poll",
|
||||||
|
function (data, status) {
|
||||||
|
if (OpenLP.slideCount != data.results.slide_count) {
|
||||||
|
OpenLP.slideCount = data.results.slide_count;
|
||||||
|
OpenLP.loadSlide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.ajaxSetup({ cache: false });
|
||||||
|
setInterval("OpenLP.pollServer();", 500);
|
||||||
|
OpenLP.pollServer();
|
||||||
|
|
@ -147,7 +147,7 @@ window.OpenLP = {
|
|||||||
},
|
},
|
||||||
pollServer: function () {
|
pollServer: function () {
|
||||||
$.getJSON(
|
$.getJSON(
|
||||||
"/stage/api/poll",
|
"/stage/poll",
|
||||||
function (data, status) {
|
function (data, status) {
|
||||||
var prevItem = OpenLP.currentItem;
|
var prevItem = OpenLP.currentItem;
|
||||||
OpenLP.currentSlide = data.results.slide;
|
OpenLP.currentSlide = data.results.slide;
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
window.OpenLP = {
|
window.OpenLP = {
|
||||||
loadService: function (event) {
|
loadService: function (event) {
|
||||||
$.getJSON(
|
$.getJSON(
|
||||||
"/stage/api/service/list",
|
"/stage/service/list",
|
||||||
function (data, status) {
|
function (data, status) {
|
||||||
OpenLP.nextSong = "";
|
OpenLP.nextSong = "";
|
||||||
$("#notes").html("");
|
$("#notes").html("");
|
||||||
@ -46,7 +46,7 @@ window.OpenLP = {
|
|||||||
},
|
},
|
||||||
loadSlides: function (event) {
|
loadSlides: function (event) {
|
||||||
$.getJSON(
|
$.getJSON(
|
||||||
"/stage/api/controller/live/text",
|
"/stage/controller/live/text",
|
||||||
function (data, status) {
|
function (data, status) {
|
||||||
OpenLP.currentSlides = data.results.slides;
|
OpenLP.currentSlides = data.results.slides;
|
||||||
OpenLP.currentSlide = 0;
|
OpenLP.currentSlide = 0;
|
||||||
@ -137,7 +137,7 @@ window.OpenLP = {
|
|||||||
},
|
},
|
||||||
pollServer: function () {
|
pollServer: function () {
|
||||||
$.getJSON(
|
$.getJSON(
|
||||||
"/stage/api/poll",
|
"/stage/poll",
|
||||||
function (data, status) {
|
function (data, status) {
|
||||||
OpenLP.updateClock(data);
|
OpenLP.updateClock(data);
|
||||||
if (OpenLP.currentItem != data.results.item ||
|
if (OpenLP.currentItem != data.results.item ||
|
||||||
|
@ -124,7 +124,7 @@ import cherrypy
|
|||||||
from mako.template import Template
|
from mako.template import Template
|
||||||
from PyQt4 import QtCore
|
from PyQt4 import QtCore
|
||||||
|
|
||||||
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
|
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte
|
||||||
from openlp.core.utils import AppLocation, translate
|
from openlp.core.utils import AppLocation, translate
|
||||||
|
|
||||||
from cherrypy._cpcompat import sha, ntob
|
from cherrypy._cpcompat import sha, ntob
|
||||||
@ -136,6 +136,7 @@ def make_sha_hash(password):
|
|||||||
"""
|
"""
|
||||||
Create an encrypted password for the given password.
|
Create an encrypted password for the given password.
|
||||||
"""
|
"""
|
||||||
|
log.debug("make_sha_hash")
|
||||||
return sha(ntob(password)).hexdigest()
|
return sha(ntob(password)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@ -143,6 +144,7 @@ def fetch_password(username):
|
|||||||
"""
|
"""
|
||||||
Fetch the password for a provided user.
|
Fetch the password for a provided user.
|
||||||
"""
|
"""
|
||||||
|
log.debug("Fetch Password")
|
||||||
if username != Settings().value(u'remotes/user id'):
|
if username != Settings().value(u'remotes/user id'):
|
||||||
return None
|
return None
|
||||||
return make_sha_hash(Settings().value(u'remotes/password'))
|
return make_sha_hash(Settings().value(u'remotes/password'))
|
||||||
@ -175,9 +177,11 @@ class HttpServer(object):
|
|||||||
self.root = self.Public()
|
self.root = self.Public()
|
||||||
self.root.files = self.Files()
|
self.root.files = self.Files()
|
||||||
self.root.stage = self.Stage()
|
self.root.stage = self.Stage()
|
||||||
|
self.root.live = self.Live()
|
||||||
self.root.router = self.router
|
self.root.router = self.router
|
||||||
self.root.files.router = self.router
|
self.root.files.router = self.router
|
||||||
self.root.stage.router = self.router
|
self.root.stage.router = self.router
|
||||||
|
self.root.live.router = self.router
|
||||||
cherrypy.tree.mount(self.root, '/', config=self.define_config())
|
cherrypy.tree.mount(self.root, '/', config=self.define_config())
|
||||||
# Turn off the flood of access messages cause by poll
|
# Turn off the flood of access messages cause by poll
|
||||||
cherrypy.log.access_log.propagate = False
|
cherrypy.log.access_log.propagate = False
|
||||||
@ -212,6 +216,9 @@ class HttpServer(object):
|
|||||||
u'tools.staticdir.dir': self.router.html_dir,
|
u'tools.staticdir.dir': self.router.html_dir,
|
||||||
u'tools.basic_auth.on': False},
|
u'tools.basic_auth.on': False},
|
||||||
u'/stage': {u'tools.staticdir.on': True,
|
u'/stage': {u'tools.staticdir.on': True,
|
||||||
|
u'tools.staticdir.dir': self.router.html_dir,
|
||||||
|
u'tools.basic_auth.on': False},
|
||||||
|
u'/live': {u'tools.staticdir.on': True,
|
||||||
u'tools.staticdir.dir': self.router.html_dir,
|
u'tools.staticdir.dir': self.router.html_dir,
|
||||||
u'tools.basic_auth.on': False}}
|
u'tools.basic_auth.on': False}}
|
||||||
return directory_config
|
return directory_config
|
||||||
@ -239,7 +246,16 @@ class HttpServer(object):
|
|||||||
|
|
||||||
class Stage(object):
|
class Stage(object):
|
||||||
"""
|
"""
|
||||||
Stageview is read only so security is not relevant and would reduce it's usability
|
Stage view is read only so security is not relevant and would reduce it's usability
|
||||||
|
"""
|
||||||
|
@cherrypy.expose
|
||||||
|
def default(self, *args, **kwargs):
|
||||||
|
url = urlparse.urlparse(cherrypy.url())
|
||||||
|
return self.router.process_http_request(url.path, *args)
|
||||||
|
|
||||||
|
class Live(object):
|
||||||
|
"""
|
||||||
|
Live view is read only so security is not relevant and would reduce it's usability
|
||||||
"""
|
"""
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def default(self, *args, **kwargs):
|
def default(self, *args, **kwargs):
|
||||||
@ -265,13 +281,16 @@ class HttpRouter(object):
|
|||||||
self.routes = [
|
self.routes = [
|
||||||
(u'^/$', self.serve_file),
|
(u'^/$', self.serve_file),
|
||||||
(u'^/(stage)$', self.serve_file),
|
(u'^/(stage)$', self.serve_file),
|
||||||
|
(u'^/(live)$', self.serve_file),
|
||||||
(r'^/files/(.*)$', self.serve_file),
|
(r'^/files/(.*)$', self.serve_file),
|
||||||
(r'^/api/poll$', self.poll),
|
(r'^/api/poll$', self.poll),
|
||||||
(r'^/stage/api/poll$', self.poll),
|
(r'^/stage/poll$', self.poll),
|
||||||
|
(r'^/live/poll$', self.live_poll),
|
||||||
|
(r'^/live/image$', self.live_image),
|
||||||
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
|
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
|
||||||
(r'^/stage/api/controller/(live|preview)/(.*)$', self.controller),
|
(r'^/stage/controller/(live|preview)/(.*)$', self.controller),
|
||||||
(r'^/api/service/(.*)$', self.service),
|
(r'^/api/service/(.*)$', self.service),
|
||||||
(r'^/stage/api/service/(.*)$', self.service),
|
(r'^/stage/service/(.*)$', self.service),
|
||||||
(r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
|
(r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
|
||||||
(r'^/api/alert$', self.alert),
|
(r'^/api/alert$', self.alert),
|
||||||
(r'^/api/plugin/(search)$', self.plugin_info),
|
(r'^/api/plugin/(search)$', self.plugin_info),
|
||||||
@ -305,6 +324,7 @@ class HttpRouter(object):
|
|||||||
if response:
|
if response:
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
|
log.debug('Path not found %s', url_path)
|
||||||
return self._http_not_found()
|
return self._http_not_found()
|
||||||
|
|
||||||
def _get_service_items(self):
|
def _get_service_items(self):
|
||||||
@ -334,6 +354,7 @@ class HttpRouter(object):
|
|||||||
self.template_vars = {
|
self.template_vars = {
|
||||||
'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'),
|
'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'),
|
||||||
'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'),
|
'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'),
|
||||||
|
'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'),
|
||||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||||
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
|
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
|
||||||
@ -359,18 +380,19 @@ class HttpRouter(object):
|
|||||||
|
|
||||||
def serve_file(self, filename=None):
|
def serve_file(self, filename=None):
|
||||||
"""
|
"""
|
||||||
Send a file to the socket. For now, just a subset of file types
|
Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
|
||||||
and must be top level inside the html folder.
|
|
||||||
If subfolders requested return 404, easier for security for the present.
|
If subfolders requested return 404, easier for security for the present.
|
||||||
|
|
||||||
Ultimately for i18n, this could first look for xx/file.html before
|
Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
|
||||||
falling back to file.html... where xx is the language, e.g. 'en'
|
where xx is the language, e.g. 'en'
|
||||||
"""
|
"""
|
||||||
log.debug(u'serve file request %s' % filename)
|
log.debug(u'serve file request %s' % filename)
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = u'index.html'
|
filename = u'index.html'
|
||||||
elif filename == u'stage':
|
elif filename == u'stage':
|
||||||
filename = u'stage.html'
|
filename = u'stage.html'
|
||||||
|
elif filename == u'live':
|
||||||
|
filename = u'live.html'
|
||||||
path = os.path.normpath(os.path.join(self.html_dir, filename))
|
path = os.path.normpath(os.path.join(self.html_dir, filename))
|
||||||
if not path.startswith(self.html_dir):
|
if not path.startswith(self.html_dir):
|
||||||
return self._http_not_found()
|
return self._http_not_found()
|
||||||
@ -425,6 +447,26 @@ class HttpRouter(object):
|
|||||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||||
return json.dumps({u'results': result})
|
return json.dumps({u'results': result})
|
||||||
|
|
||||||
|
def live_poll(self):
|
||||||
|
"""
|
||||||
|
Poll OpenLP to determine the current slide count.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
u'slide_count': self.live_controller.slide_count
|
||||||
|
}
|
||||||
|
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||||
|
return json.dumps({u'results': result})
|
||||||
|
|
||||||
|
def live_image(self):
|
||||||
|
"""
|
||||||
|
Return the latest display image as a byte stream.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
u'slide_image': u'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image))
|
||||||
|
}
|
||||||
|
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||||
|
return json.dumps({u'results': result})
|
||||||
|
|
||||||
def display(self, action):
|
def display(self, action):
|
||||||
"""
|
"""
|
||||||
Hide or show the display screen.
|
Hide or show the display screen.
|
||||||
|
@ -86,6 +86,12 @@ class RemoteTab(SettingsTab):
|
|||||||
self.stage_url.setObjectName(u'stage_url')
|
self.stage_url.setObjectName(u'stage_url')
|
||||||
self.stage_url.setOpenExternalLinks(True)
|
self.stage_url.setOpenExternalLinks(True)
|
||||||
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
||||||
|
self.live_url_label = QtGui.QLabel(self.http_settings_group_box)
|
||||||
|
self.live_url_label.setObjectName(u'live_url_label')
|
||||||
|
self.live_url = QtGui.QLabel(self.http_settings_group_box)
|
||||||
|
self.live_url.setObjectName(u'live_url')
|
||||||
|
self.live_url.setOpenExternalLinks(True)
|
||||||
|
self.http_setting_layout.addRow(self.live_url_label, self.live_url)
|
||||||
self.left_layout.addWidget(self.http_settings_group_box)
|
self.left_layout.addWidget(self.http_settings_group_box)
|
||||||
self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
|
self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
|
||||||
self.https_settings_group_box.setCheckable(True)
|
self.https_settings_group_box.setCheckable(True)
|
||||||
@ -116,6 +122,12 @@ class RemoteTab(SettingsTab):
|
|||||||
self.stage_https_url.setObjectName(u'stage_https_url')
|
self.stage_https_url.setObjectName(u'stage_https_url')
|
||||||
self.stage_https_url.setOpenExternalLinks(True)
|
self.stage_https_url.setOpenExternalLinks(True)
|
||||||
self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
|
self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
|
||||||
|
self.live_https_url_label = QtGui.QLabel(self.https_settings_group_box)
|
||||||
|
self.live_https_url_label.setObjectName(u'live_url_label')
|
||||||
|
self.live_https_url = QtGui.QLabel(self.https_settings_group_box)
|
||||||
|
self.live_https_url.setObjectName(u'live_https_url')
|
||||||
|
self.live_https_url.setOpenExternalLinks(True)
|
||||||
|
self.https_settings_layout.addRow(self.live_https_url_label, self.live_https_url)
|
||||||
self.left_layout.addWidget(self.https_settings_group_box)
|
self.left_layout.addWidget(self.https_settings_group_box)
|
||||||
self.user_login_group_box = QtGui.QGroupBox(self.left_column)
|
self.user_login_group_box = QtGui.QGroupBox(self.left_column)
|
||||||
self.user_login_group_box.setCheckable(True)
|
self.user_login_group_box.setCheckable(True)
|
||||||
@ -163,6 +175,7 @@ class RemoteTab(SettingsTab):
|
|||||||
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
||||||
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
||||||
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
||||||
|
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
||||||
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
||||||
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
|
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
|
||||||
self.qr_description_label.setText(translate('RemotePlugin.RemoteTab',
|
self.qr_description_label.setText(translate('RemotePlugin.RemoteTab',
|
||||||
@ -176,6 +189,7 @@ class RemoteTab(SettingsTab):
|
|||||||
self.https_port_label.setText(self.port_label.text())
|
self.https_port_label.setText(self.port_label.text())
|
||||||
self.remote_https_url_label.setText(self.remote_url_label.text())
|
self.remote_https_url_label.setText(self.remote_url_label.text())
|
||||||
self.stage_https_url_label.setText(self.stage_url_label.text())
|
self.stage_https_url_label.setText(self.stage_url_label.text())
|
||||||
|
self.live_https_url_label.setText(self.live_url_label.text())
|
||||||
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
|
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
|
||||||
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
|
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
|
||||||
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
|
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
|
||||||
@ -203,10 +217,14 @@ class RemoteTab(SettingsTab):
|
|||||||
https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
|
https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
|
||||||
self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
|
self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
|
||||||
self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
|
self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
|
||||||
http_url += u'stage'
|
http_url_temp = http_url + u'stage'
|
||||||
https_url += u'stage'
|
https_url_temp = https_url + u'stage'
|
||||||
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
|
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
|
||||||
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
|
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
|
||||||
|
http_url_temp = http_url + u'live'
|
||||||
|
https_url_temp = https_url + u'live'
|
||||||
|
self.live_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
|
||||||
|
self.live_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""
|
"""
|
||||||
|
@ -52,3 +52,4 @@ This allows OpenLP to use ``self.object`` for all the GUI elements while keeping
|
|||||||
them separate from the functionality, so that it is easier to recreate the GUI
|
them separate from the functionality, so that it is easier to recreate the GUI
|
||||||
from the .ui files later if necessary.
|
from the .ui files later if necessary.
|
||||||
"""
|
"""
|
||||||
|
from editsongform import EditSongForm
|
||||||
|
358
openlp/plugins/songs/forms/duplicatesongremovalform.py
Normal file
358
openlp/plugins/songs/forms/duplicatesongremovalform.py
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2013 Raoul Snyman #
|
||||||
|
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
|
||||||
|
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||||
|
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||||
|
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||||
|
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||||
|
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||||
|
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
The duplicate song removal logic for OpenLP.
|
||||||
|
"""
|
||||||
|
from __future__ import division
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import Registry, translate
|
||||||
|
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
|
||||||
|
from openlp.core.utils import AppLocation
|
||||||
|
from openlp.plugins.songs.lib import delete_song
|
||||||
|
from openlp.plugins.songs.lib.db import Song, MediaFile
|
||||||
|
from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget
|
||||||
|
from openlp.plugins.songs.lib.songcompare import songs_probably_equal
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DuplicateSongRemovalForm(OpenLPWizard):
|
||||||
|
"""
|
||||||
|
This is the Duplicate Song Removal Wizard. It provides functionality to
|
||||||
|
search for and remove duplicate songs in the database.
|
||||||
|
"""
|
||||||
|
log.info(u'DuplicateSongRemovalForm loaded')
|
||||||
|
|
||||||
|
def __init__(self, plugin):
|
||||||
|
"""
|
||||||
|
Instantiate the wizard, and run any extra setup we need to.
|
||||||
|
|
||||||
|
``parent``
|
||||||
|
The QWidget-derived parent of the wizard.
|
||||||
|
|
||||||
|
``plugin``
|
||||||
|
The songs plugin.
|
||||||
|
"""
|
||||||
|
self.duplicate_song_list = []
|
||||||
|
self.review_current_count = 0
|
||||||
|
self.review_total_count = 0
|
||||||
|
# Used to interrupt ongoing searches when cancel is clicked.
|
||||||
|
self.break_search = False
|
||||||
|
OpenLPWizard.__init__(self, self.main_window, plugin, u'duplicateSongRemovalWizard',
|
||||||
|
u':/wizards/wizard_duplicateremoval.bmp', False)
|
||||||
|
self.setMinimumWidth(730)
|
||||||
|
|
||||||
|
def custom_signals(self):
|
||||||
|
"""
|
||||||
|
Song wizard specific signals.
|
||||||
|
"""
|
||||||
|
self.finish_button.clicked.connect(self.on_wizard_exit)
|
||||||
|
self.cancel_button.clicked.connect(self.on_wizard_exit)
|
||||||
|
|
||||||
|
def add_custom_pages(self):
|
||||||
|
"""
|
||||||
|
Add song wizard specific pages.
|
||||||
|
"""
|
||||||
|
# Add custom pages.
|
||||||
|
self.searching_page = QtGui.QWizardPage()
|
||||||
|
self.searching_page.setObjectName(u'searching_page')
|
||||||
|
self.searching_vertical_layout = QtGui.QVBoxLayout(self.searching_page)
|
||||||
|
self.searching_vertical_layout.setObjectName(u'searching_vertical_layout')
|
||||||
|
self.duplicate_search_progress_bar = QtGui.QProgressBar(self.searching_page)
|
||||||
|
self.duplicate_search_progress_bar.setObjectName(u'duplicate_search_progress_bar')
|
||||||
|
self.duplicate_search_progress_bar.setFormat(WizardStrings.PercentSymbolFormat)
|
||||||
|
self.searching_vertical_layout.addWidget(self.duplicate_search_progress_bar)
|
||||||
|
self.found_duplicates_edit = QtGui.QPlainTextEdit(self.searching_page)
|
||||||
|
self.found_duplicates_edit.setUndoRedoEnabled(False)
|
||||||
|
self.found_duplicates_edit.setReadOnly(True)
|
||||||
|
self.found_duplicates_edit.setObjectName(u'found_duplicates_edit')
|
||||||
|
self.searching_vertical_layout.addWidget(self.found_duplicates_edit)
|
||||||
|
self.searching_page_id = self.addPage(self.searching_page)
|
||||||
|
self.review_page = QtGui.QWizardPage()
|
||||||
|
self.review_page.setObjectName(u'review_page')
|
||||||
|
self.review_layout = QtGui.QVBoxLayout(self.review_page)
|
||||||
|
self.review_layout.setObjectName(u'review_layout')
|
||||||
|
self.review_scroll_area = QtGui.QScrollArea(self.review_page)
|
||||||
|
self.review_scroll_area.setObjectName(u'review_scroll_area')
|
||||||
|
self.review_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||||
|
self.review_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||||
|
self.review_scroll_area.setWidgetResizable(True)
|
||||||
|
self.review_scroll_area_widget = QtGui.QWidget(self.review_scroll_area)
|
||||||
|
self.review_scroll_area_widget.setObjectName(u'review_scroll_area_widget')
|
||||||
|
self.review_scroll_area_layout = QtGui.QHBoxLayout(self.review_scroll_area_widget)
|
||||||
|
self.review_scroll_area_layout.setObjectName(u'review_scroll_area_layout')
|
||||||
|
self.review_scroll_area_layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize)
|
||||||
|
self.review_scroll_area_layout.setMargin(0)
|
||||||
|
self.review_scroll_area_layout.setSpacing(0)
|
||||||
|
self.review_scroll_area.setWidget(self.review_scroll_area_widget)
|
||||||
|
self.review_layout.addWidget(self.review_scroll_area)
|
||||||
|
self.review_page_id = self.addPage(self.review_page)
|
||||||
|
# Add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the
|
||||||
|
#review page.
|
||||||
|
self.dummy_page = QtGui.QWizardPage()
|
||||||
|
self.dummy_page_id = self.addPage(self.dummy_page)
|
||||||
|
|
||||||
|
def retranslateUi(self):
|
||||||
|
"""
|
||||||
|
Song wizard localisation.
|
||||||
|
"""
|
||||||
|
self.setWindowTitle(translate(u'Wizard', u'Wizard'))
|
||||||
|
self.title_label.setText(WizardStrings.HeaderStyle % translate(u'OpenLP.Ui',
|
||||||
|
u'Welcome to the Duplicate Song Removal Wizard'))
|
||||||
|
self.information_label.setText(translate("Wizard",
|
||||||
|
u'This wizard will help you to remove duplicate songs from the song database. You will have a chance to '
|
||||||
|
u'review every potential duplicate song before it is deleted. So no songs will be deleted without your '
|
||||||
|
u'explicit approval.'))
|
||||||
|
self.searching_page.setTitle(translate(u'Wizard', u'Searching for duplicate songs.'))
|
||||||
|
self.searching_page.setSubTitle(translate(u'Wizard', u'Please wait while your songs database is analyzed.'))
|
||||||
|
self.update_review_counter_text()
|
||||||
|
self.review_page.setSubTitle(translate(u'Wizard',
|
||||||
|
u'Here you can decide which songs to remove and which ones to keep.'))
|
||||||
|
|
||||||
|
def update_review_counter_text(self):
|
||||||
|
"""
|
||||||
|
Set the wizard review page header text.
|
||||||
|
"""
|
||||||
|
self.review_page.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \
|
||||||
|
(self.review_current_count, self.review_total_count))
|
||||||
|
|
||||||
|
def custom_page_changed(self, page_id):
|
||||||
|
"""
|
||||||
|
Called when changing the wizard page.
|
||||||
|
|
||||||
|
``page_id``
|
||||||
|
ID of the page the wizard changed to.
|
||||||
|
"""
|
||||||
|
# Hide back button.
|
||||||
|
self.button(QtGui.QWizard.BackButton).hide()
|
||||||
|
if page_id == self.searching_page_id:
|
||||||
|
self.application.set_busy_cursor()
|
||||||
|
try:
|
||||||
|
self.button(QtGui.QWizard.NextButton).hide()
|
||||||
|
# Search duplicate songs.
|
||||||
|
max_songs = self.plugin.manager.get_object_count(Song)
|
||||||
|
if max_songs == 0 or max_songs == 1:
|
||||||
|
self.duplicate_search_progress_bar.setMaximum(1)
|
||||||
|
self.duplicate_search_progress_bar.setValue(1)
|
||||||
|
self.notify_no_duplicates()
|
||||||
|
return
|
||||||
|
# With x songs we have x*(x - 1) / 2 comparisons.
|
||||||
|
max_progress_count = max_songs * (max_songs - 1) // 2
|
||||||
|
self.duplicate_search_progress_bar.setMaximum(max_progress_count)
|
||||||
|
songs = self.plugin.manager.get_all_objects(Song)
|
||||||
|
for outer_song_counter in range(max_songs - 1):
|
||||||
|
for inner_song_counter in range(outer_song_counter + 1, max_songs):
|
||||||
|
if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]):
|
||||||
|
duplicate_added = self.add_duplicates_to_song_list(songs[outer_song_counter],
|
||||||
|
songs[inner_song_counter])
|
||||||
|
if duplicate_added:
|
||||||
|
self.found_duplicates_edit.appendPlainText(songs[outer_song_counter].title + " = " +
|
||||||
|
songs[inner_song_counter].title)
|
||||||
|
self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1)
|
||||||
|
# The call to process_events() will keep the GUI responsive.
|
||||||
|
self.application.process_events()
|
||||||
|
if self.break_search:
|
||||||
|
return
|
||||||
|
self.review_total_count = len(self.duplicate_song_list)
|
||||||
|
if self.review_total_count == 0:
|
||||||
|
self.notify_no_duplicates()
|
||||||
|
else:
|
||||||
|
self.button(QtGui.QWizard.NextButton).show()
|
||||||
|
finally:
|
||||||
|
self.application.set_normal_cursor()
|
||||||
|
elif page_id == self.review_page_id:
|
||||||
|
self.process_current_duplicate_entry()
|
||||||
|
|
||||||
|
def notify_no_duplicates(self):
|
||||||
|
"""
|
||||||
|
Notifies the user, that there were no duplicates found in the database.
|
||||||
|
"""
|
||||||
|
self.button(QtGui.QWizard.FinishButton).show()
|
||||||
|
self.button(QtGui.QWizard.FinishButton).setEnabled(True)
|
||||||
|
self.button(QtGui.QWizard.NextButton).hide()
|
||||||
|
self.button(QtGui.QWizard.CancelButton).hide()
|
||||||
|
QtGui.QMessageBox.information(self, translate(u'Wizard', u'Information'),
|
||||||
|
translate(u'Wizard', u'No duplicate songs have been found in the database.'),
|
||||||
|
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok))
|
||||||
|
|
||||||
|
def add_duplicates_to_song_list(self, search_song, duplicate_song):
|
||||||
|
"""
|
||||||
|
Inserts a song duplicate (two similar songs) to the duplicate song list.
|
||||||
|
If one of the two songs is already part of the duplicate song list,
|
||||||
|
don't add another duplicate group but add the other song to that group.
|
||||||
|
Returns True if at least one of the songs was added, False if both were already
|
||||||
|
member of a group.
|
||||||
|
|
||||||
|
``search_song``
|
||||||
|
The song we searched the duplicate for.
|
||||||
|
|
||||||
|
``duplicate_song``
|
||||||
|
The duplicate song.
|
||||||
|
"""
|
||||||
|
duplicate_group_found = False
|
||||||
|
duplicate_added = False
|
||||||
|
for duplicate_group in self.duplicate_song_list:
|
||||||
|
# Skip the first song in the duplicate lists, since the first one has to be an earlier song.
|
||||||
|
if search_song in duplicate_group and not duplicate_song in duplicate_group:
|
||||||
|
duplicate_group.append(duplicate_song)
|
||||||
|
duplicate_group_found = True
|
||||||
|
duplicate_added = True
|
||||||
|
break
|
||||||
|
elif not search_song in duplicate_group and duplicate_song in duplicate_group:
|
||||||
|
duplicate_group.append(search_song)
|
||||||
|
duplicate_group_found = True
|
||||||
|
duplicate_added = True
|
||||||
|
break
|
||||||
|
elif search_song in duplicate_group and duplicate_song in duplicate_group:
|
||||||
|
duplicate_group_found = True
|
||||||
|
duplicate_added = False
|
||||||
|
break
|
||||||
|
if not duplicate_group_found:
|
||||||
|
self.duplicate_song_list.append([search_song, duplicate_song])
|
||||||
|
duplicate_added = True
|
||||||
|
return duplicate_added
|
||||||
|
|
||||||
|
def on_wizard_exit(self):
|
||||||
|
"""
|
||||||
|
Once the wizard is finished, refresh the song list,
|
||||||
|
since we potentially removed songs from it.
|
||||||
|
"""
|
||||||
|
self.break_search = True
|
||||||
|
self.plugin.media_item.on_search_text_button_clicked()
|
||||||
|
|
||||||
|
def setDefaults(self):
|
||||||
|
"""
|
||||||
|
Set default form values for the song import wizard.
|
||||||
|
"""
|
||||||
|
self.restart()
|
||||||
|
self.duplicate_search_progress_bar.setValue(0)
|
||||||
|
self.found_duplicates_edit.clear()
|
||||||
|
|
||||||
|
def validateCurrentPage(self):
|
||||||
|
"""
|
||||||
|
Controls whether we should switch to the next wizard page. This method loops
|
||||||
|
on the review page as long as there are more song duplicates to review.
|
||||||
|
"""
|
||||||
|
if self.currentId() == self.review_page_id:
|
||||||
|
# As long as it's not the last duplicate list entry we revisit the review page.
|
||||||
|
if len(self.duplicate_song_list) == 1:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.proceed_to_next_review()
|
||||||
|
return False
|
||||||
|
return OpenLPWizard.validateCurrentPage(self)
|
||||||
|
|
||||||
|
def remove_button_clicked(self, song_review_widget):
|
||||||
|
"""
|
||||||
|
Removes a song from the database, removes the GUI element representing the
|
||||||
|
song on the review page, and disable the remove button if only one duplicate
|
||||||
|
is left.
|
||||||
|
|
||||||
|
``song_review_widget``
|
||||||
|
The SongReviewWidget whose song we should delete.
|
||||||
|
"""
|
||||||
|
# Remove song from duplicate song list.
|
||||||
|
self.duplicate_song_list[-1].remove(song_review_widget.song)
|
||||||
|
# Remove song from the database.
|
||||||
|
delete_song(song_review_widget.song.id, self.plugin)
|
||||||
|
# Remove GUI elements for the song.
|
||||||
|
self.review_scroll_area_layout.removeWidget(song_review_widget)
|
||||||
|
song_review_widget.setParent(None)
|
||||||
|
# Check if we only have one duplicate left:
|
||||||
|
# 2 stretches + 1 SongReviewWidget = 3
|
||||||
|
# The SongReviewWidget is then at position 1.
|
||||||
|
if len(self.duplicate_song_list[-1]) == 1:
|
||||||
|
self.review_scroll_area_layout.itemAt(1).widget().song_remove_button.setEnabled(False)
|
||||||
|
|
||||||
|
def proceed_to_next_review(self):
|
||||||
|
"""
|
||||||
|
Removes the previous review UI elements and calls process_current_duplicate_entry.
|
||||||
|
"""
|
||||||
|
# Remove last duplicate group.
|
||||||
|
self.duplicate_song_list.pop()
|
||||||
|
# Remove all previous elements.
|
||||||
|
for i in reversed(range(self.review_scroll_area_layout.count())):
|
||||||
|
item = self.review_scroll_area_layout.itemAt(i)
|
||||||
|
if isinstance(item, QtGui.QWidgetItem):
|
||||||
|
# The order is important here, if the .setParent(None) call is done
|
||||||
|
# before the .removeItem() call, a segfault occurs.
|
||||||
|
widget = item.widget()
|
||||||
|
self.review_scroll_area_layout.removeItem(item)
|
||||||
|
widget.setParent(None)
|
||||||
|
else:
|
||||||
|
self.review_scroll_area_layout.removeItem(item)
|
||||||
|
# Process next set of duplicates.
|
||||||
|
self.process_current_duplicate_entry()
|
||||||
|
|
||||||
|
def process_current_duplicate_entry(self):
|
||||||
|
"""
|
||||||
|
Update the review counter in the wizard header, add song widgets for
|
||||||
|
the current duplicate group to review, if it's the last
|
||||||
|
duplicate song group, hide the "next" button and show the "finish" button.
|
||||||
|
"""
|
||||||
|
# Update the counter.
|
||||||
|
self.review_current_count = self.review_total_count - (len(self.duplicate_song_list) - 1)
|
||||||
|
self.update_review_counter_text()
|
||||||
|
# Add song elements to the UI.
|
||||||
|
if len(self.duplicate_song_list) > 0:
|
||||||
|
self.review_scroll_area_layout.addStretch(1)
|
||||||
|
for duplicate in self.duplicate_song_list[-1]:
|
||||||
|
song_review_widget = SongReviewWidget(self.review_page, duplicate)
|
||||||
|
song_review_widget.song_remove_button_clicked.connect(self.remove_button_clicked)
|
||||||
|
self.review_scroll_area_layout.addWidget(song_review_widget)
|
||||||
|
self.review_scroll_area_layout.addStretch(1)
|
||||||
|
# Change next button to finish button on last review.
|
||||||
|
if len(self.duplicate_song_list) == 1:
|
||||||
|
self.button(QtGui.QWizard.FinishButton).show()
|
||||||
|
self.button(QtGui.QWizard.FinishButton).setEnabled(True)
|
||||||
|
self.button(QtGui.QWizard.NextButton).hide()
|
||||||
|
self.button(QtGui.QWizard.CancelButton).hide()
|
||||||
|
|
||||||
|
def _get_main_window(self):
|
||||||
|
"""
|
||||||
|
Adds the main window to the class dynamically.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, u'_main_window'):
|
||||||
|
self._main_window = Registry().get(u'main_window')
|
||||||
|
return self._main_window
|
||||||
|
|
||||||
|
main_window = property(_get_main_window)
|
||||||
|
|
||||||
|
def _get_application(self):
|
||||||
|
"""
|
||||||
|
Adds the openlp to the class dynamically
|
||||||
|
"""
|
||||||
|
if not hasattr(self, u'_application'):
|
||||||
|
self._application = Registry().get(u'application')
|
||||||
|
return self._application
|
||||||
|
|
||||||
|
application = property(_get_application)
|
@ -235,7 +235,7 @@ class SongExportForm(OpenLPWizard):
|
|||||||
self.availableListWidget.addItem(item)
|
self.availableListWidget.addItem(item)
|
||||||
self.application.set_normal_cursor()
|
self.application.set_normal_cursor()
|
||||||
|
|
||||||
def preWizard(self):
|
def pre_wizard(self):
|
||||||
"""
|
"""
|
||||||
Perform pre export tasks.
|
Perform pre export tasks.
|
||||||
"""
|
"""
|
||||||
|
@ -325,7 +325,7 @@ class SongImportForm(OpenLPWizard):
|
|||||||
self.error_copy_to_button.setHidden(True)
|
self.error_copy_to_button.setHidden(True)
|
||||||
self.error_save_to_button.setHidden(True)
|
self.error_save_to_button.setHidden(True)
|
||||||
|
|
||||||
def preWizard(self):
|
def pre_wizard(self):
|
||||||
"""
|
"""
|
||||||
Perform pre import tasks
|
Perform pre import tasks
|
||||||
"""
|
"""
|
||||||
|
213
openlp/plugins/songs/forms/songreviewwidget.py
Normal file
213
openlp/plugins/songs/forms/songreviewwidget.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2013 Raoul Snyman #
|
||||||
|
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
|
||||||
|
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||||
|
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||||
|
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||||
|
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||||
|
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||||
|
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
A widget representing a song in the duplicate song removal wizard review page.
|
||||||
|
"""
|
||||||
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import build_icon
|
||||||
|
from openlp.plugins.songs.lib import VerseType
|
||||||
|
from openlp.plugins.songs.lib.xml import SongXML
|
||||||
|
|
||||||
|
|
||||||
|
class SongReviewWidget(QtGui.QWidget):
|
||||||
|
"""
|
||||||
|
A widget representing a song on the duplicate song review page.
|
||||||
|
It displays most of the information a song contains and
|
||||||
|
provides a "remove" button to remove the song from the database.
|
||||||
|
The remove logic is not implemented here, but a signal is provided
|
||||||
|
when the remove button is clicked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Signals have to be class variables and not instance variables. Otherwise
|
||||||
|
# they are not registered by Qt (missing emit and connect methods are artifacts of this).
|
||||||
|
# To use SongReviewWidget as a signal parameter one would have to assigning the class
|
||||||
|
# variable after the class is declared. While this is possible, it also messes Qts meta
|
||||||
|
# object system up. The result is an
|
||||||
|
# "Object::connect: Use the SIGNAL macro to bind SongReviewWidget::(QWidget*)" error on
|
||||||
|
# connect calls.
|
||||||
|
# That's why we cheat a little and use QWidget instead of SongReviewWidget as parameter.
|
||||||
|
# While not being entirely correct, it does work.
|
||||||
|
song_remove_button_clicked = QtCore.pyqtSignal(QtGui.QWidget)
|
||||||
|
|
||||||
|
def __init__(self, parent, song):
|
||||||
|
"""
|
||||||
|
``parent``
|
||||||
|
The QWidget-derived parent of the wizard.
|
||||||
|
|
||||||
|
``song``
|
||||||
|
The Song which this SongReviewWidget should represent.
|
||||||
|
"""
|
||||||
|
QtGui.QWidget.__init__(self, parent)
|
||||||
|
self.song = song
|
||||||
|
self.setupUi()
|
||||||
|
self.retranslateUi()
|
||||||
|
self.song_remove_button.clicked.connect(self.on_remove_button_clicked)
|
||||||
|
|
||||||
|
def setupUi(self):
|
||||||
|
self.song_vertical_layout = QtGui.QVBoxLayout(self)
|
||||||
|
self.song_vertical_layout.setObjectName(u'song_vertical_layout')
|
||||||
|
self.song_group_box = QtGui.QGroupBox(self)
|
||||||
|
self.song_group_box.setObjectName(u'song_group_box')
|
||||||
|
self.song_group_box.setFixedWidth(300)
|
||||||
|
self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box)
|
||||||
|
self.song_group_box_layout.setObjectName(u'song_group_box_layout')
|
||||||
|
self.song_info_form_layout = QtGui.QFormLayout()
|
||||||
|
self.song_info_form_layout.setObjectName(u'song_info_form_layout')
|
||||||
|
# Add title widget.
|
||||||
|
self.song_title_label = QtGui.QLabel(self)
|
||||||
|
self.song_title_label.setObjectName(u'song_title_label')
|
||||||
|
self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.song_title_label)
|
||||||
|
self.song_title_content = QtGui.QLabel(self)
|
||||||
|
self.song_title_content.setObjectName(u'song_title_content')
|
||||||
|
self.song_title_content.setText(self.song.title)
|
||||||
|
self.song_title_content.setWordWrap(True)
|
||||||
|
self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.song_title_content)
|
||||||
|
# Add alternate title widget.
|
||||||
|
self.song_alternate_title_label = QtGui.QLabel(self)
|
||||||
|
self.song_alternate_title_label.setObjectName(u'song_alternate_title_label')
|
||||||
|
self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.song_alternate_title_label)
|
||||||
|
self.song_alternate_title_content = QtGui.QLabel(self)
|
||||||
|
self.song_alternate_title_content.setObjectName(u'song_alternate_title_content')
|
||||||
|
self.song_alternate_title_content.setText(self.song.alternate_title)
|
||||||
|
self.song_alternate_title_content.setWordWrap(True)
|
||||||
|
self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.song_alternate_title_content)
|
||||||
|
# Add CCLI number widget.
|
||||||
|
self.song_ccli_number_label = QtGui.QLabel(self)
|
||||||
|
self.song_ccli_number_label.setObjectName(u'song_ccli_number_label')
|
||||||
|
self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.song_ccli_number_label)
|
||||||
|
self.song_ccli_number_content = QtGui.QLabel(self)
|
||||||
|
self.song_ccli_number_content.setObjectName(u'song_ccli_number_content')
|
||||||
|
self.song_ccli_number_content.setText(self.song.ccli_number)
|
||||||
|
self.song_ccli_number_content.setWordWrap(True)
|
||||||
|
self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.song_ccli_number_content)
|
||||||
|
# Add copyright widget.
|
||||||
|
self.song_copyright_label = QtGui.QLabel(self)
|
||||||
|
self.song_copyright_label.setObjectName(u'song_copyright_label')
|
||||||
|
self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.song_copyright_label)
|
||||||
|
self.song_copyright_content = QtGui.QLabel(self)
|
||||||
|
self.song_copyright_content.setObjectName(u'song_copyright_content')
|
||||||
|
self.song_copyright_content.setWordWrap(True)
|
||||||
|
self.song_copyright_content.setText(self.song.copyright)
|
||||||
|
self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.song_copyright_content)
|
||||||
|
# Add comments widget.
|
||||||
|
self.song_comments_label = QtGui.QLabel(self)
|
||||||
|
self.song_comments_label.setObjectName(u'song_comments_label')
|
||||||
|
self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.song_comments_label)
|
||||||
|
self.song_comments_content = QtGui.QLabel(self)
|
||||||
|
self.song_comments_content.setObjectName(u'song_comments_content')
|
||||||
|
self.song_comments_content.setText(self.song.comments)
|
||||||
|
self.song_comments_content.setWordWrap(True)
|
||||||
|
self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.song_comments_content)
|
||||||
|
# Add authors widget.
|
||||||
|
self.song_authors_label = QtGui.QLabel(self)
|
||||||
|
self.song_authors_label.setObjectName(u'song_authors_label')
|
||||||
|
self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.LabelRole, self.song_authors_label)
|
||||||
|
self.song_authors_content = QtGui.QLabel(self)
|
||||||
|
self.song_authors_content.setObjectName(u'song_authors_content')
|
||||||
|
self.song_authors_content.setWordWrap(True)
|
||||||
|
authors_text = u', '.join([author.display_name for author in self.song.authors])
|
||||||
|
self.song_authors_content.setText(authors_text)
|
||||||
|
self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content)
|
||||||
|
# Add verse order widget.
|
||||||
|
self.song_verse_order_label = QtGui.QLabel(self)
|
||||||
|
self.song_verse_order_label.setObjectName(u'song_verse_order_label')
|
||||||
|
self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.LabelRole, self.song_verse_order_label)
|
||||||
|
self.song_verse_order_content = QtGui.QLabel(self)
|
||||||
|
self.song_verse_order_content.setObjectName(u'song_verse_order_content')
|
||||||
|
self.song_verse_order_content.setText(self.song.verse_order)
|
||||||
|
self.song_verse_order_content.setWordWrap(True)
|
||||||
|
self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content)
|
||||||
|
self.song_group_box_layout.addLayout(self.song_info_form_layout)
|
||||||
|
# Add verses widget.
|
||||||
|
self.song_info_verse_list_widget = QtGui.QTableWidget(self.song_group_box)
|
||||||
|
self.song_info_verse_list_widget.setColumnCount(1)
|
||||||
|
self.song_info_verse_list_widget.horizontalHeader().setVisible(False)
|
||||||
|
self.song_info_verse_list_widget.setObjectName(u'song_info_verse_list_widget')
|
||||||
|
self.song_info_verse_list_widget.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
|
||||||
|
self.song_info_verse_list_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||||
|
self.song_info_verse_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||||
|
self.song_info_verse_list_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||||
|
self.song_info_verse_list_widget.setAlternatingRowColors(True)
|
||||||
|
song_xml = SongXML()
|
||||||
|
verses = song_xml.get_verses(self.song.lyrics)
|
||||||
|
self.song_info_verse_list_widget.setRowCount(len(verses))
|
||||||
|
song_tags = []
|
||||||
|
for verse_number, verse in enumerate(verses):
|
||||||
|
item = QtGui.QTableWidgetItem()
|
||||||
|
item.setText(verse[1])
|
||||||
|
self.song_info_verse_list_widget.setItem(verse_number, 0, item)
|
||||||
|
|
||||||
|
# We cannot use from_loose_input() here, because database
|
||||||
|
# is supposed to contain English lowercase singlechar tags.
|
||||||
|
verse_tag = verse[0][u'type']
|
||||||
|
verse_index = None
|
||||||
|
if len(verse_tag) > 1:
|
||||||
|
verse_index = VerseType.from_translated_string(verse_tag)
|
||||||
|
if verse_index is None:
|
||||||
|
verse_index = VerseType.from_string(verse_tag, None)
|
||||||
|
if verse_index is None:
|
||||||
|
verse_index = VerseType.from_tag(verse_tag)
|
||||||
|
verse_tag = VerseType.translated_tags[verse_index].upper()
|
||||||
|
song_tags.append(unicode(verse_tag + verse[0]['label']))
|
||||||
|
self.song_info_verse_list_widget.setVerticalHeaderLabels(song_tags)
|
||||||
|
# Resize table fields to content and table to columns
|
||||||
|
self.song_info_verse_list_widget.setColumnWidth(0, self.song_group_box.width())
|
||||||
|
self.song_info_verse_list_widget.resizeRowsToContents()
|
||||||
|
# The 6 is a trial and error value since verticalHeader().length() + offset() is a little bit to small.
|
||||||
|
# It seems there is no clean way to determine the real height of the table contents.
|
||||||
|
# The "correct" value slightly fluctuates depending on the theme used, in the worst case
|
||||||
|
# Some pixels are missing at the bottom of the table, but all themes I tried still allowed
|
||||||
|
# to read the last verse line, so I'll just leave it at that.
|
||||||
|
self.song_info_verse_list_widget.setFixedHeight(self.song_info_verse_list_widget.verticalHeader().length() +
|
||||||
|
self.song_info_verse_list_widget.verticalHeader().offset() + 6)
|
||||||
|
self.song_group_box_layout.addWidget(self.song_info_verse_list_widget)
|
||||||
|
self.song_group_box_layout.addStretch()
|
||||||
|
self.song_vertical_layout.addWidget(self.song_group_box)
|
||||||
|
self.song_remove_button = QtGui.QPushButton(self)
|
||||||
|
self.song_remove_button.setObjectName(u'song_remove_button')
|
||||||
|
self.song_remove_button.setIcon(build_icon(u':/songs/song_delete.png'))
|
||||||
|
self.song_remove_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
|
||||||
|
self.song_vertical_layout.addWidget(self.song_remove_button, alignment = QtCore.Qt.AlignHCenter)
|
||||||
|
|
||||||
|
def retranslateUi(self):
|
||||||
|
self.song_remove_button.setText(u'Remove')
|
||||||
|
self.song_title_label.setText(u'Title:')
|
||||||
|
self.song_alternate_title_label.setText(u'Alternate Title:')
|
||||||
|
self.song_ccli_number_label.setText(u'CCLI Number:')
|
||||||
|
self.song_verse_order_label.setText(u'Verse Order:')
|
||||||
|
self.song_copyright_label.setText(u'Copyright:')
|
||||||
|
self.song_comments_label.setText(u'Comments:')
|
||||||
|
self.song_authors_label.setText(u'Authors:')
|
||||||
|
|
||||||
|
def on_remove_button_clicked(self):
|
||||||
|
"""
|
||||||
|
Signal emitted when the "remove" button is clicked.
|
||||||
|
"""
|
||||||
|
self.song_remove_button_clicked.emit(self)
|
@ -29,15 +29,21 @@
|
|||||||
"""
|
"""
|
||||||
The :mod:`~openlp.plugins.songs.lib` module contains a number of library functions and classes used in the Songs plugin.
|
The :mod:`~openlp.plugins.songs.lib` module contains a number of library functions and classes used in the Songs plugin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from PyQt4 import QtGui
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
from openlp.core.lib import translate
|
from openlp.core.lib import translate
|
||||||
from openlp.core.utils import CONTROL_CHARS
|
from openlp.core.utils import AppLocation, CONTROL_CHARS
|
||||||
|
from openlp.plugins.songs.lib.db import MediaFile, Song
|
||||||
from db import Author
|
from db import Author
|
||||||
from ui import SongStrings
|
from ui import SongStrings
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
WHITESPACE = re.compile(r'[\W_]+', re.UNICODE)
|
WHITESPACE = re.compile(r'[\W_]+', re.UNICODE)
|
||||||
APOSTROPHE = re.compile(u'[\'`’ʻ′]', re.UNICODE)
|
APOSTROPHE = re.compile(u'[\'`’ʻ′]', re.UNICODE)
|
||||||
PATTERN = re.compile(r"\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z])|([{}])|[\r\n]+|(.)", re.I)
|
PATTERN = re.compile(r"\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z])|([{}])|[\r\n]+|(.)", re.I)
|
||||||
@ -593,3 +599,29 @@ def strip_rtf(text, default_encoding=None):
|
|||||||
text = u''.join(out)
|
text = u''.join(out)
|
||||||
return text, default_encoding
|
return text, default_encoding
|
||||||
|
|
||||||
|
|
||||||
|
def delete_song(song_id, song_plugin):
|
||||||
|
"""
|
||||||
|
Deletes a song from the database. Media files associated to the song
|
||||||
|
are removed prior to the deletion of the song.
|
||||||
|
|
||||||
|
``song_id``
|
||||||
|
The ID of the song to delete.
|
||||||
|
|
||||||
|
``song_plugin``
|
||||||
|
The song plugin instance.
|
||||||
|
"""
|
||||||
|
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
|
||||||
|
for media_file in media_files:
|
||||||
|
try:
|
||||||
|
os.remove(media_file.file_name)
|
||||||
|
except:
|
||||||
|
log.exception('Could not remove file: %s', media_file.file_name)
|
||||||
|
try:
|
||||||
|
save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
|
||||||
|
if os.path.exists(save_path):
|
||||||
|
os.rmdir(save_path)
|
||||||
|
except OSError:
|
||||||
|
log.exception(u'Could not remove directory: %s', save_path)
|
||||||
|
song_plugin.manager.delete_object(Song, song_id)
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ from openlp.plugins.songs.forms.editsongform import EditSongForm
|
|||||||
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
|
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
|
||||||
from openlp.plugins.songs.forms.songimportform import SongImportForm
|
from openlp.plugins.songs.forms.songimportform import SongImportForm
|
||||||
from openlp.plugins.songs.forms.songexportform import SongExportForm
|
from openlp.plugins.songs.forms.songexportform import SongExportForm
|
||||||
from openlp.plugins.songs.lib import VerseType, clean_string
|
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
|
||||||
from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile
|
from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile
|
||||||
from openlp.plugins.songs.lib.ui import SongStrings
|
from openlp.plugins.songs.lib.ui import SongStrings
|
||||||
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
|
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
|
||||||
@ -72,10 +72,7 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
def __init__(self, parent, plugin):
|
def __init__(self, parent, plugin):
|
||||||
self.icon_path = u'songs/song'
|
self.icon_path = u'songs/song'
|
||||||
MediaManagerItem.__init__(self, parent, plugin)
|
MediaManagerItem.__init__(self, parent, plugin)
|
||||||
self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
|
|
||||||
self.openLyrics = OpenLyrics(self.plugin.manager)
|
|
||||||
self.single_service_item = False
|
self.single_service_item = False
|
||||||
self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
|
|
||||||
# Holds information about whether the edit is remotely triggered and which Song is required.
|
# Holds information about whether the edit is remotely triggered and which Song is required.
|
||||||
self.remote_song = -1
|
self.remote_song = -1
|
||||||
self.edit_item = None
|
self.edit_item = None
|
||||||
@ -132,6 +129,12 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
'Maintain the lists of authors, topics and books.'))
|
'Maintain the lists of authors, topics and books.'))
|
||||||
|
|
||||||
def initialise(self):
|
def initialise(self):
|
||||||
|
"""
|
||||||
|
Initialise variables when they cannot be initialised in the constructor.
|
||||||
|
"""
|
||||||
|
self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
|
||||||
|
self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
|
||||||
|
self.openLyrics = OpenLyrics(self.plugin.manager)
|
||||||
self.search_text_edit.set_search_types([
|
self.search_text_edit.set_search_types([
|
||||||
(SongSearch.Entire, u':/songs/song_search_all.png',
|
(SongSearch.Entire, u':/songs/song_search_all.png',
|
||||||
translate('SongsPlugin.MediaItem', 'Entire Song'),
|
translate('SongsPlugin.MediaItem', 'Entire Song'),
|
||||||
@ -157,7 +160,6 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type())
|
Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type())
|
||||||
# Reload the list considering the new search type.
|
# Reload the list considering the new search type.
|
||||||
search_keywords = unicode(self.search_text_edit.displayText())
|
search_keywords = unicode(self.search_text_edit.displayText())
|
||||||
search_results = []
|
|
||||||
search_type = self.search_text_edit.current_search_type()
|
search_type = self.search_text_edit.current_search_type()
|
||||||
if search_type == SongSearch.Entire:
|
if search_type == SongSearch.Entire:
|
||||||
log.debug(u'Entire Song Search')
|
log.debug(u'Entire Song Search')
|
||||||
@ -366,19 +368,7 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
self.main_window.display_progress_bar(len(items))
|
self.main_window.display_progress_bar(len(items))
|
||||||
for item in items:
|
for item in items:
|
||||||
item_id = item.data(QtCore.Qt.UserRole)
|
item_id = item.data(QtCore.Qt.UserRole)
|
||||||
media_files = self.plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == item_id)
|
delete_song(item_id, self.plugin)
|
||||||
for media_file in media_files:
|
|
||||||
try:
|
|
||||||
os.remove(media_file.file_name)
|
|
||||||
except:
|
|
||||||
log.exception('Could not remove file: %s', media_file.file_name)
|
|
||||||
try:
|
|
||||||
save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(item_id))
|
|
||||||
if os.path.exists(save_path):
|
|
||||||
os.rmdir(save_path)
|
|
||||||
except OSError:
|
|
||||||
log.exception(u'Could not remove directory: %s', save_path)
|
|
||||||
self.plugin.manager.delete_object(Song, item_id)
|
|
||||||
self.main_window.increment_progress_bar()
|
self.main_window.increment_progress_bar()
|
||||||
self.main_window.finished_progress_bar()
|
self.main_window.finished_progress_bar()
|
||||||
self.application.set_normal_cursor()
|
self.application.set_normal_cursor()
|
||||||
@ -457,14 +447,7 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
for slide in verses:
|
for slide in verses:
|
||||||
service_item.add_from_text(unicode(slide))
|
service_item.add_from_text(unicode(slide))
|
||||||
service_item.title = song.title
|
service_item.title = song.title
|
||||||
author_list = [unicode(author.display_name) for author in song.authors]
|
author_list = self.generate_footer(service_item, song)
|
||||||
service_item.raw_footer.append(song.title)
|
|
||||||
service_item.raw_footer.append(create_separated_list(author_list))
|
|
||||||
service_item.raw_footer.append(song.copyright)
|
|
||||||
if Settings().value(u'core/ccli number'):
|
|
||||||
service_item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
|
|
||||||
Settings().value(u'core/ccli number'))
|
|
||||||
service_item.audit = [song.title, author_list, song.copyright, unicode(song.ccli_number)]
|
|
||||||
service_item.data_string = {u'title': song.search_title, u'authors': u', '.join(author_list)}
|
service_item.data_string = {u'title': song.search_title, u'authors': u', '.join(author_list)}
|
||||||
service_item.xml_version = self.openLyrics.song_to_xml(song)
|
service_item.xml_version = self.openLyrics.song_to_xml(song)
|
||||||
# Add the audio file to the service item.
|
# Add the audio file to the service item.
|
||||||
@ -473,6 +456,30 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
service_item.background_audio = [m.file_name for m in song.media_files]
|
service_item.background_audio = [m.file_name for m in song.media_files]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def generate_footer(self, item, song):
|
||||||
|
"""
|
||||||
|
Generates the song footer based on a song and adds details to a service item.
|
||||||
|
author_list is only required for initial song generation.
|
||||||
|
|
||||||
|
``item``
|
||||||
|
The service item to be amended
|
||||||
|
|
||||||
|
``song``
|
||||||
|
The song to be used to generate the footer
|
||||||
|
"""
|
||||||
|
author_list = [unicode(author.display_name) for author in song.authors]
|
||||||
|
item.audit = [
|
||||||
|
song.title, author_list, song.copyright, unicode(song.ccli_number)
|
||||||
|
]
|
||||||
|
item.raw_footer = []
|
||||||
|
item.raw_footer.append(song.title)
|
||||||
|
item.raw_footer.append(create_separated_list(author_list))
|
||||||
|
item.raw_footer.append(song.copyright)
|
||||||
|
if Settings().value(u'core/ccli number'):
|
||||||
|
item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
|
||||||
|
Settings().value(u'core/ccli number'))
|
||||||
|
return author_list
|
||||||
|
|
||||||
def service_load(self, item):
|
def service_load(self, item):
|
||||||
"""
|
"""
|
||||||
Triggered by a song being loaded by the service manager.
|
Triggered by a song being loaded by the service manager.
|
||||||
@ -490,9 +497,8 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
else:
|
else:
|
||||||
search_results = self.plugin.manager.get_all_objects(Song,
|
search_results = self.plugin.manager.get_all_objects(Song,
|
||||||
Song.search_title == item.data_string[u'title'], Song.search_title.asc())
|
Song.search_title == item.data_string[u'title'], Song.search_title.asc())
|
||||||
editId = 0
|
edit_id = 0
|
||||||
add_song = True
|
add_song = True
|
||||||
temporary = False
|
|
||||||
if search_results:
|
if search_results:
|
||||||
for song in search_results:
|
for song in search_results:
|
||||||
author_list = item.data_string[u'authors']
|
author_list = item.data_string[u'authors']
|
||||||
@ -505,7 +511,7 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
break
|
break
|
||||||
if same_authors and author_list.strip(u', ') == u'':
|
if same_authors and author_list.strip(u', ') == u'':
|
||||||
add_song = False
|
add_song = False
|
||||||
editId = song.id
|
edit_id = song.id
|
||||||
break
|
break
|
||||||
# If there's any backing tracks, copy them over.
|
# If there's any backing tracks, copy them over.
|
||||||
if item.background_audio:
|
if item.background_audio:
|
||||||
@ -523,11 +529,11 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
# If there's any backing tracks, copy them over.
|
# If there's any backing tracks, copy them over.
|
||||||
if item.background_audio:
|
if item.background_audio:
|
||||||
self._update_background_audio(song, item)
|
self._update_background_audio(song, item)
|
||||||
editId = song.id
|
edit_id = song.id
|
||||||
temporary = True
|
# Update service with correct song id and return it to caller.
|
||||||
# Update service with correct song id.
|
item.edit_id = edit_id
|
||||||
if editId:
|
self.generate_footer(item, song)
|
||||||
self.service_manager.service_item_update(editId, item.unique_identifier, temporary)
|
return item
|
||||||
|
|
||||||
def search(self, string, showError):
|
def search(self, string, showError):
|
||||||
"""
|
"""
|
||||||
|
139
openlp/plugins/songs/lib/songcompare.py
Normal file
139
openlp/plugins/songs/lib/songcompare.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2013 Raoul Snyman #
|
||||||
|
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
|
||||||
|
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||||
|
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||||
|
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||||
|
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||||
|
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||||
|
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`songcompare` module provides functionality to search for
|
||||||
|
duplicate songs. It has one single :function:`songs_probably_equal`.
|
||||||
|
|
||||||
|
The algorithm is based on the diff algorithm.
|
||||||
|
First a diffset is calculated for two songs.
|
||||||
|
To compensate for typos all differences that are smaller than a
|
||||||
|
limit (<max_typo_size) and are surrounded by larger equal blocks
|
||||||
|
(>min_fragment_size) are removed and the surrounding equal parts are merged.
|
||||||
|
Finally two conditions can qualify a song tuple to be a duplicate:
|
||||||
|
1. There is a block of equal content that is at least min_block_size large.
|
||||||
|
This condition should hit for all larger songs that have a long enough
|
||||||
|
equal part. Even if only one verse is equal this condition should still hit.
|
||||||
|
2. Two thirds of the smaller song is contained in the larger song.
|
||||||
|
This condition should hit if one of the two songs (or both) is small (smaller
|
||||||
|
than the min_block_size), but most of the song is contained in the other song.
|
||||||
|
"""
|
||||||
|
from __future__ import division
|
||||||
|
import difflib
|
||||||
|
|
||||||
|
|
||||||
|
MIN_FRAGMENT_SIZE = 5
|
||||||
|
MIN_BLOCK_SIZE = 70
|
||||||
|
MAX_TYPO_SIZE = 3
|
||||||
|
|
||||||
|
|
||||||
|
def songs_probably_equal(song1, song2):
|
||||||
|
"""
|
||||||
|
Calculate and return whether two songs are probably equal.
|
||||||
|
|
||||||
|
``song1``
|
||||||
|
The first song to compare.
|
||||||
|
|
||||||
|
``song2``
|
||||||
|
The second song to compare.
|
||||||
|
"""
|
||||||
|
if len(song1.search_lyrics) < len(song2.search_lyrics):
|
||||||
|
small = song1.search_lyrics
|
||||||
|
large = song2.search_lyrics
|
||||||
|
else:
|
||||||
|
small = song2.search_lyrics
|
||||||
|
large = song1.search_lyrics
|
||||||
|
differ = difflib.SequenceMatcher(a=large, b=small)
|
||||||
|
diff_tuples = differ.get_opcodes()
|
||||||
|
diff_no_typos = _remove_typos(diff_tuples)
|
||||||
|
# Check 1: Similarity based on the absolute length of equal parts.
|
||||||
|
# Calculate the total length of all equal blocks of the set.
|
||||||
|
# Blocks smaller than min_block_size are not counted.
|
||||||
|
length_of_equal_blocks = 0
|
||||||
|
for element in diff_no_typos:
|
||||||
|
if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE:
|
||||||
|
length_of_equal_blocks += _op_length(element)
|
||||||
|
if length_of_equal_blocks >= MIN_BLOCK_SIZE:
|
||||||
|
return True
|
||||||
|
# Check 2: Similarity based on the relative length of the longest equal block.
|
||||||
|
# Calculate the length of the largest equal block of the diff set.
|
||||||
|
length_of_longest_equal_block = 0
|
||||||
|
for element in diff_no_typos:
|
||||||
|
if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block:
|
||||||
|
length_of_longest_equal_block = _op_length(element)
|
||||||
|
if length_of_equal_blocks >= MIN_BLOCK_SIZE or length_of_longest_equal_block > len(small) * 2 // 3:
|
||||||
|
return True
|
||||||
|
# Both checks failed. We assume the songs are not equal.
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _op_length(opcode):
|
||||||
|
"""
|
||||||
|
Return the length of a given difference.
|
||||||
|
|
||||||
|
``opcode``
|
||||||
|
The difference.
|
||||||
|
"""
|
||||||
|
return max(opcode[2] - opcode[1], opcode[4] - opcode[3])
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_typos(diff):
|
||||||
|
"""
|
||||||
|
Remove typos from a diff set. A typo is a small difference (<max_typo_size)
|
||||||
|
surrounded by larger equal passages (>min_fragment_size).
|
||||||
|
|
||||||
|
``diff``
|
||||||
|
The diff set to remove the typos from.
|
||||||
|
"""
|
||||||
|
# Remove typo at beginning of the string.
|
||||||
|
if len(diff) >= 2:
|
||||||
|
if diff[0][0] != "equal" and _op_length(diff[0]) <= MAX_TYPO_SIZE and \
|
||||||
|
_op_length(diff[1]) >= MIN_FRAGMENT_SIZE:
|
||||||
|
del diff[0]
|
||||||
|
# Remove typos in the middle of the string.
|
||||||
|
if len(diff) >= 3:
|
||||||
|
for index in range(len(diff) - 3, -1, -1):
|
||||||
|
if _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \
|
||||||
|
diff[index + 1][0] != "equal" and _op_length(diff[index + 1]) <= MAX_TYPO_SIZE and \
|
||||||
|
_op_length(diff[index + 2]) >= MIN_FRAGMENT_SIZE:
|
||||||
|
del diff[index + 1]
|
||||||
|
# Remove typo at the end of the string.
|
||||||
|
if len(diff) >= 2:
|
||||||
|
if _op_length(diff[-2]) >= MIN_FRAGMENT_SIZE and \
|
||||||
|
diff[-1][0] != "equal" and _op_length(diff[-1]) <= MAX_TYPO_SIZE:
|
||||||
|
del diff[-1]
|
||||||
|
|
||||||
|
# Merge the bordering equal passages that occured by removing differences.
|
||||||
|
for index in range(len(diff) - 2, -1, -1):
|
||||||
|
if diff[index][0] == "equal" and _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \
|
||||||
|
diff[index + 1][0] == "equal" and _op_length(diff[index + 1]) >= MIN_FRAGMENT_SIZE:
|
||||||
|
diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3],
|
||||||
|
diff[index + 1][4])
|
||||||
|
del diff[index + 1]
|
||||||
|
|
||||||
|
return diff
|
@ -50,6 +50,8 @@ from openlp.plugins.songs.lib.importer import SongFormat
|
|||||||
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
|
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
|
||||||
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
||||||
from openlp.plugins.songs.lib.songstab import SongsTab
|
from openlp.plugins.songs.lib.songstab import SongsTab
|
||||||
|
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
__default_settings__ = {
|
__default_settings__ = {
|
||||||
@ -97,10 +99,12 @@ class SongsPlugin(Plugin):
|
|||||||
self.song_import_item.setVisible(True)
|
self.song_import_item.setVisible(True)
|
||||||
self.song_export_item.setVisible(True)
|
self.song_export_item.setVisible(True)
|
||||||
self.tools_reindex_item.setVisible(True)
|
self.tools_reindex_item.setVisible(True)
|
||||||
|
self.tools_find_duplicates.setVisible(True)
|
||||||
action_list = ActionList.get_instance()
|
action_list = ActionList.get_instance()
|
||||||
action_list.add_action(self.song_import_item, UiStrings().Import)
|
action_list.add_action(self.song_import_item, UiStrings().Import)
|
||||||
action_list.add_action(self.song_export_item, UiStrings().Export)
|
action_list.add_action(self.song_export_item, UiStrings().Export)
|
||||||
action_list.add_action(self.tools_reindex_item, UiStrings().Tools)
|
action_list.add_action(self.tools_reindex_item, UiStrings().Tools)
|
||||||
|
action_list.add_action(self.tools_find_duplicates, UiStrings().Tools)
|
||||||
|
|
||||||
def add_import_menu_item(self, import_menu):
|
def add_import_menu_item(self, import_menu):
|
||||||
"""
|
"""
|
||||||
@ -136,7 +140,7 @@ class SongsPlugin(Plugin):
|
|||||||
|
|
||||||
def add_tools_menu_item(self, tools_menu):
|
def add_tools_menu_item(self, tools_menu):
|
||||||
"""
|
"""
|
||||||
Give the alerts plugin the opportunity to add items to the
|
Give the Songs plugin the opportunity to add items to the
|
||||||
**Tools** menu.
|
**Tools** menu.
|
||||||
|
|
||||||
``tools_menu``
|
``tools_menu``
|
||||||
@ -150,6 +154,12 @@ class SongsPlugin(Plugin):
|
|||||||
statustip=translate('SongsPlugin', 'Re-index the songs database to improve searching and ordering.'),
|
statustip=translate('SongsPlugin', 'Re-index the songs database to improve searching and ordering.'),
|
||||||
visible=False, triggers=self.on_tools_reindex_item_triggered)
|
visible=False, triggers=self.on_tools_reindex_item_triggered)
|
||||||
tools_menu.addAction(self.tools_reindex_item)
|
tools_menu.addAction(self.tools_reindex_item)
|
||||||
|
self.tools_find_duplicates = create_action(tools_menu, u'toolsFindDuplicates',
|
||||||
|
text=translate('SongsPlugin', 'Find &Duplicate Songs'),
|
||||||
|
statustip=translate('SongsPlugin',
|
||||||
|
'Find and remove duplicate songs in the song database.'),
|
||||||
|
visible=False, triggers=self.on_tools_find_duplicates_triggered, can_shortcuts=True)
|
||||||
|
tools_menu.addAction(self.tools_find_duplicates)
|
||||||
|
|
||||||
def on_tools_reindex_item_triggered(self):
|
def on_tools_reindex_item_triggered(self):
|
||||||
"""
|
"""
|
||||||
@ -169,6 +179,12 @@ class SongsPlugin(Plugin):
|
|||||||
self.manager.save_objects(songs)
|
self.manager.save_objects(songs)
|
||||||
self.media_item.on_search_text_button_clicked()
|
self.media_item.on_search_text_button_clicked()
|
||||||
|
|
||||||
|
def on_tools_find_duplicates_triggered(self):
|
||||||
|
"""
|
||||||
|
Search for duplicates in the song database.
|
||||||
|
"""
|
||||||
|
DuplicateSongRemovalForm(self).exec_()
|
||||||
|
|
||||||
def on_song_import_item_clicked(self):
|
def on_song_import_item_clicked(self):
|
||||||
if self.media_item:
|
if self.media_item:
|
||||||
self.media_item.on_import_click()
|
self.media_item.on_import_click()
|
||||||
@ -235,8 +251,7 @@ class SongsPlugin(Plugin):
|
|||||||
u'delete': translate('SongsPlugin', 'Delete the selected song.'),
|
u'delete': translate('SongsPlugin', 'Delete the selected song.'),
|
||||||
u'preview': translate('SongsPlugin', 'Preview the selected song.'),
|
u'preview': translate('SongsPlugin', 'Preview the selected song.'),
|
||||||
u'live': translate('SongsPlugin', 'Send the selected song live.'),
|
u'live': translate('SongsPlugin', 'Send the selected song live.'),
|
||||||
u'service': translate('SongsPlugin',
|
u'service': translate('SongsPlugin', 'Add the selected song to the service.')
|
||||||
'Add the selected song to the service.')
|
|
||||||
}
|
}
|
||||||
self.set_plugin_ui_text_strings(tooltips)
|
self.set_plugin_ui_text_strings(tooltips)
|
||||||
|
|
||||||
@ -288,10 +303,12 @@ class SongsPlugin(Plugin):
|
|||||||
self.song_import_item.setVisible(False)
|
self.song_import_item.setVisible(False)
|
||||||
self.song_export_item.setVisible(False)
|
self.song_export_item.setVisible(False)
|
||||||
self.tools_reindex_item.setVisible(False)
|
self.tools_reindex_item.setVisible(False)
|
||||||
|
self.tools_find_duplicates.setVisible(False)
|
||||||
action_list = ActionList.get_instance()
|
action_list = ActionList.get_instance()
|
||||||
action_list.remove_action(self.song_import_item, UiStrings().Import)
|
action_list.remove_action(self.song_import_item, UiStrings().Import)
|
||||||
action_list.remove_action(self.song_export_item, UiStrings().Export)
|
action_list.remove_action(self.song_export_item, UiStrings().Export)
|
||||||
action_list.remove_action(self.tools_reindex_item, UiStrings().Tools)
|
action_list.remove_action(self.tools_reindex_item, UiStrings().Tools)
|
||||||
|
action_list.remove_action(self.tools_find_duplicates, UiStrings().Tools)
|
||||||
Plugin.finalise(self)
|
Plugin.finalise(self)
|
||||||
|
|
||||||
def new_service_created(self):
|
def new_service_created(self):
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
<file>topic_maintenance.png</file>
|
<file>topic_maintenance.png</file>
|
||||||
<file>song_author_edit.png</file>
|
<file>song_author_edit.png</file>
|
||||||
<file>song_book_edit.png</file>
|
<file>song_book_edit.png</file>
|
||||||
|
<file>song_delete.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="images">
|
<qresource prefix="images">
|
||||||
<file>image_group.png</file>
|
<file>image_group.png</file>
|
||||||
@ -101,6 +102,7 @@
|
|||||||
<file>wizard_importbible.bmp</file>
|
<file>wizard_importbible.bmp</file>
|
||||||
<file>wizard_firsttime.bmp</file>
|
<file>wizard_firsttime.bmp</file>
|
||||||
<file>wizard_createtheme.bmp</file>
|
<file>wizard_createtheme.bmp</file>
|
||||||
|
<file>wizard_duplicateremoval.bmp</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="services">
|
<qresource prefix="services">
|
||||||
<file>service_collapse_all.png</file>
|
<file>service_collapse_all.png</file>
|
||||||
|
BIN
resources/images/wizard_duplicateremoval.bmp
Normal file
BIN
resources/images/wizard_duplicateremoval.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 KiB |
@ -98,9 +98,9 @@ OPTIONAL_MODULES = [
|
|||||||
w = sys.stdout.write
|
w = sys.stdout.write
|
||||||
|
|
||||||
def check_vers(version, required, text):
|
def check_vers(version, required, text):
|
||||||
if type(version) is not str:
|
if not isinstance(version, str):
|
||||||
version = '.'.join(map(str, version))
|
version = '.'.join(map(str, version))
|
||||||
if type(required) is not str:
|
if not isinstance(required, str):
|
||||||
required = '.'.join(map(str, required))
|
required = '.'.join(map(str, required))
|
||||||
w(' %s >= %s ... ' % (text, required))
|
w(' %s >= %s ... ' % (text, required))
|
||||||
if LooseVersion(version) >= LooseVersion(required):
|
if LooseVersion(version) >= LooseVersion(required):
|
||||||
|
68
setup.py
68
setup.py
@ -27,12 +27,15 @@
|
|||||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
import re
|
import re
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
|
||||||
VERSION_FILE = 'openlp/.version'
|
VERSION_FILE = 'openlp/.version'
|
||||||
SPLIT_ALPHA_DIGITS = re.compile(r'(\d+|\D+)')
|
SPLIT_ALPHA_DIGITS = re.compile(r'(\d+|\D+)')
|
||||||
|
|
||||||
|
|
||||||
def try_int(s):
|
def try_int(s):
|
||||||
"""
|
"""
|
||||||
Convert string s to an integer if possible. Fail silently and return
|
Convert string s to an integer if possible. Fail silently and return
|
||||||
@ -46,6 +49,7 @@ def try_int(s):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def natural_sort_key(s):
|
def natural_sort_key(s):
|
||||||
"""
|
"""
|
||||||
Return a tuple by which s is sorted.
|
Return a tuple by which s is sorted.
|
||||||
@ -55,6 +59,7 @@ def natural_sort_key(s):
|
|||||||
"""
|
"""
|
||||||
return map(try_int, SPLIT_ALPHA_DIGITS.findall(s))
|
return map(try_int, SPLIT_ALPHA_DIGITS.findall(s))
|
||||||
|
|
||||||
|
|
||||||
def natural_compare(a, b):
|
def natural_compare(a, b):
|
||||||
"""
|
"""
|
||||||
Compare two strings naturally and return the result.
|
Compare two strings naturally and return the result.
|
||||||
@ -67,6 +72,7 @@ def natural_compare(a, b):
|
|||||||
"""
|
"""
|
||||||
return cmp(natural_sort_key(a), natural_sort_key(b))
|
return cmp(natural_sort_key(a), natural_sort_key(b))
|
||||||
|
|
||||||
|
|
||||||
def natural_sort(seq, compare=natural_compare):
|
def natural_sort(seq, compare=natural_compare):
|
||||||
"""
|
"""
|
||||||
Returns a copy of seq, sorted by natural string sort.
|
Returns a copy of seq, sorted by natural string sort.
|
||||||
@ -76,38 +82,50 @@ def natural_sort(seq, compare=natural_compare):
|
|||||||
temp.sort(compare)
|
temp.sort(compare)
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
|
# NOTE: The following code is a duplicate of the code in openlp/core/utils/__init__.py. Any fix applied here should also
|
||||||
|
# be applied there.
|
||||||
try:
|
try:
|
||||||
# Try to import Bazaar
|
# Get the revision of this tree.
|
||||||
from bzrlib.branch import Branch
|
bzr = Popen((u'bzr', u'revno'), stdout=PIPE)
|
||||||
b = Branch.open_containing('.')[0]
|
tree_revision, error = bzr.communicate()
|
||||||
b.lock_read()
|
code = bzr.wait()
|
||||||
try:
|
if code != 0:
|
||||||
# Get the branch's latest revision number.
|
raise Exception(u'Error running bzr log')
|
||||||
revno = b.revno()
|
|
||||||
# Convert said revision number into a bzr revision id.
|
# Get all tags.
|
||||||
revision_id = b.dotted_revno_to_revision_id((revno,))
|
bzr = Popen((u'bzr', u'tags'), stdout=PIPE)
|
||||||
# Get a dict of tags, with the revision id as the key.
|
output, error = bzr.communicate()
|
||||||
tags = b.tags.get_reverse_tag_dict()
|
code = bzr.wait()
|
||||||
# Check if the latest
|
if code != 0:
|
||||||
if revision_id in tags:
|
raise Exception(u'Error running bzr tags')
|
||||||
version = u'%s' % tags[revision_id][0]
|
tags = output.splitlines()
|
||||||
else:
|
if not tags:
|
||||||
version = '%s-bzr%s' % \
|
tag_version = u'0.0.0'
|
||||||
(natural_sort(b.tags.get_tag_dict().keys())[-1], revno)
|
tag_revision = u'0'
|
||||||
ver_file = open(VERSION_FILE, u'w')
|
else:
|
||||||
ver_file.write(version)
|
# Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
|
||||||
ver_file.close()
|
# another series.
|
||||||
finally:
|
tags = [tag for tag in tags if tag.split()[-1].strip() != u'?']
|
||||||
b.unlock()
|
# Get the last tag and split it in a revision and tag name.
|
||||||
|
tag_version, tag_revision = tags[-1].split()
|
||||||
|
# If they are equal, then this tree is tarball with the source for the release. We do not want the revision number
|
||||||
|
# in the version string.
|
||||||
|
if tree_revision == tag_revision:
|
||||||
|
version_string = tag_version
|
||||||
|
else:
|
||||||
|
version_string = u'%s-bzr%s' % (tag_version, tree_revision)
|
||||||
|
ver_file = open(VERSION_FILE, u'w')
|
||||||
|
ver_file.write(version_string)
|
||||||
except:
|
except:
|
||||||
ver_file = open(VERSION_FILE, u'r')
|
ver_file = open(VERSION_FILE, u'r')
|
||||||
version = ver_file.read().strip()
|
version_string = ver_file.read().strip()
|
||||||
|
finally:
|
||||||
ver_file.close()
|
ver_file.close()
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='OpenLP',
|
name='OpenLP',
|
||||||
version=version,
|
version=version_string,
|
||||||
description="Open source Church presentation and lyrics projection application.",
|
description="Open source Church presentation and lyrics projection application.",
|
||||||
long_description="""\
|
long_description="""\
|
||||||
OpenLP (previously openlp.org) is free church presentation software, or lyrics projection software, used to display slides of songs, Bible verses, videos, images, and even presentations (if PowerPoint is installed) for church worship using a computer and a data projector.""",
|
OpenLP (previously openlp.org) is free church presentation software, or lyrics projection software, used to display slides of songs, Bible verses, videos, images, and even presentations (if PowerPoint is installed) for church worship using a computer and a data projector.""",
|
||||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@ -1,13 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
Package to test the openlp.core.lib package.
|
Package to test the openlp.core.lib package.
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from mock import MagicMock, patch
|
from mock import MagicMock, patch
|
||||||
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import str_to_bool, create_thumb, translate, check_directory_exists, get_text_file_string, \
|
||||||
|
build_icon, image_to_byte, check_item_selected, validate_thumb, create_separated_list, clean_tags, expand_tags
|
||||||
|
|
||||||
|
|
||||||
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources'))
|
||||||
|
|
||||||
from openlp.core.lib import str_to_bool, translate, check_directory_exists, get_text_file_string, build_icon, \
|
|
||||||
image_to_byte, check_item_selected, validate_thumb, create_separated_list, clean_tags, expand_tags
|
|
||||||
|
|
||||||
class TestLib(TestCase):
|
class TestLib(TestCase):
|
||||||
|
|
||||||
@ -125,7 +132,7 @@ class TestLib(TestCase):
|
|||||||
Test the check_directory_exists() function
|
Test the check_directory_exists() function
|
||||||
"""
|
"""
|
||||||
with patch(u'openlp.core.lib.os.path.exists') as mocked_exists, \
|
with patch(u'openlp.core.lib.os.path.exists') as mocked_exists, \
|
||||||
patch(u'openlp.core.lib.os.makedirs') as mocked_makedirs:
|
patch(u'openlp.core.lib.os.makedirs') as mocked_makedirs:
|
||||||
# GIVEN: A directory to check and a mocked out os.makedirs and os.path.exists
|
# GIVEN: A directory to check and a mocked out os.makedirs and os.path.exists
|
||||||
directory_to_check = u'existing/directory'
|
directory_to_check = u'existing/directory'
|
||||||
|
|
||||||
@ -219,7 +226,7 @@ class TestLib(TestCase):
|
|||||||
Test the build_icon() function with a resource URI
|
Test the build_icon() function with a resource URI
|
||||||
"""
|
"""
|
||||||
with patch(u'openlp.core.lib.QtGui') as MockedQtGui, \
|
with patch(u'openlp.core.lib.QtGui') as MockedQtGui, \
|
||||||
patch(u'openlp.core.lib.QtGui.QPixmap') as MockedQPixmap:
|
patch(u'openlp.core.lib.QtGui.QPixmap') as MockedQPixmap:
|
||||||
# GIVEN: A mocked QIcon and a mocked QPixmap
|
# GIVEN: A mocked QIcon and a mocked QPixmap
|
||||||
MockedQtGui.QIcon = MagicMock
|
MockedQtGui.QIcon = MagicMock
|
||||||
MockedQtGui.QIcon.Normal = 1
|
MockedQtGui.QIcon.Normal = 1
|
||||||
@ -261,9 +268,43 @@ class TestLib(TestCase):
|
|||||||
mocked_byte_array.toBase64.assert_called_with()
|
mocked_byte_array.toBase64.assert_called_with()
|
||||||
assert result == u'base64mock', u'The result should be the return value of the mocked out base64 method'
|
assert result == u'base64mock', u'The result should be the return value of the mocked out base64 method'
|
||||||
|
|
||||||
|
def create_thumb_with_size_test(self):
|
||||||
|
"""
|
||||||
|
Test the create_thumb() function
|
||||||
|
"""
|
||||||
|
# GIVEN: An image to create a thumb of.
|
||||||
|
image_path = os.path.join(TEST_PATH, u'church.jpg')
|
||||||
|
thumb_path = os.path.join(TEST_PATH, u'church_thumb.jpg')
|
||||||
|
thumb_size = QtCore.QSize(10, 20)
|
||||||
|
|
||||||
|
# Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
|
||||||
|
# last test.
|
||||||
|
try:
|
||||||
|
os.remove(thumb_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Only continue when the thumb does not exist.
|
||||||
|
assert not os.path.exists(thumb_path), u'Test was not ran, because the thumb already exists.'
|
||||||
|
|
||||||
|
# WHEN: Create the thumb.
|
||||||
|
icon = create_thumb(image_path, thumb_path, size=thumb_size)
|
||||||
|
|
||||||
|
# THEN: Check if the thumb was created.
|
||||||
|
assert os.path.exists(thumb_path), u'Test was not ran, because the thumb already exists.'
|
||||||
|
assert isinstance(icon, QtGui.QIcon), u'The icon should be a QIcon.'
|
||||||
|
assert not icon.isNull(), u'The icon should not be null.'
|
||||||
|
assert QtGui.QImageReader(thumb_path).size() == thumb_size, u'The thumb should have the given size.'
|
||||||
|
|
||||||
|
# Remove the thumb so that the test actually tests if the thumb will be created.
|
||||||
|
try:
|
||||||
|
os.remove(thumb_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def check_item_selected_true_test(self):
|
def check_item_selected_true_test(self):
|
||||||
"""
|
"""
|
||||||
Test that the check_item_selected() function returns True when there are selected indexes.
|
Test that the check_item_selected() function returns True when there are selected indexes
|
||||||
"""
|
"""
|
||||||
# GIVEN: A mocked out QtGui module and a list widget with selected indexes
|
# GIVEN: A mocked out QtGui module and a list widget with selected indexes
|
||||||
MockedQtGui = patch(u'openlp.core.lib.QtGui')
|
MockedQtGui = patch(u'openlp.core.lib.QtGui')
|
||||||
@ -423,7 +464,7 @@ class TestLib(TestCase):
|
|||||||
|
|
||||||
def create_separated_list_qlocate_test(self):
|
def create_separated_list_qlocate_test(self):
|
||||||
"""
|
"""
|
||||||
Test the create_separated_list function using the Qt provided method.
|
Test the create_separated_list function using the Qt provided method
|
||||||
"""
|
"""
|
||||||
with patch(u'openlp.core.lib.Qt') as mocked_qt, \
|
with patch(u'openlp.core.lib.Qt') as mocked_qt, \
|
||||||
patch(u'openlp.core.lib.QtCore.QLocale.createSeparatedList') as mocked_createSeparatedList:
|
patch(u'openlp.core.lib.QtCore.QLocale.createSeparatedList') as mocked_createSeparatedList:
|
||||||
@ -442,7 +483,7 @@ class TestLib(TestCase):
|
|||||||
|
|
||||||
def create_separated_list_empty_list_test(self):
|
def create_separated_list_empty_list_test(self):
|
||||||
"""
|
"""
|
||||||
Test the create_separated_list function with an empty list.
|
Test the create_separated_list function with an empty list
|
||||||
"""
|
"""
|
||||||
with patch(u'openlp.core.lib.Qt') as mocked_qt:
|
with patch(u'openlp.core.lib.Qt') as mocked_qt:
|
||||||
# GIVEN: An empty list and the mocked Qt module.
|
# GIVEN: An empty list and the mocked Qt module.
|
||||||
@ -458,7 +499,7 @@ class TestLib(TestCase):
|
|||||||
|
|
||||||
def create_separated_list_with_one_item_test(self):
|
def create_separated_list_with_one_item_test(self):
|
||||||
"""
|
"""
|
||||||
Test the create_separated_list function with a list consisting of only one entry.
|
Test the create_separated_list function with a list consisting of only one entry
|
||||||
"""
|
"""
|
||||||
with patch(u'openlp.core.lib.Qt') as mocked_qt:
|
with patch(u'openlp.core.lib.Qt') as mocked_qt:
|
||||||
# GIVEN: A list with a string and the mocked Qt module.
|
# GIVEN: A list with a string and the mocked Qt module.
|
||||||
@ -474,7 +515,7 @@ class TestLib(TestCase):
|
|||||||
|
|
||||||
def create_separated_list_with_two_items_test(self):
|
def create_separated_list_with_two_items_test(self):
|
||||||
"""
|
"""
|
||||||
Test the create_separated_list function with a list of two entries.
|
Test the create_separated_list function with a list of two entries
|
||||||
"""
|
"""
|
||||||
with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate:
|
with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate:
|
||||||
# GIVEN: A list of strings and the mocked Qt module.
|
# GIVEN: A list of strings and the mocked Qt module.
|
||||||
@ -491,7 +532,7 @@ class TestLib(TestCase):
|
|||||||
|
|
||||||
def create_separated_list_with_three_items_test(self):
|
def create_separated_list_with_three_items_test(self):
|
||||||
"""
|
"""
|
||||||
Test the create_separated_list function with a list of three items.
|
Test the create_separated_list function with a list of three items
|
||||||
"""
|
"""
|
||||||
with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate:
|
with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate:
|
||||||
# GIVEN: A list with a string and the mocked Qt module.
|
# GIVEN: A list with a string and the mocked Qt module.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Package to test the openlp.core.lib.screenlist package.
|
Package to test the openlp.core.lib.screenlist package.
|
||||||
"""
|
"""
|
||||||
import copy
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
Package to test the openlp.core.lib package.
|
Package to test the openlp.core.lib package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import cPickle
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from mock import MagicMock, patch
|
from mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.lib import ItemCapabilities, ServiceItem, Registry
|
from openlp.core.lib import ItemCapabilities, ServiceItem, Registry
|
||||||
|
from tests.utils.osdinteraction import read_service_from_file
|
||||||
|
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||||
|
|
||||||
|
|
||||||
VERSE = u'The Lord said to {r}Noah{/r}: \n'\
|
VERSE = u'The Lord said to {r}Noah{/r}: \n'\
|
||||||
@ -18,8 +19,6 @@ VERSE = u'The Lord said to {r}Noah{/r}: \n'\
|
|||||||
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
||||||
FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
|
FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources'))
|
|
||||||
|
|
||||||
|
|
||||||
class TestServiceItem(TestCase):
|
class TestServiceItem(TestCase):
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ class TestServiceItem(TestCase):
|
|||||||
service_item.name = u'test'
|
service_item.name = u'test'
|
||||||
|
|
||||||
# WHEN: adding image to a service item
|
# WHEN: adding image to a service item
|
||||||
test_image = os.path.join(TEST_PATH, u'church.jpg')
|
test_image = os.path.join(TEST_RESOURCES_PATH, u'church.jpg')
|
||||||
service_item.add_from_image(test_image, u'Image Title')
|
service_item.add_from_image(test_image, u'Image Title')
|
||||||
|
|
||||||
# THEN: We should get back a valid service item
|
# THEN: We should get back a valid service item
|
||||||
@ -133,8 +132,8 @@ class TestServiceItem(TestCase):
|
|||||||
service_item.name = u'test'
|
service_item.name = u'test'
|
||||||
|
|
||||||
# WHEN: adding image to a service item
|
# WHEN: adding image to a service item
|
||||||
test_file = os.path.join(TEST_PATH, u'church.jpg')
|
test_file = os.path.join(TEST_RESOURCES_PATH, u'church.jpg')
|
||||||
service_item.add_from_command(TEST_PATH, u'church.jpg', test_file)
|
service_item.add_from_command(TEST_RESOURCES_PATH, u'church.jpg', test_file)
|
||||||
|
|
||||||
# THEN: We should get back a valid service item
|
# THEN: We should get back a valid service item
|
||||||
assert service_item.is_valid is True, u'The new service item should be valid'
|
assert service_item.is_valid is True, u'The new service item should be valid'
|
||||||
@ -151,7 +150,7 @@ class TestServiceItem(TestCase):
|
|||||||
assert len(service) == 2, u'The saved service should have two parts'
|
assert len(service) == 2, u'The saved service should have two parts'
|
||||||
assert service[u'header'][u'name'] == u'test', u'A test plugin should be returned'
|
assert service[u'header'][u'name'] == u'test', u'A test plugin should be returned'
|
||||||
assert service[u'data'][0][u'title'] == u'church.jpg', u'The first title name should be "church,jpg"'
|
assert service[u'data'][0][u'title'] == u'church.jpg', u'The first title name should be "church,jpg"'
|
||||||
assert service[u'data'][0][u'path'] == TEST_PATH, u'The path should match the input path'
|
assert service[u'data'][0][u'path'] == TEST_RESOURCES_PATH, u'The path should match the input path'
|
||||||
assert service[u'data'][0][u'image'] == test_file, u'The image should match the full path to image'
|
assert service[u'data'][0][u'image'] == test_file, u'The image should match the full path to image'
|
||||||
|
|
||||||
# WHEN validating a service item
|
# WHEN validating a service item
|
||||||
@ -170,13 +169,12 @@ class TestServiceItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
Test the Service Item - adding a custom slide from a saved service
|
Test the Service Item - adding a custom slide from a saved service
|
||||||
"""
|
"""
|
||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
|
||||||
|
|
||||||
# WHEN: adding a custom from a saved Service
|
# WHEN: adding a custom from a saved Service
|
||||||
line = self.convert_file_service_item(u'serviceitem_custom_1.osd')
|
service = read_service_from_file(u'serviceitem_custom_1.osd')
|
||||||
service_item.set_from_service(line)
|
service_item.set_from_service(service[0])
|
||||||
|
|
||||||
# THEN: We should get back a valid service item
|
# THEN: We should get back a valid service item
|
||||||
assert service_item.is_valid is True, u'The new service item should be valid'
|
assert service_item.is_valid is True, u'The new service item should be valid'
|
||||||
@ -195,22 +193,20 @@ class TestServiceItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
Test the Service Item - adding an image from a saved service
|
Test the Service Item - adding an image from a saved service
|
||||||
"""
|
"""
|
||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item
|
||||||
image_name = u'image_1.jpg'
|
image_name = u'image_1.jpg'
|
||||||
test_file = os.path.join(TEST_PATH, image_name)
|
test_file = os.path.join(TEST_RESOURCES_PATH, image_name)
|
||||||
frame_array = {u'path': test_file, u'title': image_name}
|
frame_array = {u'path': test_file, u'title': image_name}
|
||||||
|
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
|
||||||
|
|
||||||
# WHEN: adding an image from a saved Service and mocked exists
|
# WHEN: adding an image from a saved Service and mocked exists
|
||||||
line = self.convert_file_service_item(u'serviceitem_image_1.osd')
|
service = read_service_from_file(u'serviceitem_image_1.osd')
|
||||||
with patch('os.path.exists'):
|
with patch('os.path.exists'):
|
||||||
service_item.set_from_service(line, TEST_PATH)
|
service_item.set_from_service(service[0], TEST_RESOURCES_PATH)
|
||||||
|
|
||||||
# THEN: We should get back a valid service item
|
# THEN: We should get back a valid service item
|
||||||
assert service_item.is_valid is True, u'The new service item should be valid'
|
assert service_item.is_valid is True, u'The new service item should be valid'
|
||||||
print service_item.get_rendered_frame(0)
|
|
||||||
assert service_item.get_rendered_frame(0) == test_file, u'The first frame should match the path to the image'
|
assert service_item.get_rendered_frame(0) == test_file, u'The first frame should match the path to the image'
|
||||||
assert service_item.get_frames()[0] == frame_array, u'The return should match frame array1'
|
assert service_item.get_frames()[0] == frame_array, u'The return should match frame array1'
|
||||||
assert service_item.get_frame_path(0) == test_file, u'The frame path should match the full path to the image'
|
assert service_item.get_frame_path(0) == test_file, u'The frame path should match the full path to the image'
|
||||||
@ -230,7 +226,7 @@ class TestServiceItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
Test the Service Item - adding an image from a saved local service
|
Test the Service Item - adding an image from a saved local service
|
||||||
"""
|
"""
|
||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item
|
||||||
image_name1 = u'image_1.jpg'
|
image_name1 = u'image_1.jpg'
|
||||||
image_name2 = u'image_2.jpg'
|
image_name2 = u'image_2.jpg'
|
||||||
test_file1 = os.path.join(u'/home/openlp', image_name1)
|
test_file1 = os.path.join(u'/home/openlp', image_name1)
|
||||||
@ -239,12 +235,11 @@ class TestServiceItem(TestCase):
|
|||||||
frame_array2 = {u'path': test_file2, u'title': image_name2}
|
frame_array2 = {u'path': test_file2, u'title': image_name2}
|
||||||
|
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
|
||||||
|
|
||||||
# WHEN: adding an image from a saved Service and mocked exists
|
# WHEN: adding an image from a saved Service and mocked exists
|
||||||
line = self.convert_file_service_item(u'serviceitem_image_2.osd')
|
service = read_service_from_file(u'serviceitem_image_2.osd')
|
||||||
with patch('os.path.exists'):
|
with patch('os.path.exists'):
|
||||||
service_item.set_from_service(line)
|
service_item.set_from_service(service[0])
|
||||||
|
|
||||||
# THEN: We should get back a valid service item
|
# THEN: We should get back a valid service item
|
||||||
assert service_item.is_valid is True, u'The new service item should be valid'
|
assert service_item.is_valid is True, u'The new service item should be valid'
|
||||||
@ -268,15 +263,22 @@ class TestServiceItem(TestCase):
|
|||||||
assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \
|
assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \
|
||||||
u'This service item should be able to have new items added to it'
|
u'This service item should be able to have new items added to it'
|
||||||
|
|
||||||
def convert_file_service_item(self, name):
|
def serviceitem_migrate_test_20_22(self):
|
||||||
service_file = os.path.join(TEST_PATH, name)
|
"""
|
||||||
try:
|
Test the Service Item - migrating a media only service item from 2.0 to 2.2 format
|
||||||
open_file = open(service_file, u'r')
|
"""
|
||||||
items = cPickle.load(open_file)
|
# GIVEN: A new service item and a mocked add icon function
|
||||||
first_line = items[0]
|
service_item = ServiceItem(None)
|
||||||
except IOError:
|
service_item.add_icon = MagicMock()
|
||||||
first_line = u''
|
|
||||||
finally:
|
|
||||||
open_file.close()
|
|
||||||
return first_line
|
|
||||||
|
|
||||||
|
# WHEN: adding an media from a saved Service and mocked exists
|
||||||
|
line = self.convert_file_service_item(u'migrate_video_20_22.osd')
|
||||||
|
with patch('os.path.exists'):
|
||||||
|
service_item.set_from_service(line, TEST_PATH)
|
||||||
|
|
||||||
|
# THEN: We should get back a converted service item
|
||||||
|
assert service_item.is_valid is True, u'The new service item should be valid'
|
||||||
|
assert service_item.processor is None, u'The Processor should have been set'
|
||||||
|
assert service_item.title is None, u'The title should be set to a value'
|
||||||
|
assert service_item.is_capable(ItemCapabilities.HasDetailedTitleDisplay) is False, \
|
||||||
|
u'The Capability should have been removed'
|
||||||
|
@ -9,7 +9,7 @@ from unittest import TestCase
|
|||||||
from mock import MagicMock, patch
|
from mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.lib import Registry
|
from openlp.core.lib import Registry
|
||||||
from openlp.plugins.images.lib.db import ImageFilenames
|
from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
|
||||||
from openlp.plugins.images.lib.mediaitem import ImageMediaItem
|
from openlp.plugins.images.lib.mediaitem import ImageMediaItem
|
||||||
|
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ class TestImageMediaItem(TestCase):
|
|||||||
Registry.create()
|
Registry.create()
|
||||||
Registry().register(u'service_list', MagicMock())
|
Registry().register(u'service_list', MagicMock())
|
||||||
Registry().register(u'main_window', self.mocked_main_window)
|
Registry().register(u'main_window', self.mocked_main_window)
|
||||||
|
Registry().register(u'live_controller', MagicMock())
|
||||||
mocked_parent = MagicMock()
|
mocked_parent = MagicMock()
|
||||||
mocked_plugin = MagicMock()
|
mocked_plugin = MagicMock()
|
||||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.__init__') as mocked_init:
|
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.__init__') as mocked_init:
|
||||||
@ -35,7 +36,7 @@ class TestImageMediaItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
# GIVEN: An empty image_list
|
# GIVEN: An empty image_list
|
||||||
image_list = []
|
image_list = []
|
||||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list:
|
||||||
self.media_item.manager = MagicMock()
|
self.media_item.manager = MagicMock()
|
||||||
|
|
||||||
# WHEN: We run save_new_images_list with the empty list
|
# WHEN: We run save_new_images_list with the empty list
|
||||||
@ -51,7 +52,7 @@ class TestImageMediaItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
# GIVEN: A list with 1 image
|
# GIVEN: A list with 1 image
|
||||||
image_list = [ u'test_image.jpg' ]
|
image_list = [ u'test_image.jpg' ]
|
||||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list:
|
||||||
ImageFilenames.filename = ''
|
ImageFilenames.filename = ''
|
||||||
self.media_item.manager = MagicMock()
|
self.media_item.manager = MagicMock()
|
||||||
|
|
||||||
@ -59,7 +60,7 @@ class TestImageMediaItem(TestCase):
|
|||||||
self.media_item.save_new_images_list(image_list, reload_list=True)
|
self.media_item.save_new_images_list(image_list, reload_list=True)
|
||||||
|
|
||||||
# THEN: load_full_list() should have been called
|
# THEN: load_full_list() should have been called
|
||||||
assert mocked_loadFullList.call_count == 1, u'load_full_list() should have been called'
|
assert mocked_load_full_list.call_count == 1, u'load_full_list() should have been called'
|
||||||
|
|
||||||
# CLEANUP: Remove added attribute from ImageFilenames
|
# CLEANUP: Remove added attribute from ImageFilenames
|
||||||
delattr(ImageFilenames, 'filename')
|
delattr(ImageFilenames, 'filename')
|
||||||
@ -70,14 +71,14 @@ class TestImageMediaItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
# GIVEN: A list with 1 image
|
# GIVEN: A list with 1 image
|
||||||
image_list = [ u'test_image.jpg' ]
|
image_list = [ u'test_image.jpg' ]
|
||||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list:
|
||||||
self.media_item.manager = MagicMock()
|
self.media_item.manager = MagicMock()
|
||||||
|
|
||||||
# WHEN: We run save_new_images_list with reload_list=False
|
# WHEN: We run save_new_images_list with reload_list=False
|
||||||
self.media_item.save_new_images_list(image_list, reload_list=False)
|
self.media_item.save_new_images_list(image_list, reload_list=False)
|
||||||
|
|
||||||
# THEN: load_full_list() should not have been called
|
# THEN: load_full_list() should not have been called
|
||||||
assert mocked_loadFullList.call_count == 0, u'load_full_list() should not have been called'
|
assert mocked_load_full_list.call_count == 0, u'load_full_list() should not have been called'
|
||||||
|
|
||||||
def save_new_images_list_multiple_images_test(self):
|
def save_new_images_list_multiple_images_test(self):
|
||||||
"""
|
"""
|
||||||
@ -85,7 +86,7 @@ class TestImageMediaItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
# GIVEN: A list with 3 images
|
# GIVEN: A list with 3 images
|
||||||
image_list = [ u'test_image_1.jpg', u'test_image_2.jpg', u'test_image_3.jpg' ]
|
image_list = [ u'test_image_1.jpg', u'test_image_2.jpg', u'test_image_3.jpg' ]
|
||||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list:
|
||||||
self.media_item.manager = MagicMock()
|
self.media_item.manager = MagicMock()
|
||||||
|
|
||||||
# WHEN: We run save_new_images_list with the list of 3 images
|
# WHEN: We run save_new_images_list with the list of 3 images
|
||||||
@ -101,7 +102,7 @@ class TestImageMediaItem(TestCase):
|
|||||||
"""
|
"""
|
||||||
# GIVEN: A list with images and objects
|
# GIVEN: A list with images and objects
|
||||||
image_list = [ u'test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg' ]
|
image_list = [ u'test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg' ]
|
||||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list:
|
||||||
self.media_item.manager = MagicMock()
|
self.media_item.manager = MagicMock()
|
||||||
|
|
||||||
# WHEN: We run save_new_images_list with the list of images and objects
|
# WHEN: We run save_new_images_list with the list of images and objects
|
||||||
@ -110,3 +111,68 @@ class TestImageMediaItem(TestCase):
|
|||||||
# THEN: load_full_list() should not have been called
|
# THEN: load_full_list() should not have been called
|
||||||
assert self.media_item.manager.save_object.call_count == 2, \
|
assert self.media_item.manager.save_object.call_count == 2, \
|
||||||
u'load_full_list() should have been called only once'
|
u'load_full_list() should have been called only once'
|
||||||
|
|
||||||
|
def on_reset_click_test(self):
|
||||||
|
"""
|
||||||
|
Test that on_reset_click() actually resets the background
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked version of reset_action
|
||||||
|
self.media_item.reset_action = MagicMock()
|
||||||
|
|
||||||
|
# WHEN: on_reset_click is called
|
||||||
|
self.media_item.on_reset_click()
|
||||||
|
|
||||||
|
# THEN: the reset_action should be set visible, and the image should be reset
|
||||||
|
self.media_item.reset_action.setVisible.assert_called_with(False)
|
||||||
|
self.media_item.live_controller.display.reset_image.assert_called_with()
|
||||||
|
|
||||||
|
def _recursively_delete_group_side_effect(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Side effect method that creates custom retun values for the recursively_delete_group method
|
||||||
|
"""
|
||||||
|
if args[1] == ImageFilenames and args[2]:
|
||||||
|
# Create some fake objects that should be removed
|
||||||
|
returned_object1 = ImageFilenames()
|
||||||
|
returned_object1.id = 1
|
||||||
|
returned_object1.filename = u'/tmp/test_file_1.jpg'
|
||||||
|
returned_object2 = ImageFilenames()
|
||||||
|
returned_object2.id = 2
|
||||||
|
returned_object2.filename = u'/tmp/test_file_2.jpg'
|
||||||
|
returned_object3 = ImageFilenames()
|
||||||
|
returned_object3.id = 3
|
||||||
|
returned_object3.filename = u'/tmp/test_file_3.jpg'
|
||||||
|
return [returned_object1, returned_object2, returned_object3]
|
||||||
|
if args[1] == ImageGroups and args[2]:
|
||||||
|
# Change the parent_id that is matched so we don't get into an endless loop
|
||||||
|
ImageGroups.parent_id = 0
|
||||||
|
# Create a fake group that will be used in the next run
|
||||||
|
returned_object1 = ImageGroups()
|
||||||
|
returned_object1.id = 1
|
||||||
|
return [returned_object1]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def recursively_delete_group_test(self):
|
||||||
|
"""
|
||||||
|
Test that recursively_delete_group() works
|
||||||
|
"""
|
||||||
|
# GIVEN: An ImageGroups object and mocked functions
|
||||||
|
with patch(u'openlp.core.utils.delete_file') as mocked_delete_file:
|
||||||
|
ImageFilenames.group_id = 1
|
||||||
|
ImageGroups.parent_id = 1
|
||||||
|
self.media_item.manager = MagicMock()
|
||||||
|
self.media_item.manager.get_all_objects.side_effect = self._recursively_delete_group_side_effect
|
||||||
|
self.media_item.servicePath = ""
|
||||||
|
test_group = ImageGroups()
|
||||||
|
test_group.id = 1
|
||||||
|
|
||||||
|
# WHEN: recursively_delete_group() is called
|
||||||
|
self.media_item.recursively_delete_group(test_group)
|
||||||
|
|
||||||
|
# THEN:
|
||||||
|
assert mocked_delete_file.call_count == 0, u'delete_file() should not be called'
|
||||||
|
assert self.media_item.manager.delete_object.call_count == 7, \
|
||||||
|
u'manager.delete_object() should be called exactly 7 times'
|
||||||
|
|
||||||
|
# CLEANUP: Remove added attribute from ImageFilenames and ImageGroups
|
||||||
|
delattr(ImageFilenames, 'group_id')
|
||||||
|
delattr(ImageGroups, 'parent_id')
|
||||||
|
@ -4,15 +4,37 @@ This module contains tests for the lib submodule of the Songs plugin.
|
|||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from mock import patch
|
from mock import patch, MagicMock
|
||||||
|
|
||||||
from openlp.plugins.songs.lib import VerseType, clean_string, clean_title
|
from openlp.plugins.songs.lib import VerseType, clean_string, clean_title
|
||||||
|
from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length
|
||||||
|
|
||||||
|
|
||||||
class TestLib(TestCase):
|
class TestLib(TestCase):
|
||||||
"""
|
"""
|
||||||
Test the functions in the :mod:`lib` module.
|
Test the functions in the :mod:`lib` module.
|
||||||
"""
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Mock up two songs and provide a set of lyrics for the songs_probably_equal tests.
|
||||||
|
"""
|
||||||
|
self.full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am
|
||||||
|
found was blind but now i see twas grace that taught my heart to fear and grace my fears relieved how
|
||||||
|
precious did that grace appear the hour i first believed through many dangers toils and snares i have already
|
||||||
|
come tis grace that brought me safe thus far and grace will lead me home'''
|
||||||
|
self.short_lyrics =u'''twas grace that taught my heart to fear and grace my fears relieved how precious did that
|
||||||
|
grace appear the hour i first believed'''
|
||||||
|
self.error_lyrics =u'''amazing how sweet the trumpet that saved a wrench like me i once was losst but now am
|
||||||
|
found waf blind but now i see it was grace that taught my heart to fear and grace my fears relieved how
|
||||||
|
precious did that grace appppppppear the hour i first believedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx snares i have
|
||||||
|
already come to this grace that brought me safe so far and grace will lead me home'''
|
||||||
|
self.different_lyrics=u'''on a hill far away stood an old rugged cross the emblem of suffering and shame and i love
|
||||||
|
that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged
|
||||||
|
cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a
|
||||||
|
crown'''
|
||||||
|
self.song1 = MagicMock()
|
||||||
|
self.song2 = MagicMock()
|
||||||
|
|
||||||
def clean_string_test(self):
|
def clean_string_test(self):
|
||||||
"""
|
"""
|
||||||
Test the clean_string() function
|
Test the clean_string() function
|
||||||
@ -39,6 +61,160 @@ class TestLib(TestCase):
|
|||||||
# THEN: The string should be cleaned up
|
# THEN: The string should be cleaned up
|
||||||
self.assertEqual(result, u'This is a dirty string', u'The title should be cleaned up properly: "%s"' % result)
|
self.assertEqual(result, u'This is a dirty string', u'The title should be cleaned up properly: "%s"' % result)
|
||||||
|
|
||||||
|
def songs_probably_equal_same_song_test(self):
|
||||||
|
"""
|
||||||
|
Test the songs_probably_equal function with twice the same song.
|
||||||
|
"""
|
||||||
|
# GIVEN: Two equal songs.
|
||||||
|
self.song1.search_lyrics = self.full_lyrics
|
||||||
|
self.song2.search_lyrics = self.full_lyrics
|
||||||
|
|
||||||
|
# WHEN: We compare those songs for equality.
|
||||||
|
result = songs_probably_equal(self.song1, self.song2)
|
||||||
|
|
||||||
|
# THEN: The result should be True.
|
||||||
|
assert result == True, u'The result should be True'
|
||||||
|
|
||||||
|
def songs_probably_equal_short_song_test(self):
|
||||||
|
"""
|
||||||
|
Test the songs_probably_equal function with a song and a shorter version of the same song.
|
||||||
|
"""
|
||||||
|
# GIVEN: A song and a short version of the same song.
|
||||||
|
self.song1.search_lyrics = self.full_lyrics
|
||||||
|
self.song2.search_lyrics = self.short_lyrics
|
||||||
|
|
||||||
|
# WHEN: We compare those songs for equality.
|
||||||
|
result = songs_probably_equal(self.song1, self.song2)
|
||||||
|
|
||||||
|
# THEN: The result should be True.
|
||||||
|
assert result == True, u'The result should be True'
|
||||||
|
|
||||||
|
def songs_probably_equal_error_song_test(self):
|
||||||
|
"""
|
||||||
|
Test the songs_probably_equal function with a song and a very erroneous version of the same song.
|
||||||
|
"""
|
||||||
|
# GIVEN: A song and the same song with lots of errors.
|
||||||
|
self.song1.search_lyrics = self.full_lyrics
|
||||||
|
self.song2.search_lyrics = self.error_lyrics
|
||||||
|
|
||||||
|
# WHEN: We compare those songs for equality.
|
||||||
|
result = songs_probably_equal(self.song1, self.song2)
|
||||||
|
|
||||||
|
# THEN: The result should be True.
|
||||||
|
assert result == True, u'The result should be True'
|
||||||
|
|
||||||
|
def songs_probably_equal_different_song_test(self):
|
||||||
|
"""
|
||||||
|
Test the songs_probably_equal function with two different songs.
|
||||||
|
"""
|
||||||
|
# GIVEN: Two different songs.
|
||||||
|
self.song1.search_lyrics = self.full_lyrics
|
||||||
|
self.song2.search_lyrics = self.different_lyrics
|
||||||
|
|
||||||
|
# WHEN: We compare those songs for equality.
|
||||||
|
result = songs_probably_equal(self.song1, self.song2)
|
||||||
|
|
||||||
|
# THEN: The result should be False.
|
||||||
|
assert result == False, u'The result should be False'
|
||||||
|
|
||||||
|
def remove_typos_beginning_test(self):
|
||||||
|
"""
|
||||||
|
Test the _remove_typos function with a typo at the beginning.
|
||||||
|
"""
|
||||||
|
# GIVEN: A diffset with a difference at the beginning.
|
||||||
|
diff = [('replace', 0, 2, 0, 1), ('equal', 2, 11, 1, 10)]
|
||||||
|
|
||||||
|
# WHEN: We remove the typos in there.
|
||||||
|
result = _remove_typos(diff)
|
||||||
|
|
||||||
|
# THEN: There should be no typos at the beginning anymore.
|
||||||
|
assert len(result) == 1, u'The result should contain only one element.'
|
||||||
|
assert result[0][0] == 'equal', u'The result should contain an equal element.'
|
||||||
|
|
||||||
|
def remove_typos_beginning_negated_test(self):
|
||||||
|
"""
|
||||||
|
Test the _remove_typos function with a large difference at the beginning.
|
||||||
|
"""
|
||||||
|
# GIVEN: A diffset with a large difference at the beginning.
|
||||||
|
diff = [('replace', 0, 20, 0, 1), ('equal', 20, 29, 1, 10)]
|
||||||
|
|
||||||
|
# WHEN: We remove the typos in there.
|
||||||
|
result = _remove_typos(list(diff))
|
||||||
|
|
||||||
|
# THEN: There diff should not have changed.
|
||||||
|
assert result == diff
|
||||||
|
|
||||||
|
def remove_typos_end_test(self):
|
||||||
|
"""
|
||||||
|
Test the _remove_typos function with a typo at the end.
|
||||||
|
"""
|
||||||
|
# GIVEN: A diffset with a difference at the end.
|
||||||
|
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11)]
|
||||||
|
|
||||||
|
# WHEN: We remove the typos in there.
|
||||||
|
result = _remove_typos(diff)
|
||||||
|
|
||||||
|
# THEN: There should be no typos at the end anymore.
|
||||||
|
assert len(result) == 1, u'The result should contain only one element.'
|
||||||
|
assert result[0][0] == 'equal', u'The result should contain an equal element.'
|
||||||
|
|
||||||
|
def remove_typos_end_negated_test(self):
|
||||||
|
"""
|
||||||
|
Test the _remove_typos function with a large difference at the end.
|
||||||
|
"""
|
||||||
|
# GIVEN: A diffset with a large difference at the end.
|
||||||
|
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 1)]
|
||||||
|
|
||||||
|
# WHEN: We remove the typos in there.
|
||||||
|
result = _remove_typos(list(diff))
|
||||||
|
|
||||||
|
# THEN: There diff should not have changed.
|
||||||
|
assert result == diff
|
||||||
|
|
||||||
|
def remove_typos_middle_test(self):
|
||||||
|
"""
|
||||||
|
Test the _remove_typos function with a typo in the middle.
|
||||||
|
"""
|
||||||
|
# GIVEN: A diffset with a difference in the middle.
|
||||||
|
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11), ('equal', 12, 22, 11, 21)]
|
||||||
|
|
||||||
|
# WHEN: We remove the typos in there.
|
||||||
|
result = _remove_typos(diff)
|
||||||
|
|
||||||
|
# THEN: There should be no typos in the middle anymore. The remaining equals should have been merged.
|
||||||
|
assert len(result) is 1, u'The result should contain only one element.'
|
||||||
|
assert result[0][0] == 'equal', u'The result should contain an equal element.'
|
||||||
|
assert result[0][1] == 0, u'The start indices should be kept.'
|
||||||
|
assert result[0][2] == 22, u'The stop indices should be kept.'
|
||||||
|
assert result[0][3] == 0, u'The start indices should be kept.'
|
||||||
|
assert result[0][4] == 21, u'The stop indices should be kept.'
|
||||||
|
|
||||||
|
def remove_typos_beginning_negated_test(self):
|
||||||
|
"""
|
||||||
|
Test the _remove_typos function with a large difference in the middle.
|
||||||
|
"""
|
||||||
|
# GIVEN: A diffset with a large difference in the middle.
|
||||||
|
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 11), ('equal', 20, 30, 11, 21)]
|
||||||
|
|
||||||
|
# WHEN: We remove the typos in there.
|
||||||
|
result = _remove_typos(list(diff))
|
||||||
|
|
||||||
|
# THEN: There diff should not have changed.
|
||||||
|
assert result == diff
|
||||||
|
|
||||||
|
def op_length_test(self):
|
||||||
|
"""
|
||||||
|
Test the _op_length function.
|
||||||
|
"""
|
||||||
|
# GIVEN: A diff entry.
|
||||||
|
diff_entry = ('replace', 0, 2, 4, 14)
|
||||||
|
|
||||||
|
# WHEN: We calculate the length of that diff.
|
||||||
|
result = _op_length(diff_entry)
|
||||||
|
|
||||||
|
# THEN: The maximum length should be returned.
|
||||||
|
assert result == 10, u'The length should be 10.'
|
||||||
|
|
||||||
|
|
||||||
class TestVerseType(TestCase):
|
class TestVerseType(TestCase):
|
||||||
"""
|
"""
|
||||||
|
125
tests/functional/openlp_plugins/songs/test_mediaitem.py
Normal file
125
tests/functional/openlp_plugins/songs/test_mediaitem.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
This module contains tests for the lib submodule of the Songs plugin.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from tempfile import mkstemp
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from mock import patch, MagicMock
|
||||||
|
|
||||||
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import Registry, ServiceItem, Settings
|
||||||
|
|
||||||
|
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
||||||
|
|
||||||
|
|
||||||
|
class TestMediaItem(TestCase):
|
||||||
|
"""
|
||||||
|
Test the functions in the :mod:`lib` module.
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Set up the components need for all tests.
|
||||||
|
"""
|
||||||
|
Registry.create()
|
||||||
|
Registry().register(u'service_list', MagicMock())
|
||||||
|
Registry().register(u'main_window', MagicMock())
|
||||||
|
with patch('openlp.core.lib.mediamanageritem.MediaManagerItem.__init__'), \
|
||||||
|
patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'):
|
||||||
|
self.media_item = SongMediaItem(MagicMock(), MagicMock())
|
||||||
|
|
||||||
|
fd, self.ini_file = mkstemp(u'.ini')
|
||||||
|
Settings().set_filename(self.ini_file)
|
||||||
|
self.application = QtGui.QApplication.instance()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""
|
||||||
|
Delete all the C++ objects at the end so that we don't have a segfault
|
||||||
|
"""
|
||||||
|
del self.application
|
||||||
|
# Not all tests use settings!
|
||||||
|
try:
|
||||||
|
os.unlink(self.ini_file)
|
||||||
|
os.unlink(Settings().fileName())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def build_song_footer_one_author_test(self):
|
||||||
|
"""
|
||||||
|
Test build songs footer with basic song and one author
|
||||||
|
"""
|
||||||
|
# GIVEN: A Song and a Service Item
|
||||||
|
mock_song = MagicMock()
|
||||||
|
mock_song.title = u'My Song'
|
||||||
|
mock_author = MagicMock()
|
||||||
|
mock_author.display_name = u'my author'
|
||||||
|
mock_song.authors = []
|
||||||
|
mock_song.authors.append(mock_author)
|
||||||
|
mock_song.copyright = u'My copyright'
|
||||||
|
service_item = ServiceItem(None)
|
||||||
|
|
||||||
|
# WHEN: I generate the Footer with default settings
|
||||||
|
author_list = self.media_item.generate_footer(service_item, mock_song)
|
||||||
|
|
||||||
|
# THEN: I get the following Array returned
|
||||||
|
self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright'],
|
||||||
|
u'The array should be returned correctly with a song, one author and copyright')
|
||||||
|
self.assertEqual(author_list, [u'my author'],
|
||||||
|
u'The author list should be returned correctly with one author')
|
||||||
|
|
||||||
|
def build_song_footer_two_authors_test(self):
|
||||||
|
"""
|
||||||
|
Test build songs footer with basic song and two authors
|
||||||
|
"""
|
||||||
|
# GIVEN: A Song and a Service Item
|
||||||
|
mock_song = MagicMock()
|
||||||
|
mock_song.title = u'My Song'
|
||||||
|
mock_author = MagicMock()
|
||||||
|
mock_author.display_name = u'my author'
|
||||||
|
mock_song.authors = []
|
||||||
|
mock_song.authors.append(mock_author)
|
||||||
|
mock_author = MagicMock()
|
||||||
|
mock_author.display_name = u'another author'
|
||||||
|
mock_song.authors.append(mock_author)
|
||||||
|
mock_song.copyright = u'My copyright'
|
||||||
|
service_item = ServiceItem(None)
|
||||||
|
|
||||||
|
# WHEN: I generate the Footer with default settings
|
||||||
|
author_list = self.media_item.generate_footer(service_item, mock_song)
|
||||||
|
|
||||||
|
# THEN: I get the following Array returned
|
||||||
|
self.assertEqual(service_item.raw_footer, [u'My Song', u'my author and another author', u'My copyright'],
|
||||||
|
u'The array should be returned correctly with a song, two authors and copyright')
|
||||||
|
self.assertEqual(author_list, [u'my author', u'another author'],
|
||||||
|
u'The author list should be returned correctly with two authors')
|
||||||
|
|
||||||
|
def build_song_footer_base_ccli_test(self):
|
||||||
|
"""
|
||||||
|
Test build songs footer with basic song and two authors
|
||||||
|
"""
|
||||||
|
# GIVEN: A Song and a Service Item and a configured CCLI license
|
||||||
|
mock_song = MagicMock()
|
||||||
|
mock_song.title = u'My Song'
|
||||||
|
mock_author = MagicMock()
|
||||||
|
mock_author.display_name = u'my author'
|
||||||
|
mock_song.authors = []
|
||||||
|
mock_song.authors.append(mock_author)
|
||||||
|
mock_song.copyright = u'My copyright'
|
||||||
|
service_item = ServiceItem(None)
|
||||||
|
Settings().setValue(u'core/ccli number', u'1234')
|
||||||
|
|
||||||
|
# WHEN: I generate the Footer with default settings
|
||||||
|
self.media_item.generate_footer(service_item, mock_song)
|
||||||
|
|
||||||
|
# THEN: I get the following Array returned
|
||||||
|
self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright', u'CCLI License: 1234'],
|
||||||
|
u'The array should be returned correctly with a song, an author, copyright and ccli')
|
||||||
|
|
||||||
|
# WHEN: I amend the CCLI value
|
||||||
|
Settings().setValue(u'core/ccli number', u'4321')
|
||||||
|
self.media_item.generate_footer(service_item, mock_song)
|
||||||
|
|
||||||
|
# THEN: I would get an amended footer string
|
||||||
|
self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright', u'CCLI License: 4321'],
|
||||||
|
u'The array should be returned correctly with a song, an author, copyright and amended ccli')
|
88
tests/interfaces/openlp_core_ui/test_listpreviewwidget.py
Normal file
88
tests/interfaces/openlp_core_ui/test_listpreviewwidget.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Package to test the openlp.core.ui.listpreviewwidget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
from mock import MagicMock, patch
|
||||||
|
|
||||||
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import Registry, ServiceItem
|
||||||
|
from openlp.core.ui import listpreviewwidget
|
||||||
|
from tests.utils.osdinteraction import read_service_from_file
|
||||||
|
|
||||||
|
class TestListPreviewWidget(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Create the UI.
|
||||||
|
"""
|
||||||
|
Registry.create()
|
||||||
|
self.app = QtGui.QApplication([])
|
||||||
|
self.main_window = QtGui.QMainWindow()
|
||||||
|
self.image = QtGui.QImage(1, 1, QtGui.QImage.Format_RGB32)
|
||||||
|
self.image_manager = MagicMock()
|
||||||
|
self.image_manager.get_image.return_value = self.image
|
||||||
|
Registry().register(u'image_manager', self.image_manager)
|
||||||
|
self.preview_widget = listpreviewwidget.ListPreviewWidget(self.main_window, 2)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""
|
||||||
|
Delete all the C++ objects at the end so that we don't have a segfault.
|
||||||
|
"""
|
||||||
|
del self.preview_widget
|
||||||
|
del self.main_window
|
||||||
|
del self.app
|
||||||
|
|
||||||
|
def initial_slide_count_test(self):
|
||||||
|
"""
|
||||||
|
Test the inital slide count.
|
||||||
|
"""
|
||||||
|
# GIVEN: A new ListPreviewWidget instance.
|
||||||
|
# WHEN: No SlideItem has been added yet.
|
||||||
|
# THEN: The count of items should be zero.
|
||||||
|
self.assertEqual(self.preview_widget.slide_count(), 0,
|
||||||
|
u'The slide list should be empty.')
|
||||||
|
|
||||||
|
def initial_slide_number_test(self):
|
||||||
|
"""
|
||||||
|
Test the inital slide number.
|
||||||
|
"""
|
||||||
|
# GIVEN: A new ListPreviewWidget instance.
|
||||||
|
# WHEN: No SlideItem has been added yet.
|
||||||
|
# THEN: The number of the current item should be -1.
|
||||||
|
self.assertEqual(self.preview_widget.current_slide_number(), -1,
|
||||||
|
u'The slide number should be -1.')
|
||||||
|
|
||||||
|
def replace_service_item_test(self):
|
||||||
|
"""
|
||||||
|
Test item counts and current number with a service item.
|
||||||
|
"""
|
||||||
|
# GIVEN: A ServiceItem with two frames.
|
||||||
|
service_item = ServiceItem(None)
|
||||||
|
service = read_service_from_file(u'serviceitem_image_2.osd')
|
||||||
|
with patch('os.path.exists'):
|
||||||
|
service_item.set_from_service(service[0])
|
||||||
|
# WHEN: Added to the preview widget.
|
||||||
|
self.preview_widget.replace_service_item(service_item, 1, 1)
|
||||||
|
# THEN: The slide count and number should fit.
|
||||||
|
self.assertEqual(self.preview_widget.slide_count(), 2,
|
||||||
|
u'The slide count should be 2.')
|
||||||
|
self.assertEqual(self.preview_widget.current_slide_number(), 1,
|
||||||
|
u'The current slide number should be 1.')
|
||||||
|
|
||||||
|
def change_slide_test(self):
|
||||||
|
"""
|
||||||
|
Test the change_slide method.
|
||||||
|
"""
|
||||||
|
# GIVEN: A ServiceItem with two frames content.
|
||||||
|
service_item = ServiceItem(None)
|
||||||
|
service = read_service_from_file(u'serviceitem_image_2.osd')
|
||||||
|
with patch('os.path.exists'):
|
||||||
|
service_item.set_from_service(service[0])
|
||||||
|
# WHEN: Added to the preview widget and switched to the second frame.
|
||||||
|
self.preview_widget.replace_service_item(service_item, 1, 0)
|
||||||
|
self.preview_widget.change_slide(1)
|
||||||
|
# THEN: The current_slide_number should reflect the change.
|
||||||
|
self.assertEqual(self.preview_widget.current_slide_number(), 1,
|
||||||
|
u'The current slide number should be 1.')
|
1
tests/interfaces/openlp_plugins/bibles/__init__.py
Normal file
1
tests/interfaces/openlp_plugins/bibles/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__author__ = 'tim'
|
73
tests/interfaces/openlp_plugins/bibles/test_lib_http.py
Normal file
73
tests/interfaces/openlp_plugins/bibles/test_lib_http.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Package to test the openlp.plugin.bible.lib.https package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
from mock import MagicMock
|
||||||
|
|
||||||
|
from openlp.core.lib import Registry
|
||||||
|
from openlp.plugins.bibles.lib.http import BGExtract, CWExtract
|
||||||
|
|
||||||
|
|
||||||
|
class TestBibleHTTP(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Set up the Registry
|
||||||
|
"""
|
||||||
|
Registry.create()
|
||||||
|
Registry().register(u'service_list', MagicMock())
|
||||||
|
Registry().register(u'application', MagicMock())
|
||||||
|
|
||||||
|
def bible_gateway_extract_books_test(self):
|
||||||
|
"""
|
||||||
|
Test the Bible Gateway retrieval of book list for NIV bible
|
||||||
|
"""
|
||||||
|
# GIVEN: A new Bible Gateway extraction class
|
||||||
|
handler = BGExtract()
|
||||||
|
|
||||||
|
# WHEN: The Books list is called
|
||||||
|
books = handler.get_books_from_http(u'NIV')
|
||||||
|
|
||||||
|
# THEN: We should get back a valid service item
|
||||||
|
assert len(books) == 66, u'The bible should not have had any books added or removed'
|
||||||
|
|
||||||
|
def bible_gateway_extract_verse_test(self):
|
||||||
|
"""
|
||||||
|
Test the Bible Gateway retrieval of verse list for NIV bible John 3
|
||||||
|
"""
|
||||||
|
# GIVEN: A new Bible Gateway extraction class
|
||||||
|
handler = BGExtract()
|
||||||
|
|
||||||
|
# WHEN: The Books list is called
|
||||||
|
results = handler.get_bible_chapter(u'NIV', u'John', 3)
|
||||||
|
|
||||||
|
# THEN: We should get back a valid service item
|
||||||
|
assert len(results.verselist) == 36, u'The book of John should not have had any verses added or removed'
|
||||||
|
|
||||||
|
def crosswalk_extract_books_test(self):
|
||||||
|
"""
|
||||||
|
Test Crosswalk retrieval of book list for NIV bible
|
||||||
|
"""
|
||||||
|
# GIVEN: A new Bible Gateway extraction class
|
||||||
|
handler = CWExtract()
|
||||||
|
|
||||||
|
# WHEN: The Books list is called
|
||||||
|
books = handler.get_books_from_http(u'niv')
|
||||||
|
|
||||||
|
# THEN: We should get back a valid service item
|
||||||
|
assert len(books) == 66, u'The bible should not have had any books added or removed'
|
||||||
|
|
||||||
|
def crosswalk_extract_verse_test(self):
|
||||||
|
"""
|
||||||
|
Test Crosswalk retrieval of verse list for NIV bible John 3
|
||||||
|
"""
|
||||||
|
# GIVEN: A new Bible Gateway extraction class
|
||||||
|
handler = CWExtract()
|
||||||
|
|
||||||
|
# WHEN: The Books list is called
|
||||||
|
results = handler.get_bible_chapter(u'niv', u'john', 3)
|
||||||
|
|
||||||
|
# THEN: We should get back a valid service item
|
||||||
|
assert len(results.verselist) == 36, u'The book of John should not have had any verses added or removed'
|
||||||
|
|
98
tests/resources/migrate_video_20_22.osd
Normal file
98
tests/resources/migrate_video_20_22.osd
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
(lp1
|
||||||
|
(dp2
|
||||||
|
Vserviceitem
|
||||||
|
p3
|
||||||
|
(dp4
|
||||||
|
Vheader
|
||||||
|
p5
|
||||||
|
(dp6
|
||||||
|
Vxml_version
|
||||||
|
p7
|
||||||
|
NsVauto_play_slides_loop
|
||||||
|
p8
|
||||||
|
I00
|
||||||
|
sVauto_play_slides_once
|
||||||
|
p9
|
||||||
|
I00
|
||||||
|
sVwill_auto_start
|
||||||
|
p10
|
||||||
|
I01
|
||||||
|
sVtitle
|
||||||
|
p11
|
||||||
|
VVLC
|
||||||
|
p12
|
||||||
|
sVcapabilities
|
||||||
|
p13
|
||||||
|
(lp14
|
||||||
|
I12
|
||||||
|
aI16
|
||||||
|
aI4
|
||||||
|
aI11
|
||||||
|
asVtheme
|
||||||
|
p15
|
||||||
|
I-1
|
||||||
|
sVbackground_audio
|
||||||
|
p16
|
||||||
|
(lp17
|
||||||
|
sVicon
|
||||||
|
p18
|
||||||
|
V:/plugins/plugin_media.png
|
||||||
|
p19
|
||||||
|
sVtype
|
||||||
|
p20
|
||||||
|
I3
|
||||||
|
sVstart_time
|
||||||
|
p21
|
||||||
|
I0
|
||||||
|
sVfrom_plugin
|
||||||
|
p22
|
||||||
|
I00
|
||||||
|
sVmedia_length
|
||||||
|
p23
|
||||||
|
I144
|
||||||
|
sVdata
|
||||||
|
p24
|
||||||
|
V
|
||||||
|
sVtimed_slide_interval
|
||||||
|
p25
|
||||||
|
I0
|
||||||
|
sVaudit
|
||||||
|
p26
|
||||||
|
V
|
||||||
|
sVsearch
|
||||||
|
p27
|
||||||
|
V
|
||||||
|
sVname
|
||||||
|
p28
|
||||||
|
Vmedia
|
||||||
|
p29
|
||||||
|
sVfooter
|
||||||
|
p30
|
||||||
|
(lp31
|
||||||
|
sVnotes
|
||||||
|
p32
|
||||||
|
V
|
||||||
|
sVplugin
|
||||||
|
p33
|
||||||
|
g29
|
||||||
|
sVtheme_overwritten
|
||||||
|
p34
|
||||||
|
I00
|
||||||
|
sVend_time
|
||||||
|
p35
|
||||||
|
I0
|
||||||
|
ssg24
|
||||||
|
(lp36
|
||||||
|
(dp37
|
||||||
|
Vpath
|
||||||
|
p38
|
||||||
|
V/home/tim/Videos/puppets
|
||||||
|
p39
|
||||||
|
sVimage
|
||||||
|
p40
|
||||||
|
V:/media/slidecontroller_multimedia.png
|
||||||
|
p41
|
||||||
|
sg11
|
||||||
|
VMVI_3405.MOV
|
||||||
|
p42
|
||||||
|
sassa.
|
0
tests/utils/__init__.py
Normal file
0
tests/utils/__init__.py
Normal file
5
tests/utils/constants.py
Normal file
5
tests/utils/constants.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
OPENLP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..'))
|
||||||
|
TEST_RESOURCES_PATH = os.path.join(OPENLP_PATH, u'tests', u'resources')
|
49
tests/utils/osdinteraction.py
Normal file
49
tests/utils/osdinteraction.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2013 Raoul Snyman #
|
||||||
|
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
|
||||||
|
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||||
|
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||||
|
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||||
|
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||||
|
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||||
|
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`osdinteraction` provides miscellaneous functions for interacting with
|
||||||
|
OSD files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import cPickle
|
||||||
|
|
||||||
|
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def read_service_from_file(file_name):
|
||||||
|
"""
|
||||||
|
Reads an OSD file and returns the first service item found therein.
|
||||||
|
@param file_name: File name of an OSD file residing in the tests/resources folder.
|
||||||
|
@return: The service contained in the file.
|
||||||
|
"""
|
||||||
|
service_file = os.path.join(TEST_RESOURCES_PATH, file_name)
|
||||||
|
with open(service_file, u'r') as open_file:
|
||||||
|
service = cPickle.load(open_file)
|
||||||
|
return service
|
Loading…
Reference in New Issue
Block a user