mirror of https://gitlab.com/openlp/openlp.git
Migrate to using Declarative Base in Songs
parent
ef1b92eccd
commit
b635bad83f
|
@ -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.
|
||||
|
|
|
@ -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?'),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))) \
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -33,3 +33,7 @@ source =
|
|||
|
||||
[tool:pytest]
|
||||
qt_api=pyqt5
|
||||
|
||||
[mypy]
|
||||
# Ignore for now, we'll come back to this later
|
||||
ignore_errors = true
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
]
|
||||
}
|
Binary file not shown.
|
@ -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"
|
||||
]
|
||||
]
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue