diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py
index d7b0acde0..29a2af99c 100644
--- a/openlp/plugins/songs/forms/editsongform.py
+++ b/openlp/plugins/songs/forms/editsongform.py
@@ -44,7 +44,7 @@ from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog
from openlp.plugins.songs.forms.editverseform import EditVerseForm
from openlp.plugins.songs.forms.mediafilesform import MediaFilesForm
from openlp.plugins.songs.lib import VerseType, clean_song
-from openlp.plugins.songs.lib.db import Author, AuthorType, Book, MediaFile, Song, SongBookEntry, Topic
+from openlp.plugins.songs.lib.db import Author, AuthorType, SongBook, MediaFile, Song, SongBookEntry, Topic
from openlp.plugins.songs.lib.openlyricsxml import SongXML
from openlp.plugins.songs.lib.ui import SongStrings, show_key_warning
@@ -406,7 +406,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
Load the Songbooks into the combobox
"""
self.songbooks = []
- self._load_objects(Book, self.songbooks_combo_box, self.songbooks)
+ self._load_objects(SongBook, self.songbooks_combo_box, self.songbooks)
def load_themes(self, theme_list: list):
"""
@@ -599,10 +599,10 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
translate('SongsPlugin.EditSongForm', 'This author does not exist, do you want to add them?'),
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
if text.find(' ') == -1:
- author = Author.populate(first_name='', last_name='', display_name=text)
+ author = Author(first_name='', last_name='', display_name=text)
else:
- author = Author.populate(first_name=text.rsplit(' ', 1)[0], last_name=text.rsplit(' ', 1)[1],
- display_name=text)
+ author = Author(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, author_type)
self.load_authors()
@@ -676,7 +676,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
self, translate('SongsPlugin.EditSongForm', 'Add Topic'),
translate('SongsPlugin.EditSongForm', 'This topic does not exist, do you want to add it?'),
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
- topic = Topic.populate(name=text)
+ topic = Topic(name=text)
self.manager.save_object(topic)
topic_item = QtWidgets.QListWidgetItem(str(topic.name))
topic_item.setData(QtCore.Qt.UserRole, topic.id)
@@ -722,7 +722,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
self, translate('SongsPlugin.EditSongForm', 'Add Songbook'),
translate('SongsPlugin.EditSongForm', 'This Songbook does not exist, do you want to add it?'),
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
- songbook = Book.populate(name=text)
+ songbook = SongBook(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()
@@ -733,7 +733,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
return
elif item >= 0:
item_id = (self.songbooks_combo_box.itemData(item))
- songbook = self.manager.get_object(Book, item_id)
+ songbook = self.manager.get_object(SongBook, 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.'))
@@ -1029,7 +1029,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
def save_song(self, preview=False):
"""
Get all the data from the widgets on the form, and then save it to the database. The form has been validated
- and all reference items (Authors, Books and Topics) have been saved before this function is called.
+ and all reference items (Authors, SongBooks and Topics) have been saved before this function is called.
:param preview: Should be ``True`` if the song is also previewed (boolean).
"""
@@ -1075,7 +1075,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
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)
+ songbook = self.manager.get_object(SongBook, 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.
diff --git a/openlp/plugins/songs/forms/songmaintenanceform.py b/openlp/plugins/songs/forms/songmaintenanceform.py
index 1da39a6e9..86d0190a3 100644
--- a/openlp/plugins/songs/forms/songmaintenanceform.py
+++ b/openlp/plugins/songs/forms/songmaintenanceform.py
@@ -30,7 +30,7 @@ from openlp.core.lib.ui import critical_error_message_box
from openlp.plugins.songs.forms.authorsform import AuthorsForm
from openlp.plugins.songs.forms.songbookform import SongBookForm
from openlp.plugins.songs.forms.topicsform import TopicsForm
-from openlp.plugins.songs.lib.db import Author, Book, Song, Topic, SongBookEntry
+from openlp.plugins.songs.lib.db import Author, SongBook, Song, Topic, SongBookEntry
from .songmaintenancedialog import Ui_SongMaintenanceDialog
@@ -163,7 +163,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
return get_natural_key(book.name)
self.song_books_list_widget.clear()
- books = self.manager.get_all_objects(Book)
+ books = self.manager.get_all_objects(SongBook)
books.sort(key=get_book_key)
for book in books:
book_name = QtWidgets.QListWidgetItem('{name} ({publisher})'.format(name=book.name,
@@ -206,7 +206,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
:param edit: Are we editing the song?
"""
books = self.manager.get_all_objects(
- Book, and_(Book.name == new_book.name, Book.publisher == new_book.publisher))
+ SongBook, and_(SongBook.name == new_book.name, SongBook.publisher == new_book.publisher))
return self._check_object_exists(books, new_book, edit)
def _check_object_exists(self, existing_objects, new_object, edit):
@@ -236,7 +236,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
"""
self.author_form.auto_display_name = True
if self.author_form.exec():
- author = Author.populate(
+ author = Author(
first_name=self.author_form.first_name,
last_name=self.author_form.last_name,
display_name=self.author_form.display_name
@@ -256,7 +256,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
Add a topic to the list.
"""
if self.topic_form.exec():
- topic = Topic.populate(name=self.topic_form.name)
+ topic = Topic(name=self.topic_form.name)
if self.check_topic_exists(topic):
if self.manager.save_object(topic):
self.reset_topics()
@@ -272,8 +272,8 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
Add a book to the list.
"""
if self.song_book_form.exec():
- book = Book.populate(name=self.song_book_form.name_edit.text(),
- publisher=self.song_book_form.publisher_edit.text())
+ book = SongBook(name=self.song_book_form.name_edit.text(),
+ publisher=self.song_book_form.publisher_edit.text())
if self.check_song_book_exists(book):
if self.manager.save_object(book):
self.reset_song_books()
@@ -370,7 +370,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
book_id = self._get_current_item_id(self.song_books_list_widget)
if book_id == -1:
return
- book = self.manager.get_object(Book, book_id)
+ book = self.manager.get_object(SongBook, book_id)
if book.publisher is None:
book.publisher = ''
self.song_book_form.name_edit.setText(book.name)
@@ -466,11 +466,11 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
"""
# Find the duplicate.
existing_book = self.manager.get_object_filtered(
- Book,
+ SongBook,
and_(
- Book.name == old_song_book.name,
- Book.publisher == old_song_book.publisher,
- Book.id != old_song_book.id
+ SongBook.name == old_song_book.name,
+ SongBook.publisher == old_song_book.publisher,
+ SongBook.id != old_song_book.id
)
)
if existing_book is None:
@@ -508,7 +508,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
self.manager.save_object(song)
self.manager.delete_object(SongBookEntry, (old_song_book.id, song_id, old_book_song_number))
- self.manager.delete_object(Book, old_song_book.id)
+ self.manager.delete_object(SongBook, old_song_book.id)
def on_delete_author_button_clicked(self):
"""
@@ -537,7 +537,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
"""
Delete the Book if the Book is not attached to any songs.
"""
- self._delete_item(Book, self.song_books_list_widget, self.reset_song_books,
+ self._delete_item(SongBook, self.song_books_list_widget, self.reset_song_books,
translate('SongsPlugin.SongMaintenanceForm', 'Delete Book'),
translate('SongsPlugin.SongMaintenanceForm',
'Are you sure you want to delete the selected book?'),
diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py
index 49c1be68f..83504ec91 100644
--- a/openlp/plugins/songs/lib/__init__.py
+++ b/openlp/plugins/songs/lib/__init__.py
@@ -387,7 +387,7 @@ def clean_song(manager, song):
name = SongStrings.AuthorUnknown
author = manager.get_object_filtered(Author, Author.display_name == name)
if author is None:
- author = Author.populate(display_name=name, last_name='', first_name='')
+ author = Author(display_name=name, last_name='', first_name='')
song.add_author(author)
if song.copyright:
song.copyright = CONTROL_CHARS.sub('', song.copyright).strip()
diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py
index 9d2ac5672..4b8717248 100644
--- a/openlp/plugins/songs/lib/db.py
+++ b/openlp/plugins/songs/lib/db.py
@@ -21,40 +21,124 @@
"""
The :mod:`db` module provides the database and schema that is the backend for
the Songs plugin
-"""
-from sqlalchemy import Column, ForeignKey, Table, types
-from sqlalchemy.orm import class_mapper, mapper, reconstructor, relation
-from sqlalchemy.sql.expression import func, text
-from sqlalchemy.orm.exc import UnmappedClassError
+The song database contains the following tables:
+
+ * authors
+ * authors_songs
+ * media_files
+ * media_files_songs
+ * song_books
+ * songs
+ * songs_songbooks
+ * songs_topics
+ * topics
+
+**authors** Table
+ This table holds the names of all the authors. It has the following
+ columns:
+
+ * id
+ * first_name
+ * last_name
+ * display_name
+
+**authors_songs Table**
+ This is a bridging table between the *authors* and *songs* tables, which
+ serves to create a many-to-many relationship between the two tables. It
+ has the following columns:
+
+ * author_id
+ * song_id
+ * author_type
+
+**media_files Table**
+ * id
+ * file_path
+ * file_hash
+ * type
+ * weight
+
+**song_books Table**
+ The *song_books* table holds a list of books that a congregation gets
+ their songs from, or old hymnals now no longer used. This table has the
+ following columns:
+
+ * id
+ * name
+ * publisher
+
+**songs Table**
+ This table contains the songs, and each song has a list of attributes.
+ The *songs* table has the following columns:
+
+ * id
+ * title
+ * alternate_title
+ * lyrics
+ * verse_order
+ * copyright
+ * comments
+ * ccli_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
+ has the following columns:
+
+ * song_id
+ * topic_id
+
+**topics Table**
+ The topics table holds a selection of topics that songs can cover. This
+ is useful when a worship leader wants to select songs with a certain
+ theme. This table has the following columns:
+
+ * id
+ * name
+"""
+from typing import Optional
+
+from sqlalchemy import Column, ForeignKey, MetaData, Table
+from sqlalchemy.ext.hybrid import hybrid_property
+from sqlalchemy.orm import reconstructor, relationship
+from sqlalchemy.sql.expression import func, text
+from sqlalchemy.types import Boolean, DateTime, Integer, Unicode, UnicodeText
+
+# Maintain backwards compatibility with older versions of SQLAlchemy while supporting SQLAlchemy 1.4+
+try:
+ from sqlalchemy.orm import declarative_base
+except ImportError:
+ from sqlalchemy.ext.declarative import declarative_base
+
from openlp.core.common.i18n import get_natural_key, translate
-from openlp.core.lib.db import BaseModel, PathType, init_db
+from openlp.core.lib.db import PathType, init_db
-class Author(BaseModel):
- """
- Author model
- """
- def get_display_name(self, author_type=None):
- if author_type:
- return "{name} ({author})".format(name=self.display_name, author=AuthorType.Types[author_type])
- return self.display_name
+Base = declarative_base(MetaData())
-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
+songs_topics_table = Table(
+ 'songs_topics', Base.metadata,
+ Column('song_id', Integer, ForeignKey('songs.id'), primary_key=True),
+ Column('topic_id', Integer, ForeignKey('topics.id'), primary_key=True)
+)
class AuthorType(object):
"""
- Enumeration for Author types.
+ Enumeration for Author
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.
@@ -99,37 +183,109 @@ class AuthorType(object):
return AuthorType.NoType
-class Book(BaseModel):
+class Author(Base):
"""
- Book model
+ Author model
"""
+ __tablename__ = 'authors'
+
+ id = Column(Integer, primary_key=True)
+ first_name = Column(Unicode(128))
+ last_name = Column(Unicode(128))
+ display_name = Column(Unicode(255), index=True, nullable=False)
+
+ authors_songs = relationship('AuthorSong', back_populates='author')
+
+ def get_display_name(self, author_type: Optional[str] = None) -> str:
+ if author_type:
+ return "{name} ({author})".format(name=self.display_name, author=AuthorType.Types[author_type])
+ return self.display_name
+
+
+class AuthorSong(Base):
+ """
+ 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
+ """
+ __tablename__ = 'authors_songs'
+
+ author_id = Column(Integer, ForeignKey('authors.id'), primary_key=True)
+ song_id = Column(Integer, ForeignKey('songs.id'), primary_key=True)
+ author_type = Column(Unicode(255), primary_key=True, nullable=False, server_default=text('""'))
+
+ author = relationship('Author', back_populates='authors_songs')
+ song = relationship('Song', back_populates='authors_songs')
+
+
+class SongBook(Base):
+ """
+ SongBook model
+ """
+ __tablename__ = 'song_books'
+
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode(128), nullable=False)
+ publisher = Column(Unicode(128))
+
+ songbook_entries = relationship('SongBookEntry', back_populates='songbook')
+
@property
def songs(self):
"""
A property to return the songs associated with this book.
"""
- return [sbe.song for sbe in self.entries]
+ return [sbe.song for sbe in self.songbook_entries]
def __repr__(self):
- return ''.format(myid=self.id,
- name=self.name,
- publisher=self.publisher)
+ return f''
-class MediaFile(BaseModel):
+class MediaFile(Base):
"""
MediaFile model
"""
- pass
+ __tablename__ = 'media_files'
+
+ id = Column(Integer, primary_key=True)
+ song_id = Column(Integer, ForeignKey('songs.id'), default=None)
+ file_path = Column(PathType, nullable=False)
+ file_hash = Column(Unicode(128), nullable=False)
+ type = Column(Unicode(64), nullable=False, default='audio')
+ weight = Column(Integer, default=0)
+
+ songs = relationship('Song', back_populates='media_files')
-class Song(BaseModel):
+class Song(Base):
"""
Song model
"""
+ __tablename__ = 'songs'
+ id = Column(Integer, primary_key=True)
+ title = Column(Unicode(255), nullable=False)
+ alternate_title = Column(Unicode(255))
+ lyrics = Column(UnicodeText, nullable=False)
+ verse_order = Column(Unicode(128))
+ copyright = Column(Unicode(255))
+ comments = Column(UnicodeText)
+ ccli_number = Column(Unicode(64))
+ theme_name = Column(Unicode(128))
+ search_title = Column(Unicode(255), index=True, nullable=False)
+ search_lyrics = Column(UnicodeText, nullable=False)
+ create_date = Column(DateTime, default=func.now())
+ last_modified = Column(DateTime, default=func.now(), onupdate=func.now())
+ temporary = Column(Boolean, default=False)
- def __init__(self):
- self.sort_key = []
+ authors_songs = relationship('AuthorSong', back_populates='song', cascade='all, delete-orphan')
+ media_files = relationship('MediaFile', back_populates='songs', order_by='MediaFile.weight')
+ songbook_entries = relationship('SongBookEntry', back_populates='song', cascade='all, delete-orphan')
+ topics = relationship('Topic', back_populates='songs', secondary=songs_topics_table)
+
+ @hybrid_property
+ def authors(self):
+ return [author_song.author for author_song in self.authors_songs]
@reconstructor
def init_on_load(self):
@@ -151,7 +307,7 @@ class Song(BaseModel):
for author_song in self.authors_songs:
if author_song.author == author and author_song.author_type == author_type:
return
- new_author_song = AuthorSong()
+ new_author_song = AuthorSong(author=author, author_type=author_type)
new_author_song.author = author
new_author_song.author_type = author_type
self.authors_songs.append(new_author_song)
@@ -185,10 +341,19 @@ class Song(BaseModel):
self.songbook_entries.append(new_songbook_entry)
-class SongBookEntry(BaseModel):
+class SongBookEntry(Base):
"""
SongBookEntry model
"""
+ __tablename__ = 'songs_songbooks'
+
+ songbook_id = Column(Integer, ForeignKey('song_books.id'), primary_key=True)
+ song_id = Column(Integer, ForeignKey('songs.id'), primary_key=True)
+ entry = Column(Unicode(255), primary_key=True, nullable=False)
+
+ songbook = relationship('SongBook', back_populates='songbook_entries')
+ song = relationship('Song', back_populates='songbook_entries')
+
def __repr__(self):
return SongBookEntry.get_display_name(self.songbook.name, self.entry)
@@ -199,11 +364,15 @@ class SongBookEntry(BaseModel):
return songbook_name
-class Topic(BaseModel):
+class Topic(Base):
"""
Topic model
"""
- pass
+ __tablename__ = 'topics'
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode(128), index=True, nullable=False)
+
+ songs = relationship('Song', back_populates='topics', secondary=songs_topics_table)
def init_schema(url):
@@ -212,214 +381,7 @@ def init_schema(url):
:param url: The database to setup
- The song database contains the following tables:
-
- * authors
- * authors_songs
- * media_files
- * media_files_songs
- * song_books
- * songs
- * songs_songbooks
- * songs_topics
- * topics
-
- **authors** Table
- This table holds the names of all the authors. It has the following
- columns:
-
- * id
- * first_name
- * last_name
- * display_name
-
- **authors_songs Table**
- This is a bridging table between the *authors* and *songs* tables, which
- serves to create a many-to-many relationship between the two tables. It
- has the following columns:
-
- * author_id
- * song_id
- * author_type
-
- **media_files Table**
- * id
- * file_path
- * file_hash
- * type
- * weight
-
- **song_books Table**
- The *song_books* table holds a list of books that a congregation gets
- their songs from, or old hymnals now no longer used. This table has the
- following columns:
-
- * id
- * name
- * publisher
-
- **songs Table**
- This table contains the songs, and each song has a list of attributes.
- The *songs* table has the following columns:
-
- * id
- * title
- * alternate_title
- * lyrics
- * verse_order
- * copyright
- * comments
- * ccli_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
- has the following columns:
-
- * song_id
- * topic_id
-
- **topics Table**
- The topics table holds a selection of topics that songs can cover. This
- is useful when a worship leader wants to select songs with a certain
- theme. This table has the following columns:
-
- * id
- * name
"""
- session, metadata = init_db(url)
-
- # Definition of the "authors" table
- authors_table = Table(
- 'authors', metadata,
- Column('id', types.Integer(), primary_key=True),
- Column('first_name', types.Unicode(128)),
- Column('last_name', types.Unicode(128)),
- Column('display_name', types.Unicode(255), index=True, nullable=False)
- )
-
- # Definition of the "media_files" table
- media_files_table = Table(
- 'media_files', metadata,
- Column('id', types.Integer(), primary_key=True),
- Column('song_id', types.Integer(), ForeignKey('songs.id'), default=None),
- Column('file_path', PathType, nullable=False),
- Column('file_hash', types.Unicode(128), nullable=False),
- Column('type', types.Unicode(64), nullable=False, default='audio'),
- Column('weight', types.Integer(), default=0)
- )
-
- # Definition of the "song_books" table
- song_books_table = Table(
- 'song_books', metadata,
- Column('id', types.Integer(), primary_key=True),
- Column('name', types.Unicode(128), nullable=False),
- Column('publisher', types.Unicode(128))
- )
-
- # Definition of the "songs" table
- songs_table = Table(
- 'songs', metadata,
- Column('id', types.Integer(), primary_key=True),
- Column('title', types.Unicode(255), nullable=False),
- Column('alternate_title', types.Unicode(255)),
- Column('lyrics', types.UnicodeText, nullable=False),
- Column('verse_order', types.Unicode(128)),
- Column('copyright', types.Unicode(255)),
- Column('comments', types.UnicodeText),
- Column('ccli_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),
- Column('create_date', types.DateTime(), default=func.now()),
- Column('last_modified', types.DateTime(), default=func.now(), onupdate=func.now()),
- Column('temporary', types.Boolean(), default=False)
- )
-
- # Definition of the "topics" table
- topics_table = Table(
- 'topics', metadata,
- Column('id', types.Integer(), primary_key=True),
- Column('name', types.Unicode(128), index=True, nullable=False)
- )
-
- # Definition of the "authors_songs" table
- 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('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,
- Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
- Column('topic_id', types.Integer(), ForeignKey('topics.id'), primary_key=True)
- )
-
- # try/except blocks are for the purposes of tests - the mappers could have been defined in a previous test
- try:
- class_mapper(Author)
- except UnmappedClassError:
- mapper(Author, authors_table, properties={
- 'songs': relation(Song, secondary=authors_songs_table, viewonly=True)
- })
- try:
- class_mapper(AuthorSong)
- except UnmappedClassError:
- mapper(AuthorSong, authors_songs_table, properties={
- 'author': relation(Author)
- })
- try:
- class_mapper(SongBookEntry)
- except UnmappedClassError:
- mapper(SongBookEntry, songs_songbooks_table, properties={
- 'songbook': relation(Book, backref='entries')
- })
- try:
- class_mapper(Book)
- except UnmappedClassError:
- mapper(Book, song_books_table)
- try:
- class_mapper(MediaFile)
- except UnmappedClassError:
- mapper(MediaFile, media_files_table)
- try:
- class_mapper(Song)
- except UnmappedClassError:
- mapper(Song, songs_table, properties={
- # Use the authors_songs relation when you need access to the 'author_type' attribute
- # or when creating new relations
- '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'),
- '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)
- })
- try:
- class_mapper(Topic)
- except UnmappedClassError:
- mapper(Topic, topics_table)
-
+ session, metadata = init_db(url, base=Base)
metadata.create_all(checkfirst=True)
return session
diff --git a/openlp/plugins/songs/lib/importers/foilpresenter.py b/openlp/plugins/songs/lib/importers/foilpresenter.py
index 7079b1153..a6f81b07f 100644
--- a/openlp/plugins/songs/lib/importers/foilpresenter.py
+++ b/openlp/plugins/songs/lib/importers/foilpresenter.py
@@ -89,7 +89,7 @@ from lxml import etree, objectify
from openlp.core.common.i18n import translate
from openlp.core.widgets.wizard import WizardStrings
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, SongBook, Song, Topic
from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.openlyricsxml import SongXML
from openlp.plugins.songs.lib.ui import SongStrings
@@ -325,8 +325,8 @@ class FoilPresenter(object):
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]))
+ author = Author(display_name=display_name, last_name=display_name.split(' ')[-1],
+ first_name=' '.join(display_name.split(' ')[:-1]))
self.manager.save_object(author)
song.add_author(author)
@@ -478,10 +478,10 @@ class FoilPresenter(object):
for bucheintrag in foilpresenterfolie.buch.bucheintrag:
book_name = to_str(bucheintrag.name)
if book_name:
- book = self.manager.get_object_filtered(Book, Book.name == book_name)
+ book = self.manager.get_object_filtered(SongBook, SongBook.name == book_name)
if book is None:
# We need to create a book, because it does not exist.
- book = Book.populate(name=book_name, publisher='')
+ book = SongBook(name=book_name, publisher='')
self.manager.save_object(book)
song.song_book_id = book.id
try:
@@ -527,7 +527,7 @@ class FoilPresenter(object):
topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text)
if topic is None:
# We need to create a topic, because it does not exist.
- topic = Topic.populate(name=topic_text)
+ topic = Topic(name=topic_text)
self.manager.save_object(topic)
song.topics.append(topic)
except AttributeError:
diff --git a/openlp/plugins/songs/lib/importers/openlp.py b/openlp/plugins/songs/lib/importers/openlp.py
index a03fdb5a8..20aa150cb 100644
--- a/openlp/plugins/songs/lib/importers/openlp.py
+++ b/openlp/plugins/songs/lib/importers/openlp.py
@@ -24,23 +24,39 @@ song databases into the current installation database.
"""
import logging
from pathlib import Path
+from sqlite3 import Row, connect as sqlite3_connect
-from sqlalchemy import MetaData, Table, create_engine
-from sqlalchemy.orm import class_mapper, mapper, relation, scoped_session, sessionmaker
-from sqlalchemy.orm.exc import UnmappedClassError
-
+from openlp.core.common import sha256_file_hash
from openlp.core.common.i18n import translate
-from openlp.core.lib.db import BaseModel
from openlp.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib import clean_song
-from openlp.plugins.songs.lib.db import Author, Book, MediaFile, Song, Topic
+from openlp.plugins.songs.lib.db import Author, SongBook, MediaFile, Song, Topic
from .songimport import SongImport
-
log = logging.getLogger(__name__)
+SONG_AUTHORS = 'SELECT * FROM authors AS a JOIN authors_songs AS s ON a.id = s.author_id WHERE s.song_id = ?'
+SONG_TOPICS = 'SELECT t.name AS name FROM topics AS t JOIN songs_topics AS st ON t.id = st.topic_id ' \
+ 'WHERE st.song_id = ?'
+SONG_BOOKS = 'SELECT b.name, b.publisher, e.entry FROM song_books AS b ' \
+ 'JOIN songs_songbooks AS e ON b.id = e.songbook_id WHERE e.song_id = ?'
+SONG_MEDIA = 'SELECT * FROM media_files WHERE song_id = ?'
+
+
+def does_table_exist(conn, table_name: str) -> bool:
+ """Check if a table exists in the database"""
+ res = conn.execute('SELECT name FROM sqlite_master WHERE type = "table" AND name = ?', (table_name,))
+ return res.fetchone() is not None
+
+
+def does_column_exist(conn, table_name: str, column_name: str) -> bool:
+ """Check if a table exists in the database"""
+ res = conn.execute(f'SELECT * FROM {table_name} LIMIT 1')
+ return column_name in res.fetchone().keys()
+
+
class OpenLPSongImport(SongImport):
"""
The :class:`OpenLPSongImport` class provides OpenLP with the ability to
@@ -62,228 +78,109 @@ class OpenLPSongImport(SongImport):
:param progress_dialog: The QProgressDialog used when importing songs from the FRW.
"""
-
- class OldAuthorSong(BaseModel):
- """
- Maps to the authors_songs table
- """
- pass
-
- class OldAuthor(BaseModel):
- """
- Maps to the authors table
- """
- pass
-
- class OldBook(BaseModel):
- """
- Maps to the songbooks table
- """
- pass
-
- class OldMediaFile(BaseModel):
- """
- Maps to the media_files table
- """
- pass
-
- class OldSong(BaseModel):
- """
- Maps to the songs table
- """
- pass
-
- class OldTopic(BaseModel):
- """
- Maps to the topics table
- """
- pass
-
- class OldSongBookEntry(BaseModel):
- """
- Maps to the songs_songbooks table
- """
- pass
-
# Check the file type
if self.import_source.suffix != '.sqlite':
self.log_error(self.import_source, translate('SongsPlugin.OpenLPSongImport',
'Not a valid OpenLP 2 song database.'))
return
- self.import_source = 'sqlite:///{url}'.format(url=self.import_source)
- # Load the db file and reflect it
- engine = create_engine(self.import_source)
- source_meta = MetaData()
- source_meta.reflect(engine)
- self.source_session = scoped_session(sessionmaker(bind=engine))
- # Run some checks to see which version of the database we have
- table_list = list(source_meta.tables.keys())
- if 'media_files' in table_list:
- has_media_files = True
- else:
- has_media_files = False
- if 'songs_songbooks' in table_list:
- has_songs_books = True
- else:
- has_songs_books = False
- if 'authors_songs' in table_list:
- has_authors_songs = True
- else:
- has_authors_songs = False
- # Load up the tabls and map them out
- try:
- source_authors_table = source_meta.tables['authors']
- source_song_books_table = source_meta.tables['song_books']
- source_songs_table = source_meta.tables['songs']
- source_topics_table = source_meta.tables['topics']
- source_authors_songs_table = source_meta.tables['authors_songs']
- source_songs_topics_table = source_meta.tables['songs_topics']
- source_media_files_songs_table = None
- except KeyError:
+ # Connect to the database
+ conn = sqlite3_connect(self.import_source)
+ conn.row_factory = Row
+ # Check that we have some of the mandatory tables
+ if not any([does_table_exist(conn, 'songs'), does_table_exist(conn, 'authors'),
+ does_table_exist(conn, 'topics'), does_table_exist(conn, 'song_books')]):
self.log_error(self.import_source, translate('SongsPlugin.OpenLPSongImport',
'Not a valid OpenLP 2 song database.'))
return
- # Set up media_files relations
- if has_media_files:
- source_media_files_table = source_meta.tables['media_files']
- source_media_files_songs_table = source_meta.tables.get('media_files_songs')
- try:
- class_mapper(OldMediaFile)
- except UnmappedClassError:
- mapper(OldMediaFile, source_media_files_table)
- if has_songs_books:
- source_songs_songbooks_table = source_meta.tables['songs_songbooks']
- try:
- class_mapper(OldSongBookEntry)
- except UnmappedClassError:
- mapper(OldSongBookEntry, source_songs_songbooks_table, properties={'songbook': relation(OldBook)})
- if has_authors_songs:
- try:
- class_mapper(OldAuthorSong)
- except UnmappedClassError:
- mapper(OldAuthorSong, source_authors_songs_table)
- if has_authors_songs and 'author_type' in source_authors_songs_table.c.keys():
- has_author_type = True
- else:
- has_author_type = False
- # Set up the songs relationships
- song_props = {
- 'authors': relation(OldAuthor, backref='songs', secondary=source_authors_songs_table),
- 'topics': relation(OldTopic, backref='songs', secondary=source_songs_topics_table)
- }
- if has_media_files:
- if isinstance(source_media_files_songs_table, Table):
- song_props['media_files'] = relation(OldMediaFile, backref='songs',
- secondary=source_media_files_songs_table)
- else:
- song_props['media_files'] = \
- relation(OldMediaFile, backref='songs',
- foreign_keys=[source_media_files_table.c.song_id],
- primaryjoin=source_songs_table.c.id == source_media_files_table.c.song_id)
- if has_songs_books:
- song_props['songbook_entries'] = relation(OldSongBookEntry, backref='song', cascade='all, delete-orphan')
- else:
- song_props['book'] = relation(OldBook, backref='songs')
- if has_authors_songs:
- song_props['authors_songs'] = relation(OldAuthorSong)
- # Map the rest of the tables
- try:
- class_mapper(OldAuthor)
- except UnmappedClassError:
- mapper(OldAuthor, source_authors_table)
- try:
- class_mapper(OldBook)
- except UnmappedClassError:
- mapper(OldBook, source_song_books_table)
- try:
- class_mapper(OldSong)
- except UnmappedClassError:
- mapper(OldSong, source_songs_table, properties=song_props)
- try:
- class_mapper(OldTopic)
- except UnmappedClassError:
- mapper(OldTopic, source_topics_table)
-
- source_songs = self.source_session.query(OldSong).all()
+ # Determine the database structure
+ has_media_files = does_table_exist(conn, 'media_files')
+ has_songs_books = does_table_exist(conn, 'songs_songbooks')
+ # has_authors_songs = does_table_exist(conn, 'authors_songs')
+ # has_author_type = has_authors_songs and does_column_exist(conn, 'authors_songs', 'author_type')
+ # Set up wizard
if self.import_wizard:
- self.import_wizard.progress_bar.setMaximum(len(source_songs))
- for song in source_songs:
+ song_count = conn.execute('SELECT COUNT(id) AS song_count FROM songs').fetchone()
+ self.import_wizard.progress_bar.setMaximum(song_count['song_count'])
+ for song in conn.execute('SELECT * FROM songs'):
new_song = Song()
- new_song.title = song.title
- if has_media_files and hasattr(song, 'alternate_title'):
- new_song.alternate_title = song.alternate_title
+ new_song.title = song['title']
+ if 'alternate_title' in song.keys():
+ new_song.alternate_title = song['alternate_title']
else:
- old_titles = song.search_title.split('@')
+ old_titles = song['search_title'].split('@')
if len(old_titles) > 1:
new_song.alternate_title = old_titles[1]
# Transfer the values to the new song object
new_song.search_title = ''
new_song.search_lyrics = ''
- new_song.lyrics = song.lyrics
- new_song.verse_order = song.verse_order
- new_song.copyright = song.copyright
- new_song.comments = song.comments
- new_song.theme_name = song.theme_name
- new_song.ccli_number = song.ccli_number
- if hasattr(song, 'song_number') and song.song_number:
- new_song.song_number = song.song_number
+ new_song.lyrics = song['lyrics']
+ new_song.verse_order = song['verse_order']
+ new_song.copyright = song['copyright']
+ new_song.comments = song['comments']
+ new_song.theme_name = song['theme_name']
+ new_song.ccli_number = song['ccli_number']
+ if 'song_number' in song.keys():
+ new_song.song_number = song['song_number']
# Find or create all the authors and add them to the new song object
- for author in song.authors:
- existing_author = self.manager.get_object_filtered(Author, Author.display_name == author.display_name)
+ song_authors = conn.execute(SONG_AUTHORS, (song['id'],))
+ for author in song_authors:
+ existing_author = self.manager.get_object_filtered(Author,
+ Author.display_name == author['display_name'])
if not existing_author:
- existing_author = Author.populate(
- first_name=author.first_name,
- last_name=author.last_name,
- display_name=author.display_name
+ existing_author = Author(
+ first_name=author['first_name'],
+ last_name=author['last_name'],
+ display_name=author['display_name']
)
# If this is a new database, we need to import the author_type too
- author_type = None
- if has_author_type:
- for author_song in song.authors_songs:
- if author_song.author_id == author.id:
- author_type = author_song.author_type
- break
+ try:
+ author_type = author['author_type']
+ except IndexError:
+ author_type = ''
new_song.add_author(existing_author, author_type)
# Find or create all the topics and add them to the new song object
- if song.topics:
- for topic in song.topics:
- existing_topic = self.manager.get_object_filtered(Topic, Topic.name == topic.name)
- if not existing_topic:
- existing_topic = Topic.populate(name=topic.name)
- new_song.topics.append(existing_topic)
+ for topic in conn.execute(SONG_TOPICS, (song['id'],)):
+ existing_topic = self.manager.get_object_filtered(Topic, Topic.name == topic['name'])
+ if not existing_topic:
+ existing_topic = Topic(name=topic['name'])
+ new_song.topics.append(existing_topic)
# Find or create all the songbooks and add them to the new song object
- if has_songs_books and song.songbook_entries:
- for entry in song.songbook_entries:
- existing_book = self.manager.get_object_filtered(Book, Book.name == entry.songbook.name)
+ if has_songs_books:
+ for entry in conn.execute(SONG_BOOKS, (song['id'],)):
+ existing_book = self.manager.get_object_filtered(SongBook, SongBook.name == entry['name'])
if not existing_book:
- existing_book = Book.populate(name=entry.songbook.name, publisher=entry.songbook.publisher)
- new_song.add_songbook_entry(existing_book, entry.entry)
- elif hasattr(song, 'book') and song.book:
- existing_book = self.manager.get_object_filtered(Book, Book.name == song.book.name)
+ existing_book = SongBook(name=entry['name'], publisher=entry['publisher'])
+ new_song.add_songbook_entry(existing_book, entry['entry'])
+ elif does_column_exist(conn, 'songs', 'book_id'):
+ book = conn.execute('SELECT name, publisher FROM song_books WHERE id = ?', (song['book_id'],))
+ existing_book = self.manager.get_object_filtered(SongBook, SongBook.name == book['name'])
if not existing_book:
- existing_book = Book.populate(name=song.book.name, publisher=song.book.publisher)
+ existing_book = SongBook(name=book['name'], publisher=book['publisher'])
# Get the song_number from "songs" table "song_number" field. (This is db structure from 2.2.1)
# If there's a number, add it to the song, otherwise it will be "".
- existing_number = song.song_number if hasattr(song, 'song_number') else ''
- if existing_number:
- new_song.add_songbook_entry(existing_book, existing_number)
+ if 'song_number' in song.keys():
+ new_song.add_songbook_entry(existing_book, song['song_number'])
else:
new_song.add_songbook_entry(existing_book, '')
# Find or create all the media files and add them to the new song object
- if has_media_files and song.media_files:
- for media_file in song.media_files:
+ if has_media_files:
+ for media_file in conn.execute(SONG_MEDIA, (song['id'],)):
# Database now uses paths rather than strings for media files, and the key name has
# changed appropriately. This catches any databases using the old key name.
try:
- media_path = media_file.file_path
- except Exception:
- media_path = Path(media_file.file_name)
- existing_media_file = self.manager.get_object_filtered(
- MediaFile, MediaFile.file_path == media_path)
+ media_path = media_file['file_path']
+ except (IndexError, KeyError):
+ media_path = Path(media_file['file_name'])
+ existing_media_file = self.manager.get_object_filtered(MediaFile,
+ MediaFile.file_path == media_path)
if existing_media_file:
new_song.media_files.append(existing_media_file)
else:
- new_song.media_files.append(MediaFile.populate(file_path=media_path))
+ if 'file_hash' in media_file.keys():
+ file_hash = media_file['file_hash']
+ else:
+ file_hash = sha256_file_hash(media_path)
+ new_song.media_files.append(MediaFile(file_path=media_path, file_hash=file_hash))
clean_song(self.manager, new_song)
self.manager.save_object(new_song)
if progress_dialog:
@@ -293,5 +190,4 @@ class OpenLPSongImport(SongImport):
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=new_song.title))
if self.stop_import_flag:
break
- self.source_session.close()
- engine.dispose()
+ conn.close()
diff --git a/openlp/plugins/songs/lib/importers/songimport.py b/openlp/plugins/songs/lib/importers/songimport.py
index 59de77f27..bf7f523cc 100644
--- a/openlp/plugins/songs/lib/importers/songimport.py
+++ b/openlp/plugins/songs/lib/importers/songimport.py
@@ -32,7 +32,7 @@ from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry
from openlp.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib import VerseType, clean_song
-from openlp.plugins.songs.lib.db import Author, Book, MediaFile, Song, Topic
+from openlp.plugins.songs.lib.db import Author, SongBook, MediaFile, Song, Topic
from openlp.plugins.songs.lib.openlyricsxml import SongXML
from openlp.plugins.songs.lib.ui import SongStrings
@@ -366,21 +366,21 @@ class SongImport(QtCore.QObject):
for author_text, author_type in self.authors:
author = self.manager.get_object_filtered(Author, Author.display_name == author_text)
if not author:
- author = Author.populate(display_name=author_text,
- last_name=author_text.split(' ')[-1],
- first_name=' '.join(author_text.split(' ')[:-1]))
+ author = Author(display_name=author_text,
+ last_name=author_text.split(' ')[-1],
+ first_name=' '.join(author_text.split(' ')[:-1]))
song.add_author(author, author_type)
if self.song_book_name:
- song_book = self.manager.get_object_filtered(Book, Book.name == self.song_book_name)
+ song_book = self.manager.get_object_filtered(SongBook, SongBook.name == self.song_book_name)
if song_book is None:
- song_book = Book.populate(name=self.song_book_name, publisher=self.song_book_pub)
+ song_book = SongBook(name=self.song_book_name, publisher=self.song_book_pub)
song.add_songbook_entry(song_book, song.song_number)
for topic_text in self.topics:
if not topic_text:
continue
topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text)
if topic is None:
- topic = Topic.populate(name=topic_text)
+ topic = Topic(name=topic_text)
song.topics.append(topic)
song.temporary = temporary_flag
# We need to save the song now, before adding the media files, so that
@@ -394,7 +394,7 @@ class SongImport(QtCore.QObject):
if not media_file:
if file_path.parent:
file_path = self.copy_media_file(song.id, file_path)
- song.media_files.append(MediaFile.populate(file_path=file_path, weight=weight))
+ song.media_files.append(MediaFile(file_path=file_path, weight=weight))
self.manager.save_object(song)
self.set_defaults()
return song.id
diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py
index 698db3231..a179b5231 100644
--- a/openlp/plugins/songs/lib/mediaitem.py
+++ b/openlp/plugins/songs/lib/mediaitem.py
@@ -44,7 +44,7 @@ from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.forms.songimportform import SongImportForm
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
-from openlp.plugins.songs.lib.db import Author, AuthorType, Book, MediaFile, Song, SongBookEntry, Topic
+from openlp.plugins.songs.lib.db import Author, AuthorType, SongBook, MediaFile, Song, SongBookEntry, Topic
from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML
from openlp.plugins.songs.lib.ui import SongStrings
@@ -84,7 +84,7 @@ class SongMediaItem(MediaManagerItem):
AppLocation.get_section_data_path(self.plugin.name) / 'audio' / str(song.id) / os.path.split(bga[0])[1]
create_paths(dest_path.parent)
copyfile(AppLocation.get_section_data_path('servicemanager') / bga[0], dest_path)
- song.media_files.append(MediaFile.populate(weight=i, file_path=dest_path, file_hash=bga[1]))
+ song.media_files.append(MediaFile(weight=i, file_path=dest_path, file_hash=bga[1]))
self.plugin.manager.save_object(song, True)
def add_middle_header_bar(self):
@@ -194,10 +194,10 @@ class SongMediaItem(MediaManagerItem):
search_keywords = search_keywords.rpartition(' ')
search_book = '{text}%'.format(text=search_keywords[0])
search_entry = '{text}%'.format(text=search_keywords[2])
- search_results = (self.plugin.manager.session.query(SongBookEntry.entry, Book.name, Song.title, Song.id)
+ search_results = (self.plugin.manager.session.query(SongBookEntry.entry, SongBook.name, Song.title, Song.id)
.join(Song)
- .join(Book)
- .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry),
+ .join(SongBook)
+ .filter(SongBook.name.like(search_book), SongBookEntry.entry.like(search_entry),
Song.temporary.is_(False)).all())
self.display_results_book(search_results)
elif search_type == SongSearch.Themes:
@@ -223,8 +223,8 @@ class SongMediaItem(MediaManagerItem):
search_string = '%{text}%'.format(text=clean_string(search_keywords))
return self.plugin.manager.session.query(Song) \
.join(SongBookEntry, isouter=True) \
- .join(Book, isouter=True) \
- .filter(or_(Book.name.like(search_string), SongBookEntry.entry.like(search_string),
+ .join(SongBook, isouter=True) \
+ .filter(or_(SongBook.name.like(search_string), SongBookEntry.entry.like(search_string),
# hint: search_title contains alternate title
Song.search_title.like(search_string), Song.search_lyrics.like(search_string),
Song.comments.like(search_string))) \
diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py
index cc91fceaf..3ea1d3992 100644
--- a/openlp/plugins/songs/lib/openlyricsxml.py
+++ b/openlp/plugins/songs/lib/openlyricsxml.py
@@ -65,7 +65,7 @@ from openlp.core.common.registry import Registry
from openlp.core.lib.formattingtags import FormattingTags
from openlp.core.version import get_version
from openlp.plugins.songs.lib import VerseType, clean_song
-from openlp.plugins.songs.lib.db import Author, AuthorType, Book, Song, Topic
+from openlp.plugins.songs.lib.db import Author, AuthorType, SongBook, Song, Topic
log = logging.getLogger(__name__)
@@ -536,9 +536,9 @@ class OpenLyrics(object):
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]))
+ author = Author(display_name=display_name,
+ last_name=display_name.split(' ')[-1],
+ first_name=' '.join(display_name.split(' ')[:-1]))
song.add_author(author, author_type)
def _process_cclinumber(self, properties, song):
@@ -784,10 +784,10 @@ class OpenLyrics(object):
for songbook in properties.songbooks.songbook:
book_name = songbook.get('name', '')
if book_name:
- book = self.manager.get_object_filtered(Book, Book.name == book_name)
+ book = self.manager.get_object_filtered(SongBook, SongBook.name == book_name)
if book is None:
# We need to create a book, because it does not exist.
- book = Book.populate(name=book_name, publisher='')
+ book = SongBook(name=book_name, publisher='')
self.manager.save_object(book)
song.add_songbook_entry(book, songbook.get('entry', ''))
@@ -819,7 +819,7 @@ class OpenLyrics(object):
topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text)
if topic is None:
# We need to create a topic, because it does not exist.
- topic = Topic.populate(name=topic_text)
+ topic = Topic(name=topic_text)
self.manager.save_object(topic)
song.topics.append(topic)
diff --git a/openlp/plugins/songs/lib/songselect.py b/openlp/plugins/songs/lib/songselect.py
index fc92e7272..0a7b80cbb 100644
--- a/openlp/plugins/songs/lib/songselect.py
+++ b/openlp/plugins/songs/lib/songselect.py
@@ -233,7 +233,7 @@ class SongSelectImport(object):
:param song: Dictionary of the song to save
:return:
"""
- db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
+ db_song = Song(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
song_xml = SongXML()
verse_order = []
for verse in song['verses']:
@@ -265,12 +265,12 @@ class SongSelectImport(object):
last_name = ''
else:
last_name = name_parts[1]
- author = Author.populate(first_name=first_name, last_name=last_name, display_name=author_name)
+ author = Author(first_name=first_name, last_name=last_name, display_name=author_name)
db_song.add_author(author)
for topic_name in song.get('topics', []):
topic = self.db_manager.get_object_filtered(Topic, Topic.name == topic_name)
if not topic:
- topic = Topic.populate(name=topic_name)
+ topic = Topic(name=topic_name)
db_song.topics.append(topic)
self.db_manager.save_object(db_song)
return db_song
diff --git a/setup.cfg b/setup.cfg
index f1afb1348..c24465853 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -33,3 +33,7 @@ source =
[tool:pytest]
qt_api=pyqt5
+
+[mypy]
+# Ignore for now, we'll come back to this later
+ignore_errors = true
diff --git a/tests/openlp_plugins/songs/forms/test_songmaintenanceform.py b/tests/openlp_plugins/songs/forms/test_songmaintenanceform.py
index cd7b82a41..ed58bcc79 100644
--- a/tests/openlp_plugins/songs/forms/test_songmaintenanceform.py
+++ b/tests/openlp_plugins/songs/forms/test_songmaintenanceform.py
@@ -31,7 +31,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings
from openlp.core.common.registry import Registry
from openlp.core.lib.db import Manager
-from openlp.plugins.songs.lib.db import init_schema, Book, Song, SongBookEntry
+from openlp.plugins.songs.lib.db import init_schema, SongBook, Song, SongBookEntry
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from sqlalchemy.sql import and_
@@ -220,7 +220,7 @@ def test_delete_book_assigned(mocked_critical_error_message_box, form_env):
# GIVEN: Some mocked items
form = form_env[0]
mocked_manager = form_env[1]
- mocked_item = create_autospec(Book, spec_set=True)
+ mocked_item = create_autospec(SongBook, spec_set=True)
mocked_item.id = 1
mocked_manager.get_object.return_value = mocked_item
mocked_critical_error_message_box.return_value = QtWidgets.QMessageBox.Yes
@@ -315,7 +315,7 @@ def test_reset_topics(MockedTopic, MockedQListWidgetItem, form_env):
@patch('openlp.plugins.songs.forms.songmaintenanceform.QtWidgets.QListWidgetItem')
-@patch('openlp.plugins.songs.forms.songmaintenanceform.Book')
+@patch('openlp.plugins.songs.forms.songmaintenanceform.SongBook')
def test_reset_song_books(MockedBook, MockedQListWidgetItem, form_env):
"""
Test the reset_song_books() method
@@ -402,7 +402,7 @@ def test_check_topic_exists(MockedTopic, form_env):
@patch('openlp.plugins.songs.forms.songmaintenanceform.and_')
-@patch('openlp.plugins.songs.forms.songmaintenanceform.Book')
+@patch('openlp.plugins.songs.forms.songmaintenanceform.SongBook')
def test_check_song_book_exists(MockedBook, mocked_and, form_env):
"""
Test the check_song_book_exists() method
@@ -498,11 +498,11 @@ def test_merge_song_books(registry, settings, temp_folder):
manager = Manager('songs', init_schema, db_file_path=db_tmp_path)
# create 2 song books, both with the same name
- book1 = Book()
+ book1 = SongBook()
book1.name = 'test book1'
book1.publisher = ''
manager.save_object(book1)
- book2 = Book()
+ book2 = SongBook()
book2.name = 'test book1'
book2.publisher = ''
manager.save_object(book2)
@@ -550,7 +550,7 @@ def test_merge_song_books(registry, settings, temp_folder):
SongBookEntry.song_id == song2.id))
song3_book2_entry = manager.get_all_objects(SongBookEntry, and_(SongBookEntry.songbook_id == book2.id,
SongBookEntry.song_id == song3.id))
- books = manager.get_all_objects(Book, Book.name == 'test book1')
+ books = manager.get_all_objects(SongBook, SongBook.name == 'test book1')
# song records should not be deleted
assert len(songs) == 3
diff --git a/tests/openlp_plugins/songs/test_db.py b/tests/openlp_plugins/songs/test_db.py
index 34827dd8b..02939e848 100644
--- a/tests/openlp_plugins/songs/test_db.py
+++ b/tests/openlp_plugins/songs/test_db.py
@@ -26,7 +26,7 @@ import shutil
from openlp.core.lib.db import upgrade_db
from openlp.plugins.songs.lib import upgrade
-from openlp.plugins.songs.lib.db import Author, AuthorType, Book, Song
+from openlp.plugins.songs.lib.db import Author, AuthorType, SongBook, Song
from tests.utils.constants import TEST_RESOURCES_PATH
@@ -174,7 +174,7 @@ def test_add_songbooks():
# GIVEN: A mocked song and songbook
song = Song()
song.songbook_entries = []
- songbook = Book()
+ songbook = SongBook()
songbook.name = "Thy Word"
# WHEN: We add two songbooks to a Song
diff --git a/tests/openlp_plugins/songs/test_mediaitem.py b/tests/openlp_plugins/songs/test_mediaitem.py
index d582ecfc2..90bce0b89 100644
--- a/tests/openlp_plugins/songs/test_mediaitem.py
+++ b/tests/openlp_plugins/songs/test_mediaitem.py
@@ -548,7 +548,7 @@ def test_build_remote_search(media_item):
assert search_results == [[123, 'My Song', 'My alternative']]
-@patch('openlp.plugins.songs.lib.mediaitem.Book')
+@patch('openlp.plugins.songs.lib.mediaitem.SongBook')
@patch('openlp.plugins.songs.lib.mediaitem.SongBookEntry')
@patch('openlp.plugins.songs.lib.mediaitem.Song')
@patch('openlp.plugins.songs.lib.mediaitem.or_')
diff --git a/tests/openlp_plugins/songs/test_openlpimporter.py b/tests/openlp_plugins/songs/test_openlpimporter.py
index 0cf6b3399..f32c71dbf 100644
--- a/tests/openlp_plugins/songs/test_openlpimporter.py
+++ b/tests/openlp_plugins/songs/test_openlpimporter.py
@@ -21,54 +21,108 @@
"""
This module contains tests for the OpenLP song importer.
"""
+import json
from pathlib import Path
-from unittest import TestCase
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, patch, call
+
+import pytest
-from openlp.core.common.registry import Registry
from openlp.plugins.songs.lib.importers.openlp import OpenLPSongImport
+# from tests.helpers.songfileimport import SongImportTestHelper
+from tests.utils.constants import RESOURCE_PATH
-class TestOpenLPImport(TestCase):
+TEST_PATH = RESOURCE_PATH / 'songs' / 'openlp'
+
+
+def test_create_importer(registry):
"""
- Test the functions in the :mod:`openlp` importer module.
+ Test creating an instance of the OpenLP database importer
"""
- def setUp(self):
- """
- Create the registry
- """
- Registry.create()
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ with patch('openlp.plugins.songs.lib.importers.openlp.SongImport'):
+ mocked_manager = MagicMock()
- def test_create_importer(self):
- """
- Test creating an instance of the OpenLP database importer
- """
- # GIVEN: A mocked out SongImport class, and a mocked out "manager"
- with patch('openlp.plugins.songs.lib.importers.openlp.SongImport'):
- mocked_manager = MagicMock()
+ # WHEN: An importer object is created
+ importer = OpenLPSongImport(mocked_manager, file_paths=[])
- # WHEN: An importer object is created
- importer = OpenLPSongImport(mocked_manager, file_paths=[])
+ # THEN: The importer object should not be None
+ assert importer is not None, 'Import should not be none'
- # THEN: The importer object should not be None
- assert importer is not None, 'Import should not be none'
- def test_invalid_import_source(self):
- """
- Test OpenLPSongImport.do_import handles different invalid import_source values
- """
- # GIVEN: A mocked out SongImport class, and a mocked out "manager"
- with patch('openlp.plugins.songs.lib.importers.openlp.SongImport'):
- mocked_manager = MagicMock()
- mocked_import_wizard = MagicMock()
- importer = OpenLPSongImport(mocked_manager, file_paths=[])
- importer.import_wizard = mocked_import_wizard
- importer.stop_import_flag = True
+def test_invalid_import_source(registry):
+ """
+ Test OpenLPSongImport.do_import handles different invalid import_source values
+ """
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ with patch('openlp.plugins.songs.lib.importers.openlp.SongImport'):
+ mocked_manager = MagicMock()
+ mocked_import_wizard = MagicMock()
+ importer = OpenLPSongImport(mocked_manager, file_paths=[])
+ importer.import_wizard = mocked_import_wizard
+ importer.stop_import_flag = True
- # WHEN: Import source is not a list
- importer.import_source = Path()
+ # WHEN: Import source is not a list
+ importer.import_source = Path()
- # THEN: do_import should return none and the progress bar maximum should not be set.
- assert importer.do_import() is None, 'do_import should return None when import_source is not a list'
- assert mocked_import_wizard.progress_bar.setMaximum.called is False, \
- 'setMaximum on import_wizard.progress_bar should not have been called'
+ # THEN: do_import should return none and the progress bar maximum should not be set.
+ assert importer.do_import() is None, 'do_import should return None when import_source is not a list'
+ assert mocked_import_wizard.progress_bar.setMaximum.called is False, \
+ 'setMaximum on import_wizard.progress_bar should not have been called'
+
+
+@pytest.mark.parametrize('base_name', ['songs-1.9.7', 'songs-2.4.6'])
+@patch('openlp.plugins.songs.lib.importers.openlp.Song')
+@patch('openlp.plugins.songs.lib.importers.openlp.Author')
+@patch('openlp.plugins.songs.lib.importers.openlp.Topic')
+@patch('openlp.plugins.songs.lib.importers.openlp.SongBook')
+@patch('openlp.plugins.songs.lib.importers.openlp.MediaFile')
+def test_openlp_db_import(MockMediaFile, MockSongBook, MockTopic, MockAuthor, MockSong, mock_settings, base_name: str):
+ """Test that OpenLP is able to import an older OpenLP database"""
+ # GIVEN: An OpenLP importer and a bunch of mocks
+ mocked_progress_dialog = MagicMock()
+ mocked_author = MagicMock()
+ MockAuthor.return_value = mocked_author
+ mocked_song = MagicMock()
+ MockSong.return_value = mocked_song
+ mocked_topic_salvation = MagicMock()
+ mocked_topic_grace = MagicMock()
+ MockTopic.side_effect = [mocked_topic_grace, mocked_topic_salvation]
+ mocked_songbook = MagicMock()
+ MockSongBook.return_value = mocked_songbook
+ mocked_media_file = MagicMock()
+ MockMediaFile.return_value = mocked_media_file
+ mocked_manager = MagicMock()
+ mocked_manager.get_object_filtered.return_value = None
+ importer = OpenLPSongImport(mocked_manager, file_path=TEST_PATH / f'{base_name}.sqlite')
+ importer.import_wizard = MagicMock()
+
+ # WHEN: The database is imported
+ importer.do_import(mocked_progress_dialog)
+
+ # THEN: The correct songs should ahve been imported
+ expected_song = json.load((TEST_PATH / f'{base_name}.json').open())
+ importer.import_wizard.progress_bar.setMaximum.assert_called_once_with(1)
+ assert mocked_song.title == expected_song['title']
+ for author in expected_song['authors']:
+ MockAuthor.assert_called_with(first_name=author['first_name'], last_name=author['last_name'],
+ display_name=author['display_name'])
+ mocked_song.add_author.assert_called_with(mocked_author, author.get('type', ''))
+ if 'verse_order' in expected_song:
+ assert mocked_song.verse_order == expected_song['verse_order']
+ if 'copyright' in expected_song:
+ assert mocked_song.copyright == expected_song['copyright']
+ if 'theme_name' in expected_song:
+ assert mocked_song.theme_name == expected_song['theme_name']
+ for topic in expected_song.get('topics', []):
+ assert call(name=topic) in MockTopic.call_args_list
+ if topic == 'Grace':
+ assert call(mocked_topic_grace) in mocked_song.topics.append.call_args_list
+ elif topic == 'Salvation':
+ assert call(mocked_topic_salvation) in mocked_song.topics.append.call_args_list
+ for songbook_entry in expected_song.get('songbooks', []):
+ MockSongBook.assert_called_with(name=songbook_entry['songbook'], publisher=None)
+ assert call(mocked_songbook, songbook_entry.get('entry', '')) in mocked_song.add_songbook_entry.call_args_list
+ for media_file in expected_song.get('media_files', []):
+ MockMediaFile.assert_called_once_with(file_path=Path(media_file["file_name"]), file_hash=None)
+ assert call(mocked_media_file) in mocked_song.media_files.append.call_args_list
diff --git a/tests/openlp_plugins/songs/test_openlyricsxml.py b/tests/openlp_plugins/songs/test_openlyricsxml.py
index e5dac2c7e..a95cae00e 100644
--- a/tests/openlp_plugins/songs/test_openlyricsxml.py
+++ b/tests/openlp_plugins/songs/test_openlyricsxml.py
@@ -166,7 +166,7 @@ def test_process_songbooks(registry, settings):
Test that _process_songbooks works
"""
# GIVEN: A OpenLyric XML with songbooks and a mocked out manager
- with patch('openlp.plugins.songs.lib.openlyricsxml.Book'):
+ with patch('openlp.plugins.songs.lib.openlyricsxml.SongBook'):
mocked_manager = MagicMock()
mocked_manager.get_object_filtered.return_value = None
ol = OpenLyrics(mocked_manager)
diff --git a/tests/openlp_plugins/songs/test_songselect.py b/tests/openlp_plugins/songs/test_songselect.py
index d597d0e99..8b7dc66e5 100644
--- a/tests/openlp_plugins/songs/test_songselect.py
+++ b/tests/openlp_plugins/songs/test_songselect.py
@@ -435,7 +435,7 @@ class TestSongSelectImport(TestCase, TestMixin):
assert 2 == mocked_db_manager.save_object.call_count, \
'The save_object() method should have been called twice'
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
- MockedAuthor.populate.assert_called_with(first_name='Public', last_name='Domain', display_name='Public Domain')
+ MockedAuthor.assert_called_with(first_name='Public', last_name='Domain', display_name='Public Domain')
assert 1 == len(result.authors_songs), 'There should only be one author'
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@@ -470,7 +470,7 @@ class TestSongSelectImport(TestCase, TestMixin):
assert 2 == mocked_db_manager.save_object.call_count, \
'The save_object() method should have been called twice'
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
- assert 0 == MockedAuthor.populate.call_count, 'A new author should not have been instantiated'
+ assert 0 == MockedAuthor.call_count, 'A new author should not have been instantiated'
assert 1 == len(result.authors_songs), 'There should only be one author'
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@@ -505,7 +505,7 @@ class TestSongSelectImport(TestCase, TestMixin):
assert 2 == mocked_db_manager.save_object.call_count, \
'The save_object() method should have been called twice'
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
- MockedAuthor.populate.assert_called_with(first_name='Unknown', last_name='', display_name='Unknown')
+ MockedAuthor.assert_called_with(first_name='Unknown', last_name='', display_name='Unknown')
assert 1 == len(result.authors_songs), 'There should only be one author'
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@@ -545,8 +545,8 @@ class TestSongSelectImport(TestCase, TestMixin):
# THEN: The return value should be a Song class and the topics should have been added
assert isinstance(result, Song), 'The returned value should be a Song object'
mocked_clean_song.assert_called_with(mocked_db_manager, result)
- assert MockedTopic.populate.call_count == 2, 'Should have created 2 new topics'
- MockedTopic.populate.assert_called_with(name='Flood')
+ assert MockedTopic.call_count == 2, 'Should have created 2 new topics'
+ MockedTopic.assert_called_with(name='Flood')
assert 1 == len(result.authors_songs), 'There should only be one author'
diff --git a/tests/resources/songs/openlp/songs-1.9.7.json b/tests/resources/songs/openlp/songs-1.9.7.json
new file mode 100644
index 000000000..37d1357fa
--- /dev/null
+++ b/tests/resources/songs/openlp/songs-1.9.7.json
@@ -0,0 +1,36 @@
+{
+ "title": "Amazing Grace",
+ "authors": [
+ {
+ "first_name": "John",
+ "last_name": "Newton",
+ "display_name": "John Newton"
+ }
+ ],
+ "verses": [
+ [
+ "Amazing grace! How sweet the sound\nThat saved a wretch like me;\nI once was lost, but now am found,\nWas blind, but now I see.",
+ "v1"
+ ],
+ [
+ "'Twas grace that taught my heart to fear,\nAnd grace my fears relieved;\nHow precious did that grace appear,\nThe hour I first believed!",
+ "v2"
+ ],
+ [
+ "Through many dangers, toils and snares\nI have already come;\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
+ "v3"
+ ],
+ [
+ "The Lord has promised good to me,\nHis word my hope secures;\nHe will my shield and portion be\nAs long as life endures.",
+ "v4"
+ ],
+ [
+ "Yes, when this heart and flesh shall fail,\nAnd mortal life shall cease,\nI shall possess within the veil\nA life of joy and peace.",
+ "v5"
+ ],
+ [
+ "When we've been there a thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise\nThan when we first begun.",
+ "v6"
+ ]
+ ]
+}
diff --git a/tests/resources/songs/openlp/songs-1.9.7.sqlite b/tests/resources/songs/openlp/songs-1.9.7.sqlite
new file mode 100644
index 000000000..98505464b
Binary files /dev/null and b/tests/resources/songs/openlp/songs-1.9.7.sqlite differ
diff --git a/tests/resources/songs/openlp/songs-2.4.6.json b/tests/resources/songs/openlp/songs-2.4.6.json
new file mode 100644
index 000000000..575fb4e91
--- /dev/null
+++ b/tests/resources/songs/openlp/songs-2.4.6.json
@@ -0,0 +1,56 @@
+{
+ "title": "Amazing Grace",
+ "authors": [
+ {
+ "first_name": "John",
+ "last_name": "Newton",
+ "display_name": "John Newton",
+ "type": "words+music"
+ }
+ ],
+ "verse_order": "v1 v2 v3 v4 v5 v6",
+ "copyright": "Public Domain",
+ "theme_name": "Moss on tree",
+ "media_files": [
+ {
+ "file_name": "/home/raoul/.local/share/openlp/songs/audio/7/Amazing-Grace.mp3",
+ "type": "audio"
+ }
+ ],
+ "songbooks": [
+ {
+ "songbook": "Hymnbook",
+ "entry": "1"
+ }
+ ],
+ "topics": [
+ "Grace",
+ "Salvation"
+ ],
+ "verses": [
+ [
+ "Amazing grace! How sweet the sound\nThat saved a wretch like me;\nI once was lost, but now am found,\nWas blind, but now I see.",
+ "v1"
+ ],
+ [
+ "'Twas grace that taught my heart to fear,\nAnd grace my fears relieved;\nHow precious did that grace appear,\nThe hour I first believed!",
+ "v2"
+ ],
+ [
+ "Through many dangers, toils and snares\nI have already come;\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
+ "v3"
+ ],
+ [
+ "The Lord has promised good to me,\nHis word my hope secures;\nHe will my shield and portion be\nAs long as life endures.",
+ "v4"
+ ],
+ [
+ "Yes, when this heart and flesh shall fail,\nAnd mortal life shall cease,\nI shall possess within the veil\nA life of joy and peace.",
+ "v5"
+ ],
+ [
+ "When we've been there a thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise\nThan when we first begun.",
+ "v6"
+ ]
+ ]
+}
diff --git a/tests/resources/songs/openlp/songs-2.4.6.sqlite b/tests/resources/songs/openlp/songs-2.4.6.sqlite
new file mode 100644
index 000000000..3c927f6c0
Binary files /dev/null and b/tests/resources/songs/openlp/songs-2.4.6.sqlite differ