Merge with trunk

This commit is contained in:
Simon Hanna 2016-01-10 17:01:43 +01:00
commit 4d7ceba301
32 changed files with 722 additions and 189 deletions

View File

@ -1 +1 @@
2.3.1
2.3.2

View File

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

View File

@ -154,7 +154,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')

71
openlp/core/utils/db.py Normal file
View File

@ -0,0 +1,71 @@
# -*- 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 sqlalchemy
import logging
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 DROP COLUMN support in SQLite
From https://github.com/klugjohannes/alembic-sqlite
"""
# 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 = [deepcopy(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)

View File

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

View File

@ -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_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):
"""
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,13 @@ 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.songbook_entry_edit.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 +438,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 +452,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 +513,10 @@ 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 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])
@ -678,6 +675,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_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()
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_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:
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)
@ -840,17 +879,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):
"""
@ -930,7 +962,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):
@ -979,12 +1011,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
@ -1003,6 +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.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_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)

View File

@ -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:'))

View File

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

View File

@ -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)
@ -153,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)
@ -288,7 +302,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 +322,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 +352,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 +400,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):
"""

View File

@ -160,6 +160,36 @@ class Song(BaseModel):
self.authors_songs.remove(author_song)
return
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 songbook_entry in self.songbook_entries:
if songbook_entry.songbook.name == songbook.name and songbook_entry.entry == entry:
return
new_songbook_entry = SongBookEntry()
new_songbook_entry.songbook = songbook
new_songbook_entry.entry = entry
self.songbook_entries.append(new_songbook_entry)
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:
return "%s #%s" % (songbook_name, entry)
return songbook_name
class Topic(BaseModel):
"""
@ -182,6 +212,7 @@ def init_schema(url):
* media_files_songs
* song_books
* songs
* songs_songbooks
* songs_topics
* topics
@ -222,7 +253,6 @@ def init_schema(url):
The *songs* table has the following columns:
* id
* song_book_id
* title
* alternate_title
* lyrics
@ -230,11 +260,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 +320,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 +327,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 +350,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 +371,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 +382,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),
'songbook_entries': relation(SongBookEntry, backref='song', cascade="all, delete-orphan"),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table)
})
mapper(Topic, topics_table)

View File

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

View File

@ -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]
}
@ -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':
@ -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

View File

@ -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
@ -152,7 +152,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))
@ -185,17 +185,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 + '%'
@ -255,21 +246,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])
songbook_entries = (self.plugin.manager.session.query(SongBookEntry)
.join(Book)
.order_by(Book.name)
.order_by(SongBookEntry.entry))
for songbook_entry in songbook_entries:
if songbook_entry.song.temporary:
continue
if search_book.lower() not in songbook_entry.songbook.name.lower():
continue
if search_entry not in songbook_entry.entry:
continue
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, songbook_entry.song.id)
self.list_view.addItem(song_name)
def on_clear_text_button_click(self):
"""
@ -524,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.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',
'CCLI License: ') + Settings().value('core/ccli number'))

View File

@ -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.songbook_entries:
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 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:
@ -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_songbook_entry(book, songbook.get('entry', ''))
def _process_titles(self, properties, song):
"""

View File

@ -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:
@ -220,3 +222,9 @@ class SongSelectImport(object):
db_song.add_author(author)
self.db_manager.save_object(db_song)
return db_song
def stop(self):
"""
Stop the search.
"""
self.run_search = False

View File

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

View File

@ -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,34 @@ 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 IS NOT NULL AND song_number IS NOT NULL')
# Drop old columns
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')

View File

@ -1,5 +1,6 @@
<RCC>
<qresource prefix="songs">
<file>song_search_stop.png</file>
<file>song_search_all.png</file>
<file>song_search_author.png</file>
<file>song_search_lyrics.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

View File

@ -29,9 +29,10 @@ from unittest import TestCase
from openlp.core.common import verify_ip_address, md5_hash, qmd5_hash
salt = '498e4a67'
pin = 'JBMIAProjectorLink'
test_hash = '5d8409bc1c3fa39749434aa3a5c38682'
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_HASH
salt = TEST_SALT
pin = TEST_PIN
test_hash = TEST_HASH
test_non_ascii_string = '이것은 한국어 시험 문자열'
test_non_ascii_hash = 'fc00c7912976f6e9c19099b514ced201'

View File

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

View File

@ -0,0 +1,63 @@
# -*- 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_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_test, 'readyRead')
@patch.object(pjlink_test, 'send_command')
@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):
"""
Fix for projector connect with PJLink authentication exception. Ticket 92187.
"""
# 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
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"))

View File

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

View File

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

View File

@ -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.songbook_entries = []
songbook = Book()
songbook.name = "Thy Word"
# WHEN: We add two songbooks to a Song
song.add_songbook_entry(songbook, "120")
song.add_songbook_entry(songbook, "550A")
# THEN: The song should have two songbook entries
self.assertEqual(len(song.songbook_entries), 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
@ -192,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')
@ -209,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')

View File

@ -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
@ -152,29 +152,36 @@ 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 = MagicMock()
mock_song.title = 'My Song'
mock_song.copyright = 'My copyright'
mock_song.book = MagicMock()
mock_song.book.name = "My songbook"
mock_song.song_number = 12
song = Song()
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_songbook_entry(book1, '12')
song.add_songbook_entry(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'])
self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12, Thy songbook #502A'])
def build_song_footer_copyright_enabled_test(self):
"""

View File

@ -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', '<Refrain> should be interpreted as <c>')
# 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 <Refrain > should be found, value true')
self.assertEqual(self.current_verse_type, 'c', '<Refrain > should be interpreted as <c>')
self.assertTrue(result, 'Versemark for <ReFrain > should be found, value true')
self.assertEqual(self.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
# 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 <Verse 1> should be found, value true')
self.assertEqual(self.current_verse_type, 'v1', u'<Verse 1> should be interpreted as <v1>')
self.assertTrue(result, 'Versemark for <VersE 1> should be found, value true')
self.assertEqual(self.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
# 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')

View File

@ -83,3 +83,13 @@ class TestSongFormat(TestCase):
"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
"""
# 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")

View File

@ -192,7 +192,7 @@ class TestSongSelectImport(TestCase, TestMixin):
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
# WHEN: The search method is called
results = importer.search('text', 1000, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
@ -234,10 +234,10 @@ class TestSongSelectImport(TestCase, TestMixin):
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
# WHEN: The search method is called
results = importer.search('text', 2, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
# THEN: callback was called twice, open was called twice, find_all was called twice, max results returned
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
@ -246,6 +246,22 @@ class TestSongSelectImport(TestCase, TestMixin):
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}]
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
@patch('openlp.plugins.songs.lib.songselect.build_opener')
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
def stop_called_test(self, MockedBeautifulSoup, mocked_build_opener):
"""
Test that the search is stopped with stop() is called
"""
# GIVEN: An importer object that is currently "searching"
importer = SongSelectImport(None)
importer.run_search = True
# WHEN: The stop method is called
results = importer.stop()
# THEN: Searching should have stopped
self.assertFalse(importer.run_search, 'Searching should have been stopped')
@patch('openlp.plugins.songs.lib.songselect.build_opener')
def get_song_page_raises_exception_test(self, mocked_build_opener):
"""
@ -686,6 +702,23 @@ class TestSongSelectForm(TestCase, TestMixin):
# THEN: The view button should be enabled
mocked_view_button.setEnabled.assert_called_with(True)
@patch('openlp.plugins.songs.forms.songselectform.SongSelectImport')
def on_stop_button_clicked_test(self, MockedSongSelectImport):
"""
Test that the search is stopped when the stop button is clicked
"""
# GIVEN: A mocked SongSelectImporter and a SongSelect form
mocked_song_select_importer = MagicMock()
MockedSongSelectImport.return_value = mocked_song_select_importer
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.initialise()
# WHEN: The stop button is clicked
ssform.on_stop_button_clicked()
# THEN: The view button should be enabled
mocked_song_select_importer.stop.assert_called_with()
class TestSongSelectFileImport(SongImportTestHelper):

View File

@ -29,6 +29,16 @@ from tempfile import gettempdir
# Test data
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
TEST_SALT = '498e4a67'
TEST_PIN = 'JBMIAProjectorLink'
TEST_HASH = '5d8409bc1c3fa39749434aa3a5c38682'
TEST_CONNECT_AUTHENTICATE = 'PJLink 1 {salt}'.format(salt=TEST_SALT)
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
TEST1_DATA = dict(ip='111.111.111.111',
port='1111',
pin='1111',
@ -49,3 +59,4 @@ TEST3_DATA = dict(ip='333.333.333.333',
name='___TEST_THREE___',
location='location three',
notes='notes three')

View File

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

View File

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