This commit is contained in:
phill-ridout 2013-07-08 06:39:32 +01:00
commit 9190019ce9
13 changed files with 1626 additions and 25 deletions

View File

@ -107,7 +107,7 @@ class Renderer(object):
del self._theme_dimensions[old_theme_name]
if theme_name in self._theme_dimensions:
del self._theme_dimensions[theme_name]
if not only_delete:
if not only_delete and theme_name:
self._set_theme(theme_name)
def _set_theme(self, theme_name):

View File

@ -35,6 +35,7 @@ import logging
import os
import shutil
import zipfile
import json
from tempfile import mkstemp
from datetime import datetime, timedelta
@ -458,7 +459,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
path_file_name = unicode(self.file_name())
path, file_name = os.path.split(path_file_name)
base_name = os.path.splitext(file_name)[0]
service_file_name = '%s.osd' % base_name
service_file_name = '%s.osj' % base_name
log.debug(u'ServiceManager.save_file - %s', path_file_name)
Settings().setValue(self.main_window.service_manager_settings_section + u'/last directory', path)
service = []
@ -512,7 +513,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
file_size = os.path.getsize(file_item)
total_size += file_size
log.debug(u'ServiceManager.save_file - ZIP contents size is %i bytes' % total_size)
service_content = cPickle.dumps(service)
service_content = json.dumps(service)
# Usual Zip file cannot exceed 2GiB, file with Zip64 cannot be extracted using unzip in UNIX.
allow_zip_64 = (total_size > 2147483648 + len(service_content))
log.debug(u'ServiceManager.save_file - allowZip64 is %s' % allow_zip_64)
@ -572,7 +573,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
path_file_name = unicode(self.file_name())
path, file_name = os.path.split(path_file_name)
base_name = os.path.splitext(file_name)[0]
service_file_name = '%s.osd' % base_name
service_file_name = '%s.osj' % base_name
log.debug(u'ServiceManager.save_file - %s', path_file_name)
Settings().setValue(self.main_window.service_manager_settings_section + u'/last directory', path)
service = []
@ -585,7 +586,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
#TODO: check for file item on save.
service.append({u'serviceitem': service_item})
self.main_window.increment_progress_bar()
service_content = cPickle.dumps(service)
service_content = json.dumps(service)
zip_file = None
success = True
self.main_window.increment_progress_bar()
@ -698,11 +699,14 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
log.debug(u'Extract file: %s', osfile)
zip_info.filename = osfile
zip_file.extract(zip_info, self.servicePath)
if osfile.endswith(u'osd'):
if osfile.endswith(u'osj') or osfile.endswith(u'osd'):
p_file = os.path.join(self.servicePath, osfile)
if 'p_file' in locals():
file_to = open(p_file, u'r')
items = cPickle.load(file_to)
if p_file.endswith(u'osj'):
items = json.load(file_to)
else:
items = cPickle.load(file_to)
file_to.close()
self.new_file()
self.set_file_name(file_name)

View File

@ -53,27 +53,25 @@ log = logging.getLogger(__name__)
class AppLocation(object):
"""
The :class:`AppLocation` class is a static class which retrieves a
directory based on the directory type.
The :class:`AppLocation` class is a static class which retrieves a directory based on the directory type.
"""
AppDir = 1
ConfigDir = 2
DataDir = 3
PluginsDir = 4
VersionDir = 5
CacheDir = 6
LanguageDir = 7
DataDir = 2
PluginsDir = 3
VersionDir = 4
CacheDir = 5
LanguageDir = 6
# Base path where data/config/cache dir is located
BaseDir = None
@staticmethod
def get_directory(dir_type=1):
def get_directory(dir_type=AppDir):
"""
Return the appropriate directory according to the directory type.
``dir_type``
The directory type you want, for instance the data directory.
The directory type you want, for instance the data directory. Default *AppLocation.AppDir*
"""
if dir_type == AppLocation.AppDir:
return _get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), os.path.split(openlp.__file__)[0])
@ -161,16 +159,13 @@ def _get_os_dir_path(dir_type):
return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp')
else:
if dir_type == AppLocation.LanguageDir:
prefixes = [u'/usr/local', u'/usr']
for prefix in prefixes:
for prefix in [u'/usr/local', u'/usr']:
directory = os.path.join(prefix, u'share', u'openlp')
if os.path.exists(directory):
return directory
return os.path.join(u'/usr', u'share', u'openlp')
if XDG_BASE_AVAILABLE:
if dir_type == AppLocation.ConfigDir:
return os.path.join(unicode(BaseDirectory.xdg_config_home, encoding), u'openlp')
elif dir_type == AppLocation.DataDir:
if dir_type == AppLocation.DataDir:
return os.path.join(unicode(BaseDirectory.xdg_data_home, encoding), u'openlp')
elif dir_type == AppLocation.CacheDir:
return os.path.join(unicode(BaseDirectory.xdg_cache_home, encoding), u'openlp')

View File

@ -74,7 +74,13 @@ if os.name == u'nt':
HAS_MEDIASHOUT = True
except ImportError:
log.exception('Error importing %s', 'MediaShoutImport')
HAS_WORSHIPCENTERPRO = False
if os.name == u'nt':
try:
from worshipcenterproimport import WorshipCenterProImport
HAS_WORSHIPCENTERPRO = True
except ImportError:
log.exception('Error importing %s', 'WorshipCenterProImport')
class SongFormatSelect(object):
"""
@ -157,7 +163,8 @@ class SongFormat(object):
SongsOfFellowship = 14
SundayPlus = 15
WordsOfWorship = 16
ZionWorx = 17
WorshipCenterPro = 17
ZionWorx = 18
# Set optional attribute defaults
__defaults__ = {
@ -300,6 +307,16 @@ class SongFormat(object):
u'filter': u'%s (*.wsg *.wow-song)' %
translate('SongsPlugin.ImportWizardForm', 'Words Of Worship Song Files')
},
WorshipCenterPro: {
u'name': u'WorshipCenter Pro',
u'prefix': u'worshipCenterPro',
u'canDisable': True,
u'selectMode': SongFormatSelect.SingleFile,
u'filter': u'%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'WorshipCenter Pro Song Files'),
u'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
'The WorshipCenter Pro importer is only supported on Windows. It has been disabled due to a missing '
'Python module. If you want to use this importer, you will need to install the "pyodbc" module.')
},
ZionWorx: {
u'class': ZionWorxImport,
u'name': u'ZionWorx',
@ -336,6 +353,7 @@ class SongFormat(object):
SongFormat.SongsOfFellowship,
SongFormat.SundayPlus,
SongFormat.WordsOfWorship,
SongFormat.WorshipCenterPro,
SongFormat.ZionWorx
]
@ -385,5 +403,9 @@ if HAS_OOO:
SongFormat.set(SongFormat.MediaShout, u'availability', HAS_MEDIASHOUT)
if HAS_MEDIASHOUT:
SongFormat.set(SongFormat.MediaShout, u'class', MediaShoutImport)
SongFormat.set(SongFormat.WorshipCenterPro, u'availability', HAS_WORSHIPCENTERPRO)
if HAS_WORSHIPCENTERPRO:
SongFormat.set(SongFormat.WorshipCenterPro, u'class', WorshipCenterProImport)
__all__ = [u'SongFormat', u'SongFormatSelect']

View File

@ -0,0 +1,85 @@
# -*- 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:`worshipcenterpro` module provides the functionality for importing
a WorshipCenter Pro database into the OpenLP database.
"""
import logging
import pyodbc
from openlp.core.lib import translate
from openlp.plugins.songs.lib.songimport import SongImport
log = logging.getLogger(__name__)
class WorshipCenterProImport(SongImport):
"""
The :class:`WorshipCenterProImport` class provides the ability to import the
WorshipCenter Pro Access Database
"""
def __init__(self, manager, **kwargs):
"""
Initialise the WorshipCenter Pro importer.
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
"""
Receive a single file to import.
"""
try:
conn = pyodbc.connect(u'DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s' % self.import_source)
except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError), e:
log.warn(u'Unable to connect the WorshipCenter Pro database %s. %s', self.import_source, unicode(e))
# Unfortunately no specific exception type
self.logError(self.import_source,
translate('SongsPlugin.WorshipCenterProImport', 'Unable to connect the WorshipCenter Pro database.'))
return
cursor = conn.cursor()
cursor.execute(u'SELECT ID, Field, Value FROM __SONGDATA')
records = cursor.fetchall()
songs = {}
for record in records:
id = record.ID
if not songs.has_key(id):
songs[id] = {}
songs[id][record.Field] = record.Value
self.import_wizard.progress_bar.setMaximum(len(songs))
for song in songs:
if self.stop_import_flag:
break
self.setDefaults()
self.title = songs[song][u'TITLE']
lyrics = songs[song][u'LYRICS'].strip(u'&crlf;&crlf;')
for verse in lyrics.split(u'&crlf;&crlf;'):
verse = verse.replace(u'&crlf;', u'\n')
self.addVerse(verse)
self.finish()

View File

@ -62,6 +62,7 @@ WIN32_MODULES = [
'win32com',
'win32ui',
'pywintypes',
'pyodbc',
]
MODULES = [

View File

@ -0,0 +1,261 @@
# -*- coding: utf-8 -*-
"""
Package to test the openlp.core.lib package.
"""
import os
import io
import cPickle
import json
import tempfile
from unittest import TestCase
from mock import MagicMock, patch
from openlp.core.lib import ItemCapabilities, ServiceItem, Registry
from lxml import objectify, etree
VERSE = u'The Lord said to {r}Noah{/r}: \n'\
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\
'The Lord said to {g}Noah{/g}:\n'\
'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n'\
'Get those children out of the muddy, muddy \n'\
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
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):
def setUp(self):
"""
Set up the Registry
"""
Registry.create()
mocked_renderer = MagicMock()
mocked_renderer.format_slide.return_value = [VERSE]
Registry().register(u'renderer', mocked_renderer)
Registry().register(u'image_manager', MagicMock())
def serviceitem_basic_test(self):
"""
Test the Service Item - basic test
"""
# GIVEN: A new service item
# WHEN: A service item is created (without a plugin)
service_item = ServiceItem(None)
# 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.missing_frames() is True, u'There should not be any frames in the service item'
def serviceitem_load_custom_from_service_test(self):
"""
Test the Service Item - adding a custom slide from a saved service
"""
# GIVEN: A new service item and a mocked add icon function
service_item = ServiceItem(None)
service_item.add_icon = MagicMock()
# WHEN: adding a custom from a saved Service
line = self.convert_file_service_item(u'serviceitem_custom_1.osj')
service_item.set_from_service(line)
# 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 len(service_item._display_frames) == 0, u'The service item should have no display frames'
assert len(service_item.capabilities) == 5, u'There should be 5 default custom item capabilities'
service_item.render(True)
assert service_item.get_display_title() == u'Test Custom', u'The title should be "Test Custom"'
assert service_item.get_frames()[0][u'text'] == VERSE[:-1], \
u'The returned text matches the input, except the last line feed'
assert service_item.get_rendered_frame(1) == VERSE.split(u'\n', 1)[0], u'The first line has been returned'
assert service_item.get_frame_title(0) == u'Slide 1', u'"Slide 1" has been returned as the title'
assert service_item.get_frame_title(1) == u'Slide 2', u'"Slide 2" has been returned as the title'
assert service_item.get_frame_title(2) == u'', u'Blank has been returned as the title of slide 3'
def serviceitem_load_image_from_service_test(self):
"""
Test the Service Item - adding an image from a saved service
"""
# GIVEN: A new service item and a mocked add icon function
image_name = u'image_1.jpg'
test_file = os.path.join(TEST_PATH, image_name)
frame_array = {u'path': test_file, u'title': image_name}
service_item = ServiceItem(None)
service_item.add_icon = MagicMock()
# WHEN: adding an image from a saved Service and mocked exists
line = self.convert_file_service_item(u'serviceitem_image_1.osj')
with patch(u'openlp.core.ui.servicemanager.os.path.exists') as mocked_exists:
mocked_exists.return_value = True
service_item.set_from_service(line, TEST_PATH)
# 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.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_frame_path(0) == test_file, u'The frame path should match the full path to the image'
assert service_item.get_frame_title(0) == image_name, u'The frame title should match the image name'
assert service_item.get_display_title() == image_name, u'The display title should match the first image name'
assert service_item.is_image() is True, u'This service item should be of an "image" type'
assert service_item.is_capable(ItemCapabilities.CanMaintain) is True, \
u'This service item should be able to be Maintained'
assert service_item.is_capable(ItemCapabilities.CanPreview) is True, \
u'This service item should be able to be be Previewed'
assert service_item.is_capable(ItemCapabilities.CanLoop) is True, \
u'This service item should be able to be run in a can be made to Loop'
assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \
u'This service item should be able to have new items added to it'
def serviceitem_load_image_from_local_service_test(self):
"""
Test the Service Item - adding an image from a saved local service
"""
# GIVEN: A new service item and a mocked add icon function
image_name1 = u'image_1.jpg'
image_name2 = u'image_2.jpg'
test_file1 = os.path.join(u'/home/openlp', image_name1)
test_file2 = os.path.join(u'/home/openlp', image_name2)
frame_array1 = {u'path': test_file1, u'title': image_name1}
frame_array2 = {u'path': test_file2, u'title': image_name2}
service_item = ServiceItem(None)
service_item.add_icon = MagicMock()
service_item2 = ServiceItem(None)
service_item2.add_icon = MagicMock()
# WHEN: adding an image from a saved Service and mocked exists
line = self.convert_file_service_item(u'serviceitem_image_2.osj')
line2 = self.convert_file_service_item(u'serviceitem_image_2.osj', 1)
with patch(u'openlp.core.ui.servicemanager.os.path.exists') as mocked_exists:
mocked_exists.return_value = True
service_item2.set_from_service(line2)
service_item.set_from_service(line)
# THEN: We should get back a valid service item
# This test is copied from service_item.py, but is changed since to conform to
# new layout of service item. The layout use in serviceitem_image_2.osd is actually invalid now.
assert service_item.is_valid is True, u'The first service item should be valid'
assert service_item2.is_valid is True, u'The second service item should be valid'
assert service_item.get_rendered_frame(0) == test_file1, u'The first frame should match the path to the image'
assert service_item2.get_rendered_frame(0) == test_file2, u'The Second frame should match the path to the image'
assert service_item.get_frames()[0] == frame_array1, u'The return should match the frame array1'
assert service_item2.get_frames()[0] == frame_array2, u'The return should match the frame array2'
assert service_item.get_frame_path(0) == test_file1, u'The frame path should match the full path to the image'
assert service_item2.get_frame_path(0) == test_file2, u'The frame path should match the full path to the image'
assert service_item.get_frame_title(0) == image_name1, u'The 1st frame title should match the image name'
assert service_item2.get_frame_title(0) == image_name2, u'The 2nd frame title should match the image name'
assert service_item.title.lower() == service_item.name, \
u'The plugin name should match the display title, as there are > 1 Images'
assert service_item.is_image() is True, u'This service item should be of an "image" type'
assert service_item.is_capable(ItemCapabilities.CanMaintain) is True, \
u'This service item should be able to be Maintained'
assert service_item.is_capable(ItemCapabilities.CanPreview) is True, \
u'This service item should be able to be be Previewed'
assert service_item.is_capable(ItemCapabilities.CanLoop) is True, \
u'This service item should be able to be run in a can be made to Loop'
assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \
u'This service item should be able to have new items added to it'
def serviceitem_convert_osd2osj_test(self):
"""
Test the Service Item - load a osd to service_item, convert to json,
load again to service_item and compare the old and new service_item.
"""
# GIVEN: A valid osd (python pickle format) service in file
service_file = os.path.join(TEST_PATH, u'serviceitem_osd2osj.osd')
osd_service_items = []
try:
open_file = open(service_file, u'r')
osd_service_items = cPickle.load(open_file)
except IOError:
osd_service_items = []
finally:
open_file.close()
# WHEN: Dumping loaded osd service to json format, and save to file and reloading to service
json_service_content = json.dumps(osd_service_items)
open_file = None
open_filename = u''
try:
(open_filep, open_filename) = tempfile.mkstemp()
open_file = open(open_filename, u'w')
open_file.write(json_service_content)
open_file.close()
open_file = open(open_filename, u'r')
json_service_content = open_file.read()
except IOError:
json_service_content = u''
finally:
open_file.close()
os.remove(open_filename)
osj_service_items = json.loads(json_service_content)
# THEN: The service loaded from osj (json format) should be the same as the service loaded from the original osd (python pickle format)
# Loop over every item and compare the osj with osd version
for osd_item, osj_item in zip(osd_service_items, osj_service_items):
# Create service item objects
service_item_osd = ServiceItem()
service_item_osd.add_icon = MagicMock()
service_item_osj = ServiceItem()
service_item_osj.add_icon = MagicMock()
with patch(u'openlp.core.ui.servicemanager.os.path.exists') as mocked_exists:
mocked_exists.return_value = True
service_item_osd.set_from_service(osd_item, TEST_PATH)
service_item_osj.set_from_service(osj_item, TEST_PATH)
# Check that the exported/imported attributes are the same
assert service_item_osj.is_valid is True, u'The osj service item should be valid'
assert service_item_osd.is_valid is True, u'The osd service item should be valid'
assert service_item_osj.name == service_item_osd.name , u'The osd and the osj attribute name should be the same!'
assert service_item_osj.theme == service_item_osd.theme , u'The osd and the osj attribute theme should be the same!'
assert service_item_osj.title == service_item_osd.title , u'The osd and the osj attribute title should be the same!'
assert service_item_osj.icon == service_item_osd.icon , u'The osd and the osj attribute icon should be the same!'
assert service_item_osj.raw_footer == service_item_osd.raw_footer , u'The osd and the osj attribute raw_footer should be the same!'
assert service_item_osj.service_item_type == service_item_osd.service_item_type , u'The osd and the osj attribute service_item_type should be the same!'
assert service_item_osj.audit == service_item_osd.audit , u'The osd and the osj attribute audit should be the same!'
assert service_item_osj.notes == service_item_osd.notes , u'The osd and the osj attribute notes should be the same!'
assert service_item_osj.from_plugin == service_item_osd.from_plugin , u'The osd and the osj attribute from_plugin should be the same!'
assert service_item_osj.capabilities == service_item_osd.capabilities , u'The osd and the osj attribute capabilities should be the same!'
assert service_item_osj.search_string == service_item_osd.search_string , u'The osd and the osj attribute search_string should be the same!'
assert service_item_osj.data_string == service_item_osd.data_string , u'The osd and the osj attribute data_string should be the same!'
# Notice that xml_version from osd needs to be utf-8 decoded, since unicode-characters
# is written as byte-codes by pickle, while json can escape unicode-characters
if service_item_osd.xml_version:
assert service_item_osj.xml_version == service_item_osd.xml_version.decode(u'utf-8') , u'The osd and the osj attribute xml_version should be the same!'
assert service_item_osj.auto_play_slides_once == service_item_osd.auto_play_slides_once , u'The osd and the osj attribute auto_play_slides_once should be the same!'
assert service_item_osj.auto_play_slides_loop == service_item_osd.auto_play_slides_loop , u'The osd and the osj attribute auto_play_slides_loop should be the same!'
assert service_item_osj.timed_slide_interval == service_item_osd.timed_slide_interval , u'The osd and the osj attribute timed_slide_interval should be the same!'
assert service_item_osj.start_time == service_item_osd.start_time , u'The osd and the osj attribute start_time should be the same!'
assert service_item_osj.end_time == service_item_osd.end_time , u'The osd and the osj attribute end_time should be the same!'
assert service_item_osj.media_length == service_item_osd.media_length , u'The osd and the osj attribute media_length should be the same!'
assert service_item_osj.background_audio == service_item_osd.background_audio , u'The osd and the osj attribute background_audio should be the same!'
assert service_item_osj.theme_overwritten == service_item_osd.theme_overwritten , u'The osd and the osj attribute theme_overwritten should be the same!'
assert service_item_osj.will_auto_start == service_item_osd.will_auto_start , u'The osd and the osj attribute will_auto_start should be the same!'
assert service_item_osj.processor == service_item_osd.processor , u'The osd and the osj attribute processor should be the same!'
def convert_file_service_item(self, name, row=0):
service_file = os.path.join(TEST_PATH, name)
try:
open_file = open(service_file, u'r')
items = json.load(open_file)
first_line = items[row]
except IOError:
first_line = u''
finally:
open_file.close()
return first_line

View File

@ -7,7 +7,7 @@ from unittest import TestCase
from mock import patch, MagicMock
from PyQt4 import QtGui
from PyQt4 import QtCore, QtGui
from openlp.core.lib import Registry, ServiceItem, Settings
@ -32,6 +32,7 @@ class TestMediaItem(TestCase):
fd, self.ini_file = mkstemp(u'.ini')
Settings().set_filename(self.ini_file)
self.application = QtGui.QApplication.instance()
QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
def tearDown(self):
"""

View File

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
"""
This module contains tests for the WorshipCenter Pro song importer.
"""
from unittest import TestCase
from mock import patch, MagicMock
import pyodbc
from openlp.plugins.songs.lib.worshipcenterproimport import WorshipCenterProImport
class TestRecord(object):
"""
Microsoft Access Driver is not available on non Microsoft Systems for this reason the :class:`TestRecord` is used
to simulate a recordset that would be returned by pyobdc.
"""
def __init__(self, id, field, value):
# The case of the following instance variables is important as it needs to be the same as the ones in use in the
# WorshipCenter Pro database.
self.ID = id
self.Field = field
self.Value = value
class WorshipCenterProImportLogger(WorshipCenterProImport):
"""
This class logs changes in the title instance variable
"""
_title_assignment_list = []
def __init__(self, manager):
WorshipCenterProImport.__init__(self, manager)
@property
def title(self):
return self._title_assignment_list[-1]
@title.setter
def title(self, title):
self._title_assignment_list.append(title)
RECORDSET_TEST_DATA = [TestRecord(1, u'TITLE', u'Amazing Grace'),
TestRecord(1, u'LYRICS',
u'Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;'
u'I once was lost,&crlf;but now am found;&crlf;Was blind, but now I see.&crlf;&crlf;'
u'\'Twas grace that&crlf;taught my heart to fear,&crlf;And grace my fears relieved;&crlf;'
u'How precious did&crlf;that grace appear&crlf;The hour I first believed.&crlf;&crlf;'
u'Through many dangers,&crlf;toils and snares,&crlf;I have already come;&crlf;'
u'\'Tis grace hath brought&crlf;me safe thus far,&crlf;'
u'And grace will lead me home.&crlf;&crlf;The Lord has&crlf;promised good to me,&crlf;'
u'His Word my hope secures;&crlf;He will my Shield&crlf;and Portion be,&crlf;'
u'As long as life endures.&crlf;&crlf;Yea, when this flesh&crlf;and heart shall fail,&crlf;'
u'And mortal life shall cease,&crlf;I shall possess,&crlf;within the veil,&crlf;'
u'A life of joy and peace.&crlf;&crlf;The earth shall soon&crlf;dissolve like snow,&crlf;'
u'The sun forbear to shine;&crlf;But God, Who called&crlf;me here below,&crlf;'
u'Shall be forever mine.&crlf;&crlf;When we\'ve been there&crlf;ten thousand years,&crlf;'
u'Bright shining as the sun,&crlf;We\'ve no less days to&crlf;sing God\'s praise&crlf;'
u'Than when we\'d first begun.&crlf;&crlf;'),
TestRecord(2, u'TITLE', u'Beautiful Garden Of Prayer, The'),
TestRecord(2, u'LYRICS',
u'There\'s a garden where&crlf;Jesus is waiting,&crlf;'
u'There\'s a place that&crlf;is wondrously fair,&crlf;For it glows with the&crlf;'
u'light of His presence.&crlf;\'Tis the beautiful&crlf;garden of prayer.&crlf;&crlf;'
u'Oh, the beautiful garden,&crlf;the garden of prayer!&crlf;Oh, the beautiful&crlf;'
u'garden of prayer!&crlf;There my Savior awaits,&crlf;and He opens the gates&crlf;'
u'To the beautiful&crlf;garden of prayer.&crlf;&crlf;There\'s a garden where&crlf;'
u'Jesus is waiting,&crlf;And I go with my&crlf;burden and care,&crlf;'
u'Just to learn from His&crlf;lips words of comfort&crlf;In the beautiful&crlf;'
u'garden of prayer.&crlf;&crlf;There\'s a garden where&crlf;Jesus is waiting,&crlf;'
u'And He bids you to come,&crlf;meet Him there;&crlf;Just to bow and&crlf;'
u'receive a new blessing&crlf;In the beautiful&crlf;garden of prayer.&crlf;&crlf;')]
SONG_TEST_DATA = [{u'title': u'Amazing Grace',
u'verses': [
(u'Amazing grace! How\nsweet the sound\nThat saved a wretch like me!\nI once was lost,\n'
u'but now am found;\nWas blind, but now I see.'),
(u'\'Twas grace that\ntaught my heart to fear,\nAnd grace my fears relieved;\nHow precious did\n'
u'that grace appear\nThe hour I first believed.'),
(u'Through many dangers,\ntoils and snares,\nI have already come;\n\'Tis grace hath brought\n'
u'me safe thus far,\nAnd grace will lead me home.'),
(u'The Lord has\npromised good to me,\nHis Word my hope secures;\n'
u'He will my Shield\nand Portion be,\nAs long as life endures.'),
(u'Yea, when this flesh\nand heart shall fail,\nAnd mortal life shall cease,\nI shall possess,\n'
u'within the veil,\nA life of joy and peace.'),
(u'The earth shall soon\ndissolve like snow,\nThe sun forbear to shine;\nBut God, Who called\n'
u'me here below,\nShall be forever mine.'),
(u'When we\'ve been there\nten thousand years,\nBright shining as the sun,\n'
u'We\'ve no less days to\nsing God\'s praise\nThan when we\'d first begun.')]},
{u'title': u'Beautiful Garden Of Prayer, The',
u'verses': [
(u'There\'s a garden where\nJesus is waiting,\nThere\'s a place that\nis wondrously fair,\n'
u'For it glows with the\nlight of His presence.\n\'Tis the beautiful\ngarden of prayer.'),
(u'Oh, the beautiful garden,\nthe garden of prayer!\nOh, the beautiful\ngarden of prayer!\n'
u'There my Savior awaits,\nand He opens the gates\nTo the beautiful\ngarden of prayer.'),
(u'There\'s a garden where\nJesus is waiting,\nAnd I go with my\nburden and care,\n'
u'Just to learn from His\nlips words of comfort\nIn the beautiful\ngarden of prayer.'),
(u'There\'s a garden where\nJesus is waiting,\nAnd He bids you to come,\nmeet Him there;\n'
u'Just to bow and\nreceive a new blessing\nIn the beautiful\ngarden of prayer.')]}]
class TestWorshipCenterProSongImport(TestCase):
"""
Test the functions in the :mod:`worshipcenterproimport` module.
"""
def create_importer_test(self):
"""
Test creating an instance of the WorshipCenter Pro file importer
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch(u'openlp.plugins.songs.lib.worshipcenterproimport.SongImport'):
mocked_manager = MagicMock()
# WHEN: An importer object is created
importer = WorshipCenterProImport(mocked_manager)
# THEN: The importer object should not be None
self.assertIsNotNone(importer, u'Import should not be none')
def pyodbc_exception_test(self):
"""
Test that exceptions raised by pyodbc are handled
"""
# GIVEN: A mocked out SongImport class, a mocked out pyodbc module, a mocked out translate method,
# a mocked "manager" and a mocked out logError method.
with patch(u'openlp.plugins.songs.lib.worshipcenterproimport.SongImport'), \
patch(u'openlp.plugins.songs.lib.worshipcenterproimport.pyodbc.connect') as mocked_pyodbc_connect, \
patch(u'openlp.plugins.songs.lib.worshipcenterproimport.translate') as mocked_translate:
mocked_manager = MagicMock()
mocked_log_error = MagicMock()
mocked_translate.return_value = u'Translated Text'
importer = WorshipCenterProImport(mocked_manager)
importer.logError = mocked_log_error
importer.import_source = u'import_source'
pyodbc_errors = [pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError]
mocked_pyodbc_connect.side_effect = pyodbc_errors
# WHEN: Calling the doImport method
for effect in pyodbc_errors:
return_value = importer.doImport()
# THEN: doImport should return None, and pyodbc, translate & logError are called with known calls
self.assertIsNone(return_value, u'doImport should return None when pyodbc raises an exception.')
mocked_pyodbc_connect.assert_called_with( u'DRIVER={Microsoft Access Driver (*.mdb)};DBQ=import_source')
mocked_translate.assert_called_with('SongsPlugin.WorshipCenterProImport',
'Unable to connect the WorshipCenter Pro database.')
mocked_log_error.assert_called_with(u'import_source', u'Translated Text')
def song_import_test(self):
"""
Test that a simulated WorshipCenter Pro recordset is imported correctly
"""
# GIVEN: A mocked out SongImport class, a mocked out pyodbc module with a simulated recordset, a mocked out
# translate method, a mocked "manager", addVerse method & mocked_finish method.
with patch(u'openlp.plugins.songs.lib.worshipcenterproimport.SongImport'), \
patch(u'openlp.plugins.songs.lib.worshipcenterproimport.pyodbc') as mocked_pyodbc, \
patch(u'openlp.plugins.songs.lib.worshipcenterproimport.translate') as mocked_translate:
mocked_manager = MagicMock()
mocked_import_wizard = MagicMock()
mocked_add_verse = MagicMock()
mocked_finish = MagicMock()
mocked_pyodbc.connect().cursor().fetchall.return_value = RECORDSET_TEST_DATA
mocked_translate.return_value = u'Translated Text'
importer = WorshipCenterProImportLogger(mocked_manager)
importer.import_source = u'import_source'
importer.import_wizard = mocked_import_wizard
importer.addVerse = mocked_add_verse
importer.stop_import_flag = False
importer.finish = mocked_finish
# WHEN: Calling the doImport method
return_value = importer.doImport()
# THEN: doImport should return None, and pyodbc, import_wizard, importer.title and addVerse are called with
# known calls
self.assertIsNone(return_value, u'doImport should return None when pyodbc raises an exception.')
mocked_pyodbc.connect.assert_called_with(u'DRIVER={Microsoft Access Driver (*.mdb)};DBQ=import_source')
mocked_pyodbc.connect().cursor.assert_any_call()
mocked_pyodbc.connect().cursor().execute.assert_called_with(u'SELECT ID, Field, Value FROM __SONGDATA')
mocked_pyodbc.connect().cursor().fetchall.assert_any_call()
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(2)
add_verse_call_count = 0
for song_data in SONG_TEST_DATA:
title_value = song_data[u'title']
self.assertIn(title_value, importer._title_assignment_list,
u'title should have been set to %s' % title_value)
verse_calls = song_data[u'verses']
add_verse_call_count += len(verse_calls)
for call in verse_calls:
mocked_add_verse.assert_any_call(call)
self.assertEqual(mocked_add_verse.call_count, add_verse_call_count,
u'Incorrect number of calls made to addVerse')

View File

@ -0,0 +1 @@
[{"serviceitem": {"header": {"xml_version": null, "auto_play_slides_loop": false, "auto_play_slides_once": false, "will_auto_start": false, "title": "Test Custom", "capabilities": [2, 1, 5, 13, 8], "theme": null, "background_audio": [], "icon": ":/plugins/plugin_custom.png", "type": 1, "start_time": 0, "from_plugin": false, "media_length": 0, "data": "", "timed_slide_interval": 0, "audit": "", "search": "", "name": "custom", "footer": ["Test Custom Credits"], "notes": "", "plugin": "custom", "theme_overwritten": false, "end_time": 0, "processor": null}, "data": [{"verseTag": null, "raw_slide": "Slide 1", "title": "Slide 1"}, {"verseTag": null, "raw_slide": "Slide 2", "title": "Slide 2"}]}}]

View File

@ -0,0 +1 @@
[{"serviceitem": {"header": {"xml_version": null, "auto_play_slides_loop": false, "auto_play_slides_once": false, "will_auto_start": false, "title": "Images", "capabilities": [3, 1, 5, 6], "theme": -1, "background_audio": [], "icon": ":/plugins/plugin_images.png", "type": 2, "start_time": 0, "from_plugin": false, "media_length": 0, "data": "", "timed_slide_interval": 0, "audit": "", "search": "", "name": "images", "footer": [], "notes": "", "plugin": "images", "theme_overwritten": false, "end_time": 0, "processor": null}, "data": ["image_1.jpg"]}}]

View File

@ -0,0 +1 @@
[{"serviceitem": {"header": {"xml_version": null, "auto_play_slides_loop": false, "auto_play_slides_once": false, "will_auto_start": false, "title": "Images", "capabilities": [3, 1, 5, 6], "theme": -1, "background_audio": [], "icon": ":/plugins/plugin_images.png", "type": 2, "start_time": 0, "from_plugin": false, "media_length": 0, "data": "", "timed_slide_interval": 0, "audit": "", "search": "", "name": "images", "footer": [], "notes": "", "plugin": "images", "theme_overwritten": false, "end_time": 0, "processor": null}, "data": [{"path": "/home/openlp/image_1.jpg", "title": "image_1.jpg"}]}}, {"serviceitem": {"header": {"xml_version": null, "auto_play_slides_loop": false, "auto_play_slides_once": false, "will_auto_start": false, "title": "Images", "capabilities": [3, 1, 5, 6], "theme": -1, "background_audio": [], "icon": ":/plugins/plugin_images.png", "type": 2, "start_time": 0, "from_plugin": false, "media_length": 0, "data": "", "timed_slide_interval": 0, "audit": "", "search": "", "name": "images", "footer": [], "notes": "", "plugin": "images", "theme_overwritten": false, "end_time": 0, "processor": null}, "data": [{"path": "/home/openlp/image_2.jpg", "title": "image_2.jpg"}]}}]

File diff suppressed because it is too large Load Diff