This commit is contained in:
Tim Bentley 2016-01-09 20:07:40 +00:00
commit 51be6b1848
12 changed files with 436 additions and 131 deletions

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
@ -70,6 +70,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)
@ -126,6 +129,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
@ -220,17 +228,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 = []
@ -328,6 +325,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):
@ -368,12 +368,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):
"""
@ -414,12 +414,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)
@ -438,18 +439,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:
@ -459,7 +453,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)
@ -521,6 +514,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])
@ -679,6 +676,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)
@ -841,17 +880,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):
"""
@ -931,7 +963,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):
@ -980,12 +1012,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
@ -1004,6 +1030,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

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

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

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

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