diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 41b445cd5..fb8a38b1f 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -31,11 +31,13 @@ import logging import os from PyQt4 import QtCore -from sqlalchemy import create_engine, MetaData -from sqlalchemy.exc import InvalidRequestError -from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy import Table, MetaData, Column, types, create_engine +from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError +from sqlalchemy.orm import scoped_session, sessionmaker, mapper from sqlalchemy.pool import NullPool +from openlp.core.lib import translate +from openlp.core.lib.ui import critical_error_message_box from openlp.core.utils import AppLocation, delete_file log = logging.getLogger(__name__) @@ -59,6 +61,48 @@ def init_db(url, auto_flush=True, auto_commit=False): autocommit=auto_commit, bind=engine)) return session, metadata + +def upgrade_db(url, upgrade): + """ + Upgrade a database. + + ``url`` + The url of the database to upgrade. + + ``upgrade`` + The python module that contains the upgrade instructions. + """ + session, metadata = init_db(url) + tables = upgrade.upgrade_setup(metadata) + metadata_table = Table(u'metadata', metadata, + Column(u'key', types.Unicode(64), primary_key=True), + Column(u'value', types.UnicodeText(), default=None) + ) + metadata_table.create(checkfirst=True) + mapper(Metadata, metadata_table) + version_meta = session.query(Metadata).get(u'version') + if version_meta is None: + version_meta = Metadata.populate(key=u'version', value=u'0') + version = 0 + else: + version = int(version_meta.value) + if version > upgrade.__version__: + return version, upgrade.__version__ + version += 1 + while hasattr(upgrade, u'upgrade_%d' % version): + log.debug(u'Running upgrade_%d', version) + try: + getattr(upgrade, u'upgrade_%d' % version)(session, metadata, tables) + version_meta.value = unicode(version) + except SQLAlchemyError, DBAPIError: + log.exception(u'Could not run database upgrade script "upgrade_%s"'\ + ', upgrade process has been halted.', version) + break + version += 1 + session.add(version_meta) + session.commit() + return int(version_meta.value), upgrade.__version__ + def delete_database(plugin_name, db_file_name=None): """ Remove a database file from the system. @@ -79,6 +123,7 @@ def delete_database(plugin_name, db_file_name=None): AppLocation.get_section_data_path(plugin_name), plugin_name) return delete_file(db_file_path) + class BaseModel(object): """ BaseModel provides a base object with a set of generic functions @@ -94,11 +139,19 @@ class BaseModel(object): return instance +class Metadata(BaseModel): + """ + Provides a class for the metadata table. + """ + pass + + class Manager(object): """ Provide generic object persistence management """ - def __init__(self, plugin_name, init_schema, db_file_name=None): + def __init__(self, plugin_name, init_schema, db_file_name=None, + upgrade_mod=None): """ Runs the initialisation process that includes creating the connection to the database and the tables if they don't exist. @@ -109,6 +162,9 @@ class Manager(object): ``init_schema`` The init_schema function for this database + ``upgrade_schema`` + The upgrade_schema function for this database + ``db_file_name`` The file name to use for this database. Defaults to None resulting in the plugin_name being used. @@ -134,7 +190,27 @@ class Manager(object): unicode(settings.value(u'db hostname').toString()), unicode(settings.value(u'db database').toString())) settings.endGroup() - self.session = init_schema(self.db_url) + if upgrade_mod: + db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod) + if db_ver > up_ver: + critical_error_message_box( + translate('OpenLP.Manager', 'Database Error'), + unicode(translate('OpenLP.Manager', 'The database being ' + 'loaded was created in a more recent version of ' + 'OpenLP. The database is version %d, while OpenLP ' + 'expects version %d. The database will not be loaded.' + '\n\nDatabase: %s')) % \ + (db_ver, up_ver, self.db_url) + ) + return + try: + self.session = init_schema(self.db_url) + except: + critical_error_message_box( + translate('OpenLP.Manager', 'Database Error'), + unicode(translate('OpenLP.Manager', 'OpenLP cannot load your ' + 'database.\n\nDatabase: %s')) % self.db_url + ) def save_object(self, object_instance, commit=True): """ diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index c5c019c3c..bee64b3ba 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -70,7 +70,6 @@ class Topic(BaseModel): """ pass - def init_schema(url): """ Setup the songs database connection and initialise the database schema. @@ -111,10 +110,6 @@ def init_schema(url): * file_name * type - **media_files_songs Table** - * media_file_id - * song_id - **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 @@ -162,7 +157,7 @@ def init_schema(url): # Definition of the "authors" table authors_table = Table(u'authors', metadata, - Column(u'id', types.Integer, primary_key=True), + Column(u'id', types.Integer(), primary_key=True), Column(u'first_name', types.Unicode(128)), Column(u'last_name', types.Unicode(128)), Column(u'display_name', types.Unicode(255), index=True, nullable=False) @@ -170,22 +165,25 @@ def init_schema(url): # Definition of the "media_files" table media_files_table = Table(u'media_files', metadata, - Column(u'id', types.Integer, primary_key=True), + Column(u'id', types.Integer(), primary_key=True), + Column(u'song_id', types.Integer(), ForeignKey(u'songs.id'), + default=None), Column(u'file_name', types.Unicode(255), nullable=False), - Column(u'type', types.Unicode(64), nullable=False, default=u'audio') + Column(u'type', types.Unicode(64), nullable=False, default=u'audio'), + Column(u'weight', types.Integer(), default=0) ) # Definition of the "song_books" table song_books_table = Table(u'song_books', metadata, - Column(u'id', types.Integer, primary_key=True), + Column(u'id', types.Integer(), primary_key=True), Column(u'name', types.Unicode(128), nullable=False), Column(u'publisher', types.Unicode(128)) ) # Definition of the "songs" table songs_table = Table(u'songs', metadata, - Column(u'id', types.Integer, primary_key=True), - Column(u'song_book_id', types.Integer, + Column(u'id', types.Integer(), primary_key=True), + Column(u'song_book_id', types.Integer(), ForeignKey(u'song_books.id'), default=None), Column(u'title', types.Unicode(255), nullable=False), Column(u'alternate_title', types.Unicode(255)), @@ -202,31 +200,23 @@ def init_schema(url): # Definition of the "topics" table topics_table = Table(u'topics', metadata, - Column(u'id', types.Integer, primary_key=True), + Column(u'id', types.Integer(), primary_key=True), Column(u'name', types.Unicode(128), index=True, nullable=False) ) # Definition of the "authors_songs" table authors_songs_table = Table(u'authors_songs', metadata, - Column(u'author_id', types.Integer, + Column(u'author_id', types.Integer(), ForeignKey(u'authors.id'), primary_key=True), - Column(u'song_id', types.Integer, - ForeignKey(u'songs.id'), primary_key=True) - ) - - # Definition of the "media_files_songs" table - media_files_songs_table = Table(u'media_files_songs', metadata, - Column(u'media_file_id', types.Integer, - ForeignKey(u'media_files.id'), primary_key=True), - Column(u'song_id', types.Integer, + Column(u'song_id', types.Integer(), ForeignKey(u'songs.id'), primary_key=True) ) # Definition of the "songs_topics" table songs_topics_table = Table(u'songs_topics', metadata, - Column(u'song_id', types.Integer, + Column(u'song_id', types.Integer(), ForeignKey(u'songs.id'), primary_key=True), - Column(u'topic_id', types.Integer, + Column(u'topic_id', types.Integer(), ForeignKey(u'topics.id'), primary_key=True) ) @@ -238,8 +228,7 @@ def init_schema(url): 'authors': relation(Author, backref='songs', secondary=authors_songs_table, lazy=False), 'book': relation(Book, backref='songs'), - 'media_files': relation(MediaFile, backref='songs', - secondary=media_files_songs_table), + 'media_files': relation(MediaFile, backref='songs'), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) }) diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py new file mode 100644 index 000000000..e0f2668d9 --- /dev/null +++ b/openlp/plugins/songs/lib/upgrade.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2011 Raoul Snyman # +# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, # +# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias # +# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`upgrade` module provides a way for the database and schema that is the backend for +the Songs plugin +""" + +from sqlalchemy import Column, ForeignKey, Table, types +from migrate import changeset +from migrate.changeset.constraint import ForeignKeyConstraint + +__version__ = 1 + +def upgrade_setup(metadata): + """ + Set up the latest revision all tables, with reflection, needed for the + upgrade process. If you want to drop a table, you need to remove it from + here, and add it to your upgrade function. + """ + tables = { + u'authors': Table(u'authors', metadata, autoload=True), + u'media_files': Table(u'media_files', metadata, autoload=True), + u'song_books': Table(u'song_books', metadata, autoload=True), + u'songs': Table(u'songs', metadata, autoload=True), + u'topics': Table(u'topics', metadata, autoload=True), + u'authors_songs': Table(u'authors_songs', metadata, autoload=True), + u'songs_topics': Table(u'songs_topics', metadata, autoload=True) + } + return tables + + +def upgrade_1(session, metadata, tables): + """ + Version 1 upgrade. + + This upgrade removes the many-to-many relationship between songs and + media_files and replaces it with a one-to-many, which is far more + representative of the real relationship between the two entities. + + In order to facilitate this one-to-many relationship, a song_id column is + added to the media_files table, and a weight column so that the media + files can be ordered. + """ + Table(u'media_files_songs', metadata, autoload=True).drop(checkfirst=True) + Column(u'song_id', types.Integer(), default=None)\ + .create(table=tables[u'media_files'], populate_default=True) + Column(u'weight', types.Integer(), default=0)\ + .create(table=tables[u'media_files'], populate_default=True) + if metadata.bind.url.get_dialect().name != 'sqlite': + # SQLite doesn't support ALTER TABLE ADD CONSTRAINT + ForeignKeyConstraint([u'song_id'], [u'songs.id'], + table=tables[u'media_files']).create() + diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 8a773be90..f2bf36790 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -36,7 +36,8 @@ from openlp.core.lib import Plugin, StringContent, build_icon, translate, \ from openlp.core.lib.db import Manager from openlp.core.lib.ui import UiStrings, base_action, icon_action from openlp.core.utils.actions import ActionList -from openlp.plugins.songs.lib import clean_song, SongMediaItem, SongsTab +from openlp.plugins.songs.lib import clean_song, upgrade, SongMediaItem, \ + SongsTab from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.olpimport import OpenLPSongImport @@ -58,8 +59,8 @@ class SongsPlugin(Plugin): Create and set up the Songs plugin. """ Plugin.__init__(self, u'songs', plugin_helpers, SongMediaItem, SongsTab) + self.manager = Manager(u'songs', init_schema, upgrade_mod=upgrade) self.weight = -10 - self.manager = Manager(u'songs', init_schema) self.icon_path = u':/plugins/plugin_songs.png' self.icon = build_icon(self.icon_path) diff --git a/resources/debian/debian/control b/resources/debian/debian/control index 220b500d2..423d81df0 100644 --- a/resources/debian/debian/control +++ b/resources/debian/debian/control @@ -11,7 +11,7 @@ Package: openlp Architecture: all Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, python-qt4, python-qt4-phonon, python-sqlalchemy, python-chardet, python-beautifulsoup, - python-lxml, python-sqlite, python-enchant + python-lxml, python-sqlite, python-enchant, python-migrate Conflicts: python-openlp Description: Church lyrics projection application OpenLP is free church presentation software, or lyrics projection software, diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 7048ceeab..5f2e4c148 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -46,14 +46,14 @@ VERS = { 'sqlalchemy': '0.5', # pyenchant 1.6 required on Windows 'enchant': '1.6' if is_win else '1.3' - } +} # pywin32 WIN32_MODULES = [ 'win32com', 'win32ui', 'pywintypes', - ] +] MODULES = [ 'PyQt4', @@ -72,7 +72,8 @@ MODULES = [ 'enchant', 'BeautifulSoup', 'mako', - ] + 'migrate', +] OPTIONAL_MODULES = [