This commit is contained in:
Tim Bentley 2011-08-26 12:44:15 +01:00
commit cc92bf7028
6 changed files with 181 additions and 37 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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