Fix SongSelect so that it detects the login URL; Fix PresentationManager importer to handle weird XML; Tests

This commit is contained in:
Raoul Snyman 2017-02-25 15:18:54 -07:00
parent 561f272e86
commit c000ba4b4f
4 changed files with 92 additions and 20 deletions

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,37 @@ 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 +126,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

@ -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

@ -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>