openlp/openlp/plugins/songs/lib/db.py

423 lines
14 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
2022-02-01 10:10:57 +00:00
# Copyright (c) 2008-2022 OpenLP Developers #
2019-04-13 13:00:22 +00:00
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
The :mod:`db` module provides the database and schema that is the backend for
the Songs plugin
"""
2011-07-07 18:03:12 +00:00
from sqlalchemy import Column, ForeignKey, Table, types
2021-09-02 06:46:09 +00:00
from sqlalchemy.orm import class_mapper, mapper, reconstructor, relation
from sqlalchemy.sql.expression import func, text
2021-09-02 06:46:09 +00:00
from sqlalchemy.orm.exc import UnmappedClassError
2018-10-02 04:39:42 +00:00
from openlp.core.common.i18n import get_natural_key, translate
2017-09-30 20:16:30 +00:00
from openlp.core.lib.db import BaseModel, PathType, init_db
2012-11-08 21:28:42 +00:00
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
2014-04-01 21:07:49 +00:00
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
2014-04-01 21:07:49 +00:00
2014-03-30 17:23:36 +00:00
class AuthorType(object):
"""
Enumeration for Author types.
They are defined by OpenLyrics: http://openlyrics.info/dataformat.html#authors
2014-04-08 18:52:05 +00:00
The 'words+music' type is not an official type, but is provided for convenience.
2014-03-30 17:23:36 +00:00
"""
2014-07-05 19:56:32 +00:00
NoType = ''
2014-03-30 17:23:36 +00:00
Words = 'words'
Music = 'music'
2014-04-08 18:52:05 +00:00
WordsAndMusic = 'words+music'
2014-03-30 17:23:36 +00:00
Translation = 'translation'
Types = {
2014-07-05 19:56:32 +00:00
NoType: '',
2014-05-21 15:09:44 +00:00
Words: translate('SongsPlugin.AuthorType', 'Words', 'Author who wrote the lyrics of a song'),
Music: translate('SongsPlugin.AuthorType', 'Music', 'Author who wrote the music of a song'),
2014-05-22 21:35:38 +00:00
WordsAndMusic: translate('SongsPlugin.AuthorType', 'Words and Music',
'Author who wrote both lyrics and music of a song'),
2014-05-21 15:09:44 +00:00
Translation: translate('SongsPlugin.AuthorType', 'Translation', 'Author who translated the song')
2014-03-30 17:23:36 +00:00
}
2014-07-05 19:56:32 +00:00
SortedTypes = [
NoType,
Words,
Music,
WordsAndMusic,
Translation
2014-07-05 19:56:32 +00:00
]
TranslatedTypes = [
Types[NoType],
Types[Words],
Types[Music],
Types[WordsAndMusic],
Types[Translation]
2014-07-05 19:56:32 +00:00
]
@staticmethod
def from_translated_text(translated_type):
"""
Get the AuthorType from a translated string.
:param translated_type: Translated Author type.
"""
for key, value in AuthorType.Types.items():
if value == translated_type:
return key
2014-07-05 20:04:17 +00:00
return AuthorType.NoType
2011-01-18 16:42:59 +00:00
2014-04-01 21:07:49 +00:00
class Book(BaseModel):
"""
Book model
"""
2022-09-28 05:11:13 +00:00
@property
def songs(self):
"""
A property to return the songs associated with this book.
"""
return [sbe.song for sbe in self.entries]
def __repr__(self):
return '<Book id="{myid:d}" name="{name}" publisher="{publisher}" />'.format(myid=self.id,
name=self.name,
publisher=self.publisher)
2014-04-01 21:07:49 +00:00
2010-07-20 12:43:21 +00:00
class MediaFile(BaseModel):
"""
MediaFile model
"""
pass
2011-01-18 16:42:59 +00:00
class Song(BaseModel):
"""
Song model
"""
def __init__(self):
2013-07-07 14:08:47 +00:00
self.sort_key = []
2012-11-08 21:28:42 +00:00
@reconstructor
def init_on_load(self):
"""
Precompute a natural sorting, locale aware sorting key.
Song sorting is performance sensitive operation.
To get maximum speed lets precompute the sorting key.
"""
self.sort_key = get_natural_key(self.title)
2014-05-07 13:59:31 +00:00
def add_author(self, author, author_type=None):
"""
Add an author to the song if it not yet exists
2014-05-07 13:59:31 +00:00
:param author: Author object
:param author_type: AuthorType constant or None
"""
for author_song in self.authors_songs:
2014-05-07 13:59:31 +00:00
if author_song.author == author and author_song.author_type == author_type:
return
new_author_song = AuthorSong()
new_author_song.author = author
new_author_song.author_type = author_type
self.authors_songs.append(new_author_song)
2014-05-07 13:59:31 +00:00
def remove_author(self, author, author_type=None):
"""
Remove an existing author from the song
2014-05-07 13:59:31 +00:00
:param author: Author object
:param author_type: AuthorType constant or None
"""
for author_song in self.authors_songs:
2014-05-07 13:59:31 +00:00
if author_song.author == author and author_song.author_type == author_type:
self.authors_songs.remove(author_song)
2014-05-07 13:59:31 +00:00
return
def add_songbook_entry(self, songbook, entry):
2016-01-04 12:11:24 +00:00
"""
Add a Songbook Entry to the song if it not yet exists
2018-02-23 08:27:33 +00:00
:param songbook: Name of the Songbook.
2016-01-04 12:11:24 +00:00
:param entry: Entry in the Songbook (usually a number)
"""
for songbook_entry in self.songbook_entries:
if songbook_entry.songbook.name == songbook.name and songbook_entry.entry == entry:
2016-01-04 12:11:24 +00:00
return
new_songbook_entry = SongBookEntry()
new_songbook_entry.songbook = songbook
new_songbook_entry.entry = entry
self.songbook_entries.append(new_songbook_entry)
2016-01-04 12:11:24 +00:00
2016-01-04 12:18:11 +00:00
2016-01-04 12:11:24 +00:00
class SongBookEntry(BaseModel):
"""
SongBookEntry model
"""
def __repr__(self):
return SongBookEntry.get_display_name(self.songbook.name, self.entry)
2016-01-04 12:11:24 +00:00
@staticmethod
def get_display_name(songbook_name, entry):
if entry:
return "{name} #{entry}".format(name=songbook_name, entry=entry)
2016-01-04 12:11:24 +00:00
return songbook_name
2011-01-18 16:42:59 +00:00
2016-01-04 12:18:11 +00:00
class Topic(BaseModel):
"""
Topic model
"""
pass
2014-03-06 20:40:08 +00:00
def init_schema(url):
"""
2011-01-18 16:42:59 +00:00
Setup the songs database connection and initialise the database schema.
2014-03-17 19:05:55 +00:00
:param url: The database to setup
2015-09-08 19:13:59 +00:00
2011-01-18 16:42:59 +00:00
The song database contains the following tables:
2011-02-03 17:11:41 +00:00
2011-01-14 17:33:48 +00:00
* authors
* authors_songs
* media_files
* media_files_songs
* song_books
* songs
2016-01-04 12:11:24 +00:00
* songs_songbooks
2011-01-14 17:33:48 +00:00
* songs_topics
* topics
2011-02-03 17:11:41 +00:00
**authors** Table
2011-01-14 17:33:48 +00:00
This table holds the names of all the authors. It has the following
columns:
2011-02-03 17:11:41 +00:00
* id
* first_name
* last_name
* display_name
**authors_songs Table**
2011-01-18 16:42:59 +00:00
This is a bridging table between the *authors* and *songs* tables, which
2011-01-14 17:33:48 +00:00
serves to create a many-to-many relationship between the two tables. It
has the following columns:
2011-02-03 17:11:41 +00:00
* author_id
* song_id
* author_type
2011-02-03 17:11:41 +00:00
**media_files Table**
* id
2017-09-30 20:16:30 +00:00
* _file_path
2011-02-03 17:11:41 +00:00
* type
**song_books Table**
2011-01-18 16:42:59 +00:00
The *song_books* table holds a list of books that a congregation gets
2011-01-14 17:33:48 +00:00
their songs from, or old hymnals now no longer used. This table has the
following columns:
2011-02-03 17:11:41 +00:00
* id
* name
* publisher
**songs Table**
2011-01-14 17:33:48 +00:00
This table contains the songs, and each song has a list of attributes.
2011-01-18 16:42:59 +00:00
The *songs* table has the following columns:
2011-02-03 17:11:41 +00:00
* id
* title
* alternate_title
* lyrics
* verse_order
* copyright
* comments
* ccli_number
* theme_name
* search_title
* search_lyrics
2016-01-04 12:11:24 +00:00
**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
2011-02-03 17:11:41 +00:00
**songs_topics Table**
2011-01-18 16:42:59 +00:00
This is a bridging table between the *songs* and *topics* tables, which
2011-01-14 17:33:48 +00:00
serves to create a many-to-many relationship between the two tables. It
has the following columns:
2011-02-03 17:11:41 +00:00
* song_id
* topic_id
**topics Table**
2011-01-14 17:33:48 +00:00
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:
2011-02-03 17:11:41 +00:00
* id
* name
"""
session, metadata = init_db(url)
# Definition of the "authors" table
2014-03-21 21:38:08 +00:00
authors_table = Table(
'authors', metadata,
2013-08-31 18:17:38 +00:00
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)
)
2010-07-20 12:43:21 +00:00
# Definition of the "media_files" table
2014-03-21 21:38:08 +00:00
media_files_table = Table(
'media_files', metadata,
2013-08-31 18:17:38 +00:00
Column('id', types.Integer(), primary_key=True),
2014-03-06 20:40:08 +00:00
Column('song_id', types.Integer(), ForeignKey('songs.id'), default=None),
2017-09-30 20:16:30 +00:00
Column('file_path', PathType, nullable=False),
2013-08-31 18:17:38 +00:00
Column('type', types.Unicode(64), nullable=False, default='audio'),
Column('weight', types.Integer(), default=0)
2010-07-20 12:43:21 +00:00
)
# Definition of the "song_books" table
2014-03-21 21:38:08 +00:00
song_books_table = Table(
'song_books', metadata,
2013-08-31 18:17:38 +00:00
Column('id', types.Integer(), primary_key=True),
Column('name', types.Unicode(128), nullable=False),
Column('publisher', types.Unicode(128))
)
# Definition of the "songs" table
2014-03-21 21:38:08 +00:00
songs_table = Table(
'songs', metadata,
2013-08-31 18:17:38 +00:00
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()),
2014-03-06 20:40:08 +00:00
Column('last_modified', types.DateTime(), default=func.now(), onupdate=func.now()),
2013-08-31 18:17:38 +00:00
Column('temporary', types.Boolean(), default=False)
)
# Definition of the "topics" table
2014-03-21 21:38:08 +00:00
topics_table = Table(
'topics', metadata,
2013-08-31 18:17:38 +00:00
Column('id', types.Integer(), primary_key=True),
Column('name', types.Unicode(128), index=True, nullable=False)
)
# Definition of the "authors_songs" table
2014-03-21 21:38:08 +00:00
authors_songs_table = Table(
'authors_songs', metadata,
2014-03-06 20:40:08 +00:00
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
2015-02-11 20:56:13 +00:00
Column('author_type', types.Unicode(255), primary_key=True, nullable=False, server_default=text('""'))
2010-07-20 12:43:21 +00:00
)
2016-01-04 12:11:24 +00:00
# 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
2014-03-21 21:38:08 +00:00
songs_topics_table = Table(
'songs_topics', metadata,
2014-03-06 20:40:08 +00:00
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
Column('topic_id', types.Integer(), ForeignKey('topics.id'), primary_key=True)
)
2021-09-02 06:46:09 +00:00
# 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={
2022-09-28 05:11:13 +00:00
'songbook': relation(Book, backref='entries')
2021-09-02 06:46:09 +00:00
})
try:
class_mapper(Book)
except UnmappedClassError:
2022-02-04 21:12:12 +00:00
mapper(Book, song_books_table)
2021-09-02 06:46:09 +00:00
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)
2010-06-12 02:14:18 +00:00
metadata.create_all(checkfirst=True)
2011-01-14 17:33:48 +00:00
return session