Port some bugfixes from 2.4 to trunk

This commit is contained in:
Raoul Snyman 2017-03-09 22:42:38 -07:00
parent e5abad85a7
commit 324652c347
9 changed files with 132 additions and 29 deletions

View File

@ -319,7 +319,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
return QtWidgets.QApplication.event(self, event) return QtWidgets.QApplication.event(self, event)
def parse_options(args): def parse_options(args=None):
""" """
Parse the command line arguments Parse the command line arguments

View File

@ -221,11 +221,17 @@ class OpenLPSongImport(SongImport):
if not existing_book: if not existing_book:
existing_book = Book.populate(name=entry.songbook.name, publisher=entry.songbook.publisher) existing_book = Book.populate(name=entry.songbook.name, publisher=entry.songbook.publisher)
new_song.add_songbook_entry(existing_book, entry.entry) new_song.add_songbook_entry(existing_book, entry.entry)
elif song.book: elif hasattr(song, 'book') and song.book:
existing_book = self.manager.get_object_filtered(Book, Book.name == song.book.name) existing_book = self.manager.get_object_filtered(Book, Book.name == song.book.name)
if not existing_book: if not existing_book:
existing_book = Book.populate(name=song.book.name, publisher=song.book.publisher) existing_book = Book.populate(name=song.book.name, publisher=song.book.publisher)
new_song.add_songbook_entry(existing_book, '') # Get the song_number from "songs" table "song_number" field. (This is db structure from 2.2.1)
# If there's a number, add it to the song, otherwise it will be "".
existing_number = song.song_number if hasattr(song, 'song_number') else ''
if existing_number:
new_song.add_songbook_entry(existing_book, existing_number)
else:
new_song.add_songbook_entry(existing_book, '')
# Find or create all the media files and add them to the new song object # Find or create all the media files and add them to the new song object
if has_media_files and song.media_files: if has_media_files and song.media_files:
for media_file in song.media_files: for media_file in song.media_files:

View File

@ -23,7 +23,7 @@
The :mod:`presentationmanager` module provides the functionality for importing The :mod:`presentationmanager` module provides the functionality for importing
Presentationmanager song files into the current database. Presentationmanager song files into the current database.
""" """
import logging
import os import os
import re import re
@ -34,6 +34,8 @@ from openlp.core.common import translate
from openlp.core.ui.lib.wizard import WizardStrings from openlp.core.ui.lib.wizard import WizardStrings
from .songimport import SongImport from .songimport import SongImport
log = logging.getLogger(__name__)
class PresentationManagerImport(SongImport): class PresentationManagerImport(SongImport):
""" """
@ -65,15 +67,36 @@ class PresentationManagerImport(SongImport):
'File is not in XML-format, which is the only format supported.')) 'File is not in XML-format, which is the only format supported.'))
continue continue
root = objectify.fromstring(etree.tostring(tree)) root = objectify.fromstring(etree.tostring(tree))
self.process_song(root) self.process_song(root, file_path)
def process_song(self, root): def _get_attr(self, elem, name):
"""
Due to PresentationManager's habit of sometimes capitilising the first letter of an element, we have to do
some gymnastics.
"""
if hasattr(elem, name):
log.debug('%s: %s', name, getattr(elem, name))
return str(getattr(elem, name))
name = name[0].upper() + name[1:]
if hasattr(elem, name):
log.debug('%s: %s', name, getattr(elem, name))
return str(getattr(elem, name))
else:
return ''
def process_song(self, root, file_path):
self.set_defaults() self.set_defaults()
self.title = str(root.attributes.title) attrs = None
self.add_author(str(root.attributes.author)) if hasattr(root, 'attributes'):
self.copyright = str(root.attributes.copyright) attrs = root.attributes
self.ccli_number = str(root.attributes.ccli_number) elif hasattr(root, 'Attributes'):
self.comments = str(root.attributes.comments) attrs = root.Attributes
if attrs is not None:
self.title = self._get_attr(root.attributes, 'title')
self.add_author(self._get_attr(root.attributes, 'author'))
self.copyright = self._get_attr(root.attributes, 'copyright')
self.ccli_number = self._get_attr(root.attributes, 'ccli_number')
self.comments = str(root.attributes.comments) if hasattr(root.attributes, 'comments') else None
verse_order_list = [] verse_order_list = []
verse_count = {} verse_count = {}
duplicates = [] duplicates = []
@ -105,4 +128,4 @@ class PresentationManagerImport(SongImport):
self.verse_order_list = verse_order_list self.verse_order_list = verse_order_list
if not self.finish(): if not self.finish():
self.log_error(self.import_source) self.log_error(os.path.basename(file_path))

View File

@ -47,7 +47,7 @@ USER_AGENTS = [
BASE_URL = 'https://songselect.ccli.com' BASE_URL = 'https://songselect.ccli.com'
LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\ LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\
'https%3a%2f%2fsongselect.ccli.com%2f' 'https%3a%2f%2fsongselect.ccli.com%2f'
LOGIN_URL = 'https://profile.ccli.com/' LOGIN_URL = 'https://profile.ccli.com'
LOGOUT_URL = BASE_URL + '/account/logout' LOGOUT_URL = BASE_URL + '/account/logout'
SEARCH_URL = BASE_URL + '/search/results' SEARCH_URL = BASE_URL + '/search/results'
@ -97,14 +97,27 @@ class SongSelectImport(object):
'password': password, 'password': password,
'RememberMe': 'false' 'RememberMe': 'false'
}) })
login_form = login_page.find('form')
if login_form:
login_url = login_form.attrs['action']
else:
login_url = '/Account/SignIn'
if not login_url.startswith('http'):
if login_url[0] != '/':
login_url = '/' + login_url
login_url = LOGIN_URL + login_url
try: try:
posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml') posted_page = BeautifulSoup(self.opener.open(login_url, data.encode('utf-8')).read(), 'lxml')
except (TypeError, URLError) as error: except (TypeError, URLError) as error:
log.exception('Could not login to SongSelect, {error}'.format(error=error)) log.exception('Could not login to SongSelect, {error}'.format(error=error))
return False return False
if callback: if callback:
callback() callback()
return posted_page.find('input', id='SearchText') is not None if posted_page.find('input', id='SearchText') is not None:
return True
else:
log.debug(posted_page)
return False
def logout(self): def logout(self):
""" """

View File

@ -22,12 +22,12 @@
import sys import sys
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch
from openlp.core import parse_options from openlp.core import parse_options
from tests.helpers.testmixin import TestMixin
class TestInitFunctions(TestMixin, TestCase): class TestInitFunctions(TestCase):
def test_parse_options_basic(self): def test_parse_options_basic(self):
""" """
@ -116,8 +116,7 @@ class TestInitFunctions(TestMixin, TestCase):
def test_parse_options_file_and_debug(self): def test_parse_options_file_and_debug(self):
""" """
Test the parse options process works with a file Test the parse options process works with a file and the debug log level
""" """
# GIVEN: a a set of system arguments. # GIVEN: a a set of system arguments.
sys.argv[1:] = ['-l debug', 'dummy_temp'] sys.argv[1:] = ['-l debug', 'dummy_temp']
@ -130,3 +129,29 @@ class TestInitFunctions(TestMixin, TestCase):
self.assertFalse(args.portable, 'The portable flag should be set to false') self.assertFalse(args.portable, 'The portable flag should be set to false')
self.assertEquals(args.style, None, 'There are no style flags to be processed') self.assertEquals(args.style, None, 'There are no style flags to be processed')
self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank') self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
class TestOpenLP(TestCase):
"""
Test the OpenLP app class
"""
@patch('openlp.core.QtWidgets.QApplication.exec')
def test_exec(self, mocked_exec):
"""
Test the exec method
"""
# GIVEN: An app
app = OpenLP([])
app.shared_memory = MagicMock()
mocked_exec.return_value = False
# WHEN: exec() is called
result = app.exec()
# THEN: The right things should be called
assert app.is_event_loop_active is True
mocked_exec.assert_called_once_with()
app.shared_memory.detach.assert_called_once_with()
assert result is False
del app

View File

@ -25,11 +25,11 @@ This module contains tests for the lib submodule of the Remotes plugin.
import os import os
import urllib.request import urllib.request
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch, mock_open
from openlp.core.common import Settings, Registry from openlp.core.common import Settings, Registry
from openlp.core.ui import ServiceManager from openlp.core.ui import ServiceManager
from openlp.plugins.remotes.lib.httpserver import HttpRouter from openlp.plugins.remotes.lib.httpserver import HttpRouter
from tests.functional import MagicMock, patch, mock_open
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
__default_settings__ = { __default_settings__ = {
@ -336,11 +336,13 @@ class TestRouter(TestCase, TestMixin):
with patch.object(self.service_manager, 'setup_ui'), \ with patch.object(self.service_manager, 'setup_ui'), \
patch.object(self.router, 'do_json_header'): patch.object(self.router, 'do_json_header'):
self.service_manager.bootstrap_initialise() self.service_manager.bootstrap_initialise()
self.app.processEvents() # Not sure why this is here, it doesn't make sense in the test and causes them to hang
# self.app.processEvents()
# WHEN: Remote next is received # WHEN: Remote next is received
self.router.service(action='next') self.router.service(action='next')
self.app.processEvents() # Not sure why this is here, it doesn't make sense in the test and causes them to hang
# self.app.processEvents()
# THEN: service_manager.next_item() should have been called # THEN: service_manager.next_item() should have been called
self.assertTrue(mocked_next_item.called, 'next_item() should have been called in service_manager') self.assertTrue(mocked_next_item.called, 'next_item() should have been called in service_manager')
@ -357,11 +359,13 @@ class TestRouter(TestCase, TestMixin):
with patch.object(self.service_manager, 'setup_ui'), \ with patch.object(self.service_manager, 'setup_ui'), \
patch.object(self.router, 'do_json_header'): patch.object(self.router, 'do_json_header'):
self.service_manager.bootstrap_initialise() self.service_manager.bootstrap_initialise()
self.app.processEvents() # Not sure why this is here, it doesn't make sense in the test and causes them to hang
# self.app.processEvents()
# WHEN: Remote next is received # WHEN: Remote next is received
self.router.service(action='previous') self.router.service(action='previous')
self.app.processEvents() # Not sure why this is here, it doesn't make sense in the test and causes them to hang
# self.app.processEvents()
# THEN: service_manager.next_item() should have been called # THEN: service_manager.next_item() should have been called
self.assertTrue(mocked_previous_item.called, 'previous_item() should have been called in service_manager') self.assertTrue(mocked_previous_item.called, 'previous_item() should have been called in service_manager')

View File

@ -25,6 +25,7 @@ This module contains tests for the CCLI SongSelect importer.
""" """
import os import os
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch, call
from urllib.error import URLError from urllib.error import URLError
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
@ -32,9 +33,8 @@ from PyQt5 import QtWidgets
from openlp.core import Registry from openlp.core import Registry
from openlp.plugins.songs.forms.songselectform import SongSelectForm, SearchWorker from openlp.plugins.songs.forms.songselectform import SongSelectForm, SearchWorker
from openlp.plugins.songs.lib import Song from openlp.plugins.songs.lib import Song
from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGOUT_URL, BASE_URL from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGIN_PAGE, LOGOUT_URL, BASE_URL
from tests.functional import MagicMock, patch, call
from tests.helpers.songfileimport import SongImportTestHelper from tests.helpers.songfileimport import SongImportTestHelper
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -112,8 +112,10 @@ class TestSongSelectImport(TestCase, TestMixin):
mocked_opener = MagicMock() mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener mocked_build_opener.return_value = mocked_opener
mocked_login_page = MagicMock() mocked_login_page = MagicMock()
mocked_login_page.find.side_effect = [{'value': 'blah'}, MagicMock()] mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
MockedBeautifulSoup.return_value = mocked_login_page mocked_posted_page = MagicMock()
mocked_posted_page.find.return_value = MagicMock()
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page]
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
@ -122,10 +124,40 @@ class TestSongSelectImport(TestCase, TestMixin):
# THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned # THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned
self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times') self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times')
self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice') self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice on the login page')
self.assertEqual(1, mocked_posted_page.find.call_count, 'find should have been called once on the posted page')
self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice') self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice')
self.assertTrue(result, 'The login method should have returned True') self.assertTrue(result, 'The login method should have returned True')
@patch('openlp.plugins.songs.lib.songselect.build_opener')
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
def login_url_from_form_test(self, MockedBeautifulSoup, mocked_build_opener):
"""
Test that the login URL is from the form
"""
# GIVEN: A bunch of mocked out stuff and an importer object
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_form = MagicMock()
mocked_form.attrs = {'action': 'do/login'}
mocked_login_page = MagicMock()
mocked_login_page.find.side_effect = [{'value': 'blah'}, mocked_form]
mocked_posted_page = MagicMock()
mocked_posted_page.find.return_value = MagicMock()
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page]
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
result = importer.login('username', 'password', mock_callback)
# THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned
self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times')
self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice on the login page')
self.assertEqual(1, mocked_posted_page.find.call_count, 'find should have been called once on the posted page')
self.assertEqual('https://profile.ccli.com/do/login', mocked_opener.open.call_args_list[1][0][0])
self.assertTrue(result, 'The login method should have returned True')
@patch('openlp.plugins.songs.lib.songselect.build_opener') @patch('openlp.plugins.songs.lib.songselect.build_opener')
def test_logout(self, mocked_build_opener): def test_logout(self, mocked_build_opener):
""" """