forked from openlp/openlp
- Implemented a seamless database upgrade system.
- Added the first upgrade as a demonstration. - Added database version check so that newer version databases are not loaded. bzr-revno: 1721
This commit is contained in:
commit
2c0df18f35
@ -31,11 +31,13 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from PyQt4 import QtCore
|
from PyQt4 import QtCore
|
||||||
from sqlalchemy import create_engine, MetaData
|
from sqlalchemy import Table, MetaData, Column, types, create_engine
|
||||||
from sqlalchemy.exc import InvalidRequestError
|
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker, mapper
|
||||||
from sqlalchemy.pool import NullPool
|
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
|
from openlp.core.utils import AppLocation, delete_file
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -59,6 +61,48 @@ def init_db(url, auto_flush=True, auto_commit=False):
|
|||||||
autocommit=auto_commit, bind=engine))
|
autocommit=auto_commit, bind=engine))
|
||||||
return session, metadata
|
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):
|
def delete_database(plugin_name, db_file_name=None):
|
||||||
"""
|
"""
|
||||||
Remove a database file from the system.
|
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)
|
AppLocation.get_section_data_path(plugin_name), plugin_name)
|
||||||
return delete_file(db_file_path)
|
return delete_file(db_file_path)
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(object):
|
class BaseModel(object):
|
||||||
"""
|
"""
|
||||||
BaseModel provides a base object with a set of generic functions
|
BaseModel provides a base object with a set of generic functions
|
||||||
@ -94,11 +139,19 @@ class BaseModel(object):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class Metadata(BaseModel):
|
||||||
|
"""
|
||||||
|
Provides a class for the metadata table.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Manager(object):
|
class Manager(object):
|
||||||
"""
|
"""
|
||||||
Provide generic object persistence management
|
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
|
Runs the initialisation process that includes creating the connection
|
||||||
to the database and the tables if they don't exist.
|
to the database and the tables if they don't exist.
|
||||||
@ -109,6 +162,9 @@ class Manager(object):
|
|||||||
``init_schema``
|
``init_schema``
|
||||||
The init_schema function for this database
|
The init_schema function for this database
|
||||||
|
|
||||||
|
``upgrade_schema``
|
||||||
|
The upgrade_schema function for this database
|
||||||
|
|
||||||
``db_file_name``
|
``db_file_name``
|
||||||
The file name to use for this database. Defaults to None resulting
|
The file name to use for this database. Defaults to None resulting
|
||||||
in the plugin_name being used.
|
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 hostname').toString()),
|
||||||
unicode(settings.value(u'db database').toString()))
|
unicode(settings.value(u'db database').toString()))
|
||||||
settings.endGroup()
|
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):
|
def save_object(self, object_instance, commit=True):
|
||||||
"""
|
"""
|
||||||
|
@ -70,7 +70,6 @@ class Topic(BaseModel):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def init_schema(url):
|
def init_schema(url):
|
||||||
"""
|
"""
|
||||||
Setup the songs database connection and initialise the database schema.
|
Setup the songs database connection and initialise the database schema.
|
||||||
@ -111,10 +110,6 @@ def init_schema(url):
|
|||||||
* file_name
|
* file_name
|
||||||
* type
|
* type
|
||||||
|
|
||||||
**media_files_songs Table**
|
|
||||||
* media_file_id
|
|
||||||
* song_id
|
|
||||||
|
|
||||||
**song_books Table**
|
**song_books Table**
|
||||||
The *song_books* table holds a list of books that a congregation gets
|
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
|
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
|
# Definition of the "authors" table
|
||||||
authors_table = Table(u'authors', metadata,
|
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'first_name', types.Unicode(128)),
|
||||||
Column(u'last_name', types.Unicode(128)),
|
Column(u'last_name', types.Unicode(128)),
|
||||||
Column(u'display_name', types.Unicode(255), index=True, nullable=False)
|
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
|
# Definition of the "media_files" table
|
||||||
media_files_table = Table(u'media_files', metadata,
|
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'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
|
# Definition of the "song_books" table
|
||||||
song_books_table = Table(u'song_books', metadata,
|
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'name', types.Unicode(128), nullable=False),
|
||||||
Column(u'publisher', types.Unicode(128))
|
Column(u'publisher', types.Unicode(128))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Definition of the "songs" table
|
# Definition of the "songs" table
|
||||||
songs_table = Table(u'songs', metadata,
|
songs_table = Table(u'songs', metadata,
|
||||||
Column(u'id', types.Integer, primary_key=True),
|
Column(u'id', types.Integer(), primary_key=True),
|
||||||
Column(u'song_book_id', types.Integer,
|
Column(u'song_book_id', types.Integer(),
|
||||||
ForeignKey(u'song_books.id'), default=None),
|
ForeignKey(u'song_books.id'), default=None),
|
||||||
Column(u'title', types.Unicode(255), nullable=False),
|
Column(u'title', types.Unicode(255), nullable=False),
|
||||||
Column(u'alternate_title', types.Unicode(255)),
|
Column(u'alternate_title', types.Unicode(255)),
|
||||||
@ -202,31 +200,23 @@ def init_schema(url):
|
|||||||
|
|
||||||
# Definition of the "topics" table
|
# Definition of the "topics" table
|
||||||
topics_table = Table(u'topics', metadata,
|
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)
|
Column(u'name', types.Unicode(128), index=True, nullable=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Definition of the "authors_songs" table
|
# Definition of the "authors_songs" table
|
||||||
authors_songs_table = Table(u'authors_songs', metadata,
|
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),
|
ForeignKey(u'authors.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 "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,
|
|
||||||
ForeignKey(u'songs.id'), primary_key=True)
|
ForeignKey(u'songs.id'), primary_key=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Definition of the "songs_topics" table
|
# Definition of the "songs_topics" table
|
||||||
songs_topics_table = Table(u'songs_topics', metadata,
|
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),
|
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)
|
ForeignKey(u'topics.id'), primary_key=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -238,8 +228,7 @@ def init_schema(url):
|
|||||||
'authors': relation(Author, backref='songs',
|
'authors': relation(Author, backref='songs',
|
||||||
secondary=authors_songs_table, lazy=False),
|
secondary=authors_songs_table, lazy=False),
|
||||||
'book': relation(Book, backref='songs'),
|
'book': relation(Book, backref='songs'),
|
||||||
'media_files': relation(MediaFile, backref='songs',
|
'media_files': relation(MediaFile, backref='songs'),
|
||||||
secondary=media_files_songs_table),
|
|
||||||
'topics': relation(Topic, backref='songs',
|
'topics': relation(Topic, backref='songs',
|
||||||
secondary=songs_topics_table)
|
secondary=songs_topics_table)
|
||||||
})
|
})
|
||||||
|
77
openlp/plugins/songs/lib/upgrade.py
Normal file
77
openlp/plugins/songs/lib/upgrade.py
Normal file
@ -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()
|
||||||
|
|
@ -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.db import Manager
|
||||||
from openlp.core.lib.ui import UiStrings, base_action, icon_action
|
from openlp.core.lib.ui import UiStrings, base_action, icon_action
|
||||||
from openlp.core.utils.actions import ActionList
|
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.db import init_schema, Song
|
||||||
from openlp.plugins.songs.lib.importer import SongFormat
|
from openlp.plugins.songs.lib.importer import SongFormat
|
||||||
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
|
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
|
||||||
@ -58,8 +59,8 @@ class SongsPlugin(Plugin):
|
|||||||
Create and set up the Songs plugin.
|
Create and set up the Songs plugin.
|
||||||
"""
|
"""
|
||||||
Plugin.__init__(self, u'songs', plugin_helpers, SongMediaItem, SongsTab)
|
Plugin.__init__(self, u'songs', plugin_helpers, SongMediaItem, SongsTab)
|
||||||
|
self.manager = Manager(u'songs', init_schema, upgrade_mod=upgrade)
|
||||||
self.weight = -10
|
self.weight = -10
|
||||||
self.manager = Manager(u'songs', init_schema)
|
|
||||||
self.icon_path = u':/plugins/plugin_songs.png'
|
self.icon_path = u':/plugins/plugin_songs.png'
|
||||||
self.icon = build_icon(self.icon_path)
|
self.icon = build_icon(self.icon_path)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ Package: openlp
|
|||||||
Architecture: all
|
Architecture: all
|
||||||
Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, python-qt4,
|
Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, python-qt4,
|
||||||
python-qt4-phonon, python-sqlalchemy, python-chardet, python-beautifulsoup,
|
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
|
Conflicts: python-openlp
|
||||||
Description: Church lyrics projection application
|
Description: Church lyrics projection application
|
||||||
OpenLP is free church presentation software, or lyrics projection software,
|
OpenLP is free church presentation software, or lyrics projection software,
|
||||||
|
@ -46,14 +46,14 @@ VERS = {
|
|||||||
'sqlalchemy': '0.5',
|
'sqlalchemy': '0.5',
|
||||||
# pyenchant 1.6 required on Windows
|
# pyenchant 1.6 required on Windows
|
||||||
'enchant': '1.6' if is_win else '1.3'
|
'enchant': '1.6' if is_win else '1.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
# pywin32
|
# pywin32
|
||||||
WIN32_MODULES = [
|
WIN32_MODULES = [
|
||||||
'win32com',
|
'win32com',
|
||||||
'win32ui',
|
'win32ui',
|
||||||
'pywintypes',
|
'pywintypes',
|
||||||
]
|
]
|
||||||
|
|
||||||
MODULES = [
|
MODULES = [
|
||||||
'PyQt4',
|
'PyQt4',
|
||||||
@ -72,7 +72,8 @@ MODULES = [
|
|||||||
'enchant',
|
'enchant',
|
||||||
'BeautifulSoup',
|
'BeautifulSoup',
|
||||||
'mako',
|
'mako',
|
||||||
]
|
'migrate',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
OPTIONAL_MODULES = [
|
OPTIONAL_MODULES = [
|
||||||
|
Loading…
Reference in New Issue
Block a user