forked from openlp/openlp
Refactor SongSelect search into a separate class, and start writing some tests for that class.
This commit is contained in:
parent
e2fcd3e8cb
commit
d2a5e8a4c4
@ -31,30 +31,15 @@ The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI fo
|
||||
"""
|
||||
|
||||
import logging
|
||||
from http.cookiejar import CookieJar
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import HTTPCookieProcessor, HTTPError, build_opener
|
||||
from html.parser import HTMLParser
|
||||
from time import sleep
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from bs4 import BeautifulSoup, NavigableString
|
||||
from openlp.core import Settings
|
||||
|
||||
from openlp.core import Settings
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib import translate
|
||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||
from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
|
||||
from openlp.plugins.songs.lib.db import Author, Song
|
||||
from openlp.plugins.songs.lib.xml import SongXML
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \
|
||||
'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \
|
||||
'Mobile Safari/534.30'
|
||||
BASE_URL = 'https://mobile.songselect.com'
|
||||
LOGIN_URL = BASE_URL + '/account/login'
|
||||
LOGOUT_URL = BASE_URL + '/account/logout'
|
||||
SEARCH_URL = BASE_URL + '/search/results'
|
||||
from openlp.plugins.songs.lib.songselect import SongSelectImport
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -65,53 +50,36 @@ class SearchWorker(QtCore.QObject):
|
||||
"""
|
||||
show_info = QtCore.pyqtSignal(str, str)
|
||||
found_song = QtCore.pyqtSignal(dict)
|
||||
finished = QtCore.pyqtSignal(list)
|
||||
finished = QtCore.pyqtSignal()
|
||||
quit = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, opener, params):
|
||||
def __init__(self, importer, search_text):
|
||||
super().__init__()
|
||||
self.opener = opener
|
||||
self.params = params
|
||||
self.html_parser = HTMLParser()
|
||||
|
||||
def _search_and_parse_results(self, params):
|
||||
params = urlencode(params)
|
||||
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + params).read(), 'lxml')
|
||||
search_results = results_page.find_all('li', 'result pane')
|
||||
songs = []
|
||||
for result in search_results:
|
||||
song = {
|
||||
'title': self.html_parser.unescape(result.find('h3').string),
|
||||
'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')],
|
||||
'link': BASE_URL + result.find('a')['href']
|
||||
}
|
||||
self.found_song.emit(song)
|
||||
songs.append(song)
|
||||
return songs
|
||||
self.importer = importer
|
||||
self.search_text = search_text
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Run a search and then parse the results page of the search.
|
||||
"""
|
||||
songs = self._search_and_parse_results(self.params)
|
||||
search_results = []
|
||||
self.params['page'] = 1
|
||||
total = 0
|
||||
while songs:
|
||||
search_results.extend(songs)
|
||||
self.params['page'] += 1
|
||||
total += len(songs)
|
||||
if total >= 1000:
|
||||
self.show_info.emit(
|
||||
translate('SongsPlugin.SongSelectForm', 'More than 1000 results'),
|
||||
translate('SongsPlugin.SongSelectForm', 'Your search has returned more than 1000 results, it has '
|
||||
'been stopped. Please refine your search to fetch better '
|
||||
'results.'))
|
||||
break
|
||||
songs = self._search_and_parse_results(self.params)
|
||||
self.finished.emit(search_results)
|
||||
songs = self.importer.search(self.search_text, 1000, self._found_song_callback)
|
||||
if len(songs) >= 1000:
|
||||
self.show_info.emit(
|
||||
translate('SongsPlugin.SongSelectForm', 'More than 1000 results'),
|
||||
translate('SongsPlugin.SongSelectForm', 'Your search has returned more than 1000 results, it has '
|
||||
'been stopped. Please refine your search to fetch better '
|
||||
'results.'))
|
||||
self.finished.emit()
|
||||
self.quit.emit()
|
||||
|
||||
def _found_song_callback(self, song):
|
||||
"""
|
||||
A callback used by the paginate function to notify watching processes when it finds a song.
|
||||
|
||||
:param song: The song that was found
|
||||
"""
|
||||
self.found_song.emit(song)
|
||||
|
||||
|
||||
class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
"""
|
||||
@ -126,10 +94,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
self.song_count = 0
|
||||
self.song = None
|
||||
self.plugin = plugin
|
||||
self.db_manager = db_manager
|
||||
self.html_parser = HTMLParser()
|
||||
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
|
||||
self.opener.addheaders = [('User-Agent', USER_AGENT)]
|
||||
self.song_select_importer = SongSelectImport(db_manager)
|
||||
self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled)
|
||||
self.login_button.clicked.connect(self.on_login_button_clicked)
|
||||
self.search_button.clicked.connect(self.on_search_button_clicked)
|
||||
@ -182,7 +147,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
self.main_window.application.process_events()
|
||||
sleep(0.5)
|
||||
self.main_window.application.process_events()
|
||||
self.opener.open(LOGOUT_URL)
|
||||
self.song_select_importer.logout()
|
||||
self.main_window.application.process_events()
|
||||
progress_dialog.setValue(2)
|
||||
return QtGui.QDialog.done(self, r)
|
||||
@ -194,6 +159,14 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
|
||||
main_window = property(_get_main_window)
|
||||
|
||||
def _update_login_progress(self):
|
||||
self.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
|
||||
self.main_window.application.process_events()
|
||||
|
||||
def _update_song_progress(self):
|
||||
self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
|
||||
self.main_window.application.process_events()
|
||||
|
||||
def _view_song(self, current_item):
|
||||
if not current_item:
|
||||
return
|
||||
@ -217,40 +190,17 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
song = {}
|
||||
for key, value in current_item.items():
|
||||
song[key] = value
|
||||
self.song_progress_bar.setValue(1)
|
||||
self.song_progress_bar.setValue(0)
|
||||
self.main_window.application.process_events()
|
||||
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
|
||||
self.song_progress_bar.setValue(2)
|
||||
self.main_window.application.process_events()
|
||||
try:
|
||||
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
|
||||
except HTTPError:
|
||||
lyrics_page = None
|
||||
self.song_progress_bar.setValue(3)
|
||||
self.main_window.application.process_events()
|
||||
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
|
||||
song['copyright'] = self.html_parser.unescape(song['copyright'])
|
||||
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
|
||||
song['verses'] = []
|
||||
if lyrics_page:
|
||||
verses = lyrics_page.find('section', 'lyrics').find_all('p')
|
||||
verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
|
||||
for counter in range(len(verses)):
|
||||
verse = {'label': verse_labels[counter].string, 'lyrics': ''}
|
||||
for v in verses[counter].contents:
|
||||
if isinstance(v, NavigableString):
|
||||
verse['lyrics'] = verse['lyrics'] + v.string
|
||||
else:
|
||||
verse['lyrics'] += '\n'
|
||||
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
|
||||
song['verses'].append(self.html_parser.unescape(verse))
|
||||
# Get the full song
|
||||
self.song_select_importer.get_song(song, self._update_song_progress)
|
||||
# Update the UI
|
||||
self.title_edit.setText(song['title'])
|
||||
self.copyright_edit.setText(song['copyright'])
|
||||
self.ccli_edit.setText(song['ccli_number'])
|
||||
for author in song['authors']:
|
||||
QtGui.QListWidgetItem(self.html_parser.unescape(author), self.author_list_widget)
|
||||
QtGui.QListWidgetItem(author, self.author_list_widget)
|
||||
for counter, verse in enumerate(song['verses']):
|
||||
log.debug('Verse type: %s', verse['label'])
|
||||
self.lyrics_table_widget.setRowCount(self.lyrics_table_widget.rowCount() + 1)
|
||||
item = QtGui.QTableWidgetItem(verse['lyrics'])
|
||||
item.setData(QtCore.Qt.UserRole, verse['label'])
|
||||
@ -296,23 +246,12 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
self.save_password_checkbox.setEnabled(False)
|
||||
self.login_button.setEnabled(False)
|
||||
self.login_spacer.setVisible(False)
|
||||
self.login_progress_bar.setValue(0)
|
||||
self.login_progress_bar.setVisible(True)
|
||||
self.login_progress_bar.setValue(1)
|
||||
self.main_window.application.process_events()
|
||||
login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
|
||||
self.login_progress_bar.setValue(2)
|
||||
self.main_window.application.process_events()
|
||||
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
|
||||
data = urlencode({
|
||||
'__RequestVerificationToken': token_input['value'],
|
||||
'UserName': self.username_edit.text(),
|
||||
'Password': self.password_edit.text(),
|
||||
'RememberMe': 'false'
|
||||
})
|
||||
posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
|
||||
self.login_progress_bar.setValue(3)
|
||||
self.main_window.application.process_events()
|
||||
if posted_page.find('input', attrs={'name': '__RequestVerificationToken'}):
|
||||
# Log the user in
|
||||
if not self.song_select_importer.login(
|
||||
self.username_edit.text(), self.password_edit.text(), self._update_login_progress):
|
||||
QtGui.QMessageBox.critical(
|
||||
self,
|
||||
translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
|
||||
@ -340,10 +279,10 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
"""
|
||||
self.view_button.setEnabled(False)
|
||||
self.search_button.setEnabled(False)
|
||||
self.search_progress_bar.setVisible(True)
|
||||
self.search_progress_bar.setMinimum(0)
|
||||
self.search_progress_bar.setMaximum(0)
|
||||
self.search_progress_bar.setValue(0)
|
||||
self.search_progress_bar.setVisible(True)
|
||||
self.search_results_widget.clear()
|
||||
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
|
||||
self.main_window.application.process_events()
|
||||
@ -353,8 +292,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
|
||||
# Create thread and run search
|
||||
self.thread = QtCore.QThread()
|
||||
self.worker = SearchWorker(self.opener, {'SearchTerm': self.search_combobox.currentText(),
|
||||
'allowredirect': 'false'})
|
||||
self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
|
||||
self.worker.moveToThread(self.thread)
|
||||
self.thread.started.connect(self.worker.start)
|
||||
self.worker.show_info.connect(self.on_search_show_info)
|
||||
@ -378,16 +316,16 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
Add a song to the list when one is found.
|
||||
:param song:
|
||||
"""
|
||||
log.debug('SongSelect (title = "%s"), (link = "%s")', song['title'], song['link'])
|
||||
self.song_count += 1
|
||||
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
|
||||
item_title = song['title'] + ' (' + ', '.join(song['authors']) + ')'
|
||||
song_item = QtGui.QListWidgetItem(item_title, self.search_results_widget)
|
||||
song_item.setData(QtCore.Qt.UserRole, song)
|
||||
|
||||
def on_search_finished(self, songs):
|
||||
def on_search_finished(self):
|
||||
"""
|
||||
Slot which is called when the search is completed.
|
||||
|
||||
:param songs:
|
||||
"""
|
||||
self.main_window.application.process_events()
|
||||
@ -410,6 +348,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
def on_search_results_widget_double_clicked(self, current_item):
|
||||
"""
|
||||
View a song from SongSelect
|
||||
|
||||
:param current_item:
|
||||
"""
|
||||
self._view_song(current_item)
|
||||
@ -425,39 +364,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
|
||||
"""
|
||||
Import a song from SongSelect.
|
||||
"""
|
||||
song = Song.populate(
|
||||
title=self.song['title'],
|
||||
copyright=self.song['copyright'],
|
||||
ccli_number=self.song['ccli_number']
|
||||
)
|
||||
song_xml = SongXML()
|
||||
verse_order = []
|
||||
for verse in self.song['verses']:
|
||||
verse_type, verse_number = verse['label'].split(' ')[:2]
|
||||
verse_type = VerseType.from_loose_input(verse_type)
|
||||
verse_number = int(verse_number)
|
||||
song_xml.add_verse_to_lyrics(
|
||||
VerseType.tags[verse_type],
|
||||
verse_number,
|
||||
verse['lyrics']
|
||||
)
|
||||
verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number))
|
||||
song.verse_order = ' '.join(verse_order)
|
||||
song.lyrics = song_xml.extract_xml()
|
||||
clean_song(self.db_manager, song)
|
||||
self.db_manager.save_object(song)
|
||||
song.authors = []
|
||||
for author_name in self.song['authors']:
|
||||
#author_name = unicode(author_name)
|
||||
author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name)
|
||||
if not author:
|
||||
author = Author.populate(
|
||||
first_name=author_name.rsplit(' ', 1)[0],
|
||||
last_name=author_name.rsplit(' ', 1)[1],
|
||||
display_name=author_name
|
||||
)
|
||||
song.authors.append(author)
|
||||
self.db_manager.save_object(song)
|
||||
self.song_select_importer.save_song(self.song)
|
||||
question_dialog = QtGui.QMessageBox()
|
||||
question_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'Song Imported'))
|
||||
question_dialog.setText(translate('SongsPlugin.SongSelectForm', 'Your song has been imported, would you like '
|
||||
|
201
openlp/plugins/songs/lib/songselect.py
Normal file
201
openlp/plugins/songs/lib/songselect.py
Normal file
@ -0,0 +1,201 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2014 Raoul Snyman #
|
||||
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
|
||||
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
|
||||
"""
|
||||
import logging
|
||||
from http.cookiejar import CookieJar
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import HTTPCookieProcessor, HTTPError, build_opener
|
||||
from html.parser import HTMLParser
|
||||
|
||||
from bs4 import BeautifulSoup, NavigableString
|
||||
|
||||
from openlp.plugins.songs.lib import Song, VerseType, clean_song, Author
|
||||
from openlp.plugins.songs.lib.xml import SongXML
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \
|
||||
'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \
|
||||
'Mobile Safari/534.30'
|
||||
BASE_URL = 'https://mobile.songselect.com'
|
||||
LOGIN_URL = BASE_URL + '/account/login'
|
||||
LOGOUT_URL = BASE_URL + '/account/logout'
|
||||
SEARCH_URL = BASE_URL + '/search/results'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SongSelectImport(object):
|
||||
"""
|
||||
The :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class contains all the code which interfaces
|
||||
with CCLI's SongSelect service and downloads the songs.
|
||||
"""
|
||||
def __init__(self, db_manager):
|
||||
"""
|
||||
Set up the song select importer
|
||||
|
||||
:param db_manager: The song database manager
|
||||
"""
|
||||
self.db_manager = db_manager
|
||||
self.html_parser = HTMLParser()
|
||||
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
|
||||
self.opener.addheaders = [('User-Agent', USER_AGENT)]
|
||||
|
||||
def login(self, username, password, callback=None):
|
||||
"""
|
||||
Log the user into SongSelect. This method takes a username and password, and runs ``callback()`` at various
|
||||
points which can be used to give the user some form of feedback.
|
||||
|
||||
:param username: SongSelect username
|
||||
:param password: SongSelect password
|
||||
:param callback: Method to notify of progress.
|
||||
:return: True on success, False on failure.
|
||||
"""
|
||||
if callback:
|
||||
callback()
|
||||
login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
|
||||
if callback:
|
||||
callback()
|
||||
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
|
||||
data = urlencode({
|
||||
'__RequestVerificationToken': token_input['value'],
|
||||
'UserName': username,
|
||||
'Password': password,
|
||||
'RememberMe': 'false'
|
||||
})
|
||||
posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
|
||||
if callback:
|
||||
callback()
|
||||
return not posted_page.find('input', attrs={'name': '__RequestVerificationToken'})
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
Log the user out of SongSelect
|
||||
"""
|
||||
self.opener.open(LOGOUT_URL)
|
||||
|
||||
def search(self, search_text, max_results, callback=None):
|
||||
"""
|
||||
Set up a search.
|
||||
|
||||
:param search_text: The text to search for.
|
||||
:param max_results: Maximum number of results to fetch.
|
||||
:param callback: A method which is called when each song is found, with the song as a parameter.
|
||||
:return: List of songs
|
||||
"""
|
||||
params = {'allowredirect': 'false', 'SearchTerm': search_text}
|
||||
current_page = 1
|
||||
songs = []
|
||||
while True:
|
||||
if current_page > 1:
|
||||
params['page'] = current_page
|
||||
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
|
||||
search_results = results_page.find_all('li', 'result pane')
|
||||
if not search_results:
|
||||
break
|
||||
for result in search_results:
|
||||
song = {
|
||||
'title': self.html_parser.unescape(result.find('h3').string),
|
||||
'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')],
|
||||
'link': BASE_URL + result.find('a')['href']
|
||||
}
|
||||
if callback:
|
||||
callback(song)
|
||||
songs.append(song)
|
||||
if len(songs) >= max_results:
|
||||
break
|
||||
current_page += 1
|
||||
return songs
|
||||
|
||||
def get_song(self, song, callback=None):
|
||||
"""
|
||||
Get the full song from SongSelect
|
||||
|
||||
:param song:
|
||||
:param callback:
|
||||
"""
|
||||
if callback:
|
||||
callback()
|
||||
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
|
||||
if callback:
|
||||
callback()
|
||||
try:
|
||||
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
|
||||
except HTTPError:
|
||||
lyrics_page = None
|
||||
if callback:
|
||||
callback()
|
||||
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
|
||||
song['copyright'] = self.html_parser.unescape(song['copyright'])
|
||||
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
|
||||
song['verses'] = []
|
||||
if lyrics_page:
|
||||
verses = lyrics_page.find('section', 'lyrics').find_all('p')
|
||||
verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
|
||||
for counter in range(len(verses)):
|
||||
verse = {'label': verse_labels[counter].string, 'lyrics': ''}
|
||||
for v in verses[counter].contents:
|
||||
if isinstance(v, NavigableString):
|
||||
verse['lyrics'] = verse['lyrics'] + v.string
|
||||
else:
|
||||
verse['lyrics'] += '\n'
|
||||
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
|
||||
song['verses'].append(self.html_parser.unescape(verse))
|
||||
for counter, author in enumerate(song['authors']):
|
||||
song['authors'][counter] = self.html_parser.unescape(author)
|
||||
|
||||
def save_song(self, song):
|
||||
"""
|
||||
Save a song to the database, using the db_manager
|
||||
|
||||
:param song:
|
||||
:return:
|
||||
"""
|
||||
db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
|
||||
song_xml = SongXML()
|
||||
verse_order = []
|
||||
for verse in song['verses']:
|
||||
verse_type, verse_number = verse['label'].split(' ')[:2]
|
||||
verse_type = VerseType.from_loose_input(verse_type)
|
||||
verse_number = int(verse_number)
|
||||
song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics'])
|
||||
verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number))
|
||||
db_song.verse_order = ' '.join(verse_order)
|
||||
db_song.lyrics = song_xml.extract_xml()
|
||||
clean_song(self.db_manager, db_song)
|
||||
self.db_manager.save_object(db_song)
|
||||
db_song.authors = []
|
||||
for author_name in song['authors']:
|
||||
author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name)
|
||||
if not author:
|
||||
author = Author.populate(first_name=author_name.rsplit(' ', 1)[0],
|
||||
last_name=author_name.rsplit(' ', 1)[1],
|
||||
display_name=author_name)
|
||||
db_song.authors.append(author)
|
||||
self.db_manager.save_object(db_song)
|
@ -38,20 +38,20 @@ from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'..', '..', '..', 'resources', 'songbeamersongs'))
|
||||
SONG_TEST_DATA = {'Lobsinget dem Herrn.sng':
|
||||
{'title': 'GL 1 - Lobsinget dem Herrn',
|
||||
'verses':
|
||||
[('1. Lobsinget dem Herrn,\no preiset Ihn gern!\n'
|
||||
'Anbetung und Lob Ihm gebühret.\n', 'v'),
|
||||
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\n'
|
||||
'zu sterben für unsere Sünden!\n', 'v'),
|
||||
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\n'
|
||||
'Er sitzet zur Rechten des Vaters.\n', 'v'),
|
||||
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\n'
|
||||
'bis Er uns zur Herrlichket führet!\n\n', 'v')],
|
||||
'song_book_name': 'Glaubenslieder I',
|
||||
'song_number': "1"}
|
||||
}
|
||||
SONG_TEST_DATA = {
|
||||
'Lobsinget dem Herrn.sng': {
|
||||
'title': 'GL 1 - Lobsinget dem Herrn',
|
||||
'verses': [
|
||||
('1. Lobsinget dem Herrn,\no preiset Ihn gern!\nAnbetung und Lob Ihm gebühret.\n', 'v'),
|
||||
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\nzu sterben für unsere Sünden!\n', 'v'),
|
||||
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\nEr sitzet zur Rechten des Vaters.\n', 'v'),
|
||||
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\nbis Er uns zur Herrlichket führet!\n\n', 'v')
|
||||
],
|
||||
'song_book_name': 'Glaubenslieder I',
|
||||
'song_number': "1"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestSongBeamerImport(TestCase):
|
||||
"""
|
||||
|
224
tests/functional/openlp_plugins/songs/test_songselect.py
Normal file
224
tests/functional/openlp_plugins/songs/test_songselect.py
Normal file
@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2014 Raoul Snyman #
|
||||
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
|
||||
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
This module contains tests for the CCLI SongSelect importer.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGIN_URL, LOGOUT_URL, BASE_URL
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestSongSelect(TestCase):
|
||||
"""
|
||||
Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class
|
||||
"""
|
||||
def constructor_test(self):
|
||||
"""
|
||||
Test that constructing a basic SongSelectImport object works correctly
|
||||
"""
|
||||
# GIVEN: The SongSelectImporter class and a mocked out build_opener
|
||||
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener:
|
||||
# WHEN: An object is instantiated
|
||||
importer = SongSelectImport(None)
|
||||
|
||||
# THEN: The object should have the correct properties
|
||||
self.assertIsNone(importer.db_manager, 'The db_manager should be None')
|
||||
self.assertIsNotNone(importer.html_parser, 'There should be a valid html_parser object')
|
||||
self.assertIsNotNone(importer.opener, 'There should be a valid opener object')
|
||||
self.assertEqual(1, mocked_build_opener.call_count, 'The build_opener method should have been called once')
|
||||
|
||||
def login_fails_test(self):
|
||||
"""
|
||||
Test that when logging in to SongSelect fails, the login method returns False
|
||||
"""
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
|
||||
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
mocked_login_page = MagicMock()
|
||||
mocked_login_page.find.return_value = {'value': 'blah'}
|
||||
MockedBeautifulSoup.return_value = mocked_login_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 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_opener.open.call_count, 'opener should have been called twice')
|
||||
self.assertFalse(result, 'The login method should have returned False')
|
||||
|
||||
def login_succeeds_test(self):
|
||||
"""
|
||||
Test that when logging in to SongSelect succeeds, the login method returns True
|
||||
"""
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
|
||||
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
|
||||
mocked_opener = MagicMock()
|
||||
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
|
||||
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')
|
||||
self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice')
|
||||
self.assertTrue(result, 'The login method should have returned True')
|
||||
|
||||
def logout_test(self):
|
||||
"""
|
||||
Test that when the logout method is called, it logs the user out of SongSelect
|
||||
"""
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener:
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
importer = SongSelectImport(None)
|
||||
|
||||
# WHEN: The login method is called after being rigged to fail
|
||||
importer.logout()
|
||||
|
||||
# THEN: The opener is called once with the logout url
|
||||
self.assertEqual(1, mocked_opener.open.call_count, 'opener should have been called once')
|
||||
mocked_opener.open.assert_called_with(LOGOUT_URL)
|
||||
|
||||
def search_returns_no_results_test(self):
|
||||
"""
|
||||
Test that when the search finds no results, it simply returns an empty list
|
||||
"""
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, patch(
|
||||
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
mocked_results_page = MagicMock()
|
||||
mocked_results_page.find_all.return_value = []
|
||||
MockedBeautifulSoup.return_value = mocked_results_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
|
||||
# WHEN: The login method is called after being rigged to fail
|
||||
results = importer.search('text', 1000, mock_callback)
|
||||
|
||||
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
|
||||
self.assertEqual(0, mock_callback.call_count, 'callback should not have been called')
|
||||
self.assertEqual(1, mocked_opener.open.call_count, 'open should have been called once')
|
||||
self.assertEqual(1, mocked_results_page.find_all.call_count, 'find_all should have been called once')
|
||||
mocked_results_page.find_all.assert_called_with('li', 'result pane')
|
||||
self.assertEqual([], results, 'The search method should have returned an empty list')
|
||||
|
||||
def search_returns_two_results_test(self):
|
||||
"""
|
||||
Test that when the search finds 2 results, it simply returns a list with 2 results
|
||||
"""
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, patch(
|
||||
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
|
||||
# first search result
|
||||
mocked_result1 = MagicMock()
|
||||
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
|
||||
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
|
||||
# second search result
|
||||
mocked_result2 = MagicMock()
|
||||
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
|
||||
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
|
||||
# rest of the stuff
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
mocked_results_page = MagicMock()
|
||||
mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2], []]
|
||||
MockedBeautifulSoup.return_value = mocked_results_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
|
||||
# WHEN: The login method is called after being rigged to fail
|
||||
results = importer.search('text', 1000, mock_callback)
|
||||
|
||||
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
|
||||
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
|
||||
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
|
||||
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
|
||||
mocked_results_page.find_all.assert_called_with('li', 'result pane')
|
||||
expected_list = [
|
||||
{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
|
||||
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}
|
||||
]
|
||||
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
|
||||
|
||||
def search_reaches_max_results_test(self):
|
||||
"""
|
||||
Test that when the search finds MAX (2) results, it simply returns a list with those (2)
|
||||
"""
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, patch(
|
||||
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
|
||||
# first search result
|
||||
mocked_result1 = MagicMock()
|
||||
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
|
||||
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
|
||||
# second search result
|
||||
mocked_result2 = MagicMock()
|
||||
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
|
||||
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
|
||||
# third search result
|
||||
mocked_result3 = MagicMock()
|
||||
mocked_result3.find.side_effect = [MagicMock(string='Title 3'), {'href': '/url3'}]
|
||||
mocked_result3.find_all.return_value = [MagicMock(string='Author 3-1'), MagicMock(string='Author 3-2')]
|
||||
# rest of the stuff
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
mocked_results_page = MagicMock()
|
||||
mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2, mocked_result3], []]
|
||||
MockedBeautifulSoup.return_value = mocked_results_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
|
||||
# WHEN: The login method is called after being rigged to fail
|
||||
results = importer.search('text', 2, mock_callback)
|
||||
|
||||
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
|
||||
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
|
||||
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
|
||||
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
|
||||
mocked_results_page.find_all.assert_called_with('li', 'result pane')
|
||||
expected_list = [{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
|
||||
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}]
|
||||
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
|
@ -41,6 +41,7 @@ from tests.functional import patch, MagicMock
|
||||
TEST_PATH = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'songshowplussongs'))
|
||||
|
||||
|
||||
class TestSongShowPlusFileImport(SongImportTestHelper):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.importer_class_name = 'SongShowPlusImport'
|
||||
@ -48,10 +49,13 @@ class TestSongShowPlusFileImport(SongImportTestHelper):
|
||||
super(TestSongShowPlusFileImport, self).__init__(*args, **kwargs)
|
||||
|
||||
def test_song_import(self):
|
||||
test_import = self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||
test_import = self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
|
||||
"""
|
||||
Test that loading a SongShow Plus file works correctly on various files
|
||||
"""
|
||||
self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||
self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
|
||||
|
||||
|
||||
class TestSongShowPlusImport(TestCase):
|
||||
|
31
tests/helpers/__init__.py
Normal file
31
tests/helpers/__init__.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2014 Raoul Snyman #
|
||||
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
|
||||
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`~tests.helpers` module provides helper classes for use in the tests.
|
||||
"""
|
@ -35,6 +35,7 @@ from unittest import TestCase
|
||||
|
||||
from tests.functional import patch, MagicMock
|
||||
|
||||
|
||||
class SongImportTestHelper(TestCase):
|
||||
"""
|
||||
This class is designed to be a helper class to reduce repition when testing the import of song files.
|
||||
|
Loading…
Reference in New Issue
Block a user