This commit is contained in:
Tim Bentley 2014-04-27 17:03:24 +01:00
commit b666196002
24 changed files with 404 additions and 74 deletions

View File

@ -6,6 +6,8 @@
*.ropeproject *.ropeproject
*.e4* *.e4*
.eric4project .eric4project
.komodotools
*.komodoproject
list list
openlp.org 2.0.e4* openlp.org 2.0.e4*
documentation/build/html documentation/build/html

View File

@ -194,6 +194,7 @@ class Manager(object):
db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod) db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
except (SQLAlchemyError, DBAPIError): except (SQLAlchemyError, DBAPIError):
log.exception('Error loading database: %s', self.db_url) log.exception('Error loading database: %s', self.db_url)
return
if db_ver > up_ver: if db_ver > up_ver:
critical_error_message_box( critical_error_message_box(
translate('OpenLP.Manager', 'Database Error'), translate('OpenLP.Manager', 'Database Error'),
@ -215,7 +216,7 @@ class Manager(object):
Save an object to the database Save an object to the database
:param object_instance: The object to save :param object_instance: The object to save
:param commit: Commit the session with this object :param commit: Commit the session with this object
""" """
for try_count in range(3): for try_count in range(3):
try: try:

View File

@ -511,7 +511,7 @@ class AdvancedTab(SettingsTab):
""" """
Select an image for the default display screen. Select an image for the default display screen.
""" """
file_filters = '%s;;%s (*.*) (*)' % (get_images_filter(), UiStrings().AllFiles) file_filters = '%s;;%s (*.*)' % (get_images_filter(), UiStrings().AllFiles)
filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.AdvancedTab', 'Open File'), '', filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.AdvancedTab', 'Open File'), '',
file_filters) file_filters)
if filename: if filename:

View File

@ -228,7 +228,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog, RegistryProperties):
""" """
files = QtGui.QFileDialog.getOpenFileName(self, translate('ImagePlugin.ExceptionDialog', 'Select Attachment'), files = QtGui.QFileDialog.getOpenFileName(self, translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
Settings().value(self.settings_section + '/last directory'), Settings().value(self.settings_section + '/last directory'),
'%s (*.*) (*)' % UiStrings().AllFiles) '%s (*)' % UiStrings().AllFiles)
log.info('New files(s) %s', str(files)) log.info('New files(s) %s', str(files))
if files: if files:
self.file_attachment = str(files) self.file_attachment = str(files)

View File

@ -114,10 +114,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
""" """
Run the wizard. Run the wizard.
""" """
self.setDefaults() self.set_defaults()
return QtGui.QWizard.exec_(self) return QtGui.QWizard.exec_(self)
def setDefaults(self): def set_defaults(self):
""" """
Set up display at start of theme edit. Set up display at start of theme edit.
""" """

View File

@ -90,7 +90,7 @@ def get_media_players():
overridden_player = 'auto' overridden_player = 'auto'
else: else:
overridden_player = '' overridden_player = ''
saved_players_list = saved_players.replace('[', '').replace(']', '').split(',') saved_players_list = saved_players.replace('[', '').replace(']', '').split(',') if saved_players else []
return saved_players_list, overridden_player return saved_players_list, overridden_player

View File

@ -90,7 +90,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
self.footer_font_combo_box.activated.connect(self.update_theme) self.footer_font_combo_box.activated.connect(self.update_theme)
self.footer_size_spin_box.valueChanged.connect(self.update_theme) self.footer_size_spin_box.valueChanged.connect(self.update_theme)
def setDefaults(self): def set_defaults(self):
""" """
Set up display at start of theme edit. Set up display at start of theme edit.
""" """
@ -261,7 +261,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
log.debug('Editing theme %s' % self.theme.theme_name) log.debug('Editing theme %s' % self.theme.theme_name)
self.temp_background_filename = '' self.temp_background_filename = ''
self.update_theme_allowed = False self.update_theme_allowed = False
self.setDefaults() self.set_defaults()
self.update_theme_allowed = True self.update_theme_allowed = True
self.theme_name_label.setVisible(not edit) self.theme_name_label.setVisible(not edit)
self.theme_name_edit.setVisible(not edit) self.theme_name_edit.setVisible(not edit)
@ -432,7 +432,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
Background Image button pushed. Background Image button pushed.
""" """
images_filter = get_images_filter() images_filter = get_images_filter()
images_filter = '%s;;%s (*.*) (*)' % (images_filter, UiStrings().AllFiles) images_filter = '%s;;%s (*.*)' % (images_filter, UiStrings().AllFiles)
filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.ThemeWizard', 'Select Image'), '', filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.ThemeWizard', 'Select Image'), '',
images_filter) images_filter)
if filename: if filename:

View File

@ -197,7 +197,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
""" """
Run the wizard. Run the wizard.
""" """
self.setDefaults() self.set_defaults()
return QtGui.QWizard.exec_(self) return QtGui.QWizard.exec_(self)
def reject(self): def reject(self):

View File

@ -465,7 +465,7 @@ class BibleImportForm(OpenLPWizard):
self.license_details_page.registerField('license_copyright', self.copyright_edit) self.license_details_page.registerField('license_copyright', self.copyright_edit)
self.license_details_page.registerField('license_permissions', self.permissions_edit) self.license_details_page.registerField('license_permissions', self.permissions_edit)
def setDefaults(self): def set_defaults(self):
""" """
Set default values for the wizard pages. Set default values for the wizard pages.
""" """

View File

@ -307,7 +307,7 @@ class BibleUpgradeForm(OpenLPWizard):
if self.currentPage() == self.progress_page: if self.currentPage() == self.progress_page:
return True return True
def setDefaults(self): def set_defaults(self):
""" """
Set default values for the wizard pages. Set default values for the wizard pages.
""" """

View File

@ -75,7 +75,7 @@ class ImageMediaItem(MediaManagerItem):
def retranslateUi(self): def retranslateUi(self):
self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)') self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)')
file_formats = get_images_filter() file_formats = get_images_filter()
self.on_new_file_masks = '%s;;%s (*.*) (*)' % (file_formats, UiStrings().AllFiles) self.on_new_file_masks = '%s;;%s (*)' % (file_formats, UiStrings().AllFiles)
self.add_group_action.setText(UiStrings().AddGroup) self.add_group_action.setText(UiStrings().AddGroup)
self.add_group_action.setToolTip(UiStrings().AddGroup) self.add_group_action.setToolTip(UiStrings().AddGroup)
self.replace_action.setText(UiStrings().ReplaceBG) self.replace_action.setText(UiStrings().ReplaceBG)

View File

@ -264,7 +264,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
self.break_search = True self.break_search = True
self.plugin.media_item.on_search_text_button_clicked() self.plugin.media_item.on_search_text_button_clicked()
def setDefaults(self): def set_defaults(self):
""" """
Set default form values for the song import wizard. Set default form values for the song import wizard.
""" """

View File

@ -118,13 +118,18 @@ class Ui_EditSongDialog(object):
self.authors_group_box.setObjectName('authors_group_box') self.authors_group_box.setObjectName('authors_group_box')
self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box) self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box)
self.authors_layout.setObjectName('authors_layout') self.authors_layout.setObjectName('authors_layout')
self.author_add_layout = QtGui.QHBoxLayout() self.author_add_layout = QtGui.QVBoxLayout()
self.author_add_layout.setObjectName('author_add_layout') self.author_add_layout.setObjectName('author_add_layout')
self.author_type_layout = QtGui.QHBoxLayout()
self.author_type_layout.setObjectName('author_type_layout')
self.authors_combo_box = create_combo_box(self.authors_group_box, 'authors_combo_box') self.authors_combo_box = create_combo_box(self.authors_group_box, 'authors_combo_box')
self.author_add_layout.addWidget(self.authors_combo_box) self.author_add_layout.addWidget(self.authors_combo_box)
self.author_types_combo_box = create_combo_box(self.authors_group_box, 'author_types_combo_box', editable=False)
self.author_type_layout.addWidget(self.author_types_combo_box)
self.author_add_button = QtGui.QPushButton(self.authors_group_box) self.author_add_button = QtGui.QPushButton(self.authors_group_box)
self.author_add_button.setObjectName('author_add_button') self.author_add_button.setObjectName('author_add_button')
self.author_add_layout.addWidget(self.author_add_button) self.author_type_layout.addWidget(self.author_add_button)
self.author_add_layout.addLayout(self.author_type_layout)
self.authors_layout.addLayout(self.author_add_layout) self.authors_layout.addLayout(self.author_add_layout)
self.authors_list_view = QtGui.QListWidget(self.authors_group_box) self.authors_list_view = QtGui.QListWidget(self.authors_group_box)
self.authors_list_view.setAlternatingRowColors(True) self.authors_list_view.setAlternatingRowColors(True)
@ -330,7 +335,7 @@ class Ui_EditSongDialog(object):
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.') translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.')
def create_combo_box(parent, name): def create_combo_box(parent, name, editable=True):
""" """
Utility method to generate a standard combo box for this dialog. Utility method to generate a standard combo box for this dialog.
@ -340,7 +345,7 @@ def create_combo_box(parent, name):
combo_box = QtGui.QComboBox(parent) combo_box = QtGui.QComboBox(parent)
combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength) combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
combo_box.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) combo_box.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
combo_box.setEditable(True) combo_box.setEditable(editable)
combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert) combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert)
combo_box.setObjectName(name) combo_box.setObjectName(name)
return combo_box return combo_box

View File

@ -42,7 +42,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 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.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 import VerseType, clean_song
from openlp.plugins.songs.lib.db import Book, Song, Author, Topic, MediaFile from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorSong, AuthorType, Topic, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.lib.xml import SongXML
from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog
@ -122,12 +122,12 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
combo.setItemData(row, obj.id) combo.setItemData(row, obj.id)
set_case_insensitive_completer(cache, combo) set_case_insensitive_completer(cache, combo)
def _add_author_to_list(self, author): def _add_author_to_list(self, author, author_type):
""" """
Add an author to the author list. Add an author to the author list.
""" """
author_item = QtGui.QListWidgetItem(str(author.display_name)) author_item = QtGui.QListWidgetItem(author.get_display_name(author_type))
author_item.setData(QtCore.Qt.UserRole, author.id) author_item.setData(QtCore.Qt.UserRole, (author.id, author_type))
self.authors_list_view.addItem(author_item) self.authors_list_view.addItem(author_item)
def _extract_verse_order(self, verse_order): def _extract_verse_order(self, verse_order):
@ -217,8 +217,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
if self.authors_list_view.count() == 0: if self.authors_list_view.count() == 0:
self.song_tab_widget.setCurrentIndex(1) self.song_tab_widget.setCurrentIndex(1)
self.authors_list_view.setFocus() self.authors_list_view.setFocus()
critical_error_message_box( critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
message=translate('SongsPlugin.EditSongForm', 'You need to have an author for this song.')) 'You need to have an author for this song.'))
return False return False
if self.verse_order_edit.text(): if self.verse_order_edit.text():
result = self._validate_verse_list(self.verse_order_edit.text(), self.verse_list_widget.rowCount()) result = self._validate_verse_list(self.verse_order_edit.text(), self.verse_list_widget.rowCount())
@ -302,6 +302,15 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.authors.append(author.display_name) self.authors.append(author.display_name)
set_case_insensitive_completer(self.authors, self.authors_combo_box) set_case_insensitive_completer(self.authors, self.authors_combo_box)
# Types
self.author_types_combo_box.clear()
self.author_types_combo_box.addItem('')
# Don't iterate over the dictionary to give them this specific order
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Words], AuthorType.Words)
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Music], AuthorType.Music)
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.WordsAndMusic], AuthorType.WordsAndMusic)
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Translation], AuthorType.Translation)
def load_topics(self): def load_topics(self):
""" """
Load the topics into the combobox. Load the topics into the combobox.
@ -454,10 +463,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.tag_rows() self.tag_rows()
# clear the results # clear the results
self.authors_list_view.clear() self.authors_list_view.clear()
for author in self.song.authors: for author_song in self.song.authors_songs:
author_name = QtGui.QListWidgetItem(str(author.display_name)) self._add_author_to_list(author_song.author, author_song.author_type)
author_name.setData(QtCore.Qt.UserRole, author.id)
self.authors_list_view.addItem(author_name)
# clear the results # clear the results
self.topics_list_view.clear() self.topics_list_view.clear()
for topic in self.song.topics: for topic in self.song.topics:
@ -496,6 +503,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
""" """
item = int(self.authors_combo_box.currentIndex()) item = int(self.authors_combo_box.currentIndex())
text = self.authors_combo_box.currentText().strip(' \r\n\t') text = self.authors_combo_box.currentText().strip(' \r\n\t')
author_type = self.author_types_combo_box.itemData(self.author_types_combo_box.currentIndex())
# This if statement is for OS X, which doesn't seem to work well with # This if statement is for OS X, which doesn't seem to work well with
# the QCompleter auto-completion class. See bug #812628. # the QCompleter auto-completion class. See bug #812628.
if text in self.authors: if text in self.authors:
@ -513,7 +521,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
author = Author.populate(first_name=text.rsplit(' ', 1)[0], last_name=text.rsplit(' ', 1)[1], author = Author.populate(first_name=text.rsplit(' ', 1)[0], last_name=text.rsplit(' ', 1)[1],
display_name=text) display_name=text)
self.manager.save_object(author) self.manager.save_object(author)
self._add_author_to_list(author) self._add_author_to_list(author, author_type)
self.load_authors() self.load_authors()
self.authors_combo_box.setCurrentIndex(0) self.authors_combo_box.setCurrentIndex(0)
else: else:
@ -521,11 +529,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
elif item > 0: elif item > 0:
item_id = (self.authors_combo_box.itemData(item)) item_id = (self.authors_combo_box.itemData(item))
author = self.manager.get_object(Author, item_id) author = self.manager.get_object(Author, item_id)
if self.authors_list_view.findItems(str(author.display_name), QtCore.Qt.MatchExactly): if self.authors_list_view.findItems(author.get_display_name(author_type), QtCore.Qt.MatchExactly):
critical_error_message_box( critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.')) message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.'))
else: else:
self._add_author_to_list(author) self._add_author_to_list(author, author_type)
self.authors_combo_box.setCurrentIndex(0) self.authors_combo_box.setCurrentIndex(0)
else: else:
QtGui.QMessageBox.warning( QtGui.QMessageBox.warning(
@ -905,13 +913,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
else: else:
self.song.theme_name = None self.song.theme_name = None
self._process_lyrics() self._process_lyrics()
self.song.authors = [] self.song.authors_songs = []
for row in range(self.authors_list_view.count()): for row in range(self.authors_list_view.count()):
item = self.authors_list_view.item(row) item = self.authors_list_view.item(row)
author_id = (item.data(QtCore.Qt.UserRole)) author_song = AuthorSong()
author = self.manager.get_object(Author, author_id) author_song.author_id = item.data(QtCore.Qt.UserRole)[0]
if author is not None: author_song.author_type = item.data(QtCore.Qt.UserRole)[1]
self.song.authors.append(author) self.song.authors_songs.append(author_song)
self.song.topics = [] self.song.topics = []
for row in range(self.topics_list_view.count()): for row in range(self.topics_list_view.count()):
item = self.topics_list_view.item(row) item = self.topics_list_view.item(row)

View File

@ -304,7 +304,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
""" """
self.source_page.emit(QtCore.SIGNAL('completeChanged()')) self.source_page.emit(QtCore.SIGNAL('completeChanged()'))
def setDefaults(self): def set_defaults(self):
""" """
Set default form values for the song import wizard. Set default form values for the song import wizard.
""" """

View File

@ -390,7 +390,7 @@ def clean_song(manager, song):
verses = SongXML().get_verses(song.lyrics) verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses]) song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
# The song does not have any author, add one. # The song does not have any author, add one.
if not song.authors: if not song.authors and not song.authors_songs: # Need to check both relations
name = SongStrings.AuthorUnknown name = SongStrings.AuthorUnknown
author = manager.get_object_filtered(Author, Author.display_name == name) author = manager.get_object_filtered(Author, Author.display_name == name)
if author is None: if author is None:

View File

@ -35,19 +35,52 @@ import re
from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.orm import mapper, relation, reconstructor
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func, text
from openlp.core.lib.db import BaseModel, init_db from openlp.core.lib.db import BaseModel, init_db
from openlp.core.utils import get_natural_key from openlp.core.utils import get_natural_key
from openlp.core.lib import translate
class Author(BaseModel): class Author(BaseModel):
""" """
Author model Author model
""" """
def get_display_name(self, author_type=None):
if author_type:
return "%s (%s)" % (self.display_name, AuthorType.Types[author_type])
return self.display_name
class AuthorSong(BaseModel):
"""
Relationship between Authors and Songs (many to many).
Need to define this relationship table explicit to get access to the
Association Object (author_type).
http://docs.sqlalchemy.org/en/latest/orm/relationships.html#association-object
"""
pass pass
class AuthorType(object):
"""
Enumeration for Author types.
They are defined by OpenLyrics: http://openlyrics.info/dataformat.html#authors
The 'words+music' type is not an official type, but is provided for convenience.
"""
Words = 'words'
Music = 'music'
WordsAndMusic = 'words+music'
Translation = 'translation'
Types = {
Words: translate('OpenLP.Ui', 'Words'),
Music: translate('OpenLP.Ui', 'Music'),
WordsAndMusic: translate('OpenLP.Ui', 'Words and Music'),
Translation: translate('OpenLP.Ui', 'Translation')
}
class Book(BaseModel): class Book(BaseModel):
""" """
Book model Book model
@ -67,6 +100,7 @@ class Song(BaseModel):
""" """
Song model Song model
""" """
def __init__(self): def __init__(self):
self.sort_key = [] self.sort_key = []
@ -120,6 +154,7 @@ def init_schema(url):
* author_id * author_id
* song_id * song_id
* author_type
**media_files Table** **media_files Table**
* id * id
@ -230,7 +265,8 @@ def init_schema(url):
authors_songs_table = Table( authors_songs_table = Table(
'authors_songs', metadata, 'authors_songs', metadata,
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True), Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True) Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
Column('author_type', types.String(), primary_key=True, nullable=False, server_default=text('""'))
) )
# Definition of the "songs_topics" table # Definition of the "songs_topics" table
@ -241,10 +277,15 @@ def init_schema(url):
) )
mapper(Author, authors_table) mapper(Author, authors_table)
mapper(AuthorSong, authors_songs_table, properties={
'author': relation(Author)
})
mapper(Book, song_books_table) mapper(Book, song_books_table)
mapper(MediaFile, media_files_table) mapper(MediaFile, media_files_table)
mapper(Song, songs_table, properties={ mapper(Song, songs_table, properties={
'authors': relation(Author, backref='songs', secondary=authors_songs_table, lazy=False), # Use the authors_songs relation when you need access to the 'author_type' attribute.
'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"),
'authors': relation(Author, secondary=authors_songs_table),
'book': relation(Book, backref='songs'), 'book': relation(Book, backref='songs'),
'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table) 'topics': relation(Topic, backref='songs', secondary=songs_topics_table)

View File

@ -44,7 +44,7 @@ from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from openlp.plugins.songs.forms.songimportform import SongImportForm from openlp.plugins.songs.forms.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
@ -234,8 +234,7 @@ class SongMediaItem(MediaManagerItem):
if song.temporary: if song.temporary:
continue continue
author_list = [author.display_name for author in song.authors] author_list = [author.display_name for author in song.authors]
song_title = str(song.title) song_detail = '%s (%s)' % (song.title, create_separated_list(author_list)) if author_list else song.title
song_detail = '%s (%s)' % (song_title, create_separated_list(author_list))
song_name = QtGui.QListWidgetItem(song_detail) song_name = QtGui.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id) song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name) self.list_view.addItem(song_name)
@ -464,23 +463,53 @@ class SongMediaItem(MediaManagerItem):
def generate_footer(self, item, song): def generate_footer(self, item, song):
""" """
Generates the song footer based on a song and adds details to a service item. Generates the song footer based on a song and adds details to a service item.
author_list is only required for initial song generation.
:param item: The service item to be amended :param item: The service item to be amended
:param song: The song to be used to generate the footer :param song: The song to be used to generate the footer
:return: List of all authors (only required for initial song generation)
""" """
author_list = [str(author.display_name) for author in song.authors] authors_words = []
authors_music = []
authors_words_music = []
authors_translation = []
authors_none = []
for author_song in song.authors_songs:
if author_song.author_type == AuthorType.Words:
authors_words.append(author_song.author.display_name)
elif author_song.author_type == AuthorType.Music:
authors_music.append(author_song.author.display_name)
elif author_song.author_type == AuthorType.WordsAndMusic:
authors_words_music.append(author_song.author.display_name)
elif author_song.author_type == AuthorType.Translation:
authors_translation.append(author_song.author.display_name)
else:
authors_none.append(author_song.author.display_name)
authors_all = authors_words_music + authors_words + authors_music + authors_translation + authors_none
item.audit = [ item.audit = [
song.title, author_list, song.copyright, str(song.ccli_number) song.title, authors_all, song.copyright, str(song.ccli_number)
] ]
item.raw_footer = [] item.raw_footer = []
item.raw_footer.append(song.title) item.raw_footer.append(song.title)
item.raw_footer.append(create_separated_list(author_list)) if authors_none:
item.raw_footer.append("%s: %s" % (translate('OpenLP.Ui', 'Written by'),
create_separated_list(authors_none)))
if authors_words_music:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.WordsAndMusic],
create_separated_list(authors_words_music)))
if authors_words:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Words],
create_separated_list(authors_words)))
if authors_music:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Music],
create_separated_list(authors_music)))
if authors_translation:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Translation],
create_separated_list(authors_translation)))
item.raw_footer.append(song.copyright) item.raw_footer.append(song.copyright)
if Settings().value('core/ccli number'): if Settings().value('core/ccli number'):
item.raw_footer.append(translate('SongsPlugin.MediaItem', item.raw_footer.append(translate('SongsPlugin.MediaItem',
'CCLI License: ') + Settings().value('core/ccli number')) 'CCLI License: ') + Settings().value('core/ccli number'))
return author_list return authors_all
def service_load(self, item): def service_load(self, item):
""" """

View File

@ -40,7 +40,7 @@ class SongStrings(object):
# These strings should need a good reason to be retranslated elsewhere. # These strings should need a good reason to be retranslated elsewhere.
Author = translate('OpenLP.Ui', 'Author', 'Singular') Author = translate('OpenLP.Ui', 'Author', 'Singular')
Authors = translate('OpenLP.Ui', 'Authors', 'Plural') Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
AuthorUnknown = 'Author Unknown' # Used to populate the database. AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.') CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular') SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular')
SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural') SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural')

View File

@ -32,7 +32,7 @@ backend for the Songs plugin
""" """
import logging import logging
from sqlalchemy import Column, types from sqlalchemy import Column, ForeignKey, types
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import func, false, null, text from sqlalchemy.sql.expression import func, false, null, text
@ -97,3 +97,25 @@ def upgrade_3(session, metadata):
op.add_column('songs', Column('temporary', types.Boolean(), server_default=false())) op.add_column('songs', Column('temporary', types.Boolean(), server_default=false()))
except OperationalError: except OperationalError:
log.info('Upgrade 3 has already been run') log.info('Upgrade 3 has already been run')
def upgrade_4(session, metadata):
"""
Version 4 upgrade.
This upgrade adds a column for author type to the authors_songs table
"""
try:
# Since SQLite doesn't support changing the primary key of a table, we need to recreate the table
# and copy the old values
op = get_upgrade_op(session)
op.create_table('authors_songs_tmp',
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
Column('author_type', types.String(), primary_key=True,
nullable=False, server_default=text('""')))
op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs')
op.drop_table('authors_songs')
op.rename_table('authors_songs_tmp', 'authors_songs')
except OperationalError:
log.info('Upgrade 4 has already been run')

View File

@ -71,7 +71,7 @@ from lxml import etree, objectify
from openlp.core.common import translate from openlp.core.common import translate
from openlp.core.lib import FormattingTags from openlp.core.lib import FormattingTags
from openlp.plugins.songs.lib import VerseType, clean_song from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Author, Book, Song, Topic from openlp.plugins.songs.lib.db import Author, AuthorSong, AuthorType, Book, Song, Topic
from openlp.core.utils import get_application_version from openlp.core.utils import get_application_version
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -166,7 +166,7 @@ class OpenLyrics(object):
supported by the :class:`OpenLyrics` class: supported by the :class:`OpenLyrics` class:
``<authors>`` ``<authors>``
OpenLP does not support the attribute *type* and *lang*. OpenLP does not support the attribute *lang*.
``<chord>`` ``<chord>``
This property is not supported. This property is not supported.
@ -269,10 +269,18 @@ class OpenLyrics(object):
'verseOrder', properties, song.verse_order.lower()) 'verseOrder', properties, song.verse_order.lower())
if song.ccli_number: if song.ccli_number:
self._add_text_to_element('ccliNo', properties, song.ccli_number) self._add_text_to_element('ccliNo', properties, song.ccli_number)
if song.authors: if song.authors_songs:
authors = etree.SubElement(properties, 'authors') authors = etree.SubElement(properties, 'authors')
for author in song.authors: for author_song in song.authors_songs:
self._add_text_to_element('author', authors, author.display_name) element = self._add_text_to_element('author', authors, author_song.author.display_name)
if author_song.author_type:
# Handle the special case 'words+music': Need to create two separate authors for that
if author_song.author_type == AuthorType.WordsAndMusic:
element.set('type', AuthorType.Words)
element = self._add_text_to_element('author', authors, author_song.author.display_name)
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) book = self.manager.get_object_filtered(Book, Book.id == song.song_book_id)
if book is not None: if book is not None:
book = book.name book = book.name
@ -501,16 +509,20 @@ class OpenLyrics(object):
if hasattr(properties, 'authors'): if hasattr(properties, 'authors'):
for author in properties.authors.author: for author in properties.authors.author:
display_name = self._text(author) display_name = self._text(author)
author_type = author.get('type', '')
if display_name: if display_name:
authors.append(display_name) authors.append((display_name, author_type))
for display_name in authors: for (display_name, author_type) in authors:
author = self.manager.get_object_filtered(Author, Author.display_name == display_name) author = self.manager.get_object_filtered(Author, Author.display_name == display_name)
if author is None: if author is None:
# We need to create a new author, as the author does not exist. # We need to create a new author, as the author does not exist.
author = Author.populate(display_name=display_name, author = Author.populate(display_name=display_name,
last_name=display_name.split(' ')[-1], last_name=display_name.split(' ')[-1],
first_name=' '.join(display_name.split(' ')[:-1])) first_name=' '.join(display_name.split(' ')[:-1]))
song.authors.append(author) author_song = AuthorSong()
author_song.author = author
author_song.author_type = author_type
song.authors_songs.append(author_song)
def _process_cclinumber(self, properties, song): def _process_cclinumber(self, properties, song):
""" """

View File

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Package to test the openlp.core.ui package.
"""
from PyQt4 import QtCore
from unittest import TestCase
from openlp.core.ui.media import get_media_players
from tests.functional import MagicMock, patch
from tests.helpers.testmixin import TestMixin
class TestMedia(TestCase, TestMixin):
def setUp(self):
pass
def test_get_media_players_no_config(self):
"""
Test that when there's no config, get_media_players() returns an empty list of players (not a string)
"""
def value_results(key):
if key == 'media/players':
return ''
else:
return False
# GIVEN: A mocked out Settings() object
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
mocked_value.side_effect = value_results
# WHEN: get_media_players() is called
used_players, overridden_player = get_media_players()
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
self.assertEqual([], used_players, 'Used players should be an empty list')
self.assertEqual('', overridden_player, 'Overridden player should be an empty string')
def test_get_media_players_no_players(self):
"""
Test that when there's no players but overridden player is set, get_media_players() returns 'auto'
"""
def value_results(key):
if key == 'media/override player':
return QtCore.Qt.Checked
else:
return ''
# GIVEN: A mocked out Settings() object
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
mocked_value.side_effect = value_results
# WHEN: get_media_players() is called
used_players, overridden_player = get_media_players()
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
self.assertEqual([], used_players, 'Used players should be an empty list')
self.assertEqual('auto', overridden_player, 'Overridden player should be "auto"')
def test_get_media_players_with_valid_list(self):
"""
Test that when get_media_players() is called the string list is interpreted correctly
"""
def value_results(key):
if key == 'media/players':
return '[vlc,webkit,phonon]'
else:
return False
# GIVEN: A mocked out Settings() object
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
mocked_value.side_effect = value_results
# WHEN: get_media_players() is called
used_players, overridden_player = get_media_players()
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct')
self.assertEqual('', overridden_player, 'Overridden player should be an empty string')
def test_get_media_players_with_overridden_player(self):
"""
Test that when get_media_players() is called the overridden player is correctly set
"""
def value_results(key):
if key == 'media/players':
return '[vlc,webkit,phonon]'
else:
return QtCore.Qt.Checked
# GIVEN: A mocked out Settings() object
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
mocked_value.side_effect = value_results
# WHEN: get_media_players() is called
used_players, overridden_player = get_media_players()
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct')
self.assertEqual('vlc,webkit,phonon', overridden_player, 'Overridden player should be a string of players')

View File

@ -10,6 +10,7 @@ from PyQt4 import QtCore, QtGui
from openlp.core.common import Registry, Settings from openlp.core.common import Registry, Settings
from openlp.core.lib import ServiceItem from openlp.core.lib import ServiceItem
from openlp.plugins.songs.lib.mediaitem import SongMediaItem from openlp.plugins.songs.lib.mediaitem import SongMediaItem
from openlp.plugins.songs.lib.db import AuthorType
from tests.functional import patch, MagicMock from tests.functional import patch, MagicMock
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -45,10 +46,12 @@ class TestMediaItem(TestCase, TestMixin):
# GIVEN: A Song and a Service Item # GIVEN: A Song and a Service Item
mock_song = MagicMock() mock_song = MagicMock()
mock_song.title = 'My Song' mock_song.title = 'My Song'
mock_song.authors_songs = []
mock_author = MagicMock() mock_author = MagicMock()
mock_author.display_name = 'my author' mock_author.display_name = 'my author'
mock_song.authors = [] mock_author_song = MagicMock()
mock_song.authors.append(mock_author) mock_author_song.author = mock_author
mock_song.authors_songs.append(mock_author_song)
mock_song.copyright = 'My copyright' mock_song.copyright = 'My copyright'
service_item = ServiceItem(None) service_item = ServiceItem(None)
@ -56,7 +59,7 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song) author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned # THEN: I get the following Array returned
self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright'], self.assertEqual(service_item.raw_footer, ['My Song', 'Written by: my author', 'My copyright'],
'The array should be returned correctly with a song, one author and copyright') 'The array should be returned correctly with a song, one author and copyright')
self.assertEqual(author_list, ['my author'], self.assertEqual(author_list, ['my author'],
'The author list should be returned correctly with one author') 'The author list should be returned correctly with one author')
@ -68,13 +71,25 @@ class TestMediaItem(TestCase, TestMixin):
# GIVEN: A Song and a Service Item # GIVEN: A Song and a Service Item
mock_song = MagicMock() mock_song = MagicMock()
mock_song.title = 'My Song' mock_song.title = 'My Song'
mock_song.authors_songs = []
mock_author = MagicMock() mock_author = MagicMock()
mock_author.display_name = 'my author' mock_author.display_name = 'my author'
mock_song.authors = [] mock_author_song = MagicMock()
mock_song.authors.append(mock_author) mock_author_song.author = mock_author
mock_author_song.author_type = AuthorType.Music
mock_song.authors_songs.append(mock_author_song)
mock_author = MagicMock() mock_author = MagicMock()
mock_author.display_name = 'another author' mock_author.display_name = 'another author'
mock_song.authors.append(mock_author) mock_author_song = MagicMock()
mock_author_song.author = mock_author
mock_author_song.author_type = AuthorType.Words
mock_song.authors_songs.append(mock_author_song)
mock_author = MagicMock()
mock_author.display_name = 'translator'
mock_author_song = MagicMock()
mock_author_song.author = mock_author
mock_author_song.author_type = AuthorType.Translation
mock_song.authors_songs.append(mock_author_song)
mock_song.copyright = 'My copyright' mock_song.copyright = 'My copyright'
service_item = ServiceItem(None) service_item = ServiceItem(None)
@ -82,22 +97,19 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song) author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned # THEN: I get the following Array returned
self.assertEqual(service_item.raw_footer, ['My Song', 'my author and another author', 'My copyright'], self.assertEqual(service_item.raw_footer, ['My Song', 'Words: another author', 'Music: my author',
'Translation: translator', 'My copyright'],
'The array should be returned correctly with a song, two authors and copyright') 'The array should be returned correctly with a song, two authors and copyright')
self.assertEqual(author_list, ['my author', 'another author'], self.assertEqual(author_list, ['another author', 'my author', 'translator'],
'The author list should be returned correctly with two authors') 'The author list should be returned correctly with two authors')
def build_song_footer_base_ccli_test(self): def build_song_footer_base_ccli_test(self):
""" """
Test build songs footer with basic song and two authors Test build songs footer with basic song and a CCLI number
""" """
# GIVEN: A Song and a Service Item and a configured CCLI license # GIVEN: A Song and a Service Item and a configured CCLI license
mock_song = MagicMock() mock_song = MagicMock()
mock_song.title = 'My Song' mock_song.title = 'My Song'
mock_author = MagicMock()
mock_author.display_name = 'my author'
mock_song.authors = []
mock_song.authors.append(mock_author)
mock_song.copyright = 'My copyright' mock_song.copyright = 'My copyright'
service_item = ServiceItem(None) service_item = ServiceItem(None)
Settings().setValue('core/ccli number', '1234') Settings().setValue('core/ccli number', '1234')
@ -106,7 +118,7 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song) self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned # THEN: I get the following Array returned
self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright', 'CCLI License: 1234'], self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 1234'],
'The array should be returned correctly with a song, an author, copyright and ccli') 'The array should be returned correctly with a song, an author, copyright and ccli')
# WHEN: I amend the CCLI value # WHEN: I amend the CCLI value
@ -114,5 +126,5 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song) self.media_item.generate_footer(service_item, mock_song)
# THEN: I would get an amended footer string # THEN: I would get an amended footer string
self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright', 'CCLI License: 4321'], self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 4321'],
'The array should be returned correctly with a song, an author, copyright and amended ccli') 'The array should be returned correctly with a song, an author, copyright and amended ccli')

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Package to test the openlp.core.__init__ package.
"""
import os
from unittest import TestCase
from unittest.mock import MagicMock, patch
from PyQt4 import QtCore
from openlp.core import OpenLP
from tests.helpers.testmixin import TestMixin
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'resources'))
class TestInit(TestCase, TestMixin):
def setUp(self):
with patch('openlp.core.common.OpenLPMixin.__init__') as constructor:
constructor.return_value = None
self.openlp = OpenLP(list())
def tearDown(self):
del self.openlp
def event_test(self):
"""
Test the reimplemented event method
"""
# GIVEN: A file path and a QEvent.
file_path = os.path.join(TEST_PATH, 'church.jpg')
mocked_file_method = MagicMock(return_value=file_path)
event = QtCore.QEvent(QtCore.QEvent.FileOpen)
event.file = mocked_file_method
# WHEN: Call the vent method.
result = self.openlp.event(event)
# THEN: The path should be inserted.
self.assertTrue(result, "The method should have returned True.")
mocked_file_method.assert_called_once_with()
self.assertEqual(self.openlp.args[0], file_path, "The path should be in args.")