← Back to team overview

openlp-core team mailing list archive

[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