diff --git a/.bzrignore b/.bzrignore index b91d4fc93..6377150e0 100644 --- a/.bzrignore +++ b/.bzrignore @@ -6,6 +6,8 @@ *.ropeproject *.e4* .eric4project +.komodotools +*.komodoproject list openlp.org 2.0.e4* documentation/build/html diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 1014c994d..d67c05c42 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -194,6 +194,7 @@ class Manager(object): db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod) except (SQLAlchemyError, DBAPIError): log.exception('Error loading database: %s', self.db_url) + return if db_ver > up_ver: critical_error_message_box( translate('OpenLP.Manager', 'Database Error'), @@ -215,7 +216,7 @@ class Manager(object): Save an object to the database :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): try: diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index b2c8cd14b..a5ce09bdf 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -511,7 +511,7 @@ class AdvancedTab(SettingsTab): """ 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'), '', file_filters) if filename: diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index ae3bc9db0..e0228a43b 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -228,7 +228,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog, RegistryProperties): """ files = QtGui.QFileDialog.getOpenFileName(self, translate('ImagePlugin.ExceptionDialog', 'Select Attachment'), Settings().value(self.settings_section + '/last directory'), - '%s (*.*) (*)' % UiStrings().AllFiles) + '%s (*)' % UiStrings().AllFiles) log.info('New files(s) %s', str(files)) if files: self.file_attachment = str(files) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index f1b875e6b..b9b5f5997 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -114,10 +114,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): """ Run the wizard. """ - self.setDefaults() + self.set_defaults() return QtGui.QWizard.exec_(self) - def setDefaults(self): + def set_defaults(self): """ Set up display at start of theme edit. """ diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 8cc10b6a1..b2b6ab0b8 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -90,7 +90,7 @@ def get_media_players(): overridden_player = 'auto' else: 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 diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index fbfc1035c..321071c49 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -90,7 +90,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): self.footer_font_combo_box.activated.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. """ @@ -261,7 +261,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): log.debug('Editing theme %s' % self.theme.theme_name) self.temp_background_filename = '' self.update_theme_allowed = False - self.setDefaults() + self.set_defaults() self.update_theme_allowed = True self.theme_name_label.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. """ 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'), '', images_filter) if filename: diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 05951d14a..5815457b5 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -197,7 +197,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties): """ Run the wizard. """ - self.setDefaults() + self.set_defaults() return QtGui.QWizard.exec_(self) def reject(self): diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index ee5bee2d0..79b0bc699 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -465,7 +465,7 @@ class BibleImportForm(OpenLPWizard): self.license_details_page.registerField('license_copyright', self.copyright_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. """ diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index d9936dfe6..09c0942b7 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -307,7 +307,7 @@ class BibleUpgradeForm(OpenLPWizard): if self.currentPage() == self.progress_page: return True - def setDefaults(self): + def set_defaults(self): """ Set default values for the wizard pages. """ diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index d24a18434..c28f1e834 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -75,7 +75,7 @@ class ImageMediaItem(MediaManagerItem): def retranslateUi(self): self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)') 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.setToolTip(UiStrings().AddGroup) self.replace_action.setText(UiStrings().ReplaceBG) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index c99dee4a7..c411c8c1c 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -264,7 +264,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties): self.break_search = True 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. """ diff --git a/openlp/plugins/songs/forms/editsongdialog.py b/openlp/plugins/songs/forms/editsongdialog.py index f2ef5af06..d0fb51a2d 100644 --- a/openlp/plugins/songs/forms/editsongdialog.py +++ b/openlp/plugins/songs/forms/editsongdialog.py @@ -118,13 +118,18 @@ class Ui_EditSongDialog(object): self.authors_group_box.setObjectName('authors_group_box') self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box) 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_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.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.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_list_view = QtGui.QListWidget(self.authors_group_box) self.authors_list_view.setAlternatingRowColors(True) @@ -330,7 +335,7 @@ class Ui_EditSongDialog(object): translate('SongsPlugin.EditSongForm', 'Warning: 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. @@ -340,7 +345,7 @@ def create_combo_box(parent, name): combo_box = QtGui.QComboBox(parent) combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength) 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.setObjectName(name) return combo_box diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 60c6eae78..1814655ea 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -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.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, 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.xml import SongXML 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) 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. """ - author_item = QtGui.QListWidgetItem(str(author.display_name)) - author_item.setData(QtCore.Qt.UserRole, author.id) + author_item = QtGui.QListWidgetItem(author.get_display_name(author_type)) + author_item.setData(QtCore.Qt.UserRole, (author.id, author_type)) self.authors_list_view.addItem(author_item) 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: self.song_tab_widget.setCurrentIndex(1) self.authors_list_view.setFocus() - critical_error_message_box( - message=translate('SongsPlugin.EditSongForm', 'You need to have an author for this song.')) + critical_error_message_box(message=translate('SongsPlugin.EditSongForm', + 'You need to have an author for this song.')) return False if self.verse_order_edit.text(): 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) 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): """ Load the topics into the combobox. @@ -454,10 +463,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties): self.tag_rows() # clear the results self.authors_list_view.clear() - for author in self.song.authors: - author_name = QtGui.QListWidgetItem(str(author.display_name)) - author_name.setData(QtCore.Qt.UserRole, author.id) - self.authors_list_view.addItem(author_name) + for author_song in self.song.authors_songs: + self._add_author_to_list(author_song.author, author_song.author_type) # clear the results self.topics_list_view.clear() for topic in self.song.topics: @@ -496,6 +503,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties): """ item = int(self.authors_combo_box.currentIndex()) 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 # the QCompleter auto-completion class. See bug #812628. 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], display_name=text) self.manager.save_object(author) - self._add_author_to_list(author) + self._add_author_to_list(author, author_type) self.load_authors() self.authors_combo_box.setCurrentIndex(0) else: @@ -521,11 +529,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties): elif item > 0: item_id = (self.authors_combo_box.itemData(item)) 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( message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.')) else: - self._add_author_to_list(author) + self._add_author_to_list(author, author_type) self.authors_combo_box.setCurrentIndex(0) else: QtGui.QMessageBox.warning( @@ -905,13 +913,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties): else: self.song.theme_name = None self._process_lyrics() - self.song.authors = [] + self.song.authors_songs = [] for row in range(self.authors_list_view.count()): item = self.authors_list_view.item(row) - author_id = (item.data(QtCore.Qt.UserRole)) - author = self.manager.get_object(Author, author_id) - if author is not None: - self.song.authors.append(author) + author_song = AuthorSong() + author_song.author_id = item.data(QtCore.Qt.UserRole)[0] + author_song.author_type = item.data(QtCore.Qt.UserRole)[1] + self.song.authors_songs.append(author_song) self.song.topics = [] for row in range(self.topics_list_view.count()): item = self.topics_list_view.item(row) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 27f0d9343..21569a034 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -304,7 +304,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties): """ self.source_page.emit(QtCore.SIGNAL('completeChanged()')) - def setDefaults(self): + def set_defaults(self): """ Set default form values for the song import wizard. """ diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index dc198d4b7..aa9fbc4c9 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -390,7 +390,7 @@ def clean_song(manager, song): verses = SongXML().get_verses(song.lyrics) song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses]) # 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 author = manager.get_object_filtered(Author, Author.display_name == name) if author is None: diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index c3965e2ed..91649c951 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -35,19 +35,52 @@ import re from sqlalchemy import Column, ForeignKey, Table, types 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.utils import get_natural_key +from openlp.core.lib import translate class Author(BaseModel): """ 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 +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): """ Book model @@ -67,6 +100,7 @@ class Song(BaseModel): """ Song model """ + def __init__(self): self.sort_key = [] @@ -120,6 +154,7 @@ def init_schema(url): * author_id * song_id + * author_type **media_files Table** * id @@ -230,7 +265,8 @@ def init_schema(url): authors_songs_table = Table( 'authors_songs', metadata, 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 @@ -241,10 +277,15 @@ def init_schema(url): ) mapper(Author, authors_table) + mapper(AuthorSong, authors_songs_table, properties={ + 'author': relation(Author) + }) mapper(Book, song_books_table) mapper(MediaFile, media_files_table) 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'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 314d1ffe7..32730ce3c 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -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.songexportform import SongExportForm 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.xml import OpenLyrics, SongXML @@ -234,8 +234,7 @@ class SongMediaItem(MediaManagerItem): if song.temporary: continue 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)) + song_detail = '%s (%s)' % (song.title, create_separated_list(author_list)) if author_list else song.title song_name = QtGui.QListWidgetItem(song_detail) song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) @@ -464,23 +463,53 @@ class SongMediaItem(MediaManagerItem): def generate_footer(self, item, song): """ 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 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 = [ - 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.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) if Settings().value('core/ccli number'): item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + Settings().value('core/ccli number')) - return author_list + return authors_all def service_load(self, item): """ diff --git a/openlp/plugins/songs/lib/ui.py b/openlp/plugins/songs/lib/ui.py index 14f4777c9..151b11b4b 100644 --- a/openlp/plugins/songs/lib/ui.py +++ b/openlp/plugins/songs/lib/ui.py @@ -40,7 +40,7 @@ class SongStrings(object): # These strings should need a good reason to be retranslated elsewhere. Author = translate('OpenLP.Ui', 'Author', 'Singular') 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.') SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular') SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural') diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py index 546343900..580ae767d 100644 --- a/openlp/plugins/songs/lib/upgrade.py +++ b/openlp/plugins/songs/lib/upgrade.py @@ -32,7 +32,7 @@ backend for the Songs plugin """ import logging -from sqlalchemy import Column, types +from sqlalchemy import Column, ForeignKey, types from sqlalchemy.exc import OperationalError 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())) except OperationalError: 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') diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py index d516b5e02..68641a1d5 100644 --- a/openlp/plugins/songs/lib/xml.py +++ b/openlp/plugins/songs/lib/xml.py @@ -71,7 +71,7 @@ from lxml import etree, objectify from openlp.core.common import translate from openlp.core.lib import FormattingTags 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 log = logging.getLogger(__name__) @@ -166,7 +166,7 @@ class OpenLyrics(object): supported by the :class:`OpenLyrics` class: ```` - OpenLP does not support the attribute *type* and *lang*. + OpenLP does not support the attribute *lang*. ```` This property is not supported. @@ -269,10 +269,18 @@ class OpenLyrics(object): 'verseOrder', properties, song.verse_order.lower()) if 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') - for author in song.authors: - self._add_text_to_element('author', authors, author.display_name) + for author_song in song.authors_songs: + 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) if book is not None: book = book.name @@ -501,16 +509,20 @@ class OpenLyrics(object): if hasattr(properties, 'authors'): for author in properties.authors.author: display_name = self._text(author) + author_type = author.get('type', '') if display_name: - authors.append(display_name) - for display_name in authors: + authors.append((display_name, author_type)) + for (display_name, author_type) in authors: author = self.manager.get_object_filtered(Author, Author.display_name == display_name) if author is None: # We need to create a new author, as the author does not exist. author = Author.populate(display_name=display_name, last_name=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): """ diff --git a/tests/functional/openlp_core_ui/test_media.py b/tests/functional/openlp_core_ui/test_media.py new file mode 100644 index 000000000..d59690949 --- /dev/null +++ b/tests/functional/openlp_core_ui/test_media.py @@ -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') \ No newline at end of file diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 2b5f02483..308881c2e 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -10,6 +10,7 @@ from PyQt4 import QtCore, QtGui 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 tests.functional import patch, MagicMock from tests.helpers.testmixin import TestMixin @@ -45,10 +46,12 @@ class TestMediaItem(TestCase, TestMixin): # GIVEN: A Song and a Service Item mock_song = MagicMock() mock_song.title = 'My Song' + mock_song.authors_songs = [] mock_author = MagicMock() mock_author.display_name = 'my author' - mock_song.authors = [] - mock_song.authors.append(mock_author) + mock_author_song = MagicMock() + mock_author_song.author = mock_author + mock_song.authors_songs.append(mock_author_song) mock_song.copyright = 'My copyright' service_item = ServiceItem(None) @@ -56,7 +59,7 @@ class TestMediaItem(TestCase, TestMixin): author_list = self.media_item.generate_footer(service_item, mock_song) # 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') self.assertEqual(author_list, ['my 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 mock_song = MagicMock() mock_song.title = 'My Song' + mock_song.authors_songs = [] mock_author = MagicMock() mock_author.display_name = 'my author' - mock_song.authors = [] - mock_song.authors.append(mock_author) + mock_author_song = MagicMock() + 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.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' service_item = ServiceItem(None) @@ -82,22 +97,19 @@ class TestMediaItem(TestCase, TestMixin): author_list = self.media_item.generate_footer(service_item, mock_song) # 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') - 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') 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 mock_song = MagicMock() 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' service_item = ServiceItem(None) Settings().setValue('core/ccli number', '1234') @@ -106,7 +118,7 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.generate_footer(service_item, mock_song) # 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') # WHEN: I amend the CCLI value @@ -114,5 +126,5 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.generate_footer(service_item, mock_song) # 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') diff --git a/tests/functional/test_init.py b/tests/functional/test_init.py new file mode 100644 index 000000000..6fc7d7f70 --- /dev/null +++ b/tests/functional/test_init.py @@ -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.")