forked from openlp/openlp
-- 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:
commit
bd3bedcaf4
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
85
openlp/core/lib/projector/pjlink2.py
Normal file
85
openlp/core/lib/projector/pjlink2.py
Normal 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
|
@ -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()))
|
||||
|
@ -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):
|
||||
|
@ -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()]:
|
||||
|
@ -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=''))
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user