openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #31768
[Merge] lp:~alisonken1/openlp/pjlink2-e into lp:openlp
Ken Roberts has proposed merging lp:~alisonken1/openlp/pjlink2-e into lp:openlp.
Commit message:
PJLink2-E updates
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~alisonken1/openlp/pjlink2-e/+merge/325388
-- 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
--------------------------------
lp:~alisonken1/openlp/pjlink2-e (revision 2752)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2080/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1990/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1906/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1284/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1134/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/263/
[SUCCESS] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/108/
--
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/pjlink2-e into lp:openlp.
=== modified file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py 2017-05-27 18:21:24 +0000
+++ openlp/core/lib/db.py 2017-06-09 14:14:27 +0000
@@ -25,12 +25,15 @@
"""
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 @@
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 @@
: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):
=== modified file 'openlp/core/lib/projector/constants.py'
--- openlp/core/lib/projector/constants.py 2017-06-01 22:35:57 +0000
+++ openlp/core/lib/projector/constants.py 2017-06-09 14:14:27 +0000
@@ -118,7 +118,7 @@
},
'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',
=== modified file 'openlp/core/lib/projector/pjlink1.py'
--- openlp/core/lib/projector/pjlink1.py 2017-05-30 23:26:37 +0000
+++ openlp/core/lib/projector/pjlink1.py 2017-06-09 14:14:27 +0000
@@ -80,25 +80,8 @@
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
- ]
-
pjlink_udp_commands = [
'ACKN',
'ERST', # Class 1 or 2
@@ -130,6 +113,7 @@
self.port = port
self.pin = pin
super().__init__()
+ self.mac_adx = None if 'mac_adx' not in kwargs else kwargs['mac_adx']
self.dbid = None
self.location = None
self.notes = None
=== added file 'openlp/core/lib/projector/pjlink2.py'
--- openlp/core/lib/projector/pjlink2.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/pjlink2.py 2017-06-09 14:14:27 +0000
@@ -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
=== modified file 'openlp/core/lib/projector/upgrade.py'
--- openlp/core/lib/projector/upgrade.py 2017-06-01 22:35:57 +0000
+++ openlp/core/lib/projector/upgrade.py 2017-06-09 14:14:27 +0000
@@ -26,7 +26,8 @@
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 @@
"""
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 @@
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 @@
: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()))
=== modified file 'openlp/core/ui/projector/manager.py'
--- openlp/core/ui/projector/manager.py 2017-05-27 18:21:24 +0000
+++ openlp/core/ui/projector/manager.py 2017-06-09 14:14:27 +0000
@@ -39,6 +39,7 @@
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
@@ -278,6 +279,10 @@
"""
Manage the projectors.
"""
+ projector_list = []
+ pjlink_udp = PJLinkUDP()
+ pjlink_udp.projector_list = projector_list
+
def __init__(self, parent=None, projectordb=None):
"""
Basic initialization.
@@ -289,7 +294,7 @@
super().__init__(parent)
self.settings_section = 'projector'
self.projectordb = projectordb
- self.projector_list = []
+ self.projector_list = self.__class__.projector_list
self.source_select_form = None
def bootstrap_initialise(self):
@@ -987,7 +992,7 @@
self.poll_time = None
self.socket_timeout = None
self.status = S_NOT_CONNECTED
- super(ProjectorItem, self).__init__()
+ super().__init__()
def not_implemented(function):
=== modified file 'openlp/plugins/songs/lib/upgrade.py'
--- openlp/plugins/songs/lib/upgrade.py 2017-03-23 05:31:51 +0000
+++ openlp/plugins/songs/lib/upgrade.py 2017-06-09 14:14:27 +0000
@@ -32,7 +32,7 @@
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
-__version__ = 6
+__version__ = 7
# TODO: When removing an upgrade path the ftw-data needs updating to the minimum supported version
@@ -52,6 +52,7 @@
: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()))
@@ -119,7 +120,7 @@
"""
Version 6 upgrade
- This version corrects the errors in upgrades 4 and 5
+ This version corrects the errors in upgrade 4
"""
op = get_upgrade_op(session)
# Move upgrade 4 to here and correct it (authors_songs table, not songs table)
@@ -137,7 +138,17 @@
op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs')
op.drop_table('authors_songs')
op.rename_table('authors_songs_tmp', 'authors_songs')
+
+
+def upgrade_7(session, metadata):
+ """
+ Version 7 upgrade
+
+ Corrects table error in upgrade 5
+ """
# Move upgrade 5 here to correct it
+ op = get_upgrade_op(session)
+ metadata.reflect()
if 'songs_songbooks' not in [t.name for t in metadata.tables.values()]:
# Create the mapping table (songs <-> songbooks)
op.create_table(
=== modified file 'openlp/plugins/songusage/lib/upgrade.py'
--- openlp/plugins/songusage/lib/upgrade.py 2016-12-31 11:01:36 +0000
+++ openlp/plugins/songusage/lib/upgrade.py 2017-06-09 14:14:27 +0000
@@ -25,17 +25,26 @@
"""
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 @@
:param metadata: SQLAlchemy MetaData object
"""
op = get_upgrade_op(session)
- 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=''))
+ 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=''))
=== modified file 'tests/functional/openlp_core_lib/test_db.py'
--- tests/functional/openlp_core_lib/test_db.py 2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_lib/test_db.py 2017-06-09 14:14:27 +0000
@@ -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.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 @@
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')
Follow ups