Migrate to using Declarative Base in Songs

merge-requests/456/merge
Raoul Snyman 2023-03-08 03:27:49 +00:00
parent ef1b92eccd
commit b635bad83f
21 changed files with 559 additions and 551 deletions

View File

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

View File

@ -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?'),

View File

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

View File

@ -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 '<Book id="{myid:d}" name="{name}" publisher="{publisher}" />'.format(myid=self.id,
name=self.name,
publisher=self.publisher)
return f'<SongBook id="{self.id}" name="{self.name}" publisher="{self.publisher}">'
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,3 +33,7 @@ source =
[tool:pytest]
qt_api=pyqt5
[mypy]
# Ignore for now, we'll come back to this later
ignore_errors = true

View File

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

View File

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

View File

@ -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_')

View File

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

View File

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

View File

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

View File

@ -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"
]
]
}

View File

@ -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"
]
]
}