- Fix SongSelect so that it detects the login URL

- Fix PresentationManager importer to handle weird XML
- Pull in OpenLP song importer fixes from Olli's branch

bzr-revno: 2675
This commit is contained in:
Raoul Snyman 2017-03-02 14:54:45 -07:00
commit 1ec27e8b05
12 changed files with 143 additions and 48 deletions

View File

@ -317,7 +317,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
import chardet import chardet
@ -32,6 +32,8 @@ from lxml import objectify, etree
from openlp.core.ui.wizard import WizardStrings from openlp.core.ui.wizard import WizardStrings
from .songimport import SongImport from .songimport import SongImport
log = logging.getLogger(__name__)
class PresentationManagerImport(SongImport): class PresentationManagerImport(SongImport):
""" """
@ -62,15 +64,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 = []
@ -102,4 +125,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, %s', error) log.exception('Could not login to SongSelect, %s', 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

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 OpenLP, parse_options
from tests.helpers.testmixin import TestMixin
class TestInitFunctions(TestMixin, TestCase): class TestInitFunctions(TestCase):
def parse_options_basic_test(self): def parse_options_basic_test(self):
""" """
@ -116,7 +116,7 @@ class TestInitFunctions(TestMixin, TestCase):
def parse_options_file_and_debug_test(self): def parse_options_file_and_debug_test(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.
@ -131,14 +131,28 @@ class TestInitFunctions(TestMixin, TestCase):
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')
def parse_options_two_files_test(self):
"""
Test the parse options process works with a file
class TestOpenLP(TestCase):
"""
Test the OpenLP app class
"""
@patch('openlp.core.QtWidgets.QApplication.exec')
def test_exec(self, mocked_exec):
""" """
# GIVEN: a a set of system arguments. Test the exec method
sys.argv[1:] = ['dummy_temp', 'dummy_temp2'] """
# WHEN: We we parse them to expand to options # GIVEN: An app
args = parse_options() app = OpenLP([])
# THEN: the following fields will have been extracted. app.shared_memory = MagicMock()
self.assertEquals(args, None, 'The args should be None') 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__ = {
@ -313,11 +313,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
# 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
# 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')
@ -334,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
# 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
# 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

@ -32,7 +32,7 @@ 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.functional import MagicMock, patch, call
from tests.helpers.songfileimport import SongImportTestHelper from tests.helpers.songfileimport import SongImportTestHelper
@ -81,7 +81,7 @@ class TestSongSelectImport(TestCase, TestMixin):
# THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned # THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned
self.assertEqual(2, mock_callback.call_count, 'callback should have been called 3 times') self.assertEqual(2, mock_callback.call_count, 'callback should have been called 3 times')
self.assertEqual(1, 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')
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.assertFalse(result, 'The login method should have returned False') self.assertFalse(result, 'The login method should have returned False')
@ -96,7 +96,9 @@ class TestSongSelectImport(TestCase, TestMixin):
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'}, None] 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 = None
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page]
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
@ -105,7 +107,8 @@ class TestSongSelectImport(TestCase, TestMixin):
# THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned # THEN: callback was called 3 times, open was called twice, find was called twice, and False 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.assertFalse(result, 'The login method should have returned False') self.assertFalse(result, 'The login method should have returned False')
@ -136,8 +139,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)
@ -146,10 +151,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 logout_test(self, mocked_build_opener): def logout_test(self, mocked_build_opener):
""" """

View File

@ -22,7 +22,7 @@
""" """
Package to test the openlp.plugins.bibles.forms.bibleimportform package. Package to test the openlp.plugins.bibles.forms.bibleimportform package.
""" """
from unittest import TestCase from unittest import TestCase, skip
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
@ -48,12 +48,12 @@ class TestBibleImportForm(TestCase, TestMixin):
Registry().register('main_window', self.main_window) Registry().register('main_window', self.main_window)
self.form = BibleImportForm(self.main_window, MagicMock(), MagicMock()) self.form = BibleImportForm(self.main_window, MagicMock(), MagicMock())
def tearDown(self): # def tearDown(self):
""" # """
Delete all the C++ objects at the end so that we don't have a segfault # Delete all the C++ objects at the end so that we don't have a segfault
""" # """
del self.form # del self.form
del self.main_window # del self.main_window
@patch('openlp.plugins.bibles.forms.bibleimportform.CWExtract.get_bibles_from_http') @patch('openlp.plugins.bibles.forms.bibleimportform.CWExtract.get_bibles_from_http')
@patch('openlp.plugins.bibles.forms.bibleimportform.BGExtract.get_bibles_from_http') @patch('openlp.plugins.bibles.forms.bibleimportform.BGExtract.get_bibles_from_http')

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<song xmlns="creativelifestyles/song"> <song xmlns="creativelifestyles/song">
<attributes> <attributes>
<title>Agnus Dei</title> <Title>Agnus Dei</Title>
<author></author> <author></author>
<copyright></copyright> <copyright></copyright>
<ccli_number></ccli_number> <ccli_number></ccli_number>