From 9f939859b732b18c46972dc4ca01aa2212b2a62b Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 26 Dec 2015 16:33:29 +0200 Subject: [PATCH 01/35] [Songs Plugin] Add a stop button to the SongSelect importer to stop searching. --- .../plugins/songs/forms/songselectdialog.py | 12 +++++- openlp/plugins/songs/forms/songselectform.py | 19 ++++++++- openlp/plugins/songs/lib/songselect.py | 10 ++++- resources/images/openlp-2.qrc | 1 + resources/images/song_search_stop.png | Bin 0 -> 722 bytes .../openlp_plugins/songs/test_songselect.py | 39 ++++++++++++++++-- 6 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 resources/images/song_search_stop.png diff --git a/openlp/plugins/songs/forms/songselectdialog.py b/openlp/plugins/songs/forms/songselectdialog.py index b1bd9b143..a2985a061 100644 --- a/openlp/plugins/songs/forms/songselectdialog.py +++ b/openlp/plugins/songs/forms/songselectdialog.py @@ -114,12 +114,19 @@ class Ui_SongSelectDialog(object): self.search_button.setObjectName('search_button') self.search_input_layout.addWidget(self.search_button) self.search_layout.addLayout(self.search_input_layout) + self.search_progress_layout = QtWidgets.QHBoxLayout() + self.search_progress_layout.setSpacing(8) + self.search_progress_layout.setObjectName('search_progress_layout') self.search_progress_bar = QtWidgets.QProgressBar(self.search_page) self.search_progress_bar.setMinimum(0) self.search_progress_bar.setMaximum(3) self.search_progress_bar.setValue(0) - self.search_progress_bar.setVisible(False) - self.search_layout.addWidget(self.search_progress_bar) + self.search_progress_layout.addWidget(self.search_progress_bar) + self.stop_button = QtWidgets.QPushButton(self.search_page) + self.stop_button.setIcon(build_icon(':/songs/song_search_stop.png')) + self.stop_button.setObjectName('stop_button') + self.search_progress_layout.addWidget(self.stop_button) + self.search_layout.addLayout(self.search_progress_layout) self.search_results_widget = QtWidgets.QListWidget(self.search_page) self.search_results_widget.setProperty("showDropIndicator", False) self.search_results_widget.setAlternatingRowColors(True) @@ -234,6 +241,7 @@ class Ui_SongSelectDialog(object): self.login_button.setText(translate('SongsPlugin.SongSelectForm', 'Login')) self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:')) self.search_button.setText(translate('SongsPlugin.SongSelectForm', 'Search')) + self.stop_button.setText(translate('SongsPlugin.SongSelectForm', 'Stop')) self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % 0) self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout')) self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View')) diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py index b8d410c43..71111de7f 100755 --- a/openlp/plugins/songs/forms/songselectform.py +++ b/openlp/plugins/songs/forms/songselectform.py @@ -94,11 +94,13 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): self.worker = None self.song_count = 0 self.song = None + self.set_progress_visible(False) self.song_select_importer = SongSelectImport(self.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) self.search_combobox.returnPressed.connect(self.on_search_button_clicked) + self.stop_button.clicked.connect(self.on_stop_button_clicked) self.logout_button.clicked.connect(self.done) self.search_results_widget.itemDoubleClicked.connect(self.on_search_results_widget_double_clicked) self.search_results_widget.itemSelectionChanged.connect(self.on_search_results_widget_selection_changed) @@ -288,7 +290,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): 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.set_progress_visible(True) self.search_results_widget.clear() self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count) self.application.process_events() @@ -308,6 +310,12 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): self.thread.finished.connect(self.thread.deleteLater) self.thread.start() + def on_stop_button_clicked(self): + """ + Stop the search when the stop button is clicked. + """ + self.song_select_importer.stop() + def on_search_show_info(self, title, message): """ Show an informational message from the search thread @@ -332,7 +340,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): Slot which is called when the search is completed. """ self.application.process_events() - self.search_progress_bar.setVisible(False) + self.set_progress_visible(False) self.search_button.setEnabled(True) self.application.process_events() @@ -380,6 +388,13 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): self.application.process_events() self.done(QtWidgets.QDialog.Accepted) + def set_progress_visible(self, is_visible): + """ + Show or hide the search progress, including the stop button. + """ + self.search_progress_bar.setVisible(is_visible) + self.stop_button.setVisible(is_visible) + @property def application(self): """ diff --git a/openlp/plugins/songs/lib/songselect.py b/openlp/plugins/songs/lib/songselect.py index d2cdcd901..1cd7bab62 100644 --- a/openlp/plugins/songs/lib/songselect.py +++ b/openlp/plugins/songs/lib/songselect.py @@ -61,6 +61,7 @@ class SongSelectImport(object): self.html_parser = HTMLParser() self.opener = build_opener(HTTPCookieProcessor(CookieJar())) self.opener.addheaders = [('User-Agent', USER_AGENT)] + self.run_search = True def login(self, username, password, callback=None): """ @@ -115,10 +116,11 @@ class SongSelectImport(object): :param callback: A method which is called when each song is found, with the song as a parameter. :return: List of songs """ + self.run_search = True params = {'allowredirect': 'false', 'SearchTerm': search_text} current_page = 1 songs = [] - while True: + while self.run_search: if current_page > 1: params['page'] = current_page try: @@ -221,3 +223,9 @@ class SongSelectImport(object): self.db_manager.save_object(db_song) return db_song + def stop(self): + """ + Stop the search. + """ + self.run_search = False + diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 11c3482da..82c6234aa 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -1,5 +1,6 @@ + song_search_stop.png song_search_all.png song_search_author.png song_search_lyrics.png diff --git a/resources/images/song_search_stop.png b/resources/images/song_search_stop.png new file mode 100644 index 0000000000000000000000000000000000000000..5b4c488bdfde337177a0fa697d72b92c91bfb4ff GIT binary patch literal 722 zcmV;@0xkWCP)T3ZYmz(SzqVG|c5sBvYC8;yxO zSMFq|3m+?&Il=G#E*49tb+=foJF~O)o2zJAzY&PF{ zcQn;xSqPzlrXf8Sr($<54-e-N;#i|37$JfeySwr|H*TiPjViKU2hYP-r|-JdeIIGH zvULAmHS=RRl4$?nxsLQ$&+S_&y<9>lNLMW?6?D~xf(rKqNMIPx zd#_(jYV!-|zE9h^AzFuq)IAV+w4c8~=fz8y0^G?-dasQn115Tdn&%~|A0}vNX+xMM zI1WZ8gLU}|!H6++cMEgqtE(6)(Z>6UZ6-?KfMBzVR#`_c{$geH4(`G{Mt4TRS=_}1 z)}K5ku0+iP0~os4W}*Zhh?V?Xnv7=Z$s~F<8$KdoN!Mj#`V-#t6m{EXEfF$}36PsM z{9MLeU!(oOL(IN@{(Swi-DdxO(vP0vC*p_z(nBUnAYrSfv!k{YxOn;`zVFlX@->=i zA^`zZ^pJ`6U`mLC?_!J(Hk0VGLEG0J+JjFDxRwQa41O0`O2Ldx?YPU)!&1&fixf!S zn2aCUOCnPtj4HxY0=|T$7Qx3gSv%o`aN{OMl2=M{z*k99Nn-Ihx?xcFJjw_rSIV_h z4e)M3BpHVn>L*IXElUe#_Z Date: Tue, 29 Dec 2015 06:01:22 -0800 Subject: [PATCH 02/35] Fix typo in pin code hashing --- openlp/core/lib/projector/pjlink1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 6320f32bf..32fd5519c 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -345,7 +345,7 @@ class PJLink1(QTcpSocket): # Authenticated login with salt log.debug('(%s) Setting hash with salt="%s"' % (self.ip, data_check[2])) log.debug('(%s) pin="%s"' % (self.ip, self.pin)) - salt = qmd5_hash(salt=data_check[2].endcode('ascii'), data=self.pin.encode('ascii')) + salt = qmd5_hash(salt=data_check[2].encode('ascii'), data=self.pin.encode('ascii')) else: salt = None # We're connected at this point, so go ahead and do regular I/O From 6e9547dc33152c88272c9270ff1433e5b8c7feae Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Sun, 3 Jan 2016 01:39:53 +0100 Subject: [PATCH 03/35] Strip whitespace from title tag on import of Songbeamer file --- openlp/plugins/songs/lib/importers/songbeamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/importers/songbeamer.py b/openlp/plugins/songs/lib/importers/songbeamer.py index 56709dad7..0146cb45b 100644 --- a/openlp/plugins/songs/lib/importers/songbeamer.py +++ b/openlp/plugins/songs/lib/importers/songbeamer.py @@ -242,7 +242,7 @@ class SongBeamerImport(SongImport): elif tag_val[0] == '#TextAlign': pass elif tag_val[0] == '#Title': - self.title = str(tag_val[1]) + self.title = str(tag_val[1]).strip() elif tag_val[0] == '#TitleAlign': pass elif tag_val[0] == '#TitleFontSize': From 96c99f3e28101834dbef27257c4e62fea8cbb472 Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Sun, 3 Jan 2016 01:53:17 +0100 Subject: [PATCH 04/35] Add test to check if title is stripped on Songbeamer import --- tests/resources/songbeamersongs/Lobsinget dem Herrn.sng | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng b/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng index c93a143fa..dd4649fc8 100644 --- a/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng +++ b/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng @@ -1,5 +1,5 @@ #LangCount=1 -#Title=GL 1 - Lobsinget dem Herrn +#Title= GL 1 - Lobsinget dem Herrn #Author=Carl Brockhaus #Melody=Johann Jakob Vetter #Editor=SongBeamer 4.20 From a023177c4dd6ff1ac216ca36dec59ac5dffc93bd Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 3 Jan 2016 00:15:54 -0800 Subject: [PATCH 05/35] Fix typo in projector authentication --- openlp/core/lib/projector/pjlink1.py | 2 +- .../openlp_core_common/test_projector_utilities.py | 6 ++++++ tests/resources/projector/data.py | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 32fd5519c..4fcdae909 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -101,7 +101,7 @@ class PJLink1(QTcpSocket): self.location = None self.notes = None self.dbid = None if 'dbid' not in kwargs else kwargs['dbid'] - self.location = None if 'location' not in kwargs else kwargs['notes'] + self.location = None if 'location' not in kwargs else kwargs['location'] self.notes = None if 'notes' not in kwargs else kwargs['notes'] # Poll time 20 seconds unless called with something else self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000 diff --git a/tests/functional/openlp_core_common/test_projector_utilities.py b/tests/functional/openlp_core_common/test_projector_utilities.py index df06a3efd..17a1ac1bf 100644 --- a/tests/functional/openlp_core_common/test_projector_utilities.py +++ b/tests/functional/openlp_core_common/test_projector_utilities.py @@ -29,9 +29,15 @@ from unittest import TestCase from openlp.core.common import verify_ip_address, md5_hash, qmd5_hash +from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_HASH +''' salt = '498e4a67' pin = 'JBMIAProjectorLink' test_hash = '5d8409bc1c3fa39749434aa3a5c38682' +''' +salt = TEST_SALT +pin = TEST_PIN +test_hash = TEST_HASH test_non_ascii_string = '이것은 한국어 시험 문자열' test_non_ascii_hash = 'fc00c7912976f6e9c19099b514ced201' diff --git a/tests/resources/projector/data.py b/tests/resources/projector/data.py index ba922212d..14bcda252 100644 --- a/tests/resources/projector/data.py +++ b/tests/resources/projector/data.py @@ -29,6 +29,14 @@ from openlp.core.lib.projector.db import Projector # Test data TEST_DB = os.path.join('tmp', 'openlp-test-projectordb.sql') +TEST_SALT = '498e4a67' + +TEST_PIN = 'JBMIAProjectorLink' + +TEST_HASH = '5d8409bc1c3fa39749434aa3a5c38682' + +TEST_CONNECT_AUTHENTICATE = 'PJLink 1 {salt}'.format(salt=TEST_SALT) + TEST1_DATA = Projector(ip='111.111.111.111', port='1111', pin='1111', From f0a5e2eddb0fb04d98a26165cc65b73909d7faaa Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 3 Jan 2016 00:18:58 -0800 Subject: [PATCH 06/35] fix extraneous docstring notes in pjlink1 test --- .../openlp_core_lib/test_projector_pjlink1.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/functional/openlp_core_lib/test_projector_pjlink1.py diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py new file mode 100644 index 000000000..a3f0c519a --- /dev/null +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2015 OpenLP Developers # +# --------------------------------------------------------------------------- # +# 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 # +############################################################################### +""" +Package to test the openlp.core.lib.projector.pjlink1 package. +""" + +from unittest import TestCase + +from mock import MagicMock, patch + +from openlp.core.lib.projector.pjlink1 import PJLink1 + +from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE + +pjlink = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True) + + +class TestPJLink(TestCase): + """ + Tests for the PJLink module + """ + @patch.object(pjlink, 'readyRead') + @patch.object(pjlink, 'send_command') + @patch.object(pjlink, 'waitForReadyRead') + @patch('openlp.core.common.qmd5_hash') + def ticket_92187_test(self, + mock_qmd5_hash, + mock_waitForReadyRead, + mock_send_command, + mock_readyRead): + """ + Fix for projector connect with PJLink authentication exception + """ + # WHEN: Calling check_login with authentication request: + pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE) + + # THEN: Should have called qmd5_hash + mock_qmd5_hash.called_with(TEST_SALT, TEST_PIN) \ No newline at end of file From 2edb32ac7f848b74861e2ff69f449e077cc07682 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 3 Jan 2016 00:32:43 -0800 Subject: [PATCH 07/35] pep8 fix --- tests/functional/openlp_core_lib/test_projector_pjlink1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index a3f0c519a..e48130c29 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -54,4 +54,4 @@ class TestPJLink(TestCase): pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE) # THEN: Should have called qmd5_hash - mock_qmd5_hash.called_with(TEST_SALT, TEST_PIN) \ No newline at end of file + mock_qmd5_hash.called_with(TEST_SALT, TEST_PIN) From 9459aff90cb7b3c6f096f58109f68800daf1609a Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Sun, 3 Jan 2016 12:47:07 +0100 Subject: [PATCH 08/35] Add test for webkit_version function --- .../openlp_core_lib/test_htmlbuilder.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_htmlbuilder.py b/tests/functional/openlp_core_lib/test_htmlbuilder.py index 7199464bc..181c485f5 100644 --- a/tests/functional/openlp_core_lib/test_htmlbuilder.py +++ b/tests/functional/openlp_core_lib/test_htmlbuilder.py @@ -4,11 +4,11 @@ Package to test the openlp.core.lib.htmlbuilder module. from unittest import TestCase -from PyQt5 import QtCore +from PyQt5 import QtCore, QtWebKit from openlp.core.common import Settings from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \ - build_lyrics_format_css, build_footer_css + build_lyrics_format_css, build_footer_css, webkit_version from openlp.core.lib.theme import HorizontalType, VerticalType from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -358,3 +358,14 @@ class Htmbuilder(TestCase, TestMixin): # THEN: Footer should wrap self.assertEqual(FOOTER_CSS_WRAP, css, 'The footer strings should be equal.') + + def webkit_version_test(self): + """ + Test the webkit_version() function + """ + # GIVEN: Webkit + webkit_ver = float(QtWebKit.qWebKitVersion()) + # WHEN: Retrieving the webkit version + # THEN: Webkit versions should match + self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one") + From d2ab4862b41ac4c8fe2dd3c6e51ef6538bd894cf Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 13:11:24 +0100 Subject: [PATCH 09/35] Initial support for multiple songbooks --- openlp/plugins/songs/forms/editsongdialog.py | 57 +++++---- openlp/plugins/songs/forms/editsongform.py | 116 ++++++++++++------- openlp/plugins/songs/lib/db.py | 50 +++++++- openlp/plugins/songs/lib/openlyricsxml.py | 18 +-- openlp/plugins/songs/lib/ui.py | 4 +- openlp/plugins/songs/lib/upgrade.py | 30 ++++- 6 files changed, 188 insertions(+), 87 deletions(-) diff --git a/openlp/plugins/songs/forms/editsongdialog.py b/openlp/plugins/songs/forms/editsongdialog.py index 9821bc745..4d0b2734b 100644 --- a/openlp/plugins/songs/forms/editsongdialog.py +++ b/openlp/plugins/songs/forms/editsongdialog.py @@ -37,7 +37,7 @@ class Ui_EditSongDialog(object): def setupUi(self, edit_song_dialog): edit_song_dialog.setObjectName('edit_song_dialog') edit_song_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) - edit_song_dialog.resize(650, 400) + edit_song_dialog.resize(900, 600) edit_song_dialog.setModal(True) self.dialog_layout = QtWidgets.QVBoxLayout(edit_song_dialog) self.dialog_layout.setSpacing(8) @@ -173,22 +173,33 @@ class Ui_EditSongDialog(object): self.topic_remove_layout.addWidget(self.topic_remove_button) self.topics_layout.addLayout(self.topic_remove_layout) self.authors_right_layout.addWidget(self.topics_group_box) - self.song_book_group_box = QtWidgets.QGroupBox(self.authors_tab) - self.song_book_group_box.setObjectName('song_book_group_box') - self.song_book_layout = QtWidgets.QFormLayout(self.song_book_group_box) - self.song_book_layout.setObjectName('song_book_layout') - self.song_book_name_label = QtWidgets.QLabel(self.song_book_group_box) - self.song_book_name_label.setObjectName('song_book_name_label') - self.song_book_combo_box = create_combo_box(self.song_book_group_box, 'song_book_combo_box') - self.song_book_name_label.setBuddy(self.song_book_combo_box) - self.song_book_layout.addRow(self.song_book_name_label, self.song_book_combo_box) - self.song_book_number_label = QtWidgets.QLabel(self.song_book_group_box) - self.song_book_number_label.setObjectName('song_book_number_label') - self.song_book_number_edit = QtWidgets.QLineEdit(self.song_book_group_box) - self.song_book_number_edit.setObjectName('song_book_number_edit') - self.song_book_number_label.setBuddy(self.song_book_number_edit) - self.song_book_layout.addRow(self.song_book_number_label, self.song_book_number_edit) - self.authors_right_layout.addWidget(self.song_book_group_box) + self.songbook_group_box = QtWidgets.QGroupBox(self.authors_tab) + self.songbook_group_box.setObjectName('songbook_group_box') + self.songbooks_layout = QtWidgets.QVBoxLayout(self.songbook_group_box) + self.songbooks_layout.setObjectName('songbooks_layout') + self.songbook_add_layout = QtWidgets.QHBoxLayout() + self.songbook_add_layout.setObjectName('songbook_add_layout') + self.songbooks_combo_box = create_combo_box(self.songbook_group_box, 'songbooks_combo_box') + self.songbook_add_layout.addWidget(self.songbooks_combo_box) + self.songbook_entry_edit = QtWidgets.QLineEdit(self.songbook_group_box) + self.songbook_entry_edit.setMaximumWidth(100) + self.songbook_add_layout.addWidget(self.songbook_entry_edit) + self.songbook_add_button = QtWidgets.QPushButton(self.songbook_group_box) + self.songbook_add_button.setObjectName('songbook_add_button') + self.songbook_add_layout.addWidget(self.songbook_add_button) + self.songbooks_layout.addLayout(self.songbook_add_layout) + self.songbooks_list_view = QtWidgets.QListWidget(self.songbook_group_box) + self.songbooks_list_view.setAlternatingRowColors(True) + self.songbooks_list_view.setObjectName('songbooks_list_view') + self.songbooks_layout.addWidget(self.songbooks_list_view) + self.songbook_remove_layout = QtWidgets.QHBoxLayout() + self.songbook_remove_layout.setObjectName('songbook_remove_layout') + self.songbook_remove_layout.addStretch() + self.songbook_remove_button = QtWidgets.QPushButton(self.songbook_group_box) + self.songbook_remove_button.setObjectName('songbook_remove_button') + self.songbook_remove_layout.addWidget(self.songbook_remove_button) + self.songbooks_layout.addLayout(self.songbook_remove_layout) + self.authors_right_layout.addWidget(self.songbook_group_box) self.authors_tab_layout.addLayout(self.authors_right_layout) self.song_tab_widget.addTab(self.authors_tab, '') # theme tab @@ -303,15 +314,15 @@ class Ui_EditSongDialog(object): self.author_add_button.setText(translate('SongsPlugin.EditSongForm', '&Add to Song')) self.author_edit_button.setText(translate('SongsPlugin.EditSongForm', '&Edit Author Type')) self.author_remove_button.setText(translate('SongsPlugin.EditSongForm', '&Remove')) - self.maintenance_button.setText(translate('SongsPlugin.EditSongForm', '&Manage Authors, Topics, Song Books')) - self.topics_group_box.setTitle(SongStrings.Topic) + self.maintenance_button.setText(translate('SongsPlugin.EditSongForm', '&Manage Authors, Topics, Songbooks')) + self.topics_group_box.setTitle(SongStrings.Topics) self.topic_add_button.setText(translate('SongsPlugin.EditSongForm', 'A&dd to Song')) self.topic_remove_button.setText(translate('SongsPlugin.EditSongForm', 'R&emove')) - self.song_book_group_box.setTitle(SongStrings.SongBook) - self.song_book_name_label.setText(translate('SongsPlugin.EditSongForm', 'Book:')) - self.song_book_number_label.setText(translate('SongsPlugin.EditSongForm', 'Number:')) + self.songbook_group_box.setTitle(SongStrings.SongBooks) + self.songbook_add_button.setText(translate('SongsPlugin.EditSongForm', 'Add &to Song')) + self.songbook_remove_button.setText(translate('SongsPlugin.EditSongForm', 'Re&move')) self.song_tab_widget.setTabText(self.song_tab_widget.indexOf(self.authors_tab), - translate('SongsPlugin.EditSongForm', 'Authors, Topics && Song Book')) + translate('SongsPlugin.EditSongForm', 'Authors, Topics && Songbooks')) self.theme_group_box.setTitle(UiStrings().Theme) self.theme_add_button.setText(translate('SongsPlugin.EditSongForm', 'New &Theme')) self.rights_group_box.setTitle(translate('SongsPlugin.EditSongForm', 'Copyright Information')) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 4158d8210..713069dd3 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -35,7 +35,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStri from openlp.core.lib import FileDialog, PluginStatus, MediaType, create_separated_list from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box from openlp.plugins.songs.lib import VerseType, clean_song -from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorType, Topic, MediaFile +from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorType, Topic, MediaFile, SongBookEntry from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import SongXML from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog @@ -69,6 +69,9 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.topic_add_button.clicked.connect(self.on_topic_add_button_clicked) self.topic_remove_button.clicked.connect(self.on_topic_remove_button_clicked) self.topics_list_view.itemClicked.connect(self.on_topic_list_view_clicked) + self.songbook_add_button.clicked.connect(self.on_songbook_add_button_clicked) + self.songbook_remove_button.clicked.connect(self.on_songbook_remove_button_clicked) + self.songbooks_list_view.itemClicked.connect(self.on_songbook_list_view_clicked) self.copyright_insert_button.clicked.connect(self.on_copyright_insert_button_triggered) self.verse_add_button.clicked.connect(self.on_verse_add_button_clicked) self.verse_list_widget.doubleClicked.connect(self.on_verse_edit_button_clicked) @@ -125,6 +128,11 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): author_item.setData(QtCore.Qt.UserRole, (author.id, author_type)) self.authors_list_view.addItem(author_item) + def add_songbookentry_to_list(self, songbook_id, songbook_name, entry): + songbookentry_item = QtWidgets.QListWidgetItem(SongBookEntry.get_display_name(songbook_name, entry)) + songbookentry_item.setData(QtCore.Qt.UserRole, (songbook_id, entry)) + self.songbooks_list_view.addItem(songbookentry_item) + def _extract_verse_order(self, verse_order): """ Split out the verse order @@ -219,17 +227,6 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): result = self._validate_verse_list(self.verse_order_edit.text(), self.verse_list_widget.rowCount()) if not result: return False - text = self.song_book_combo_box.currentText() - if self.song_book_combo_box.findText(text, QtCore.Qt.MatchExactly) < 0: - if QtWidgets.QMessageBox.question( - self, translate('SongsPlugin.EditSongForm', 'Add Book'), - translate('SongsPlugin.EditSongForm', 'This song book does not exist, do you want to add it?'), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes: - book = Book.populate(name=text, publisher='') - self.manager.save_object(book) - else: - return False # Validate tags (lp#1199639) misplaced_tags = [] verse_tags = [] @@ -327,6 +324,9 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): if self.topics_combo_box.hasFocus() and self.topics_combo_box.currentText(): self.on_topic_add_button_clicked() return + if self.songbooks_combo_box.hasFocus() and self.songbooks_combo_box.currentText(): + self.on_songbook_add_button_clicked() + return QtWidgets.QDialog.keyPressEvent(self, event) def initialise(self): @@ -367,12 +367,12 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.topics = [] self._load_objects(Topic, self.topics_combo_box, self.topics) - def load_books(self): + def load_songbooks(self): """ - Load the song books into the combobox + Load the Songbooks into the combobox """ - self.books = [] - self._load_objects(Book, self.song_book_combo_box, self.books) + self.songbooks = [] + self._load_objects(Book, self.songbooks_combo_box, self.songbooks) def load_themes(self, theme_list): """ @@ -413,12 +413,12 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.verse_list_widget.setRowCount(0) self.authors_list_view.clear() self.topics_list_view.clear() + self.songbooks_list_view.clear() self.audio_list_widget.clear() self.title_edit.setFocus() - self.song_book_number_edit.clear() self.load_authors() self.load_topics() - self.load_books() + self.load_songbooks() self.load_media_files() self.theme_combo_box.setEditText('') self.theme_combo_box.setCurrentIndex(0) @@ -437,18 +437,11 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.song_tab_widget.setCurrentIndex(0) self.load_authors() self.load_topics() - self.load_books() + self.load_songbooks() self.load_media_files() self.song = self.manager.get_object(Song, song_id) self.title_edit.setText(self.song.title) - self.alternative_edit.setText( - self.song.alternate_title if self.song.alternate_title else '') - if self.song.song_book_id != 0: - book_name = self.manager.get_object(Book, self.song.song_book_id) - find_and_set_in_combo_box(self.song_book_combo_box, str(book_name.name)) - else: - self.song_book_combo_box.setEditText('') - self.song_book_combo_box.setCurrentIndex(0) + self.alternative_edit.setText(self.song.alternate_title if self.song.alternate_title else '') if self.song.theme_name: find_and_set_in_combo_box(self.theme_combo_box, str(self.song.theme_name)) else: @@ -458,7 +451,6 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.copyright_edit.setText(self.song.copyright if self.song.copyright else '') self.comments_edit.setPlainText(self.song.comments if self.song.comments else '') self.ccli_number_edit.setText(self.song.ccli_number if self.song.ccli_number else '') - self.song_book_number_edit.setText(self.song.song_number if self.song.song_number else '') # lazy xml migration for now self.verse_list_widget.clear() self.verse_list_widget.setRowCount(0) @@ -520,6 +512,9 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic_name = QtWidgets.QListWidgetItem(str(topic.name)) topic_name.setData(QtCore.Qt.UserRole, topic.id) self.topics_list_view.addItem(topic_name) + self.songbooks_list_view.clear() + for songbookentry in self.song.songbookentries: + self.add_songbookentry_to_list(songbookentry.songbook.id, songbookentry.songbook.name, songbookentry.entry) self.audio_list_widget.clear() for media in self.song.media_files: media_file = QtWidgets.QListWidgetItem(os.path.split(media.file_name)[1]) @@ -678,6 +673,48 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): row = self.topics_list_view.row(item) self.topics_list_view.takeItem(row) + def on_songbook_add_button_clicked(self): + item = int(self.songbooks_combo_box.currentIndex()) + text = self.songbooks_combo_box.currentText() + if item == 0 and text: + if QtWidgets.QMessageBox.question( + self, translate('SongsPlugin.EditSongForm', 'Add Songbook'), + translate('SongsPlugin.EditSongForm', 'This Songbook does not exist, do you want to add it?'), + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes: + songbook = Book.populate(name=text) + self.manager.save_object(songbook) + self.add_songbookentry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) + self.load_songbooks() + self.songbooks_combo_box.setCurrentIndex(0) + self.songbook_entry_edit.setText("") + else: + return + elif item > 0: + item_id = (self.songbooks_combo_box.itemData(item)) + songbook = self.manager.get_object(Book, item_id) + if self.songbooks_list_view.findItems(str(songbook.name), QtCore.Qt.MatchExactly): + critical_error_message_box( + message=translate('SongsPlugin.EditSongForm', 'This Songbook is already in the list.')) + else: + self.add_songbookentry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) + self.songbooks_combo_box.setCurrentIndex(0) + self.songbook_entry_edit.setText("") + else: + QtWidgets.QMessageBox.warning( + self, UiStrings().NISs, + translate('SongsPlugin.EditSongForm', 'You have not selected a valid Songbook. Either select a ' + 'Songbook from the list, or type in a new Songbook and click the "Add to Song" ' + 'button to add the new Songbook.')) + + def on_songbook_list_view_clicked(self): + self.songbook_remove_button.setEnabled(True) + + def on_songbook_remove_button_clicked(self): + self.songbook_remove_button.setEnabled(False) + row = self.songbooks_list_view.row(self.songbooks_list_view.currentItem()) + self.songbooks_list_view.takeItem(row) + def on_verse_list_view_clicked(self): self.verse_edit_button.setEnabled(True) self.verse_delete_button.setEnabled(True) @@ -838,17 +875,10 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): """ Maintenance button pressed """ - temp_song_book = None - item = int(self.song_book_combo_box.currentIndex()) - text = self.song_book_combo_box.currentText() - if item == 0 and text: - temp_song_book = text self.media_item.song_maintenance_form.exec(True) self.load_authors() - self.load_books() + self.load_songbooks() self.load_topics() - if temp_song_book: - self.song_book_combo_box.setEditText(temp_song_book) def on_preview(self, button): """ @@ -928,7 +958,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): log.debug('SongEditForm.clearCaches') self.authors = [] self.themes = [] - self.books = [] + self.songbooks = [] self.topics = [] def reject(self): @@ -977,12 +1007,6 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): order.append('%s%s' % (verse_tag, verse_num)) self.song.verse_order = ' '.join(order) self.song.ccli_number = self.ccli_number_edit.text() - self.song.song_number = self.song_book_number_edit.text() - book_name = self.song_book_combo_box.currentText() - if book_name: - self.song.book = self.manager.get_object_filtered(Book, Book.name == book_name) - else: - self.song.book = None theme_name = self.theme_combo_box.currentText() if theme_name: self.song.theme_name = theme_name @@ -1001,6 +1025,12 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic = self.manager.get_object(Topic, topic_id) if topic is not None: self.song.topics.append(topic) + for row in range(self.songbooks_list_view.count()): + item = self.songbooks_list_view.item(row) + songbook_id = item.data(QtCore.Qt.UserRole)[0] + songbook = self.manager.get_object(Book, songbook_id) + entry = item.data(QtCore.Qt.UserRole)[1] + self.song.add_songbookentry(songbook, entry) # Save the song here because we need a valid id for the audio files. clean_song(self.manager, self.song) self.manager.save_object(self.song) diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 89f9bf26b..2a617e9db 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -160,6 +160,31 @@ class Song(BaseModel): self.authors_songs.remove(author_song) return + def add_songbookentry(self, songbook, entry): + """ + Add a Songbook Entry to the song if it not yet exists + + :param songbook_name: Name of the Songbook. + :param entry: Entry in the Songbook (usually a number) + """ + for songbookentry in self.songbookentries: + if songbookentry.songbook.name == songbook.name and songbookentry.entry == entry: + return + + new_songbookentry = SongBookEntry() + new_songbookentry.songbook = songbook + new_songbookentry.entry = entry + self.songbookentries.append(new_songbookentry) + +class SongBookEntry(BaseModel): + """ + SongBookEntry model + """ + @staticmethod + def get_display_name(songbook_name, entry): + if entry: + return "%s #%s" % (songbook_name, entry) + return songbook_name class Topic(BaseModel): """ @@ -182,6 +207,7 @@ def init_schema(url): * media_files_songs * song_books * songs + * songs_songbooks * songs_topics * topics @@ -222,7 +248,6 @@ def init_schema(url): The *songs* table has the following columns: * id - * song_book_id * title * alternate_title * lyrics @@ -230,11 +255,17 @@ def init_schema(url): * copyright * comments * ccli_number - * song_number * theme_name * search_title * search_lyrics + **songs_songsbooks Table** + This is a mapping table between the *songs* and the *song_books* tables. It has the following columns: + + * songbook_id + * song_id + * entry # The song number, like 120 or 550A + **songs_topics Table** This is a bridging table between the *songs* and *topics* tables, which serves to create a many-to-many relationship between the two tables. It @@ -284,7 +315,6 @@ def init_schema(url): songs_table = Table( 'songs', metadata, Column('id', types.Integer(), primary_key=True), - Column('song_book_id', types.Integer(), ForeignKey('song_books.id'), default=None), Column('title', types.Unicode(255), nullable=False), Column('alternate_title', types.Unicode(255)), Column('lyrics', types.UnicodeText, nullable=False), @@ -292,7 +322,6 @@ def init_schema(url): Column('copyright', types.Unicode(255)), Column('comments', types.UnicodeText), Column('ccli_number', types.Unicode(64)), - Column('song_number', types.Unicode(64)), Column('theme_name', types.Unicode(128)), Column('search_title', types.Unicode(255), index=True, nullable=False), Column('search_lyrics', types.UnicodeText, nullable=False), @@ -316,6 +345,14 @@ def init_schema(url): Column('author_type', types.Unicode(255), primary_key=True, nullable=False, server_default=text('""')) ) + # Definition of the "songs_songbooks" table + songs_songbooks_table = Table( + 'songs_songbooks', metadata, + Column('songbook_id', types.Integer(), ForeignKey('song_books.id'), primary_key=True), + Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True), + Column('entry', types.Unicode(255), primary_key=True, nullable=False) + ) + # Definition of the "songs_topics" table songs_topics_table = Table( 'songs_topics', metadata, @@ -329,6 +366,9 @@ def init_schema(url): mapper(AuthorSong, authors_songs_table, properties={ 'author': relation(Author) }) + mapper(SongBookEntry, songs_songbooks_table, properties={ + 'songbook': relation(Book) + }) mapper(Book, song_books_table) mapper(MediaFile, media_files_table) mapper(Song, songs_table, properties={ @@ -337,8 +377,8 @@ def init_schema(url): 'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"), # Use lazy='joined' to always load authors when the song is fetched from the database (bug 1366198) 'authors': relation(Author, secondary=authors_songs_table, viewonly=True, lazy='joined'), - 'book': relation(Book, backref='songs'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), + 'songbookentries': relation(SongBookEntry, cascade="all, delete-orphan"), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) }) mapper(Topic, topics_table) diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py index 5c6951dfd..2e96f8926 100644 --- a/openlp/plugins/songs/lib/openlyricsxml.py +++ b/openlp/plugins/songs/lib/openlyricsxml.py @@ -266,13 +266,12 @@ class OpenLyrics(object): element.set('type', AuthorType.Music) else: element.set('type', author_song.author_type) - book = self.manager.get_object_filtered(Book, Book.id == song.song_book_id) - if book is not None: - book = book.name + if song.songbookentries: songbooks = etree.SubElement(properties, 'songbooks') - element = self._add_text_to_element('songbook', songbooks, None, book) - if song.song_number: - element.set('entry', song.song_number) + for songbookentry in song.songbookentries: + element = self._add_text_to_element('songbook', songbooks, None, songbookentry.songbook) + if songbookentry.entry: + element.set('entry', songbookentry.entry) if song.topics: themes = etree.SubElement(properties, 'themes') for topic in song.topics: @@ -744,8 +743,6 @@ class OpenLyrics(object): :param properties: The property object (lxml.objectify.ObjectifiedElement). :param song: The song object. """ - song.song_book_id = None - song.song_number = '' if hasattr(properties, 'songbooks'): for songbook in properties.songbooks.songbook: book_name = songbook.get('name', '') @@ -755,10 +752,7 @@ class OpenLyrics(object): # We need to create a book, because it does not exist. book = Book.populate(name=book_name, publisher='') self.manager.save_object(book) - song.song_book_id = book.id - song.song_number = songbook.get('entry', '') - # We only support one song book, so take the first one. - break + song.add_songbookentry(book, songbook.get('entry', '')) def _process_titles(self, properties, song): """ diff --git a/openlp/plugins/songs/lib/ui.py b/openlp/plugins/songs/lib/ui.py index 4e13d9c35..bc0f75e72 100644 --- a/openlp/plugins/songs/lib/ui.py +++ b/openlp/plugins/songs/lib/ui.py @@ -35,8 +35,8 @@ class SongStrings(object): Authors = translate('OpenLP.Ui', 'Authors', 'Plural') AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database. CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.') - SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular') - SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural') + SongBook = translate('OpenLP.Ui', 'Songbook', 'Singular') + SongBooks = translate('OpenLP.Ui', 'Songbooks', 'Plural') SongIncomplete = translate('OpenLP.Ui', 'Title and/or verses not found') SongMaintenance = translate('OpenLP.Ui', 'Song Maintenance') Topic = translate('OpenLP.Ui', 'Topic', 'Singular') diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py index 9b2cf70a2..7aa95b819 100644 --- a/openlp/plugins/songs/lib/upgrade.py +++ b/openlp/plugins/songs/lib/upgrade.py @@ -29,10 +29,10 @@ from sqlalchemy import Table, Column, ForeignKey, types from sqlalchemy.sql.expression import func, false, null, text from openlp.core.lib.db import get_upgrade_op -from openlp.core.common import trace_error_handler +from openlp.core.utils.db import drop_columns log = logging.getLogger(__name__) -__version__ = 4 +__version__ = 5 # TODO: When removing an upgrade path the ftw-data needs updating to the minimum supported version @@ -117,3 +117,29 @@ def upgrade_4(session, metadata): op.rename_table('authors_songs_tmp', 'authors_songs') else: log.warning('Skipping upgrade_4 step of upgrading the song db') + + +def upgrade_5(session, metadata): + """ + Version 5 upgrade. + + This upgrade adds support for multiple songbooks + """ + op = get_upgrade_op(session) + songs_table = Table('songs', metadata) + if 'song_book_id' in [col.name for col in songs_table.c.values()]: + log.warning('Skipping upgrade_5 step of upgrading the song db') + return + + # Create the mapping table (songs <-> songbooks) + op.create_table('songs_songbooks', + Column('songbook_id', types.Integer(), ForeignKey('song_books.id'), primary_key=True), + Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True), + Column('entry', types.Unicode(255), primary_key=True, nullable=False)) + + # Migrate old data + op.execute('INSERT INTO songs_songbooks SELECT song_book_id, id, song_number FROM songs\ + WHERE song_book_id NOT NULL AND song_number NOT NULL') + + # Drop old columns + drop_columns(op, 'songs', ['song_book_id', 'song_number']) From c7d7d1692107e2d1acbb71283fab838b19309ca4 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 13:18:11 +0100 Subject: [PATCH 10/35] PEP8 --- openlp/core/common/settings.py | 9 ++++++--- openlp/plugins/songs/lib/db.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 56a48319a..13ca5fb8f 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -252,7 +252,8 @@ class Settings(QtCore.QSettings): 'shortcuts/blankScreen': [QtGui.QKeySequence(QtCore.Qt.Key_Period)], 'shortcuts/collapse': [QtGui.QKeySequence(QtCore.Qt.Key_Minus)], 'shortcuts/desktopScreen': [QtGui.QKeySequence(QtCore.Qt.Key_D)], - 'shortcuts/delete': [QtGui.QKeySequence(QtGui.QKeySequence.Delete), QtGui.QKeySequence(QtCore.Qt.Key_Delete)], + 'shortcuts/delete': [QtGui.QKeySequence(QtGui.QKeySequence.Delete), + QtGui.QKeySequence(QtCore.Qt.Key_Delete)], 'shortcuts/down': [QtGui.QKeySequence(QtCore.Qt.Key_Down)], 'shortcuts/editSong': [], 'shortcuts/escapeItem': [QtGui.QKeySequence(QtCore.Qt.Key_Escape)], @@ -329,7 +330,8 @@ class Settings(QtCore.QSettings): 'shortcuts/moveBottom': [QtGui.QKeySequence(QtCore.Qt.Key_End)], 'shortcuts/moveDown': [QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], 'shortcuts/nextTrackItem': [], - 'shortcuts/nextItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Down), QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], + 'shortcuts/nextItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Down), + QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], 'shortcuts/nextItem_preview': [QtGui.QKeySequence(QtCore.Qt.Key_Down), QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], 'shortcuts/nextService': [QtGui.QKeySequence(QtCore.Qt.Key_Right)], @@ -339,7 +341,8 @@ class Settings(QtCore.QSettings): QtGui.QKeySequence(QtCore.Qt.ALT + QtCore.Qt.Key_F1)], 'shortcuts/openService': [], 'shortcuts/saveService': [], - 'shortcuts/previousItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Up), QtGui.QKeySequence(QtCore.Qt.Key_PageUp)], + 'shortcuts/previousItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Up), + QtGui.QKeySequence(QtCore.Qt.Key_PageUp)], 'shortcuts/playbackPause': [], 'shortcuts/playbackPlay': [], 'shortcuts/playbackStop': [], diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 2a617e9db..3bdd952a2 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -176,6 +176,7 @@ class Song(BaseModel): new_songbookentry.entry = entry self.songbookentries.append(new_songbookentry) + class SongBookEntry(BaseModel): """ SongBookEntry model @@ -186,6 +187,7 @@ class SongBookEntry(BaseModel): return "%s #%s" % (songbook_name, entry) return songbook_name + class Topic(BaseModel): """ Topic model From a106245d8b1f308d9c3c284d7740a7052a5cb353 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 13:48:47 +0100 Subject: [PATCH 11/35] Display multiple songbooks in the footer --- openlp/plugins/songs/forms/editsongform.py | 5 +++-- openlp/plugins/songs/lib/db.py | 3 +++ openlp/plugins/songs/lib/mediaitem.py | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 713069dd3..92a570b9e 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -414,6 +414,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.authors_list_view.clear() self.topics_list_view.clear() self.songbooks_list_view.clear() + self.songbook_entry_edit.clear() self.audio_list_widget.clear() self.title_edit.setFocus() self.load_authors() @@ -687,7 +688,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.add_songbookentry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) self.load_songbooks() self.songbooks_combo_box.setCurrentIndex(0) - self.songbook_entry_edit.setText("") + self.songbook_entry_edit.clear() else: return elif item > 0: @@ -699,7 +700,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): else: self.add_songbookentry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) self.songbooks_combo_box.setCurrentIndex(0) - self.songbook_entry_edit.setText("") + self.songbook_entry_edit.clear() else: QtWidgets.QMessageBox.warning( self, UiStrings().NISs, diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 3bdd952a2..986739b08 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -181,6 +181,9 @@ class SongBookEntry(BaseModel): """ SongBookEntry model """ + def __repr__(self): + return SongBookEntry.get_display_name(self.songbook.name, self.entry) + @staticmethod def get_display_name(songbook_name, entry): if entry: diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 687aac9ac..56f5f99fd 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -37,7 +37,7 @@ from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm from openlp.plugins.songs.forms.songexportform import SongExportForm from openlp.plugins.songs.lib import VerseType, clean_string, delete_song -from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile +from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, SongBookEntry from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML @@ -523,8 +523,9 @@ class SongMediaItem(MediaManagerItem): item.raw_footer.append("%s %s" % (SongStrings.CopyrightSymbol, song.copyright)) else: item.raw_footer.append(song.copyright) - if self.display_songbook and song.book: - item.raw_footer.append("%s #%s" % (song.book.name, song.song_number)) + if self.display_songbook and song.songbookentries: + songbooks = [str(songbookentry) for songbookentry in song.songbookentries] + item.raw_footer.append(", ".join(songbooks)) if Settings().value('core/ccli number'): item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + Settings().value('core/ccli number')) From e4ecc3e76381c56500360ff4812a523cead361cb Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 19:46:00 +0100 Subject: [PATCH 12/35] Fix searching with multiple songbooks --- openlp/plugins/songs/forms/songbookdialog.py | 4 +- openlp/plugins/songs/lib/db.py | 2 +- openlp/plugins/songs/lib/mediaitem.py | 49 ++++++++++---------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/openlp/plugins/songs/forms/songbookdialog.py b/openlp/plugins/songs/forms/songbookdialog.py index 5bfed8e87..f132f6eb1 100644 --- a/openlp/plugins/songs/forms/songbookdialog.py +++ b/openlp/plugins/songs/forms/songbookdialog.py @@ -28,7 +28,7 @@ from openlp.core.lib.ui import create_button_box class Ui_SongBookDialog(object): """ - The user interface for the song book dialog. + The user interface for the Songbook dialog. """ def setupUi(self, song_book_dialog): """ @@ -63,6 +63,6 @@ class Ui_SongBookDialog(object): """ Translate the UI on the fly. """ - song_book_dialog.setWindowTitle(translate('SongsPlugin.SongBookForm', 'Song Book Maintenance')) + song_book_dialog.setWindowTitle(translate('SongsPlugin.SongBookForm', 'Songbook Maintenance')) self.name_label.setText(translate('SongsPlugin.SongBookForm', '&Name:')) self.publisher_label.setText(translate('SongsPlugin.SongBookForm', '&Publisher:')) diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 986739b08..5cb56c4db 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -383,7 +383,7 @@ def init_schema(url): # Use lazy='joined' to always load authors when the song is fetched from the database (bug 1366198) 'authors': relation(Author, secondary=authors_songs_table, viewonly=True, lazy='joined'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), - 'songbookentries': relation(SongBookEntry, cascade="all, delete-orphan"), + 'songbookentries': relation(SongBookEntry, backref='song', cascade="all, delete-orphan"), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) }) mapper(Topic, topics_table) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 56f5f99fd..295acee4b 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -151,7 +151,7 @@ class SongMediaItem(MediaManagerItem): (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors, translate('SongsPlugin.MediaItem', 'Search Authors...')), (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks, - translate('SongsPlugin.MediaItem', 'Search Song Books...')), + translate('SongsPlugin.MediaItem', 'Search Songbooks...')), (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes) ]) self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section)) @@ -184,17 +184,8 @@ class SongMediaItem(MediaManagerItem): Author, Author.display_name.like(search_string), Author.display_name.asc()) self.display_results_author(search_results) elif search_type == SongSearch.Books: - log.debug('Books Search') - search_string = '%' + search_keywords + '%' - search_results = self.plugin.manager.get_all_objects(Book, Book.name.like(search_string), Book.name.asc()) - song_number = False - if not search_results: - search_keywords = search_keywords.rpartition(' ') - search_string = '%' + search_keywords[0] + '%' - search_results = self.plugin.manager.get_all_objects(Book, - Book.name.like(search_string), Book.name.asc()) - song_number = re.sub(r'[^0-9]', '', search_keywords[2]) - self.display_results_book(search_results, song_number) + log.debug('Songbook Search') + self.display_results_book(search_keywords) elif search_type == SongSearch.Themes: log.debug('Theme Search') search_string = '%' + search_keywords + '%' @@ -254,21 +245,29 @@ class SongMediaItem(MediaManagerItem): song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) - def display_results_book(self, search_results, song_number=False): + def display_results_book(self, search_keywords): log.debug('display results Book') self.list_view.clear() - for book in search_results: - songs = sorted(book.songs, key=lambda song: int(re.match(r'[0-9]+', '0' + song.song_number).group())) - for song in songs: - # Do not display temporary songs - if song.temporary: - continue - if song_number and song_number not in song.song_number: - continue - song_detail = '%s - %s (%s)' % (book.name, song.song_number, song.title) - song_name = QtWidgets.QListWidgetItem(song_detail) - song_name.setData(QtCore.Qt.UserRole, song.id) - self.list_view.addItem(song_name) + + search_keywords = search_keywords.rpartition(' ') + search_book = search_keywords[0] + search_entry = re.sub(r'[^0-9]', '', search_keywords[2]) + + songbookentries = (self.plugin.manager.session.query(SongBookEntry) + .join(Book) + .order_by(Book.name) + .order_by(SongBookEntry.entry)) + for songbookentry in songbookentries: + if songbookentry.song.temporary: + continue + if search_book.lower() not in songbookentry.songbook.name.lower(): + continue + if search_entry not in songbookentry.entry: + continue + song_detail = '%s #%s: %s' % (songbookentry.songbook.name, songbookentry.entry, songbookentry.song.title) + song_name = QtWidgets.QListWidgetItem(song_detail) + song_name.setData(QtCore.Qt.UserRole, songbookentry.song.id) + self.list_view.addItem(song_name) def on_clear_text_button_click(self): """ From 245e374171553fc710adc23e4ff79bb052bdc592 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 19:50:42 +0100 Subject: [PATCH 13/35] Fix removing a songbook --- openlp/plugins/songs/forms/editsongform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 51d5a47de..90923a6d0 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -1026,6 +1026,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic = self.manager.get_object(Topic, topic_id) if topic is not None: self.song.topics.append(topic) + self.song.songbookentries = [] for row in range(self.songbooks_list_view.count()): item = self.songbooks_list_view.item(row) songbook_id = item.data(QtCore.Qt.UserRole)[0] From d274b6cb08e8ccf006c8e88113b3fccac391b7d6 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 20:00:19 +0100 Subject: [PATCH 14/35] Fix exporting OpenLyrics --- openlp/plugins/songs/lib/openlyricsxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py index 2d0863955..63d9f1025 100644 --- a/openlp/plugins/songs/lib/openlyricsxml.py +++ b/openlp/plugins/songs/lib/openlyricsxml.py @@ -269,7 +269,7 @@ class OpenLyrics(object): if song.songbookentries: songbooks = etree.SubElement(properties, 'songbooks') for songbookentry in song.songbookentries: - element = self._add_text_to_element('songbook', songbooks, None, songbookentry.songbook) + element = self._add_text_to_element('songbook', songbooks, None, songbookentry.songbook.name) if songbookentry.entry: element.set('entry', songbookentry.entry) if song.topics: From ecd2c28cc7add70caab26ce7e58d1585e26f3d94 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 20:14:00 +0100 Subject: [PATCH 15/35] Add test --- .../openlp_plugins/songs/test_db.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_plugins/songs/test_db.py b/tests/functional/openlp_plugins/songs/test_db.py index 142579c19..f159802f1 100644 --- a/tests/functional/openlp_plugins/songs/test_db.py +++ b/tests/functional/openlp_plugins/songs/test_db.py @@ -27,7 +27,7 @@ import shutil from unittest import TestCase from tempfile import mkdtemp -from openlp.plugins.songs.lib.db import Song, Author, AuthorType +from openlp.plugins.songs.lib.db import Song, Author, AuthorType, Book from openlp.plugins.songs.lib import upgrade from openlp.core.lib.db import upgrade_db from tests.utils.constants import TEST_RESOURCES_PATH @@ -179,6 +179,23 @@ class TestDB(TestCase): # THEN: It should return the name with the type in brackets self.assertEqual("John Doe (Translation)", display_name) + def test_add_songbooks(self): + """ + Test that adding songbooks to a song works correctly + """ + # GIVEN: A mocked song and songbook + song = Song() + song.songbookentries = [] + songbook = Book() + songbook.name = "Thy Word" + + # WHEN: We add two songbooks to a Song + song.add_songbookentry(songbook, "120") + song.add_songbookentry(songbook, "550A") + + # THEN: The song should have two songbook entries + self.assertEqual(len(song.songbookentries), 2, 'There should be two Songbook entries.') + def test_upgrade_old_song_db(self): """ Test that we can upgrade an old song db to the current schema From ccd8b0625f51cf2ee93ce911c1927c599630de29 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 20:20:21 +0100 Subject: [PATCH 16/35] Fix test --- tests/functional/openlp_plugins/songs/test_mediaitem.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index a19f7d704..be7774b67 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -29,7 +29,7 @@ from PyQt5 import QtCore from openlp.core.common import Registry, Settings from openlp.core.lib import ServiceItem from openlp.plugins.songs.lib.mediaitem import SongMediaItem -from openlp.plugins.songs.lib.db import AuthorType +from openlp.plugins.songs.lib.db import AuthorType, Song from tests.functional import patch, MagicMock from tests.helpers.testmixin import TestMixin @@ -155,12 +155,15 @@ class TestMediaItem(TestCase, TestMixin): Test build songs footer with basic song and a songbook """ # GIVEN: A Song and a Service Item - mock_song = MagicMock() + mock_song = Song() mock_song.title = 'My Song' mock_song.copyright = 'My copyright' + mock_song.authors_songs = [] + mock_song.ccli_number = '' mock_song.book = MagicMock() mock_song.book.name = "My songbook" - mock_song.song_number = 12 + mock_song.songbookentries = [] + mock_song.add_songbookentry(mock_song.book, '12') service_item = ServiceItem(None) # WHEN: I generate the Footer with default settings From bfdd9e81abe1d09d0fa90c112c05e2dc46a0e5cf Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 4 Jan 2016 20:23:42 +0100 Subject: [PATCH 17/35] Extend test for multiple songbooks --- .../openlp_plugins/songs/test_mediaitem.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index be7774b67..1b1116e48 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -152,7 +152,7 @@ class TestMediaItem(TestCase, TestMixin): def build_song_footer_base_songbook_test(self): """ - Test build songs footer with basic song and a songbook + Test build songs footer with basic song and multiple songbooks """ # GIVEN: A Song and a Service Item mock_song = Song() @@ -160,10 +160,13 @@ class TestMediaItem(TestCase, TestMixin): mock_song.copyright = 'My copyright' mock_song.authors_songs = [] mock_song.ccli_number = '' - mock_song.book = MagicMock() - mock_song.book.name = "My songbook" + book1 = MagicMock() + book1.name = "My songbook" + book2 = MagicMock() + book2.name = "Thy songbook" mock_song.songbookentries = [] - mock_song.add_songbookentry(mock_song.book, '12') + mock_song.add_songbookentry(book1, '12') + mock_song.add_songbookentry(book2, '502A') service_item = ServiceItem(None) # WHEN: I generate the Footer with default settings @@ -177,7 +180,7 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.generate_footer(service_item, mock_song) # THEN: The songbook should be in the footer - self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12']) + self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12, Thy songbook #502A']) def build_song_footer_copyright_enabled_test(self): """ From 6714df645e47c73241cc6884a411d38500cf1cad Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 4 Jan 2016 22:28:41 +0200 Subject: [PATCH 18/35] [song select] Stop search on viewing a song. --- openlp/plugins/songs/forms/songselectform.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py index 71111de7f..9ef3019f3 100755 --- a/openlp/plugins/songs/forms/songselectform.py +++ b/openlp/plugins/songs/forms/songselectform.py @@ -155,18 +155,30 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): return QtWidgets.QDialog.done(self, r) def _update_login_progress(self): + """ + Update the progress bar as the user logs in. + """ self.login_progress_bar.setValue(self.login_progress_bar.value() + 1) self.application.process_events() def _update_song_progress(self): + """ + Update the progress bar as the song is being downloaded. + """ self.song_progress_bar.setValue(self.song_progress_bar.value() + 1) self.application.process_events() def _view_song(self, current_item): + """ + Load a song into the song view. + """ if not current_item: return else: current_item = current_item.data(QtCore.Qt.UserRole) + # Stop the current search, if it's running + self.song_select_importer.stop() + # Clear up the UI self.song_progress_bar.setVisible(True) self.import_button.setEnabled(False) self.back_button.setEnabled(False) From b9a7170cdfc62e97c45ec8a4ed94e73eecafefee Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Tue, 5 Jan 2016 08:57:01 +0100 Subject: [PATCH 19/35] Add missing file --- openlp/core/utils/db.py | 73 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 openlp/core/utils/db.py diff --git a/openlp/core/utils/db.py b/openlp/core/utils/db.py new file mode 100644 index 000000000..59f01585b --- /dev/null +++ b/openlp/core/utils/db.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# 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:`db` module provides helper functions for database related methods. +""" +import logging + +log = logging.getLogger(__name__) + +import sqlalchemy + + +def drop_columns(op, tablename, columns): + """ + Column dropping functionality for SQLite, as there is no native support, neither in Alembic, nor in SQLite + + From https://github.com/klugjohannes/alembic-sqlite + """ + + # we need copy to make a deep copy of the column attributes + from copy import copy + + # get the db engine and reflect database tables + engine = op.get_bind() + meta = sqlalchemy.MetaData(bind=engine) + meta.reflect() + + # create a select statement from the old table + old_table = meta.tables[tablename] + select = sqlalchemy.sql.select([c for c in old_table.c if c.name not in columns]) + + # get remaining columns without table attribute attached + remaining_columns = [copy(c) for c in old_table.columns if c.name not in columns] + for column in remaining_columns: + column.table = None + + # create a temporary new table + new_tablename = '{0}_new'.format(tablename) + op.create_table(new_tablename, *remaining_columns) + meta.reflect() + new_table = meta.tables[new_tablename] + + # copy data from old table + insert = sqlalchemy.sql.insert(new_table).from_select([c.name for c in remaining_columns], select) + engine.execute(insert) + + # drop the old table and rename the new table to take the old tables + # position + op.drop_table(tablename) + op.rename_table(new_tablename, tablename) + + +def drop_column(tablename, columnname): + drop_column(tablename, [columnname]) From 5f2e0ba0ee641f4aab0eecdedf373c590b04c3aa Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Tue, 5 Jan 2016 16:14:58 +0100 Subject: [PATCH 20/35] Add test for deleting columns in a db --- openlp/core/utils/db.py | 4 +- tests/functional/openlp_core_utils/test_db.py | 96 +++++++++++++++++++ .../openlp_plugins/songs/test_db.py | 4 +- 3 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/functional/openlp_core_utils/test_db.py diff --git a/openlp/core/utils/db.py b/openlp/core/utils/db.py index 59f01585b..2edec4b90 100644 --- a/openlp/core/utils/db.py +++ b/openlp/core/utils/db.py @@ -69,5 +69,5 @@ def drop_columns(op, tablename, columns): op.rename_table(new_tablename, tablename) -def drop_column(tablename, columnname): - drop_column(tablename, [columnname]) +def drop_column(op, tablename, columnname): + drop_columns(op, tablename, [columnname]) diff --git a/tests/functional/openlp_core_utils/test_db.py b/tests/functional/openlp_core_utils/test_db.py new file mode 100644 index 000000000..01ed9d0d5 --- /dev/null +++ b/tests/functional/openlp_core_utils/test_db.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# 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 # +############################################################################### +""" +Package to test the openlp.core.utils.db package. +""" +import os +import shutil +import sqlalchemy +from unittest import TestCase +from tempfile import mkdtemp + +from openlp.core.utils.db import drop_column, drop_columns +from openlp.core.lib.db import init_db, get_upgrade_op +from tests.utils.constants import TEST_RESOURCES_PATH + + +class TestUtilsDBFunctions(TestCase): + + def setUp(self): + """ + Create temp folder for keeping db file + """ + self.tmp_folder = mkdtemp() + + def tearDown(self): + """ + Clean up + """ + shutil.rmtree(self.tmp_folder) + + def delete_column_test(self): + """ + Test deleting a single column in a table + """ + # GIVEN: A temporary song db + db_path = os.path.join(TEST_RESOURCES_PATH, 'songs', 'songs-1.9.7.sqlite') + db_tmp_path = os.path.join(self.tmp_folder, 'songs-1.9.7.sqlite') + shutil.copyfile(db_path, db_tmp_path) + db_url = 'sqlite:///' + db_tmp_path + session, metadata = init_db(db_url) + op = get_upgrade_op(session) + + # WHEN: Deleting a columns in a table + drop_column(op, 'songs', 'song_book_id') + + # THEN: The column should have been deleted + meta = sqlalchemy.MetaData(bind=op.get_bind()) + meta.reflect() + columns = meta.tables['songs'].columns + + for column in columns: + if column.name == 'song_book_id': + self.fail("The column 'song_book_id' should have been deleted.") + + def delete_columns_test(self): + """ + Test deleting multiple columns in a table + """ + # GIVEN: A temporary song db + db_path = os.path.join(TEST_RESOURCES_PATH, 'songs', 'songs-1.9.7.sqlite') + db_tmp_path = os.path.join(self.tmp_folder, 'songs-1.9.7.sqlite') + shutil.copyfile(db_path, db_tmp_path) + db_url = 'sqlite:///' + db_tmp_path + session, metadata = init_db(db_url) + op = get_upgrade_op(session) + + # WHEN: Deleting a columns in a table + drop_columns(op, 'songs', ['song_book_id', 'song_number']) + + # THEN: The columns should have been deleted + meta = sqlalchemy.MetaData(bind=op.get_bind()) + meta.reflect() + columns = meta.tables['songs'].columns + + for column in columns: + if column.name == 'song_book_id' or column.name == 'song_number': + self.fail("The column '%s' should have been deleted." % column.name) diff --git a/tests/functional/openlp_plugins/songs/test_db.py b/tests/functional/openlp_plugins/songs/test_db.py index f159802f1..9aff5b9a1 100644 --- a/tests/functional/openlp_plugins/songs/test_db.py +++ b/tests/functional/openlp_plugins/songs/test_db.py @@ -209,7 +209,7 @@ class TestDB(TestCase): # WHEN: upgrading the db updated_to_version, latest_version = upgrade_db(db_url, upgrade) - # Then the song db should have been upgraded to the latest version + # THEN: the song db should have been upgraded to the latest version self.assertEqual(updated_to_version, latest_version, 'The song DB should have been upgrade to the latest version') @@ -226,6 +226,6 @@ class TestDB(TestCase): # WHEN: upgrading the db updated_to_version, latest_version = upgrade_db(db_url, upgrade) - # Then the song db should have been upgraded to the latest version without errors + # THEN: the song db should have been upgraded to the latest version without errors self.assertEqual(updated_to_version, latest_version, 'The song DB should have been upgrade to the latest version') From 8780b7434223fb561ed354053e91d5cd4f189d94 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Tue, 5 Jan 2016 16:17:22 +0100 Subject: [PATCH 21/35] Use deepcopy instead of copy --- openlp/core/utils/db.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openlp/core/utils/db.py b/openlp/core/utils/db.py index 2edec4b90..8fdcbbfb9 100644 --- a/openlp/core/utils/db.py +++ b/openlp/core/utils/db.py @@ -22,11 +22,12 @@ """ The :mod:`db` module provides helper functions for database related methods. """ +import sqlalchemy import logging -log = logging.getLogger(__name__) +from copy import deepcopy -import sqlalchemy +log = logging.getLogger(__name__) def drop_columns(op, tablename, columns): @@ -36,9 +37,6 @@ def drop_columns(op, tablename, columns): From https://github.com/klugjohannes/alembic-sqlite """ - # we need copy to make a deep copy of the column attributes - from copy import copy - # get the db engine and reflect database tables engine = op.get_bind() meta = sqlalchemy.MetaData(bind=engine) @@ -49,7 +47,7 @@ def drop_columns(op, tablename, columns): select = sqlalchemy.sql.select([c for c in old_table.c if c.name not in columns]) # get remaining columns without table attribute attached - remaining_columns = [copy(c) for c in old_table.columns if c.name not in columns] + remaining_columns = [deepcopy(c) for c in old_table.columns if c.name not in columns] for column in remaining_columns: column.table = None From ddefceee04e27484abf4e86233f693e30f3ef91d Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Thu, 7 Jan 2016 23:12:03 +0100 Subject: [PATCH 22/35] Fix migration for non-sqlite --- openlp/core/utils/db.py | 9 ++++----- openlp/plugins/songs/lib/upgrade.py | 9 +++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openlp/core/utils/db.py b/openlp/core/utils/db.py index 8fdcbbfb9..3009dad91 100644 --- a/openlp/core/utils/db.py +++ b/openlp/core/utils/db.py @@ -30,9 +30,12 @@ from copy import deepcopy log = logging.getLogger(__name__) +def drop_column(op, tablename, columnname): + drop_columns(op, tablename, [columnname]) + def drop_columns(op, tablename, columns): """ - Column dropping functionality for SQLite, as there is no native support, neither in Alembic, nor in SQLite + Column dropping functionality for SQLite, as there is no DROP COLUMN support in SQLite From https://github.com/klugjohannes/alembic-sqlite """ @@ -65,7 +68,3 @@ def drop_columns(op, tablename, columns): # position op.drop_table(tablename) op.rename_table(new_tablename, tablename) - - -def drop_column(op, tablename, columnname): - drop_columns(op, tablename, [columnname]) diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py index a132d341a..09f7ce92a 100644 --- a/openlp/plugins/songs/lib/upgrade.py +++ b/openlp/plugins/songs/lib/upgrade.py @@ -139,7 +139,12 @@ def upgrade_5(session, metadata): # Migrate old data op.execute('INSERT INTO songs_songbooks SELECT song_book_id, id, song_number FROM songs\ - WHERE song_book_id NOT NULL AND song_number NOT NULL') + WHERE song_book_id IS NOT NULL AND song_number IS NOT NULL') # Drop old columns - drop_columns(op, 'songs', ['song_book_id', 'song_number']) + if metadata.bind.url.get_dialect().name == 'sqlite': + drop_columns(op, 'songs', ['song_book_id', 'song_number']) + else: + op.drop_constraint('songs_ibfk_1', 'songs', 'foreignkey') + op.drop_column('songs', 'song_book_id') + op.drop_column('songs', 'song_number') From 69e1660d6c58fff54b3f62461936f9115889841e Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Fri, 8 Jan 2016 15:05:54 +0100 Subject: [PATCH 23/35] PEP8 --- openlp/core/utils/db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/core/utils/db.py b/openlp/core/utils/db.py index 3009dad91..1e18167ab 100644 --- a/openlp/core/utils/db.py +++ b/openlp/core/utils/db.py @@ -33,6 +33,7 @@ log = logging.getLogger(__name__) def drop_column(op, tablename, columnname): drop_columns(op, tablename, [columnname]) + def drop_columns(op, tablename, columns): """ Column dropping functionality for SQLite, as there is no DROP COLUMN support in SQLite From ad5246fc4aa081ff550668fecbac54a1912cdf6a Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sat, 9 Jan 2016 16:23:11 +0100 Subject: [PATCH 24/35] songbookentry -> songbook_entry, mock_song -> song --- openlp/plugins/songs/forms/editsongform.py | 20 +++++++++---------- openlp/plugins/songs/lib/db.py | 16 +++++++-------- openlp/plugins/songs/lib/mediaitem.py | 18 ++++++++--------- openlp/plugins/songs/lib/openlyricsxml.py | 12 +++++------ .../openlp_plugins/songs/test_mediaitem.py | 20 +++++++++---------- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 94b3731c0..aefee873f 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -129,10 +129,10 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): author_item.setData(QtCore.Qt.UserRole, (author.id, author_type)) self.authors_list_view.addItem(author_item) - def add_songbookentry_to_list(self, songbook_id, songbook_name, entry): - songbookentry_item = QtWidgets.QListWidgetItem(SongBookEntry.get_display_name(songbook_name, entry)) - songbookentry_item.setData(QtCore.Qt.UserRole, (songbook_id, entry)) - self.songbooks_list_view.addItem(songbookentry_item) + def add_songbook_entry_to_list(self, songbook_id, songbook_name, entry): + songbook_entry_item = QtWidgets.QListWidgetItem(SongBookEntry.get_display_name(songbook_name, entry)) + songbook_entry_item.setData(QtCore.Qt.UserRole, (songbook_id, entry)) + self.songbooks_list_view.addItem(songbook_entry_item) def _extract_verse_order(self, verse_order): """ @@ -515,8 +515,8 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic_name.setData(QtCore.Qt.UserRole, topic.id) self.topics_list_view.addItem(topic_name) self.songbooks_list_view.clear() - for songbookentry in self.song.songbookentries: - self.add_songbookentry_to_list(songbookentry.songbook.id, songbookentry.songbook.name, songbookentry.entry) + for songbook_entry in self.song.songbook_entries: + self.add_songbook_entry_to_list(songbook_entry.songbook.id, songbook_entry.songbook.name, songbook_entry.entry) self.audio_list_widget.clear() for media in self.song.media_files: media_file = QtWidgets.QListWidgetItem(os.path.split(media.file_name)[1]) @@ -686,7 +686,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes: songbook = Book.populate(name=text) self.manager.save_object(songbook) - self.add_songbookentry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) + self.add_songbook_entry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) self.load_songbooks() self.songbooks_combo_box.setCurrentIndex(0) self.songbook_entry_edit.clear() @@ -699,7 +699,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): critical_error_message_box( message=translate('SongsPlugin.EditSongForm', 'This Songbook is already in the list.')) else: - self.add_songbookentry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) + self.add_songbook_entry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) self.songbooks_combo_box.setCurrentIndex(0) self.songbook_entry_edit.clear() else: @@ -1029,13 +1029,13 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic = self.manager.get_object(Topic, topic_id) if topic is not None: self.song.topics.append(topic) - self.song.songbookentries = [] + self.song.songbook_entries = [] for row in range(self.songbooks_list_view.count()): item = self.songbooks_list_view.item(row) songbook_id = item.data(QtCore.Qt.UserRole)[0] songbook = self.manager.get_object(Book, songbook_id) entry = item.data(QtCore.Qt.UserRole)[1] - self.song.add_songbookentry(songbook, entry) + self.song.add_songbook_entry(songbook, entry) # Save the song here because we need a valid id for the audio files. clean_song(self.manager, self.song) self.manager.save_object(self.song) diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 243e35704..8fc6e1a4a 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -160,21 +160,21 @@ class Song(BaseModel): self.authors_songs.remove(author_song) return - def add_songbookentry(self, songbook, entry): + def add_songbook_entry(self, songbook, entry): """ Add a Songbook Entry to the song if it not yet exists :param songbook_name: Name of the Songbook. :param entry: Entry in the Songbook (usually a number) """ - for songbookentry in self.songbookentries: - if songbookentry.songbook.name == songbook.name and songbookentry.entry == entry: + for songbook_entry in self.songbook_entries: + if songbook_entry.songbook.name == songbook.name and songbook_entry.entry == entry: return - new_songbookentry = SongBookEntry() - new_songbookentry.songbook = songbook - new_songbookentry.entry = entry - self.songbookentries.append(new_songbookentry) + new_songbook_entry = SongBookEntry() + new_songbook_entry.songbook = songbook + new_songbook_entry.entry = entry + self.songbook_entries.append(new_songbook_entry) class SongBookEntry(BaseModel): @@ -383,7 +383,7 @@ def init_schema(url): # Use lazy='joined' to always load authors when the song is fetched from the database (bug 1366198) 'authors': relation(Author, secondary=authors_songs_table, viewonly=True, lazy='joined'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), - 'songbookentries': relation(SongBookEntry, backref='song', cascade="all, delete-orphan"), + 'songbook_entries': relation(SongBookEntry, backref='song', cascade="all, delete-orphan"), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) }) mapper(Topic, topics_table) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index bc6ed5f0d..f3cd2ee3e 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -254,20 +254,20 @@ class SongMediaItem(MediaManagerItem): search_book = search_keywords[0] search_entry = re.sub(r'[^0-9]', '', search_keywords[2]) - songbookentries = (self.plugin.manager.session.query(SongBookEntry) + songbook_entries = (self.plugin.manager.session.query(SongBookEntry) .join(Book) .order_by(Book.name) .order_by(SongBookEntry.entry)) - for songbookentry in songbookentries: - if songbookentry.song.temporary: + for songbook_entry in songbook_entries: + if songbook_entry.song.temporary: continue - if search_book.lower() not in songbookentry.songbook.name.lower(): + if search_book.lower() not in songbook_entry.songbook.name.lower(): continue - if search_entry not in songbookentry.entry: + if search_entry not in songbook_entry.entry: continue - song_detail = '%s #%s: %s' % (songbookentry.songbook.name, songbookentry.entry, songbookentry.song.title) + song_detail = '%s #%s: %s' % (songbook_entry.songbook.name, songbook_entry.entry, songbook_entry.song.title) song_name = QtWidgets.QListWidgetItem(song_detail) - song_name.setData(QtCore.Qt.UserRole, songbookentry.song.id) + song_name.setData(QtCore.Qt.UserRole, songbook_entry.song.id) self.list_view.addItem(song_name) def on_clear_text_button_click(self): @@ -523,8 +523,8 @@ class SongMediaItem(MediaManagerItem): item.raw_footer.append("%s %s" % (SongStrings.CopyrightSymbol, song.copyright)) else: item.raw_footer.append(song.copyright) - if self.display_songbook and song.songbookentries: - songbooks = [str(songbookentry) for songbookentry in song.songbookentries] + if self.display_songbook and song.songbook_entries: + songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries] item.raw_footer.append(", ".join(songbooks)) if Settings().value('core/ccli number'): item.raw_footer.append(translate('SongsPlugin.MediaItem', diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py index 63d9f1025..bba60baa2 100644 --- a/openlp/plugins/songs/lib/openlyricsxml.py +++ b/openlp/plugins/songs/lib/openlyricsxml.py @@ -266,12 +266,12 @@ class OpenLyrics(object): element.set('type', AuthorType.Music) else: element.set('type', author_song.author_type) - if song.songbookentries: + if song.songbook_entries: songbooks = etree.SubElement(properties, 'songbooks') - for songbookentry in song.songbookentries: - element = self._add_text_to_element('songbook', songbooks, None, songbookentry.songbook.name) - if songbookentry.entry: - element.set('entry', songbookentry.entry) + for songbook_entry in song.songbook_entries: + element = self._add_text_to_element('songbook', songbooks, None, songbook_entry.songbook.name) + if songbook_entry.entry: + element.set('entry', songbook_entry.entry) if song.topics: themes = etree.SubElement(properties, 'themes') for topic in song.topics: @@ -752,7 +752,7 @@ class OpenLyrics(object): # We need to create a book, because it does not exist. book = Book.populate(name=book_name, publisher='') self.manager.save_object(book) - song.add_songbookentry(book, songbook.get('entry', '')) + song.add_songbook_entry(book, songbook.get('entry', '')) def _process_titles(self, properties, song): """ diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 702414545..2aa5c1c6b 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -155,29 +155,29 @@ class TestMediaItem(TestCase, TestMixin): Test build songs footer with basic song and multiple songbooks """ # GIVEN: A Song and a Service Item - mock_song = Song() - mock_song.title = 'My Song' - mock_song.copyright = 'My copyright' - mock_song.authors_songs = [] - mock_song.ccli_number = '' + song = Song() + song.title = 'My Song' + song.copyright = 'My copyright' + song.authors_songs = [] + song.ccli_number = '' book1 = MagicMock() book1.name = "My songbook" book2 = MagicMock() book2.name = "Thy songbook" - mock_song.songbookentries = [] - mock_song.add_songbookentry(book1, '12') - mock_song.add_songbookentry(book2, '502A') + song.songbookentries = [] + song.add_songbookentry(book1, '12') + song.add_songbookentry(book2, '502A') service_item = ServiceItem(None) # WHEN: I generate the Footer with default settings - self.media_item.generate_footer(service_item, mock_song) + self.media_item.generate_footer(service_item, song) # THEN: The songbook should not be in the footer self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright']) # WHEN: I activate the "display songbook" option self.media_item.display_songbook = True - self.media_item.generate_footer(service_item, mock_song) + self.media_item.generate_footer(service_item, song) # THEN: The songbook should be in the footer self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12, Thy songbook #502A']) From 1e5c1875520054ea293d887df47fe2e6630e8ac1 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sat, 9 Jan 2016 16:30:27 +0100 Subject: [PATCH 25/35] PEP8 --- openlp/plugins/songs/forms/editsongform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index aefee873f..dfc29719d 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -516,7 +516,8 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.topics_list_view.addItem(topic_name) self.songbooks_list_view.clear() for songbook_entry in self.song.songbook_entries: - self.add_songbook_entry_to_list(songbook_entry.songbook.id, songbook_entry.songbook.name, songbook_entry.entry) + self.add_songbook_entry_to_list(songbook_entry.songbook.id, songbook_entry.songbook.name, + songbook_entry.entry) self.audio_list_widget.clear() for media in self.song.media_files: media_file = QtWidgets.QListWidgetItem(os.path.split(media.file_name)[1]) From 2785fbaa2d46a491241f71f7d7cd1c6b8fa0ee6c Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sat, 9 Jan 2016 16:53:49 +0100 Subject: [PATCH 26/35] Fix tests --- tests/functional/openlp_plugins/songs/test_db.py | 8 ++++---- tests/functional/openlp_plugins/songs/test_mediaitem.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_db.py b/tests/functional/openlp_plugins/songs/test_db.py index 9aff5b9a1..b2955b87e 100644 --- a/tests/functional/openlp_plugins/songs/test_db.py +++ b/tests/functional/openlp_plugins/songs/test_db.py @@ -185,16 +185,16 @@ class TestDB(TestCase): """ # GIVEN: A mocked song and songbook song = Song() - song.songbookentries = [] + song.songbook_entries = [] songbook = Book() songbook.name = "Thy Word" # WHEN: We add two songbooks to a Song - song.add_songbookentry(songbook, "120") - song.add_songbookentry(songbook, "550A") + song.add_songbook_entry(songbook, "120") + song.add_songbook_entry(songbook, "550A") # THEN: The song should have two songbook entries - self.assertEqual(len(song.songbookentries), 2, 'There should be two Songbook entries.') + self.assertEqual(len(song.songbook_entries), 2, 'There should be two Songbook entries.') def test_upgrade_old_song_db(self): """ diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 2aa5c1c6b..c94469050 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -159,14 +159,15 @@ class TestMediaItem(TestCase, TestMixin): song.title = 'My Song' song.copyright = 'My copyright' song.authors_songs = [] + song.songbook_entries = [] song.ccli_number = '' book1 = MagicMock() book1.name = "My songbook" book2 = MagicMock() book2.name = "Thy songbook" song.songbookentries = [] - song.add_songbookentry(book1, '12') - song.add_songbookentry(book2, '502A') + song.add_songbook_entry(book1, '12') + song.add_songbook_entry(book2, '502A') service_item = ServiceItem(None) # WHEN: I generate the Footer with default settings From 94df2d2d86d3aebee8e609194c1552062912ad13 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sat, 9 Jan 2016 09:21:20 -0800 Subject: [PATCH 27/35] Fix test for pjlink ticket 921817 --- .../test_projector_utilities.py | 5 ---- .../openlp_core_lib/test_projector_pjlink1.py | 16 +++++++++---- tests/resources/projector/data.py | 23 +------------------ 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/tests/functional/openlp_core_common/test_projector_utilities.py b/tests/functional/openlp_core_common/test_projector_utilities.py index 7798da0d6..d29267de0 100644 --- a/tests/functional/openlp_core_common/test_projector_utilities.py +++ b/tests/functional/openlp_core_common/test_projector_utilities.py @@ -30,11 +30,6 @@ from unittest import TestCase from openlp.core.common import verify_ip_address, md5_hash, qmd5_hash from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_HASH -''' -salt = '498e4a67' -pin = 'JBMIAProjectorLink' -test_hash = '5d8409bc1c3fa39749434aa3a5c38682' -''' salt = TEST_SALT pin = TEST_PIN test_hash = TEST_HASH diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index e48130c29..ad11e93f6 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -31,16 +31,16 @@ from openlp.core.lib.projector.pjlink1 import PJLink1 from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE -pjlink = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True) +pjlink_test = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True) class TestPJLink(TestCase): """ Tests for the PJLink module """ - @patch.object(pjlink, 'readyRead') - @patch.object(pjlink, 'send_command') - @patch.object(pjlink, 'waitForReadyRead') + @patch.object(pjlink_test, 'readyRead') + @patch.object(pjlink_test, 'send_command') + @patch.object(pjlink_test, 'waitForReadyRead') @patch('openlp.core.common.qmd5_hash') def ticket_92187_test(self, mock_qmd5_hash, @@ -50,8 +50,14 @@ class TestPJLink(TestCase): """ Fix for projector connect with PJLink authentication exception """ + # GIVEN: Test object + pjlink = pjlink_test + # WHEN: Calling check_login with authentication request: pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE) # THEN: Should have called qmd5_hash - mock_qmd5_hash.called_with(TEST_SALT, TEST_PIN) + self.assertTrue(mock_qmd5_hash.called_with(TEST_SALT, + "Connection request should have been called with TEST_SALT")) + self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN, + "Connection request should have been called with TEST_PIN")) diff --git a/tests/resources/projector/data.py b/tests/resources/projector/data.py index 58e5f2df7..fca9c0ca0 100644 --- a/tests/resources/projector/data.py +++ b/tests/resources/projector/data.py @@ -37,27 +37,6 @@ TEST_HASH = '5d8409bc1c3fa39749434aa3a5c38682' TEST_CONNECT_AUTHENTICATE = 'PJLink 1 {salt}'.format(salt=TEST_SALT) -TEST1_DATA = Projector(ip='111.111.111.111', - port='1111', - pin='1111', - name='___TEST_ONE___', - location='location one', - notes='notes one') - -TEST2_DATA = Projector(ip='222.222.222.222', - port='2222', - pin='2222', - name='___TEST_TWO___', - location='location two', - notes='notes two') - -TEST3_DATA = Projector(ip='333.333.333.333', - port='3333', - pin='3333', - name='___TEST_THREE___', - location='location three', - notes='notes three') - TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql') TEST1_DATA = dict(ip='111.111.111.111', @@ -80,4 +59,4 @@ TEST3_DATA = dict(ip='333.333.333.333', name='___TEST_THREE___', location='location three', notes='notes three') ->>>>>>> MERGE-SOURCE + From f8d353efcb787389478e2aba682669e9187c05d3 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sat, 9 Jan 2016 09:43:38 -0800 Subject: [PATCH 28/35] Rename test method --- tests/functional/openlp_core_lib/test_projector_pjlink1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index ad11e93f6..5345995c6 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -42,13 +42,13 @@ class TestPJLink(TestCase): @patch.object(pjlink_test, 'send_command') @patch.object(pjlink_test, 'waitForReadyRead') @patch('openlp.core.common.qmd5_hash') - def ticket_92187_test(self, + def authenticated_connection_call_test(self, mock_qmd5_hash, mock_waitForReadyRead, mock_send_command, mock_readyRead): """ - Fix for projector connect with PJLink authentication exception + Fix for projector connect with PJLink authentication exception. Ticket 92187. """ # GIVEN: Test object pjlink = pjlink_test From bcbb5233758dfd4d26c62af2b5c997fe8f562e43 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sat, 9 Jan 2016 09:46:20 -0800 Subject: [PATCH 29/35] pep8 --- .../functional/openlp_core_lib/test_projector_pjlink1.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index 5345995c6..4079ab9f0 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -43,10 +43,10 @@ class TestPJLink(TestCase): @patch.object(pjlink_test, 'waitForReadyRead') @patch('openlp.core.common.qmd5_hash') def authenticated_connection_call_test(self, - mock_qmd5_hash, - mock_waitForReadyRead, - mock_send_command, - mock_readyRead): + mock_qmd5_hash, + mock_waitForReadyRead, + mock_send_command, + mock_readyRead): """ Fix for projector connect with PJLink authentication exception. Ticket 92187. """ From 9904ba8da8ccb36219612b0b6cba10b4a4fe99df Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 9 Jan 2016 21:10:56 +0200 Subject: [PATCH 30/35] Remove media manager stylesheet for now --- openlp/core/ui/mainwindow.py | 3 +- .../openlp_core_ui/test_mainwindow.py | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index aed44468e..0b35cf890 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -153,7 +153,8 @@ class Ui_MainWindow(object): # Create the MediaManager self.media_manager_dock = OpenLPDockWidget(main_window, 'media_manager_dock', ':/system/system_mediamanager.png') - self.media_manager_dock.setStyleSheet(MEDIA_MANAGER_STYLE) + # TODO: Figure out how to fix the stylesheet and add it back in + # self.media_manager_dock.setStyleSheet(MEDIA_MANAGER_STYLE) # Create the media toolbox self.media_tool_box = QtWidgets.QToolBox(self.media_manager_dock) self.media_tool_box.setObjectName('media_tool_box') diff --git a/tests/functional/openlp_core_ui/test_mainwindow.py b/tests/functional/openlp_core_ui/test_mainwindow.py index 76566815b..5499a78a7 100644 --- a/tests/functional/openlp_core_ui/test_mainwindow.py +++ b/tests/functional/openlp_core_ui/test_mainwindow.py @@ -56,6 +56,15 @@ class TestMainWindow(TestCase, TestMixin): patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \ patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \ patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer: + self.mocked_settings_form = mocked_settings_form + self.mocked_image_manager = mocked_image_manager + self.mocked_live_controller = mocked_live_controller + self.mocked_preview_controller = mocked_preview_controller + self.mocked_dock_widget = mocked_dock_widget + self.mocked_q_tool_box_class = mocked_q_tool_box_class + self.mocked_add_dock_method = mocked_add_dock_method + self.mocked_theme_manager = mocked_theme_manager + self.mocked_renderer = mocked_renderer self.main_window = MainWindow() def tearDown(self): @@ -146,3 +155,39 @@ class TestMainWindow(TestCase, TestMixin): 'registered.') self.assertTrue('plugin_manager' in self.registry.service_list, 'The plugin_manager should have been registered.') + + def on_search_shortcut_triggered_shows_media_manager_test(self): + """ + Test that the media manager is made visible when the search shortcut is triggered + """ + # GIVEN: A build main window set up for testing + with patch.object(self.main_window, 'media_manager_dock') as mocked_media_manager_dock, \ + patch.object(self.main_window, 'media_tool_box') as mocked_media_tool_box: + mocked_media_manager_dock.isVisible.return_value = False + mocked_media_tool_box.currentWidget.return_value = None + + # WHEN: The search shortcut is triggered + self.main_window.on_search_shortcut_triggered() + + # THEN: The media manager dock is made visible + mocked_media_manager_dock.setVisible.assert_called_with(True) + + def on_search_shortcut_triggered_focuses_widget_test(self): + """ + Test that the focus is set on the widget when the search shortcut is triggered + """ + # GIVEN: A build main window set up for testing + with patch.object(self.main_window, 'media_manager_dock') as mocked_media_manager_dock, \ + patch.object(self.main_window, 'media_tool_box') as mocked_media_tool_box: + mocked_media_manager_dock.isVisible.return_value = True + mocked_widget = MagicMock() + mocked_media_tool_box.currentWidget.return_value = mocked_widget + + # WHEN: The search shortcut is triggered + self.main_window.on_search_shortcut_triggered() + + # THEN: The media manager dock is made visible + self.assertEqual(0, mocked_media_manager_dock.setVisible.call_count) + mocked_widget.on_focus.assert_called_with() + + From 076cc57d390c7a3dfd2a598eccbc70dc502f85ce Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Sun, 10 Jan 2016 01:17:58 +0100 Subject: [PATCH 31/35] Fix Song import --- openlp/plugins/songs/lib/importer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 545e14d60..5e099dde9 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -390,7 +390,7 @@ class SongFormat(object): """ Return a list of the supported song formats. """ - return [ + return sorted([ SongFormat.OpenLyrics, SongFormat.OpenLP2, SongFormat.Generic, @@ -400,6 +400,7 @@ class SongFormat(object): SongFormat.EasyWorshipDB, SongFormat.EasyWorshipService, SongFormat.FoilPresenter, + SongFormat.Lyrix, SongFormat.MediaShout, SongFormat.OpenSong, SongFormat.PowerPraise, @@ -411,13 +412,12 @@ class SongFormat(object): SongFormat.SongShowPlus, SongFormat.SongsOfFellowship, SongFormat.SundayPlus, + SongFormat.VideoPsalm, SongFormat.WordsOfWorship, SongFormat.WorshipAssistant, SongFormat.WorshipCenterPro, SongFormat.ZionWorx, - SongFormat.Lyrix, - SongFormat.VideoPsalm - ] + ]) @staticmethod def get(song_format, *attributes): From 70e1f3926b5717d5b941dd558b4310348d47815a Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Sun, 10 Jan 2016 01:18:27 +0100 Subject: [PATCH 32/35] Add test for fix --- tests/functional/openlp_plugins/songs/test_songformat.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/functional/openlp_plugins/songs/test_songformat.py b/tests/functional/openlp_plugins/songs/test_songformat.py index 0c82b049c..3611bf749 100644 --- a/tests/functional/openlp_plugins/songs/test_songformat.py +++ b/tests/functional/openlp_plugins/songs/test_songformat.py @@ -81,3 +81,11 @@ class TestSongFormat(TestCase): # THEN: Return all attributes that were specified self.assertEquals(len(SongFormat.get(song_format, 'canDisable', 'availability')), 2, "Did not return the correct number of attributes when retrieving multiple attributes at once") + + def test_get_format_list_returns_ordered_list(self): + """ + Test that get_format_list() returns a list that is ordered + according to the order specified in SongFormat + """ + self.assertEquals(sorted(SongFormat.get_format_list()), SongFormat.get_format_list(), + "The list returned should be sorted according to the ordering in SongFormat") From 9525453679b9005219d55810d7f27d69835b9a71 Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Sun, 10 Jan 2016 01:22:31 +0100 Subject: [PATCH 33/35] Ignore case when matching verse tags during SongBeamerImport --- .../plugins/songs/lib/importers/songbeamer.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/openlp/plugins/songs/lib/importers/songbeamer.py b/openlp/plugins/songs/lib/importers/songbeamer.py index 0146cb45b..71d047ab5 100644 --- a/openlp/plugins/songs/lib/importers/songbeamer.py +++ b/openlp/plugins/songs/lib/importers/songbeamer.py @@ -36,28 +36,28 @@ log = logging.getLogger(__name__) class SongBeamerTypes(object): MarkTypes = { - 'Refrain': VerseType.tags[VerseType.Chorus], - 'Chorus': VerseType.tags[VerseType.Chorus], - 'Vers': VerseType.tags[VerseType.Verse], - 'Verse': VerseType.tags[VerseType.Verse], - 'Strophe': VerseType.tags[VerseType.Verse], - 'Intro': VerseType.tags[VerseType.Intro], - 'Coda': VerseType.tags[VerseType.Ending], - 'Ending': VerseType.tags[VerseType.Ending], - 'Bridge': VerseType.tags[VerseType.Bridge], - 'Interlude': VerseType.tags[VerseType.Bridge], - 'Zwischenspiel': VerseType.tags[VerseType.Bridge], - 'Pre-Chorus': VerseType.tags[VerseType.PreChorus], - 'Pre-Refrain': VerseType.tags[VerseType.PreChorus], - 'Misc': VerseType.tags[VerseType.Other], - 'Pre-Bridge': VerseType.tags[VerseType.Other], - 'Pre-Coda': VerseType.tags[VerseType.Other], - 'Part': VerseType.tags[VerseType.Other], - 'Teil': VerseType.tags[VerseType.Other], - 'Unbekannt': VerseType.tags[VerseType.Other], - 'Unknown': VerseType.tags[VerseType.Other], - 'Unbenannt': VerseType.tags[VerseType.Other], - '$$M=': VerseType.tags[VerseType.Other] + 'refrain': VerseType.tags[VerseType.Chorus], + 'chorus': VerseType.tags[VerseType.Chorus], + 'vers': VerseType.tags[VerseType.Verse], + 'verse': VerseType.tags[VerseType.Verse], + 'strophe': VerseType.tags[VerseType.Verse], + 'intro': VerseType.tags[VerseType.Intro], + 'coda': VerseType.tags[VerseType.Ending], + 'ending': VerseType.tags[VerseType.Ending], + 'bridge': VerseType.tags[VerseType.Bridge], + 'interlude': VerseType.tags[VerseType.Bridge], + 'zwischenspiel': VerseType.tags[VerseType.Bridge], + 'pre-chorus': VerseType.tags[VerseType.PreChorus], + 'pre-refrain': VerseType.tags[VerseType.PreChorus], + 'misc': VerseType.tags[VerseType.Other], + 'pre-bridge': VerseType.tags[VerseType.Other], + 'pre-coda': VerseType.tags[VerseType.Other], + 'part': VerseType.tags[VerseType.Other], + 'teil': VerseType.tags[VerseType.Other], + 'unbekannt': VerseType.tags[VerseType.Other], + 'unknown': VerseType.tags[VerseType.Other], + 'unbenannt': VerseType.tags[VerseType.Other], + '$$m=': VerseType.tags[VerseType.Other] } @@ -267,20 +267,20 @@ class SongBeamerImport(SongImport): def check_verse_marks(self, line): """ - Check and add the verse's MarkType. Returns ``True`` if the given linE contains a correct verse mark otherwise + Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise ``False``. :param line: The line to check for marks (unicode). """ marks = line.split(' ') - if len(marks) <= 2 and marks[0] in SongBeamerTypes.MarkTypes: - self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0]] + if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes: + self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()] if len(marks) == 2: # If we have a digit, we append it to current_verse_type. if marks[1].isdigit(): self.current_verse_type += marks[1] return True - elif marks[0].startswith('$$M='): # this verse-mark cannot be numbered - self.current_verse_type = SongBeamerTypes.MarkTypes['$$M='] + elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered + self.current_verse_type = SongBeamerTypes.MarkTypes['$$m='] return True return False From 0f1d6718bd1c118741b7b63edb079693f2f4ee83 Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Sun, 10 Jan 2016 01:34:53 +0100 Subject: [PATCH 34/35] Add tests for ignored case --- .../songs/test_songbeamerimport.py | 23 +++++++++++++------ .../openlp_plugins/songs/test_songformat.py | 5 +++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_songbeamerimport.py b/tests/functional/openlp_plugins/songs/test_songbeamerimport.py index d4b23b207..9f20e6a08 100644 --- a/tests/functional/openlp_plugins/songs/test_songbeamerimport.py +++ b/tests/functional/openlp_plugins/songs/test_songbeamerimport.py @@ -28,7 +28,7 @@ from unittest import TestCase from tests.helpers.songfileimport import SongImportTestHelper from tests.functional import MagicMock, patch -from openlp.plugins.songs.lib.importers.songbeamer import SongBeamerImport +from openlp.plugins.songs.lib.importers.songbeamer import SongBeamerImport, SongBeamerTypes from openlp.core.common import Registry TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), @@ -131,22 +131,22 @@ class TestSongBeamerImport(TestCase): self.assertEqual(self.current_verse_type, 'c', ' should be interpreted as ') # GIVEN: line with unnumbered verse-type and trailing space - line = 'Refrain ' + line = 'ReFrain ' self.current_verse_type = None # WHEN: line is being checked for verse marks result = SongBeamerImport.check_verse_marks(self, line) # THEN: we should get back true and c as self.current_verse_type - self.assertTrue(result, 'Versemark for should be found, value true') - self.assertEqual(self.current_verse_type, 'c', ' should be interpreted as ') + self.assertTrue(result, 'Versemark for should be found, value true') + self.assertEqual(self.current_verse_type, 'c', ' should be interpreted as ') # GIVEN: line with numbered verse-type - line = 'Verse 1' + line = 'VersE 1' self.current_verse_type = None # WHEN: line is being checked for verse marks result = SongBeamerImport.check_verse_marks(self, line) # THEN: we should get back true and v1 as self.current_verse_type - self.assertTrue(result, 'Versemark for should be found, value true') - self.assertEqual(self.current_verse_type, 'v1', u' should be interpreted as ') + self.assertTrue(result, 'Versemark for should be found, value true') + self.assertEqual(self.current_verse_type, 'v1', u' should be interpreted as ') # GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags) line = '$$M=special' @@ -192,3 +192,12 @@ class TestSongBeamerImport(TestCase): # THEN: we should get back false and none as self.current_verse_type self.assertFalse(result, 'No versemark for <> should be found, value false') self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark') + + def test_verse_marks_defined_in_lowercase(self): + """ + Test that the verse marks are all defined in lowercase + """ + # GIVEN: SongBeamber MarkTypes + for tag in SongBeamerTypes.MarkTypes.keys(): + # THEN: tag should be defined in lowercase + self.assertEquals(tag, tag.lower(), 'Tags should be defined in lowercase') diff --git a/tests/functional/openlp_plugins/songs/test_songformat.py b/tests/functional/openlp_plugins/songs/test_songformat.py index 3611bf749..19e1470c3 100644 --- a/tests/functional/openlp_plugins/songs/test_songformat.py +++ b/tests/functional/openlp_plugins/songs/test_songformat.py @@ -87,5 +87,8 @@ class TestSongFormat(TestCase): Test that get_format_list() returns a list that is ordered according to the order specified in SongFormat """ + # GIVEN: The SongFormat class + # WHEN: Retrieving all formats + # THEN: The returned list should be sorted according to the ordering defined in SongFormat self.assertEquals(sorted(SongFormat.get_format_list()), SongFormat.get_format_list(), - "The list returned should be sorted according to the ordering in SongFormat") + "The list returned should be sorted according to the ordering in SongFormat") From 0e8dc032dac4e78809216c174d01de78fcb30634 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sun, 10 Jan 2016 15:15:22 +0200 Subject: [PATCH 35/35] Release 2.3.2 bzr-revno: 2602 --- openlp/.version | 2 +- tests/utils/test_bzr_tags.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/.version b/openlp/.version index 2bf1c1ccf..f90b1afc0 100644 --- a/openlp/.version +++ b/openlp/.version @@ -1 +1 @@ -2.3.1 +2.3.2 diff --git a/tests/utils/test_bzr_tags.py b/tests/utils/test_bzr_tags.py index 34066452f..a92253814 100644 --- a/tests/utils/test_bzr_tags.py +++ b/tests/utils/test_bzr_tags.py @@ -29,7 +29,7 @@ from subprocess import Popen, PIPE TAGS1 = {'1.9.0', '1.9.1', '1.9.2', '1.9.3', '1.9.4', '1.9.5', '1.9.6', '1.9.7', '1.9.8', '1.9.9', '1.9.10', '1.9.11', '1.9.12', '2.0', '2.1.0', '2.1.1', '2.1.2', '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.2', - '2.3.1'} + '2.3.1', '2.3.2'} class TestBzrTags(TestCase):