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
Presentationmanager song files into the current database.
"""
import logging
import os
import re
import chardet
@ -32,6 +32,8 @@ from lxml import objectify, etree
from openlp.core.ui.wizard import WizardStrings
from .songimport import SongImport
log = logging.getLogger(__name__)
class PresentationManagerImport(SongImport):
"""
@ -62,15 +64,37 @@ class PresentationManagerImport(SongImport):
'File is not in XML-format, which is the only format supported.'))
continue
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.title = str(root.attributes.title)
self.add_author(str(root.attributes.author))
self.copyright = str(root.attributes.copyright)
self.ccli_number = str(root.attributes.ccli_number)
self.comments = str(root.attributes.comments)
attrs = None
if hasattr(root, 'attributes'):
attrs = root.attributes
elif hasattr(root, 'Attributes'):
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_count = {}
duplicates = []
@ -102,4 +126,4 @@ class PresentationManagerImport(SongImport):
self.verse_order_list = verse_order_list
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'
LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl=' \
'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'
SEARCH_URL = BASE_URL + '/search/results'
@ -97,14 +97,27 @@ class SongSelectImport(object):
'password': password,
'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:
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:
log.exception('Could not login to SongSelect, %s', error)
return False
if 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):
"""

View File

@ -32,7 +32,7 @@ from PyQt5 import QtWidgets
from openlp.core import Registry
from openlp.plugins.songs.forms.songselectform import SongSelectForm, SearchWorker
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
@ -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
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.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_login_page = 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 = None
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page]
mock_callback = MagicMock()
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
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.assertFalse(result, 'The login method should have returned False')
@ -136,8 +139,10 @@ class TestSongSelectImport(TestCase, TestMixin):
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_login_page = MagicMock()
mocked_login_page.find.side_effect = [{'value': 'blah'}, MagicMock()]
MockedBeautifulSoup.return_value = mocked_login_page
mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
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)
@ -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
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.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')
def logout_test(self, mocked_build_opener):
"""

View File

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