Fix db.Manager upgrade/PJlink2 update D

This commit is contained in:
Ken Roberts 2017-05-27 11:21:24 -07:00
parent 533bef159a
commit 3ea37800b7
12 changed files with 235 additions and 84 deletions

View File

@ -144,6 +144,7 @@ def upgrade_db(url, upgrade):
:param url: The url of the database to upgrade. :param url: The url of the database to upgrade.
:param upgrade: The python module that contains the upgrade instructions. :param upgrade: The python module that contains the upgrade instructions.
""" """
log.debug('Checking upgrades for DB {db}'.format(db=url))
session, metadata = init_db(url) session, metadata = init_db(url)
class Metadata(BaseModel): class Metadata(BaseModel):
@ -160,17 +161,15 @@ def upgrade_db(url, upgrade):
metadata_table.create(checkfirst=True) metadata_table.create(checkfirst=True)
mapper(Metadata, metadata_table) mapper(Metadata, metadata_table)
version_meta = session.query(Metadata).get('version') version_meta = session.query(Metadata).get('version')
if version_meta is None: if version_meta:
# Tables have just been created - fill the version field with the most recent version version = int(version_meta.value)
if session.query(Metadata).get('dbversion'): else:
version = 0 # Due to issues with other checks, if the version is not set in the DB then default to 0
else: # and let the upgrade function handle the checks
version = upgrade.__version__ version = 0
version_meta = Metadata.populate(key='version', value=version) version_meta = Metadata.populate(key='version', value=version)
session.add(version_meta) session.add(version_meta)
session.commit() session.commit()
else:
version = int(version_meta.value)
if version > upgrade.__version__: if version > upgrade.__version__:
session.remove() session.remove()
return version, upgrade.__version__ return version, upgrade.__version__

View File

@ -59,33 +59,61 @@ TIMEOUT = 30.0
PJLINK_MAX_PACKET = 136 PJLINK_MAX_PACKET = 136
# NOTE: Change format to account for some commands are both class 1 and 2 # NOTE: Change format to account for some commands are both class 1 and 2
PJLINK_VALID_CMD = { PJLINK_VALID_CMD = {
'ACKN': ['2', ], # UDP Reply to 'SRCH' 'ACKN': {'version': ['2', ], # UDP Reply to 'SRCH'
'AVMT': ['1', ], # Shutter option 'description': 'Acknowledge a PJLink SRCH command - returns MAC address.'},
'CLSS': ['1', ], # PJLink class support query 'AVMT': {'version': ['1', ],
'ERST': ['1', '2'], # Error status option 'description': 'Blank/unblank video and/or mute audio.'},
'FILT': ['2', ], # Get current filter usage time 'CLSS': {'version': ['1', ],
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected 'description': 'Query projector PJLink class support.'},
'INF1': ['1', ], # Manufacturer name query 'ERST': {'version': ['1', '2'],
'INF2': ['1', ], # Product name query 'description': 'Query error status from projector. '
'INFO': ['1', ], # Other information query 'Returns fan/lamp/temp/cover/filter/other error status.'},
'INNM': ['2', ], # Get Video source input terminal name 'FILT': {'version': ['2', ], # Assume (!) time in hours
'INPT': ['1', ], # Video sources option 'description': 'Query number of hours on filter.'},
'INST': ['1', ], # Input sources available query 'FREZ': {'version': ['2', ],
'IRES': ['2', ], # Get Video source resolution 'description': 'Freeze or unfreeze current image being projected.'},
'LAMP': ['1', ], # Lamp(s) query (Includes fans) 'INF1': {'version': ['1', ],
'LKUP': ['2', ], # UPD Linkup status notification 'description': 'Query projector manufacturer name.'},
'MVOL': ['2', ], # Set microphone volume 'INF2': {'version': ['1', ],
'NAME': ['1', ], # Projector name query 'description': 'Query projector product name.'},
'PJLINK': ['1', ], # Initial connection 'INFO': {'version': ['1', ],
'POWR': ['1', ], # Power option 'description': 'Query projector for other information set by manufacturer.'},
'RFIL': ['2', ], # Get replacement air filter model number 'INNM': {'version': ['2', ],
'RLMP': ['2', ], # Get lamp replacement model number 'description': 'Query specified input source name'},
'RRES': ['2', ], # Get projector recommended video resolution 'INPT': {'version': ['1', ],
'SNUM': ['2', ], # Get projector serial number 'description': 'Switch to specified video source.'},
'SRCH': ['2', ], # UDP broadcast search for available projectors on local network 'INST': {'version': ['1', ],
'SVER': ['2', ], # Get projector software version 'description': 'Query available input sources.'},
'SVOL': ['2', ] # Set speaker volume 'IRES': {'version:': ['2', ],
'description': 'Query current input resolution.'},
'LAMP': {'version': ['1', ],
'description': 'Query lamp time and on/off status. Multiple lamps supported.'},
'LKUP': {'version': ['2', ],
'description': 'UDP Status notify. Returns MAC address.'},
'MVOL': {'version': ['2', ],
'description': 'Adjust microphone volume by 1 step.'},
'NAME': {'version': ['1', ],
'description': 'Query customer-set projector name.'},
'PJLINK': {'version': ['1', ],
'description': 'Initial connection with authentication/no authentication request.'},
'POWR': {'version': ['1', ],
'description': 'Turn lamp on or off/standby.'},
'RFIL': {'version': ['2', ],
'description': 'Query replacement air filter model number.'},
'RLMP': {'version': ['2', ],
'description': 'Query replacement lamp model number.'},
'RRES': {'version': ['2', ],
'description': 'Query recommended resolution.'},
'SNUM': {'version': ['2', ],
'description': 'Query projector serial number.'},
'SRCH': {'version': ['2', ],
'description': 'UDP broadcast search request for available projectors.'},
'SVER': {'version': ['2', ],
'description': 'Query projector software version number.'},
'SVOL': {'version': ['2', ],
'description': 'Adjust speaker volume by 1 step.'}
} }
# Error and status codes # Error and status codes
S_OK = E_OK = 0 # E_OK included since I sometimes forget S_OK = E_OK = 0 # E_OK included since I sometimes forget
# Error codes. Start at 200 so we don't duplicate system error codes. # Error codes. Start at 200 so we don't duplicate system error codes.

View File

@ -44,6 +44,7 @@ from sqlalchemy.orm import relationship
from openlp.core.lib.db import Manager, init_db, init_url from openlp.core.lib.db import Manager, init_db, init_url
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
from openlp.core.lib.projector import upgrade
Base = declarative_base(MetaData()) Base = declarative_base(MetaData())
@ -243,7 +244,9 @@ class ProjectorDB(Manager):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
log.debug('ProjectorDB().__init__(args="{arg}", kwargs="{kwarg}")'.format(arg=args, kwarg=kwargs)) log.debug('ProjectorDB().__init__(args="{arg}", kwargs="{kwarg}")'.format(arg=args, kwarg=kwargs))
super().__init__(plugin_name='projector', init_schema=self.init_schema) super(ProjectorDB, self).__init__(plugin_name='projector',
init_schema=self.init_schema,
upgrade_mod=upgrade)
log.debug('ProjectorDB() Initialized using db url {db}'.format(db=self.db_url)) log.debug('ProjectorDB() Initialized using db url {db}'.format(db=self.db_url))
log.debug('Session: {session}'.format(session=self.session)) log.debug('Session: {session}'.format(session=self.session))

View File

@ -186,10 +186,15 @@ class PJLink(QtNetwork.QTcpSocket):
self.pjlink_name = None self.pjlink_name = None
self.manufacturer = None self.manufacturer = None
self.model = None self.model = None
self.serial_no = None
self.sw_version = None
self.shutter = None self.shutter = None
self.mute = None self.mute = None
self.lamp = None self.lamp = None
self.model_lamp = None
self.fan = None self.fan = None
self.filter_time = None
self.model_filter = None
self.source_available = None self.source_available = None
self.source = None self.source = None
self.other_info = None self.other_info = None
@ -451,14 +456,14 @@ class PJLink(QtNetwork.QTcpSocket):
return return
data_split = data.split('=') data_split = data.split('=')
try: try:
(prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1]) (prefix, version, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1])
except ValueError as e: except ValueError as e:
log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip)) log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip))
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip())) log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
self.change_status(E_INVALID_DATA) self.change_status(E_INVALID_DATA)
self.receive_data_signal() self.receive_data_signal()
return return
if not (cmd in PJLINK_VALID_CMD and class_ in PJLINK_VALID_CMD[cmd]): if cmd not in PJLINK_VALID_CMD:
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd)) log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
self.receive_data_signal() self.receive_data_signal()
return return
@ -507,14 +512,25 @@ class PJLink(QtNetwork.QTcpSocket):
log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip)) log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip))
self.send_queue = [] self.send_queue = []
return return
if cmd not in PJLINK_VALID_CMD:
log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.ip))
return
self.projectorNetwork.emit(S_NETWORK_SENDING) self.projectorNetwork.emit(S_NETWORK_SENDING)
log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip, log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip,
command=cmd, command=cmd,
data=opts, data=opts,
salt='' if salt is None salt='' if salt is None
else ' with hash')) else ' with hash'))
# TODO: Check for class of command rather than default to projector PJLink class cmd_ver = PJLINK_VALID_CMD[cmd]['version']
header = PJLINK_HEADER.format(linkclass=self.pjlink_class) if self.pjlink_class in cmd_ver:
header = PJLINK_HEADER.format(linkclass=self.pjlink_class)
elif len(cmd_ver) == 1 and (int(cmd_ver[0]) < int(self.pjlink_class)):
# Typically a class 1 only command
header = PJLINK_HEADER.format(linkclass=cmd_ver[0])
else:
# NOTE: Once we get to version 3 then think about looping
log.error('({ip}): send_command(): PJLink class check issue? aborting'.format(ip=self.ip))
return
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt, out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
header=header, header=header,
command=cmd, command=cmd,
@ -589,10 +605,13 @@ class PJLink(QtNetwork.QTcpSocket):
cmd=cmd, cmd=cmd,
data=data)) data=data))
# Check if we have a future command not available yet # Check if we have a future command not available yet
if cmd in self.pjlink_future: if cmd not in PJLINK_VALID_CMD:
self._not_implemented(cmd) log.error('({ip}) Unknown command received - ignoring'.format(ip=self.ip))
return return
if data in PJLINK_ERRORS: elif cmd not in self.pjlink_functions:
log.warn('({ip}) Future command received - unable to process yet'.format(ip=self.ip))
return
elif data in PJLINK_ERRORS:
# Oops - projector error # Oops - projector error
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data)) log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
if data.upper() == 'ERRA': if data.upper() == 'ERRA':
@ -624,14 +643,11 @@ class PJLink(QtNetwork.QTcpSocket):
self.send_busy = False self.send_busy = False
self.projectorReceivedData.emit() self.projectorReceivedData.emit()
return return
# Command checks already passed
if cmd in self.pjlink_functions: log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
self.pjlink_functions[cmd](data)
else:
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
self.send_busy = False self.send_busy = False
self.projectorReceivedData.emit() self.projectorReceivedData.emit()
self.pjlink_functions[cmd](data)
def process_lamp(self, data): def process_lamp(self, data):
""" """

View File

@ -0,0 +1,73 @@
# -*- 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 #
###############################################################################
"""
The :mod:`upgrade` module provides a way for the database and schema that is the
backend for the Songs plugin
"""
import logging
# Not all imports used at this time, but keep for future upgrades
from sqlalchemy import Column, types
from sqlalchemy.sql.expression import null
from openlp.core.common.db import drop_columns
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
# Initial projector DB was unversioned
__version__ = 2
log.debug('Projector DB upgrade module loading')
def upgrade_1(session, metadata):
"""
Version 1 upgrade - old db might/might not be versioned.
"""
pass
def upgrade_2(session, metadata):
"""
Version 2 upgrade.
Update Projector() table to include new data defined in PJLink version 2 changes
serial_no: Column(String(30))
sw_version: Column(String(30))
model_filter: Column(String(30))
model_lamp: Column(String(30))
: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()]:
log.debug("Upgrading projector DB to version '2'")
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()))
new_op.add_column('projector', Column('model_filter', types.String(30), server_default=null()))
new_op.add_column('projector', Column('model_lamp', types.String(30), server_default=null()))
else:
log_warn("Skipping upgrade_2 of projector DB")

View File

@ -662,6 +662,20 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
message = '%s<b>%s</b>: %s<br />' % (message, message = '%s<b>%s</b>: %s<br />' % (message,
translate('OpenLP.ProjectorManager', 'Current source input is'), translate('OpenLP.ProjectorManager', 'Current source input is'),
projector.link.source) projector.link.source)
if projector.link.pjlink_class == '2':
# Information only available for PJLink Class 2 projectors
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Serial Number'),
data=projector.serial_no)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Software Version'),
data=projector.sw_version)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Lamp type'),
data=projector.model_lamp)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Filter type'),
data=projector.model_filter)
count = 1 count = 1
for item in projector.link.lamp: for item in projector.link.lamp:
message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager', message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager',

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
############################################################################### ###############################################################################

View File

@ -69,7 +69,7 @@ class Ui_SongUsageDetailDialog(object):
self.file_horizontal_layout.setSpacing(8) self.file_horizontal_layout.setSpacing(8)
self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8) self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
self.file_horizontal_layout.setObjectName('file_horizontal_layout') self.file_horizontal_layout.setObjectName('file_horizontal_layout')
self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False) self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False)
self.file_horizontal_layout.addWidget(self.report_path_edit) self.file_horizontal_layout.addWidget(self.report_path_edit)
self.vertical_layout.addWidget(self.file_group_box) self.vertical_layout.addWidget(self.file_group_box)
self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok']) self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])

View File

@ -384,21 +384,6 @@ class TestPJLink(TestCase):
self.assertEquals("{test}".format(test=mock_send_command.call_args), self.assertEquals("{test}".format(test=mock_send_command.call_args),
"call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH)) "call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
@patch.object(pjlink_test, '_not_implemented')
def not_implemented_test(self, mock_not_implemented):
"""
Test PJLink._not_implemented method being called
"""
# GIVEN: test object
pjlink = pjlink_test
test_cmd = 'TESTMEONLY'
# WHEN: A future command is called that is not implemented yet
pjlink.process_command(test_cmd, "Garbage data for test only")
# THEN: PJLink.__not_implemented should have been called with test_cmd
mock_not_implemented.assert_called_with(test_cmd)
@patch.object(pjlink_test, 'disconnect_from_host') @patch.object(pjlink_test, 'disconnect_from_host')
def socket_abort_test(self, mock_disconnect): def socket_abort_test(self, mock_disconnect):
""" """

View File

@ -27,11 +27,15 @@ PREREQUISITE: add_record() and get_all() functions validated.
""" """
import os import os
import shutil import shutil
from unittest import TestCase from tempfile import mkdtemp
from unittest import TestCase, skip
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source from openlp.core.lib.projector import upgrade
from openlp.core.lib.db import upgrade_db
from openlp.core.lib.projector.constants import PJLINK_PORT from openlp.core.lib.projector.constants import PJLINK_PORT
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
from tests.utils.constants import TEST_RESOURCES_PATH from tests.utils.constants import TEST_RESOURCES_PATH
@ -85,6 +89,42 @@ def add_records(projector_db, test):
return added return added
class TestProjectorDBUpdate(TestCase):
"""
Test case for upgrading Projector DB.
NOTE: Separate class so I don't have to look for upgrade tests.
"""
def setUp(self):
"""
Setup for tests
"""
self.tmp_folder = mkdtemp(prefix='openlp_')
def tearDown(self):
"""
Clean up after tests
"""
# Ignore errors since windows can have problems with locked files
shutil.rmtree(self.tmp_folder, ignore_errors=True)
def test_upgrade_old_projector_db(self):
"""
Test that we can upgrade an old song db to the current schema
"""
# GIVEN: An old song db
old_db = os.path.join(TEST_RESOURCES_PATH, "projector", TEST_DB_PJLINK1)
tmp_db = os.path.join(self.tmp_folder, TEST_DB)
shutil.copyfile(old_db, tmp_db)
db_url = 'sqlite:///{db}'.format(db=tmp_db)
# WHEN: upgrading the db
updated_to_version, latest_version = upgrade_db(db_url, upgrade)
# THEN: the song db should have been upgraded to the latest version
self.assertEqual(updated_to_version, latest_version,
'The projector DB should have been upgrade to the latest version')
class TestProjectorDB(TestCase): class TestProjectorDB(TestCase):
""" """
Test case for ProjectorDB Test case for ProjectorDB
@ -94,7 +134,9 @@ class TestProjectorDB(TestCase):
""" """
Set up anything necessary for all tests Set up anything necessary for all tests
""" """
mocked_init_url.return_value = 'sqlite:///{db}'.format(db=TEST_DB) self.tmp_folder = mkdtemp(prefix='openlp_')
tmpdb_url = 'sqlite:///{db}'.format(db=os.path.join(self.tmp_folder, TEST_DB))
mocked_init_url.return_value = tmpdb_url
self.projector = ProjectorDB() self.projector = ProjectorDB()
def tearDown(self): def tearDown(self):
@ -103,15 +145,8 @@ class TestProjectorDB(TestCase):
""" """
self.projector.session.close() self.projector.session.close()
self.projector = None self.projector = None
retries = 0 # Ignore errors since windows can have problems with locked files
while retries < 5: shutil.rmtree(self.tmp_folder, ignore_errors=True)
try:
if os.path.exists(TEST_DB):
os.unlink(TEST_DB)
break
except:
time.sleep(1)
retries += 1
def test_find_record_by_ip(self): def test_find_record_by_ip(self):
""" """

View File

@ -29,7 +29,7 @@ from tempfile import gettempdir
# Test data # Test data
TEST_DB_PJLINK1 = 'projector_pjlink1.sqlite' TEST_DB_PJLINK1 = 'projector_pjlink1.sqlite'
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql') TEST_DB = 'openlp-test-projectordb.sqlite'
TEST_SALT = '498e4a67' TEST_SALT = '498e4a67'
@ -39,8 +39,6 @@ TEST_HASH = '5d8409bc1c3fa39749434aa3a5c38682'
TEST_CONNECT_AUTHENTICATE = 'PJLink 1 {salt}'.format(salt=TEST_SALT) TEST_CONNECT_AUTHENTICATE = 'PJLink 1 {salt}'.format(salt=TEST_SALT)
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
TEST1_DATA = dict(ip='111.111.111.111', TEST1_DATA = dict(ip='111.111.111.111',
port='1111', port='1111',
pin='1111', pin='1111',