forked from openlp/openlp
Fix SongSelect so that it detects the login URL; Fix PresentationManager importer to handle weird XML; Tests
This commit is contained in:
parent
561f272e86
commit
c000ba4b4f
@ -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))
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user