-- Fix docstring for pjlink2 db upgrade

-- Add PJLink2 module for UDP socket (skeleton)
-- Move ProjectorManager.projector_list to class attribute
-- Added database_exists check for first time install skip upgrade checks
-- Fix db upgrade for songs
-- Fix db upgrade for songusage
-- Added database_exists from sqlalchemy_utils
-- Added test for skipping upgrade on no db
-- Revert songs db upgrade_7 back to upgrade_6
-- kwargs cleanup

--------------------------------
lp:~alisonken1/openlp/pjli...

bzr-revno: 2751
This commit is contained in:
Ken Roberts 2017-06-22 17:54:33 +01:00 committed by Tim Bentley
commit bd3bedcaf4
9 changed files with 221 additions and 35 deletions

View File

@ -25,12 +25,15 @@ The :mod:`db` module provides the core database functionality for OpenLP
"""
import logging
import os
from copy import copy
from urllib.parse import quote_plus as urlquote
from sqlalchemy import Table, MetaData, Column, types, create_engine
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError, ProgrammingError
from sqlalchemy.orm import scoped_session, sessionmaker, mapper
from sqlalchemy.pool import NullPool
from alembic.migration import MigrationContext
from alembic.operations import Operations
@ -40,6 +43,66 @@ from openlp.core.lib.ui import critical_error_message_box
log = logging.getLogger(__name__)
def database_exists(url):
"""Check if a database exists.
:param url: A SQLAlchemy engine URL.
Performs backend-specific testing to quickly determine if a database
exists on the server. ::
database_exists('postgres://postgres@localhost/name') #=> False
create_database('postgres://postgres@localhost/name')
database_exists('postgres://postgres@localhost/name') #=> True
Supports checking against a constructed URL as well. ::
engine = create_engine('postgres://postgres@localhost/name')
database_exists(engine.url) #=> False
create_database(engine.url)
database_exists(engine.url) #=> True
Borrowed from SQLAlchemy_Utils (v0.32.14 )since we only need this one function.
"""
url = copy(make_url(url))
database = url.database
if url.drivername.startswith('postgresql'):
url.database = 'template1'
else:
url.database = None
engine = create_engine(url)
if engine.dialect.name == 'postgresql':
text = "SELECT 1 FROM pg_database WHERE datname='{db}'".format(db=database)
return bool(engine.execute(text).scalar())
elif engine.dialect.name == 'mysql':
text = ("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA "
"WHERE SCHEMA_NAME = '{db}'".format(db=database))
return bool(engine.execute(text).scalar())
elif engine.dialect.name == 'sqlite':
if database:
return database == ':memory:' or os.path.exists(database)
else:
# The default SQLAlchemy database is in memory,
# and :memory is not required, thus we should support that use-case
return True
else:
text = 'SELECT 1'
try:
url.database = database
engine = create_engine(url)
engine.execute(text)
return True
except (ProgrammingError, OperationalError):
return False
def init_db(url, auto_flush=True, auto_commit=False, base=None):
"""
Initialise and return the session and metadata for a database
@ -144,7 +207,12 @@ def upgrade_db(url, upgrade):
:param url: The url of the database to upgrade.
:param upgrade: The python module that contains the upgrade instructions.
"""
if not database_exists(url):
log.warn("Database {db} doesn't exist - skipping upgrade checks".format(db=url))
return (0, 0)
log.debug('Checking upgrades for DB {db}'.format(db=url))
session, metadata = init_db(url)
class Metadata(BaseModel):

View File

@ -118,7 +118,7 @@ PJLINK_VALID_CMD = {
},
'LKUP': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'UDP Status notify. Includes MAC address.')
'UDP Status - Projector is now available on network. Includes MAC address.')
},
'MVOL': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',

View File

@ -80,25 +80,8 @@ class PJLink(QtNetwork.QTcpSocket):
projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
# New commands available in PJLink Class 2
pjlink_future = [
'ACKN', # UDP Reply to 'SRCH'
'FILT', # Get current filter usage time
'FREZ', # Set freeze/unfreeze picture being projected
'INNM', # Get Video source input terminal name
'IRES', # Get Video source resolution
'LKUP', # UPD Linkup status notification
'MVOL', # Set microphone volume
'RFIL', # Get replacement air filter model number
'RLMP', # Get lamp replacement model number
'RRES', # Get projector recommended video resolution
'SNUM', # Get projector serial number
'SRCH', # UDP broadcast search for available projectors on local network
'SVER', # Get projector software version
'SVOL', # Set speaker volume
'TESTMEONLY' # For testing when other commands have been implemented
]
# New commands available in PJLink Class 2
pjlink_udp_commands = [
'ACKN',
'ERST', # Class 1 or 2
@ -130,12 +113,13 @@ class PJLink(QtNetwork.QTcpSocket):
self.port = port
self.pin = pin
super().__init__()
self.mac_adx = kwargs.get('mac_adx')
self.dbid = None
self.location = None
self.notes = None
self.dbid = None if 'dbid' not in kwargs else kwargs['dbid']
self.location = None if 'location' not in kwargs else kwargs['location']
self.notes = None if 'notes' not in kwargs else kwargs['notes']
self.dbid = kwargs.get('dbid')
self.location = kwargs.get('location')
self.notes = kwargs.get('notes')
# Poll time 20 seconds unless called with something else
self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
# Timeout 5 seconds unless called with something else

View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# 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 #
###############################################################################
"""
:mod:`openlp.core.lib.projector.pjlink2` module provides the PJLink Class 2
updates from PJLink Class 1.
This module only handles the UDP socket functionality. Command/query/status
change messages will still be processed by the PJLink 1 module.
Currently, the only variance is the addition of a UDP "search" command to
query the local network for Class 2 capable projectors,
and UDP "notify" messages from projectors to connected software of status
changes (i.e., power change, input change, error changes).
Differences between Class 1 and Class 2 PJLink specifications are as follows.
New Functionality:
* Search - UDP Query local network for Class 2 capabable projector(s).
* Status - UDP Status change with connected projector(s). Status change
messages consist of:
* Initial projector power up when network communication becomes available
* Lamp off/standby to warmup or on
* Lamp on to cooldown or off/standby
* Input source select change completed
* Error status change (i.e., fan/lamp/temp/cover open/filter/other error(s))
New Commands:
* Query serial number of projector
* Query version number of projector software
* Query model number of replacement lamp
* Query model number of replacement air filter
* Query current projector screen resolution
* Query recommended screen resolution
* Query name of specific input terminal (video source)
* Adjust projector microphone in 1-step increments
* Adjust projector speacker in 1-step increments
Extended Commands:
* Addition of INTERNAL terminal (video source) for a total of 6 types of terminals.
* Number of terminals (video source) has been expanded from [1-9]
to [1-9a-z] (Addition of 26 terminals for each type of input).
See PJLink Class 2 Specifications for details.
http://pjlink.jbmia.or.jp/english/dl_class2.html
Section 5-1 PJLink Specifications
Section 5-5 Guidelines for Input Terminals
"""
import logging
log = logging.getLogger(__name__)
log.debug('pjlink2 loaded')
from PyQt5 import QtCore, QtNetwork
class PJLinkUDP(QtNetwork.QTcpSocket):
"""
Socket service for handling datagram (UDP) sockets.
"""
log.debug('PJLinkUDP loaded')
# Class varialbe for projector list. Should be replaced by ProjectorManager's
# projector list after being loaded there.
projector_list = None
projectors_found = None # UDP search found list

View File

@ -26,7 +26,8 @@ backend for the projector setup.
import logging
# Not all imports used at this time, but keep for future upgrades
from sqlalchemy import Column, types
from sqlalchemy import Table, Column, types, inspect
from sqlalchemy.exc import NoSuchTableError
from sqlalchemy.sql.expression import null
from openlp.core.common.db import drop_columns
@ -44,7 +45,7 @@ def upgrade_1(session, metadata):
"""
Version 1 upgrade - old db might/might not be versioned.
"""
pass
log.debug('Skipping upgrade_1 of projector DB - not used')
def upgrade_2(session, metadata):
@ -53,6 +54,7 @@ def upgrade_2(session, metadata):
Update Projector() table to include new data defined in PJLink version 2 changes
mac_adx: Column(String(18))
serial_no: Column(String(30))
sw_version: Column(String(30))
model_filter: Column(String(30))
@ -61,10 +63,10 @@ def upgrade_2(session, metadata):
:param session: DB session instance
:param metadata: Metadata of current DB
"""
new_op = get_upgrade_op(session)
if 'serial_no' not in [t.name for t in metadata.tables.values()]:
projector_table = Table('projector', metadata, autoload=True)
if 'mac_adx' not in [col.name for col in projector_table.c.values()]:
log.debug("Upgrading projector DB to version '2'")
new_op = get_upgrade_op(session)
new_op.add_column('projector', Column('mac_adx', types.String(18), server_default=null()))
new_op.add_column('projector', Column('serial_no', types.String(30), server_default=null()))
new_op.add_column('projector', Column('sw_version', types.String(30), server_default=null()))

View File

@ -39,6 +39,7 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
from openlp.core.lib.projector.db import ProjectorDB
from openlp.core.lib.projector.pjlink1 import PJLink
from openlp.core.lib.projector.pjlink2 import PJLinkUDP
from openlp.core.ui.projector.editform import ProjectorEditForm
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
@ -290,6 +291,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
self.settings_section = 'projector'
self.projectordb = projectordb
self.projector_list = []
self.pjlink_udp = PJLinkUDP()
self.pjlink_udp.projector_list = self.projector_list
self.source_select_form = None
def bootstrap_initialise(self):
@ -987,7 +990,7 @@ class ProjectorItem(QtCore.QObject):
self.poll_time = None
self.socket_timeout = None
self.status = S_NOT_CONNECTED
super(ProjectorItem, self).__init__()
super().__init__()
def not_implemented(function):

View File

@ -52,6 +52,7 @@ def upgrade_1(session, metadata):
:param metadata:
"""
op = get_upgrade_op(session)
metadata.reflect()
if 'media_files_songs' in [t.name for t in metadata.tables.values()]:
op.drop_table('media_files_songs')
op.add_column('media_files', Column('song_id', types.Integer(), server_default=null()))
@ -122,6 +123,7 @@ def upgrade_6(session, metadata):
This version corrects the errors in upgrades 4 and 5
"""
op = get_upgrade_op(session)
metadata.reflect()
# Move upgrade 4 to here and correct it (authors_songs table, not songs table)
authors_songs = Table('authors_songs', metadata, autoload=True)
if 'author_type' not in [col.name for col in authors_songs.c.values()]:

View File

@ -25,17 +25,26 @@ backend for the SongsUsage plugin
"""
import logging
from sqlalchemy import Column, types
from sqlalchemy import Table, Column, types
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
__version__ = 1
__version__ = 2
def upgrade_1(session, metadata):
"""
Version 1 upgrade.
Version 1 upgrade
Skip due to possible missed update from a 2.4-2.6 upgrade
"""
pass
def upgrade_2(session, metadata):
"""
Version 2 upgrade.
This upgrade adds two new fields to the songusage database
@ -43,5 +52,7 @@ def upgrade_1(session, metadata):
:param metadata: SQLAlchemy MetaData object
"""
op = get_upgrade_op(session)
songusage_table = Table('songusage_data', metadata, autoload=True)
if 'plugin_name' not in [col.name for col in songusage_table.c.values()]:
op.add_column('songusage_data', Column('plugin_name', types.Unicode(20), server_default=''))
op.add_column('songusage_data', Column('source', types.Unicode(10), server_default=''))

View File

@ -23,6 +23,9 @@
Package to test the openlp.core.lib package.
"""
import os
import shutil
from tempfile import mkdtemp
from unittest import TestCase
from unittest.mock import patch, MagicMock
@ -30,13 +33,27 @@ from sqlalchemy.pool import NullPool
from sqlalchemy.orm.scoping import ScopedSession
from sqlalchemy import MetaData
from openlp.core.lib.db import init_db, get_upgrade_op, delete_database
from openlp.core.lib.db import init_db, get_upgrade_op, delete_database, upgrade_db
from openlp.core.lib.projector import upgrade as pjlink_upgrade
class TestDB(TestCase):
"""
A test case for all the tests for the :mod:`~openlp.core.lib.db` module.
"""
def setUp(self):
"""
Set up anything necessary for all tests
"""
self.tmp_folder = mkdtemp(prefix='openlp_')
def tearDown(self):
"""
Clean up
"""
# Ignore errors since windows can have problems with locked files
shutil.rmtree(self.tmp_folder, ignore_errors=True)
def test_init_db_calls_correct_functions(self):
"""
Test that the init_db function makes the correct function calls
@ -145,3 +162,17 @@ class TestDB(TestCase):
MockedAppLocation.get_section_data_path.assert_called_with(test_plugin)
mocked_delete_file.assert_called_with(test_location)
self.assertFalse(result, 'The result of delete_file should be False (was rigged that way)')
@patch('tests.functional.openlp_core_lib.test_db.pjlink_upgrade')
def test_skip_db_upgrade_with_no_database(self, mocked_upgrade):
"""
Test the upgrade_db function does not try to update a missing database
"""
# GIVEN: Database URL that does not (yet) exist
url = 'sqlite:///{tmp}/test_db.sqlite'.format(tmp=self.tmp_folder)
# WHEN: We attempt to upgrade a non-existant database
upgrade_db(url, pjlink_upgrade)
# THEN: upgrade should NOT have been called
self.assertFalse(mocked_upgrade.called, 'Database upgrade function should NOT have been called')