forked from openlp/openlp
Merge with trunk
This commit is contained in:
commit
4d7ceba301
@ -1 +1 @@
|
||||
2.3.1
|
||||
2.3.2
|
||||
|
@ -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
|
||||
|
@ -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
71
openlp/core/utils/db.py
Normal 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)
|
@ -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'))
|
||||
|
@ -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)
|
||||
|
@ -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:'))
|
||||
|
@ -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'))
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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'))
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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>
|
||||
|
BIN
resources/images/song_search_stop.png
Normal file
BIN
resources/images/song_search_stop.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 722 B |
@ -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'
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
63
tests/functional/openlp_core_lib/test_projector_pjlink1.py
Normal file
63
tests/functional/openlp_core_lib/test_projector_pjlink1.py
Normal 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"))
|
@ -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()
|
||||
|
||||
|
||||
|
96
tests/functional/openlp_core_utils/test_db.py
Normal file
96
tests/functional/openlp_core_utils/test_db.py
Normal 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)
|
@ -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')
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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')
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user