This commit is contained in:
Andreas Preikschat 2013-04-20 13:38:27 +02:00
commit 58b21030e0
29 changed files with 781 additions and 214 deletions

View File

@ -103,6 +103,9 @@ class MediaManagerItem(QtGui.QWidget):
self.retranslateUi() self.retranslateUi()
self.auto_select_id = -1 self.auto_select_id = -1
Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load) Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load)
# Need to use event as called across threads and UI is updated
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote)
def required_icons(self): def required_icons(self):
""" """
@ -481,6 +484,15 @@ class MediaManagerItem(QtGui.QWidget):
else: else:
self.go_live() self.go_live()
def go_live_remote(self, message):
"""
Remote Call wrapper
``message``
The passed data item_id:Remote.
"""
self.go_live(message[0], remote=message[1])
def go_live(self, item_id=None, remote=False): def go_live(self, item_id=None, remote=False):
""" """
Make the currently selected item go live. Make the currently selected item go live.
@ -523,6 +535,15 @@ class MediaManagerItem(QtGui.QWidget):
for item in items: for item in items:
self.add_to_service(item) self.add_to_service(item)
def add_to_service_remote(self, message):
"""
Remote Call wrapper
``message``
The passed data item:Remote.
"""
self.add_to_service(message[0], remote=message[1])
def add_to_service(self, item=None, replace=None, remote=False): def add_to_service(self, item=None, replace=None, remote=False):
""" """
Add this item to the current service. Add this item to the current service.

View File

@ -103,7 +103,7 @@ class Plugin(QtCore.QObject):
``add_export_menu_Item(export_menu)`` ``add_export_menu_Item(export_menu)``
Add an item to the Export menu. Add an item to the Export menu.
``create_settings_Tab()`` ``create_settings_tab()``
Creates a new instance of SettingsTabItem to be used in the Settings Creates a new instance of SettingsTabItem to be used in the Settings
dialog. dialog.
@ -252,7 +252,7 @@ class Plugin(QtCore.QObject):
""" """
pass pass
def create_settings_Tab(self, parent): def create_settings_tab(self, parent):
""" """
Create a tab for the settings window to display the configurable options Create a tab for the settings window to display the configurable options
for this plugin to the user. for this plugin to the user.

View File

@ -153,7 +153,7 @@ class PluginManager(object):
""" """
for plugin in self.plugins: for plugin in self.plugins:
if plugin.status is not PluginStatus.Disabled: if plugin.status is not PluginStatus.Disabled:
plugin.create_settings_Tab(self.settings_form) plugin.create_settings_tab(self.settings_form)
def hook_import_menu(self): def hook_import_menu(self):
""" """

View File

@ -62,12 +62,10 @@ class ItemCapabilities(object):
tab when making the previous item live. tab when making the previous item live.
``CanEdit`` ``CanEdit``
The capability to allow the ServiceManager to allow the item to be The capability to allow the ServiceManager to allow the item to be edited
edited
``CanMaintain`` ``CanMaintain``
The capability to allow the ServiceManager to allow the item to be The capability to allow the ServiceManager to allow the item to be reordered.
reordered.
``RequiresMedia`` ``RequiresMedia``
Determines is the service_item needs a Media Player Determines is the service_item needs a Media Player

View File

@ -77,6 +77,11 @@ try:
ICU_VERSION = u'OK' ICU_VERSION = u'OK'
except ImportError: except ImportError:
ICU_VERSION = u'-' ICU_VERSION = u'-'
try:
import cherrypy
CHERRYPY_VERSION = cherrypy.__version__
except ImportError:
CHERRYPY_VERSION = u'-'
try: try:
import uno import uno
arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue') arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue')
@ -151,6 +156,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog):
u'PyEnchant: %s\n' % ENCHANT_VERSION + \ u'PyEnchant: %s\n' % ENCHANT_VERSION + \
u'PySQLite: %s\n' % SQLITE_VERSION + \ u'PySQLite: %s\n' % SQLITE_VERSION + \
u'Mako: %s\n' % MAKO_VERSION + \ u'Mako: %s\n' % MAKO_VERSION + \
u'CherryPy: %s\n' % CHERRYPY_VERSION + \
u'pyICU: %s\n' % ICU_VERSION + \ u'pyICU: %s\n' % ICU_VERSION + \
u'pyUNO bridge: %s\n' % UNO_VERSION + \ u'pyUNO bridge: %s\n' % UNO_VERSION + \
u'VLC: %s\n' % VLC_VERSION u'VLC: %s\n' % VLC_VERSION

View File

@ -273,7 +273,6 @@ class ServiceManagerDialog(object):
Registry().register_function(u'config_screen_changed', self.regenerate_service_Items) Registry().register_function(u'config_screen_changed', self.regenerate_service_Items)
Registry().register_function(u'theme_update_global', self.theme_change) Registry().register_function(u'theme_update_global', self.theme_change)
Registry().register_function(u'mediaitem_suffix_reset', self.reset_supported_suffixes) Registry().register_function(u'mediaitem_suffix_reset', self.reset_supported_suffixes)
Registry().register_function(u'servicemanager_set_item', self.on_set_item)
def drag_enter_event(self, event): def drag_enter_event(self, event):
""" """
@ -315,6 +314,8 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
self.layout.setSpacing(0) self.layout.setSpacing(0)
self.layout.setMargin(0) self.layout.setMargin(0)
self.setup_ui(self) self.setup_ui(self)
# Need to use event as called across threads and UI is updated
QtCore.QObject.connect(self, QtCore.SIGNAL(u'servicemanager_set_item'), self.on_set_item)
def set_modified(self, modified=True): def set_modified(self, modified=True):
""" """
@ -993,7 +994,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
def on_set_item(self, message): def on_set_item(self, message):
""" """
Called by a signal to select a specific item. Called by a signal to select a specific item and make it live usually from remote.
""" """
self.set_item(int(message)) self.set_item(int(message))

View File

@ -96,6 +96,7 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog):
""" """
Process the form saving the settings Process the form saving the settings
""" """
log.debug(u'Processing settings exit')
for tabIndex in range(self.stacked_layout.count()): for tabIndex in range(self.stacked_layout.count()):
self.stacked_layout.widget(tabIndex).save() self.stacked_layout.widget(tabIndex).save()
# if the display of image background are changing we need to regenerate the image cache # if the display of image background are changing we need to regenerate the image cache

View File

@ -360,8 +360,9 @@ class SlideController(DisplayController):
# Signals # Signals
self.preview_list_widget.clicked.connect(self.onSlideSelected) self.preview_list_widget.clicked.connect(self.onSlideSelected)
if self.is_live: if self.is_live:
# Need to use event as called across threads and UI is updated
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display)
Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay) Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay)
Registry().register_function(u'slidecontroller_toggle_display', self.toggle_display)
self.toolbar.set_widget_visible(self.loop_list, False) self.toolbar.set_widget_visible(self.loop_list, False)
self.toolbar.set_widget_visible(self.wide_menu, False) self.toolbar.set_widget_visible(self.wide_menu, False)
else: else:
@ -373,13 +374,16 @@ class SlideController(DisplayController):
else: else:
self.preview_list_widget.addActions([self.nextItem, self.previous_item]) self.preview_list_widget.addActions([self.nextItem, self.previous_item])
Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop) Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop)
Registry().register_function(u'slidecontroller_%s_next' % self.type_prefix, self.on_slide_selected_next)
Registry().register_function(u'slidecontroller_%s_previous' % self.type_prefix, self.on_slide_selected_previous)
Registry().register_function(u'slidecontroller_%s_change' % self.type_prefix, self.on_slide_change) Registry().register_function(u'slidecontroller_%s_change' % self.type_prefix, self.on_slide_change)
Registry().register_function(u'slidecontroller_%s_set' % self.type_prefix, self.on_slide_selected_index)
Registry().register_function(u'slidecontroller_%s_blank' % self.type_prefix, self.on_slide_blank) Registry().register_function(u'slidecontroller_%s_blank' % self.type_prefix, self.on_slide_blank)
Registry().register_function(u'slidecontroller_%s_unblank' % self.type_prefix, self.on_slide_unblank) Registry().register_function(u'slidecontroller_%s_unblank' % self.type_prefix, self.on_slide_unblank)
Registry().register_function(u'slidecontroller_update_slide_limits', self.update_slide_limits) Registry().register_function(u'slidecontroller_update_slide_limits', self.update_slide_limits)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix),
self.on_slide_selected_index)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_next' % self.type_prefix),
self.on_slide_selected_next)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_previous' % self.type_prefix),
self.on_slide_selected_previous)
def _slideShortcutActivated(self): def _slideShortcutActivated(self):
""" """

View File

@ -49,10 +49,12 @@ class AlertsManager(QtCore.QObject):
def __init__(self, parent): def __init__(self, parent):
QtCore.QObject.__init__(self, parent) QtCore.QObject.__init__(self, parent)
Registry().register(u'alerts_manager', self)
self.timer_id = 0 self.timer_id = 0
self.alert_list = [] self.alert_list = []
Registry().register_function(u'live_display_active', self.generate_alert) Registry().register_function(u'live_display_active', self.generate_alert)
Registry().register_function(u'alerts_text', self.alert_text) Registry().register_function(u'alerts_text', self.alert_text)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'alerts_text'), self.alert_text)
def alert_text(self, message): def alert_text(self, message):
""" """

View File

@ -55,6 +55,7 @@ UGLY_CHARS = {
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class BGExtract(object): class BGExtract(object):
""" """
Extract verses from BibleGateway Extract verses from BibleGateway
@ -671,6 +672,7 @@ class HTTPBible(BibleDB):
application = property(_get_application) application = property(_get_application)
def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None,
pre_parse_substitute=None, cleaner=None): pre_parse_substitute=None, cleaner=None):
""" """
@ -715,6 +717,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None,
Registry().get(u'application').process_events() Registry().get(u'application').process_events()
return soup return soup
def send_error_message(error_type): def send_error_message(error_type):
""" """
Send a standard error message informing the user of an issue. Send a standard error message informing the user of an issue.

View File

@ -54,7 +54,7 @@ class MediaPlugin(Plugin):
# passed with drag and drop messages # passed with drag and drop messages
self.dnd_id = u'Media' self.dnd_id = u'Media'
def create_settings_Tab(self, parent): def create_settings_tab(self, parent):
""" """
Create the settings Tab Create the settings Tab
""" """

View File

@ -69,7 +69,7 @@ class PresentationPlugin(Plugin):
self.icon_path = u':/plugins/plugin_presentations.png' self.icon_path = u':/plugins/plugin_presentations.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)
def create_settings_Tab(self, parent): def create_settings_tab(self, parent):
""" """
Create the settings Tab Create the settings Tab
""" """

View File

@ -147,7 +147,7 @@ window.OpenLP = {
}, },
pollServer: function () { pollServer: function () {
$.getJSON( $.getJSON(
"/api/poll", "/stage/api/poll",
function (data, status) { function (data, status) {
var prevItem = OpenLP.currentItem; var prevItem = OpenLP.currentItem;
OpenLP.currentSlide = data.results.slide; OpenLP.currentSlide = data.results.slide;

View File

@ -26,7 +26,7 @@
window.OpenLP = { window.OpenLP = {
loadService: function (event) { loadService: function (event) {
$.getJSON( $.getJSON(
"/api/service/list", "/stage/api/service/list",
function (data, status) { function (data, status) {
OpenLP.nextSong = ""; OpenLP.nextSong = "";
$("#notes").html(""); $("#notes").html("");
@ -46,7 +46,7 @@ window.OpenLP = {
}, },
loadSlides: function (event) { loadSlides: function (event) {
$.getJSON( $.getJSON(
"/api/controller/live/text", "/stage/api/controller/live/text",
function (data, status) { function (data, status) {
OpenLP.currentSlides = data.results.slides; OpenLP.currentSlides = data.results.slides;
OpenLP.currentSlide = 0; OpenLP.currentSlide = 0;
@ -137,7 +137,7 @@ window.OpenLP = {
}, },
pollServer: function () { pollServer: function () {
$.getJSON( $.getJSON(
"/api/poll", "/stage/api/poll",
function (data, status) { function (data, status) {
OpenLP.updateClock(data); OpenLP.updateClock(data);
if (OpenLP.currentItem != data.results.item || if (OpenLP.currentItem != data.results.item ||

View File

@ -43,7 +43,7 @@ the remotes.
``/files/{filename}`` ``/files/{filename}``
Serve a static file. Serve a static file.
``/api/poll`` ``/stage/api/poll``
Poll to see if there are any changes. Returns a JSON-encoded dict of Poll to see if there are any changes. Returns a JSON-encoded dict of
any changes that occurred:: any changes that occurred::
@ -119,122 +119,198 @@ import os
import re import re
import urllib import urllib
import urlparse import urlparse
import cherrypy
from PyQt4 import QtCore, QtNetwork
from mako.template import Template from mako.template import Template
from PyQt4 import QtCore
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
from openlp.core.utils import AppLocation, translate from openlp.core.utils import AppLocation, translate
from cherrypy._cpcompat import sha, ntob
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class HttpResponse(object): def make_sha_hash(password):
""" """
A simple object to encapsulate a pseudo-http response. Create an encrypted password for the given password.
""" """
code = '200 OK' return sha(ntob(password)).hexdigest()
content = ''
headers = {
'Content-Type': 'text/html; charset="utf-8"\r\n'
}
def __init__(self, content='', headers=None, code=None):
if headers is None: def fetch_password(username):
headers = {} """
self.content = content Fetch the password for a provided user.
for key, value in headers.iteritems(): """
self.headers[key] = value if username != Settings().value(u'remotes/user id'):
if code: return None
self.code = code return make_sha_hash(Settings().value(u'remotes/password'))
class HttpServer(object): class HttpServer(object):
""" """
Ability to control OpenLP via a web browser. Ability to control OpenLP via a web browser.
This class controls the Cherrypy server and configuration.
""" """
def __init__(self, plugin): _cp_config = {
'tools.sessions.on': True,
'tools.auth.on': True
}
def __init__(self):
""" """
Initialise the httpserver, and start the server. Initialise the http server, and start the server.
""" """
log.debug(u'Initialise httpserver') log.debug(u'Initialise httpserver')
self.plugin = plugin self.settings_section = u'remotes'
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') self.router = HttpRouter()
self.connections = []
self.start_tcp()
def start_tcp(self): def start_server(self):
""" """
Start the http server, use the port in the settings default to 4316. Start the http server based on configuration.
Listen out for slide and song changes so they can be broadcast to
clients. Listen out for socket connections.
""" """
log.debug(u'Start TCP server') log.debug(u'Start CherryPy server')
port = Settings().value(self.plugin.settings_section + u'/port') # Define to security levels and inject the router code
address = Settings().value(self.plugin.settings_section + u'/ip address') self.root = self.Public()
self.server = QtNetwork.QTcpServer() self.root.files = self.Files()
self.server.listen(QtNetwork.QHostAddress(address), port) self.root.stage = self.Stage()
self.server.newConnection.connect(self.new_connection) self.root.router = self.router
log.debug(u'TCP listening on port %d' % port) self.root.files.router = self.router
self.root.stage.router = self.router
cherrypy.tree.mount(self.root, '/', config=self.define_config())
# Turn off the flood of access messages cause by poll
cherrypy.log.access_log.propagate = False
cherrypy.engine.start()
def new_connection(self): def define_config(self):
""" """
A new http connection has been made. Create a client object to handle Define the configuration of the server.
communication.
""" """
log.debug(u'new http connection') if Settings().value(self.settings_section + u'/https enabled'):
socket = self.server.nextPendingConnection() port = Settings().value(self.settings_section + u'/https port')
if socket: address = Settings().value(self.settings_section + u'/ip address')
self.connections.append(HttpConnection(self, socket)) local_data = AppLocation.get_directory(AppLocation.DataDir)
cherrypy.config.update({u'server.socket_host': str(address),
u'server.socket_port': port,
u'server.ssl_certificate': os.path.join(local_data, u'remotes', u'openlp.crt'),
u'server.ssl_private_key': os.path.join(local_data, u'remotes', u'openlp.key')})
else:
port = Settings().value(self.settings_section + u'/port')
address = Settings().value(self.settings_section + u'/ip address')
cherrypy.config.update({u'server.socket_host': str(address)})
cherrypy.config.update({u'server.socket_port': port})
cherrypy.config.update({u'environment': u'embedded'})
cherrypy.config.update({u'engine.autoreload_on': False})
directory_config = {u'/': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': Settings().value(u'remotes/authentication enabled'),
u'tools.basic_auth.realm': u'OpenLP Remote Login',
u'tools.basic_auth.users': fetch_password,
u'tools.basic_auth.encrypt': make_sha_hash},
u'/files': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False},
u'/stage': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False}}
return directory_config
def close_connection(self, connection): class Public(object):
""" """
The connection has been closed. Clean up Main access class with may have security enabled on it.
""" """
log.debug(u'close http connection') @cherrypy.expose
if connection in self.connections: def default(self, *args, **kwargs):
self.connections.remove(connection) self.router.request_data = None
if isinstance(kwargs, dict):
self.router.request_data = kwargs.get(u'data', None)
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
class Files(object):
"""
Provides access to files and has no security available. These are read only accesses
"""
@cherrypy.expose
def default(self, *args, **kwargs):
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
class Stage(object):
"""
Stageview is read only so security is not relevant and would reduce it's usability
"""
@cherrypy.expose
def default(self, *args, **kwargs):
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
def close(self): def close(self):
""" """
Close down the http server. Close down the http server.
""" """
log.debug(u'close http server') log.debug(u'close http server')
self.server.close() cherrypy.engine.exit()
class HttpConnection(object): class HttpRouter(object):
""" """
A single connection, this handles communication between the server This code is called by the HttpServer upon a request and it processes it based on the routing table.
and the client.
""" """
def __init__(self, parent, socket): def __init__(self):
""" """
Initialise the http connection. Listen out for socket signals. Initialise the router
""" """
log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress())
self.socket = socket
self.parent = parent
self.routes = [ self.routes = [
(u'^/$', self.serve_file), (u'^/$', self.serve_file),
(u'^/(stage)$', self.serve_file), (u'^/(stage)$', self.serve_file),
(r'^/files/(.*)$', self.serve_file), (r'^/files/(.*)$', self.serve_file),
(r'^/api/poll$', self.poll), (r'^/api/poll$', self.poll),
(r'^/stage/api/poll$', self.poll),
(r'^/api/controller/(live|preview)/(.*)$', self.controller), (r'^/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/stage/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service), (r'^/api/service/(.*)$', self.service),
(r'^/stage/api/service/(.*)$', self.service),
(r'^/api/display/(hide|show|blank|theme|desktop)$', self.display), (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
(r'^/api/alert$', self.alert), (r'^/api/alert$', self.alert),
(r'^/api/plugin/(search)$', self.pluginInfo), (r'^/api/plugin/(search)$', self.plugin_info),
(r'^/api/(.*)/search$', self.search), (r'^/api/(.*)/search$', self.search),
(r'^/api/(.*)/live$', self.go_live), (r'^/api/(.*)/live$', self.go_live),
(r'^/api/(.*)/add$', self.add_to_service) (r'^/api/(.*)/add$', self.add_to_service)
] ]
self.socket.readyRead.connect(self.ready_read)
self.socket.disconnected.connect(self.disconnected)
self.translate() self.translate()
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
def process_http_request(self, url_path, *args):
"""
Common function to process HTTP requests
``url_path``
The requested URL.
``*args``
Any passed data.
"""
response = None
for route, func in self.routes:
match = re.match(route, url_path)
if match:
log.debug('Route "%s" matched "%s"', route, url_path)
args = []
for param in match.groups():
args.append(param)
response = func(*args)
break
if response:
return response
else:
return self._http_not_found()
def _get_service_items(self): def _get_service_items(self):
"""
Read the service item in use and return the data as a json object
"""
service_items = [] service_items = []
if self.live_controller.service_item: if self.live_controller.service_item:
current_unique_identifier = self.live_controller.service_item.unique_identifier current_unique_identifier = self.live_controller.service_item.unique_identifier
@ -281,40 +357,6 @@ class HttpConnection(object):
'slides': translate('RemotePlugin.Mobile', 'Slides') 'slides': translate('RemotePlugin.Mobile', 'Slides')
} }
def ready_read(self):
"""
Data has been sent from the client. Respond to it
"""
log.debug(u'ready to read socket')
if self.socket.canReadLine():
data = str(self.socket.readLine())
try:
log.debug(u'received: ' + data)
except UnicodeDecodeError:
# Malicious request containing non-ASCII characters.
self.close()
return
words = data.split(' ')
response = None
if words[0] == u'GET':
url = urlparse.urlparse(words[1])
self.url_params = urlparse.parse_qs(url.query)
# Loop through the routes we set up earlier and execute them
for route, func in self.routes:
match = re.match(route, url.path)
if match:
log.debug('Route "%s" matched "%s"', route, url.path)
args = []
for param in match.groups():
args.append(param)
response = func(*args)
break
if response:
self.send_response(response)
else:
self.send_response(HttpResponse(code='404 Not Found'))
self.close()
def serve_file(self, filename=None): def serve_file(self, filename=None):
""" """
Send a file to the socket. For now, just a subset of file types Send a file to the socket. For now, just a subset of file types
@ -329,9 +371,9 @@ class HttpConnection(object):
filename = u'index.html' filename = u'index.html'
elif filename == u'stage': elif filename == u'stage':
filename = u'stage.html' filename = u'stage.html'
path = os.path.normpath(os.path.join(self.parent.html_dir, filename)) path = os.path.normpath(os.path.join(self.html_dir, filename))
if not path.startswith(self.parent.html_dir): if not path.startswith(self.html_dir):
return HttpResponse(code=u'404 Not Found') return self._http_not_found()
ext = os.path.splitext(filename)[1] ext = os.path.splitext(filename)[1]
html = None html = None
if ext == u'.html': if ext == u'.html':
@ -360,11 +402,12 @@ class HttpConnection(object):
content = file_handle.read() content = file_handle.read()
except IOError: except IOError:
log.exception(u'Failed to open %s' % path) log.exception(u'Failed to open %s' % path)
return HttpResponse(code=u'404 Not Found') return self._http_not_found()
finally: finally:
if file_handle: if file_handle:
file_handle.close() file_handle.close()
return HttpResponse(content, {u'Content-Type': mimetype}) cherrypy.response.headers['Content-Type'] = mimetype
return content
def poll(self): def poll(self):
""" """
@ -379,18 +422,20 @@ class HttpConnection(object):
u'theme': self.live_controller.theme_screen.isChecked(), u'theme': self.live_controller.theme_screen.isChecked(),
u'display': self.live_controller.desktop_screen.isChecked() u'display': self.live_controller.desktop_screen.isChecked()
} }
return HttpResponse(json.dumps({u'results': result}), {u'Content-Type': u'application/json'}) cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result})
def display(self, action): def display(self, action):
""" """
Hide or show the display screen. Hide or show the display screen.
This is a cross Thread call and UI is updated so Events need to be used.
``action`` ``action``
This is the action, either ``hide`` or ``show``. This is the action, either ``hide`` or ``show``.
""" """
Registry().execute(u'slidecontroller_toggle_display', action) self.live_controller.emit(QtCore.SIGNAL(u'slidecontroller_toggle_display'), action)
return HttpResponse(json.dumps({u'results': {u'success': True}}), cherrypy.response.headers['Content-Type'] = u'application/json'
{u'Content-Type': u'application/json'}) return json.dumps({u'results': {u'success': True}})
def alert(self): def alert(self):
""" """
@ -399,16 +444,16 @@ class HttpConnection(object):
plugin = self.plugin_manager.get_plugin_by_name("alerts") plugin = self.plugin_manager.get_plugin_by_name("alerts")
if plugin.status == PluginStatus.Active: if plugin.status == PluginStatus.Active:
try: try:
text = json.loads(self.url_params[u'data'][0])[u'request'][u'text'] text = json.loads(self.request_data)[u'request'][u'text']
except KeyError, ValueError: except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request') return self._http_bad_request()
text = urllib.unquote(text) text = urllib.unquote(text)
Registry().execute(u'alerts_text', [text]) self.alerts_manager.emit(QtCore.SIGNAL(u'alerts_text'), [text])
success = True success = True
else: else:
success = False success = False
return HttpResponse(json.dumps({u'results': {u'success': success}}), cherrypy.response.headers['Content-Type'] = u'application/json'
{u'Content-Type': u'application/json'}) return json.dumps({u'results': {u'success': success}})
def controller(self, display_type, action): def controller(self, display_type, action):
""" """
@ -444,44 +489,44 @@ class HttpConnection(object):
if current_item: if current_item:
json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier
else: else:
if self.url_params and self.url_params.get(u'data'): if self.request_data:
try: try:
data = json.loads(self.url_params[u'data'][0]) data = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError: except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request') return self._http_bad_request()
log.info(data) log.info(data)
# This slot expects an int within a list. # This slot expects an int within a list.
id = data[u'request'][u'id'] self.live_controller.emit(QtCore.SIGNAL(event), [data])
Registry().execute(event, [id])
else: else:
Registry().execute(event) self.live_controller.emit(QtCore.SIGNAL(event))
json_data = {u'results': {u'success': True}} json_data = {u'results': {u'success': True}}
return HttpResponse(json.dumps(json_data), {u'Content-Type': u'application/json'}) cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps(json_data)
def service(self, action): def service(self, action):
""" """
Handles requests for service items Handles requests for service items in the service manager
``action`` ``action``
The action to perform. The action to perform.
""" """
event = u'servicemanager_%s' % action event = u'servicemanager_%s' % action
if action == u'list': if action == u'list':
return HttpResponse(json.dumps({u'results': {u'items': self._get_service_items()}}), cherrypy.response.headers['Content-Type'] = u'application/json'
{u'Content-Type': u'application/json'}) return json.dumps({u'results': {u'items': self._get_service_items()}})
else:
event += u'_item' event += u'_item'
if self.url_params and self.url_params.get(u'data'): if self.request_data:
try: try:
data = json.loads(self.url_params[u'data'][0]) data = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError: except KeyError:
return HttpResponse(code=u'400 Bad Request') return self._http_bad_request()
Registry().execute(event, data[u'request'][u'id']) self.service_manager.emit(QtCore.SIGNAL(event), data)
else: else:
Registry().execute(event) Registry().execute(event)
return HttpResponse(json.dumps({u'results': {u'success': True}}), {u'Content-Type': u'application/json'}) cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'success': True}})
def pluginInfo(self, action): def plugin_info(self, action):
""" """
Return plugin related information, based on the action. Return plugin related information, based on the action.
@ -493,8 +538,9 @@ class HttpConnection(object):
searches = [] searches = []
for plugin in self.plugin_manager.plugins: for plugin in self.plugin_manager.plugins:
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])]) searches.append([plugin.name, unicode(plugin.text_strings[StringContent.Name][u'plural'])])
return HttpResponse(json.dumps({u'results': {u'items': searches}}), {u'Content-Type': u'application/json'}) cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': searches}})
def search(self, plugin_name): def search(self, plugin_name):
""" """
@ -504,69 +550,63 @@ class HttpConnection(object):
The plugin name to search in. The plugin name to search in.
""" """
try: try:
text = json.loads(self.url_params[u'data'][0])[u'request'][u'text'] text = json.loads(self.request_data)[u'request'][u'text']
except KeyError, ValueError: except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request') return self._http_bad_request()
text = urllib.unquote(text) text = urllib.unquote(text)
plugin = self.plugin_manager.get_plugin_by_name(plugin_name) plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
results = plugin.media_item.search(text, False) results = plugin.media_item.search(text, False)
else: else:
results = [] results = []
return HttpResponse(json.dumps({u'results': {u'items': results}}), {u'Content-Type': u'application/json'}) cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': results}})
def go_live(self, plugin_name): def go_live(self, plugin_name):
""" """
Go live on an item of type ``plugin``. Go live on an item of type ``plugin``.
""" """
try: try:
id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] id = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError: except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request') return self._http_bad_request()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name) plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item: if plugin.status == PluginStatus.Active and plugin.media_item:
plugin.media_item.go_live(id, remote=True) plugin.media_item.emit(QtCore.SIGNAL(u'%s_go_live' % plugin_name), [id, True])
return HttpResponse(code=u'200 OK') return self._http_success()
def add_to_service(self, plugin_name): def add_to_service(self, plugin_name):
""" """
Add item of type ``plugin_name`` to the end of the service. Add item of type ``plugin_name`` to the end of the service.
""" """
try: try:
id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] id = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError: except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request') return self._http_bad_request()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name) plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item: if plugin.status == PluginStatus.Active and plugin.media_item:
item_id = plugin.media_item.createItemFromId(id) item_id = plugin.media_item.create_item_from_id(id)
plugin.media_item.add_to_service(item_id, remote=True) plugin.media_item.emit(QtCore.SIGNAL(u'%s_add_to_service' % plugin_name), [item_id, True])
return HttpResponse(code=u'200 OK') self._http_success()
def send_response(self, response): def _http_success(self):
http = u'HTTP/1.1 %s\r\n' % response.code """
for header, value in response.headers.iteritems(): Set the HTTP success return code.
http += '%s: %s\r\n' % (header, value) """
http += '\r\n' cherrypy.response.status = 200
self.socket.write(http)
self.socket.write(response.content)
def disconnected(self): def _http_bad_request(self):
""" """
The client has disconnected. Tidy up Set the HTTP bad response return code.
""" """
log.debug(u'socket disconnected') cherrypy.response.status = 400
self.close()
def close(self): def _http_not_found(self):
""" """
The server has closed the connection. Tidy up Set the HTTP not found return code.
""" """
if not self.socket: cherrypy.response.status = 404
return cherrypy.response.body = ["<html><body>Sorry, an error occurred </body></html>"]
log.debug(u'close socket')
self.socket.close()
self.socket = None
self.parent.close_connection(self)
def _get_service_manager(self): def _get_service_manager(self):
""" """
@ -597,3 +637,13 @@ class HttpConnection(object):
return self._plugin_manager return self._plugin_manager
plugin_manager = property(_get_plugin_manager) plugin_manager = property(_get_plugin_manager)
def _get_alerts_manager(self):
"""
Adds the alerts manager to the class dynamically
"""
if not hasattr(self, u'_alerts_manager'):
self._alerts_manager = Registry().get(u'alerts_manager')
return self._alerts_manager
alerts_manager = property(_get_alerts_manager)

View File

@ -27,9 +27,12 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import os.path
from PyQt4 import QtCore, QtGui, QtNetwork from PyQt4 import QtCore, QtGui, QtNetwork
from openlp.core.lib import Registry, Settings, SettingsTab, translate from openlp.core.lib import Settings, SettingsTab, translate
from openlp.core.utils import AppLocation
ZERO_URL = u'0.0.0.0' ZERO_URL = u'0.0.0.0'
@ -53,32 +56,84 @@ class RemoteTab(SettingsTab):
self.address_label.setObjectName(u'address_label') self.address_label.setObjectName(u'address_label')
self.address_edit = QtGui.QLineEdit(self.server_settings_group_box) self.address_edit = QtGui.QLineEdit(self.server_settings_group_box)
self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp( self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'),
u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self)) self))
self.address_edit.setObjectName(u'address_edit') self.address_edit.setObjectName(u'address_edit')
self.server_settings_layout.addRow(self.address_label, self.address_edit) self.server_settings_layout.addRow(self.address_label, self.address_edit)
self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box) self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box)
self.twelve_hour_check_box.setObjectName(u'twelve_hour_check_box') self.twelve_hour_check_box.setObjectName(u'twelve_hour_check_box')
self.server_settings_layout.addRow(self.twelve_hour_check_box) self.server_settings_layout.addRow(self.twelve_hour_check_box)
self.port_label = QtGui.QLabel(self.server_settings_group_box) self.left_layout.addWidget(self.server_settings_group_box)
self.http_settings_group_box = QtGui.QGroupBox(self.left_column)
self.http_settings_group_box.setObjectName(u'http_settings_group_box')
self.http_setting_layout = QtGui.QFormLayout(self.http_settings_group_box)
self.http_setting_layout.setObjectName(u'http_setting_layout')
self.port_label = QtGui.QLabel(self.http_settings_group_box)
self.port_label.setObjectName(u'port_label') self.port_label.setObjectName(u'port_label')
self.port_spin_box = QtGui.QSpinBox(self.server_settings_group_box) self.port_spin_box = QtGui.QSpinBox(self.http_settings_group_box)
self.port_spin_box.setMaximum(32767) self.port_spin_box.setMaximum(32767)
self.port_spin_box.setObjectName(u'port_spin_box') self.port_spin_box.setObjectName(u'port_spin_box')
self.server_settings_layout.addRow(self.port_label, self.port_spin_box) self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
self.remote_url_label = QtGui.QLabel(self.server_settings_group_box) self.remote_url_label = QtGui.QLabel(self.http_settings_group_box)
self.remote_url_label.setObjectName(u'remote_url_label') self.remote_url_label.setObjectName(u'remote_url_label')
self.remote_url = QtGui.QLabel(self.server_settings_group_box) self.remote_url = QtGui.QLabel(self.http_settings_group_box)
self.remote_url.setObjectName(u'remote_url') self.remote_url.setObjectName(u'remote_url')
self.remote_url.setOpenExternalLinks(True) self.remote_url.setOpenExternalLinks(True)
self.server_settings_layout.addRow(self.remote_url_label, self.remote_url) self.http_setting_layout.addRow(self.remote_url_label, self.remote_url)
self.stage_url_label = QtGui.QLabel(self.server_settings_group_box) self.stage_url_label = QtGui.QLabel(self.http_settings_group_box)
self.stage_url_label.setObjectName(u'stage_url_label') self.stage_url_label.setObjectName(u'stage_url_label')
self.stage_url = QtGui.QLabel(self.server_settings_group_box) self.stage_url = QtGui.QLabel(self.http_settings_group_box)
self.stage_url.setObjectName(u'stage_url') self.stage_url.setObjectName(u'stage_url')
self.stage_url.setOpenExternalLinks(True) self.stage_url.setOpenExternalLinks(True)
self.server_settings_layout.addRow(self.stage_url_label, self.stage_url) self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
self.left_layout.addWidget(self.server_settings_group_box) self.left_layout.addWidget(self.http_settings_group_box)
self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
self.https_settings_group_box.setCheckable(True)
self.https_settings_group_box.setChecked(False)
self.https_settings_group_box.setObjectName(u'https_settings_group_box')
self.https_settings_layout = QtGui.QFormLayout(self.https_settings_group_box)
self.https_settings_layout.setObjectName(u'https_settings_layout')
self.https_error_label = QtGui.QLabel(self.https_settings_group_box)
self.https_error_label.setVisible(False)
self.https_error_label.setWordWrap(True)
self.https_error_label.setObjectName(u'https_error_label')
self.https_settings_layout.addRow(self.https_error_label)
self.https_port_label = QtGui.QLabel(self.https_settings_group_box)
self.https_port_label.setObjectName(u'https_port_label')
self.https_port_spin_box = QtGui.QSpinBox(self.https_settings_group_box)
self.https_port_spin_box.setMaximum(32767)
self.https_port_spin_box.setObjectName(u'https_port_spin_box')
self.https_settings_layout.addRow(self.https_port_label, self.https_port_spin_box)
self.remote_https_url = QtGui.QLabel(self.https_settings_group_box)
self.remote_https_url.setObjectName(u'remote_http_url')
self.remote_https_url.setOpenExternalLinks(True)
self.remote_https_url_label = QtGui.QLabel(self.https_settings_group_box)
self.remote_https_url_label.setObjectName(u'remote_http_url_label')
self.https_settings_layout.addRow(self.remote_https_url_label, self.remote_https_url)
self.stage_https_url_label = QtGui.QLabel(self.http_settings_group_box)
self.stage_https_url_label.setObjectName(u'stage_https_url_label')
self.stage_https_url = QtGui.QLabel(self.https_settings_group_box)
self.stage_https_url.setObjectName(u'stage_https_url')
self.stage_https_url.setOpenExternalLinks(True)
self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
self.left_layout.addWidget(self.https_settings_group_box)
self.user_login_group_box = QtGui.QGroupBox(self.left_column)
self.user_login_group_box.setCheckable(True)
self.user_login_group_box.setChecked(False)
self.user_login_group_box.setObjectName(u'user_login_group_box')
self.user_login_layout = QtGui.QFormLayout(self.user_login_group_box)
self.user_login_layout.setObjectName(u'user_login_layout')
self.user_id_label = QtGui.QLabel(self.user_login_group_box)
self.user_id_label.setObjectName(u'user_id_label')
self.user_id = QtGui.QLineEdit(self.user_login_group_box)
self.user_id.setObjectName(u'user_id')
self.user_login_layout.addRow(self.user_id_label, self.user_id)
self.password_label = QtGui.QLabel(self.user_login_group_box)
self.password_label.setObjectName(u'password_label')
self.password = QtGui.QLineEdit(self.user_login_group_box)
self.password.setObjectName(u'password')
self.user_login_layout.addRow(self.password_label, self.password)
self.left_layout.addWidget(self.user_login_group_box)
self.android_app_group_box = QtGui.QGroupBox(self.right_column) self.android_app_group_box = QtGui.QGroupBox(self.right_column)
self.android_app_group_box.setObjectName(u'android_app_group_box') self.android_app_group_box.setObjectName(u'android_app_group_box')
self.right_layout.addWidget(self.android_app_group_box) self.right_layout.addWidget(self.android_app_group_box)
@ -96,9 +151,11 @@ class RemoteTab(SettingsTab):
self.qr_layout.addWidget(self.qr_description_label) self.qr_layout.addWidget(self.qr_description_label)
self.left_layout.addStretch() self.left_layout.addStretch()
self.right_layout.addStretch() self.right_layout.addStretch()
self.twelve_hour_check_box.stateChanged.connect(self.onTwelveHourCheckBoxChanged) self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
self.address_edit.textChanged.connect(self.set_urls) self.address_edit.textChanged.connect(self.set_urls)
self.port_spin_box.valueChanged.connect(self.set_urls) self.port_spin_box.valueChanged.connect(self.set_urls)
self.https_port_spin_box.valueChanged.connect(self.set_urls)
self.https_settings_group_box.clicked.connect(self.https_changed)
def retranslateUi(self): def retranslateUi(self):
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings')) self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
@ -112,8 +169,21 @@ class RemoteTab(SettingsTab):
'Scan the QR code or click <a href="https://play.google.com/store/' 'Scan the QR code or click <a href="https://play.google.com/store/'
'apps/details?id=org.openlp.android">download</a> to install the ' 'apps/details?id=org.openlp.android">download</a> to install the '
'Android app from Google Play.')) 'Android app from Google Play.'))
self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
self.https_error_label.setText(translate('RemotePlugin.RemoteTab',
'Could not find an SSL certificate. The HTTPS server will not be available unless an SSL certificate '
'is found. Please see the manual for more information.'))
self.https_port_label.setText(self.port_label.text())
self.remote_https_url_label.setText(self.remote_url_label.text())
self.stage_https_url_label.setText(self.stage_url_label.text())
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
def set_urls(self): def set_urls(self):
"""
Update the display based on the data input on the screen
"""
ip_address = u'localhost' ip_address = u'localhost'
if self.address_edit.text() == ZERO_URL: if self.address_edit.text() == ZERO_URL:
interfaces = QtNetwork.QNetworkInterface.allInterfaces() interfaces = QtNetwork.QNetworkInterface.allInterfaces()
@ -129,31 +199,73 @@ class RemoteTab(SettingsTab):
break break
else: else:
ip_address = self.address_edit.text() ip_address = self.address_edit.text()
url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value()) http_url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
self.remote_url.setText(u'<a href="%s">%s</a>' % (url, url)) https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
url += u'stage' self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
self.stage_url.setText(u'<a href="%s">%s</a>' % (url, url)) self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
http_url += u'stage'
https_url += u'stage'
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
def load(self): def load(self):
"""
Load the configuration and update the server configuration if necessary
"""
self.port_spin_box.setValue(Settings().value(self.settings_section + u'/port')) self.port_spin_box.setValue(Settings().value(self.settings_section + u'/port'))
self.https_port_spin_box.setValue(Settings().value(self.settings_section + u'/https port'))
self.address_edit.setText(Settings().value(self.settings_section + u'/ip address')) self.address_edit.setText(Settings().value(self.settings_section + u'/ip address'))
self.twelve_hour = Settings().value(self.settings_section + u'/twelve hour') self.twelve_hour = Settings().value(self.settings_section + u'/twelve hour')
self.twelve_hour_check_box.setChecked(self.twelve_hour) self.twelve_hour_check_box.setChecked(self.twelve_hour)
local_data = AppLocation.get_directory(AppLocation.DataDir)
if not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.crt')) or \
not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.key')):
self.https_settings_group_box.setChecked(False)
self.https_settings_group_box.setEnabled(False)
self.https_error_label.setVisible(True)
else:
self.https_settings_group_box.setChecked(Settings().value(self.settings_section + u'/https enabled'))
self.https_settings_group_box.setEnabled(True)
self.https_error_label.setVisible(False)
self.user_login_group_box.setChecked(Settings().value(self.settings_section + u'/authentication enabled'))
self.user_id.setText(Settings().value(self.settings_section + u'/user id'))
self.password.setText(Settings().value(self.settings_section + u'/password'))
self.set_urls() self.set_urls()
self.https_changed()
def save(self): def save(self):
changed = False """
Save the configuration and update the server configuration if necessary
"""
if Settings().value(self.settings_section + u'/ip address') != self.address_edit.text() or \ if Settings().value(self.settings_section + u'/ip address') != self.address_edit.text() or \
Settings().value(self.settings_section + u'/port') != self.port_spin_box.value(): Settings().value(self.settings_section + u'/port') != self.port_spin_box.value() or \
changed = True Settings().value(self.settings_section + u'/https port') != self.https_port_spin_box.value() or \
Settings().value(self.settings_section + u'/https enabled') != \
self.https_settings_group_box.isChecked() or \
Settings().value(self.settings_section + u'/authentication enabled') != \
self.user_login_group_box.isChecked():
self.settings_form.register_post_process(u'remotes_config_updated')
Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value()) Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value())
Settings().setValue(self.settings_section + u'/https port', self.https_port_spin_box.value())
Settings().setValue(self.settings_section + u'/https enabled', self.https_settings_group_box.isChecked())
Settings().setValue(self.settings_section + u'/ip address', self.address_edit.text()) Settings().setValue(self.settings_section + u'/ip address', self.address_edit.text())
Settings().setValue(self.settings_section + u'/twelve hour', self.twelve_hour) Settings().setValue(self.settings_section + u'/twelve hour', self.twelve_hour)
if changed: Settings().setValue(self.settings_section + u'/authentication enabled', self.user_login_group_box.isChecked())
Registry().execute(u'remotes_config_updated') Settings().setValue(self.settings_section + u'/user id', self.user_id.text())
Settings().setValue(self.settings_section + u'/password', self.password.text())
def onTwelveHourCheckBoxChanged(self, check_state): def on_twelve_hour_check_box_changed(self, check_state):
"""
Toggle the 12 hour check box.
"""
self.twelve_hour = False self.twelve_hour = False
# we have a set value convert to True/False # we have a set value convert to True/False
if check_state == QtCore.Qt.Checked: if check_state == QtCore.Qt.Checked:
self.twelve_hour = True self.twelve_hour = True
def https_changed(self):
"""
Invert the HTTP group box based on Https group settings
"""
self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())

View File

@ -29,6 +29,8 @@
import logging import logging
from PyQt4 import QtGui
from openlp.core.lib import Plugin, StringContent, translate, build_icon from openlp.core.lib import Plugin, StringContent, translate, build_icon
from openlp.plugins.remotes.lib import RemoteTab, HttpServer from openlp.plugins.remotes.lib import RemoteTab, HttpServer
@ -37,6 +39,11 @@ log = logging.getLogger(__name__)
__default_settings__ = { __default_settings__ = {
u'remotes/twelve hour': True, u'remotes/twelve hour': True,
u'remotes/port': 4316, u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0' u'remotes/ip address': u'0.0.0.0'
} }
@ -60,7 +67,8 @@ class RemotesPlugin(Plugin):
""" """
log.debug(u'initialise') log.debug(u'initialise')
Plugin.initialise(self) Plugin.initialise(self)
self.server = HttpServer(self) self.server = HttpServer()
self.server.start_server()
def finalise(self): def finalise(self):
""" """
@ -70,6 +78,7 @@ class RemotesPlugin(Plugin):
Plugin.finalise(self) Plugin.finalise(self)
if self.server: if self.server:
self.server.close() self.server.close()
self.server = None
def about(self): def about(self):
""" """
@ -99,5 +108,6 @@ class RemotesPlugin(Plugin):
""" """
Called when Config is changed to restart the server on new address or port Called when Config is changed to restart the server on new address or port
""" """
self.finalise() log.debug(u'remote config changed')
self.initialise() self.main_window.information_message(translate('RemotePlugin', 'Configuration Change'),
translate('RemotePlugin', 'OpenLP will need to be restarted for the Remote changes to become active.'))

View File

@ -81,6 +81,7 @@ MODULES = [
'enchant', 'enchant',
'BeautifulSoup', 'BeautifulSoup',
'mako', 'mako',
'cherrypy',
'migrate', 'migrate',
'uno', 'uno',
'icu', 'icu',

View File

@ -74,7 +74,7 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_settings_tabs() # WHEN: We run hook_settings_tabs()
plugin_manager.hook_settings_tabs() plugin_manager.hook_settings_tabs()
# THEN: The create_settings_Tab() method should have been called # THEN: The hook_settings_tabs() method should have been called
assert mocked_plugin.create_media_manager_item.call_count == 0, \ assert mocked_plugin.create_media_manager_item.call_count == 0, \
u'The create_media_manager_item() method should not have been called.' u'The create_media_manager_item() method should not have been called.'
@ -94,8 +94,8 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_settings_tabs() # WHEN: We run hook_settings_tabs()
plugin_manager.hook_settings_tabs() plugin_manager.hook_settings_tabs()
# THEN: The create_settings_Tab() method should not have been called, but the plugins lists should be the same # THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same
assert mocked_plugin.create_settings_Tab.call_count == 0, \ assert mocked_plugin.create_settings_tab.call_count == 0, \
u'The create_media_manager_item() method should not have been called.' u'The create_media_manager_item() method should not have been called.'
self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins, self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
u'The plugins on the settings form should be the same as the plugins in the plugin manager') u'The plugins on the settings form should be the same as the plugins in the plugin manager')
@ -117,7 +117,7 @@ class TestPluginManager(TestCase):
plugin_manager.hook_settings_tabs() plugin_manager.hook_settings_tabs()
# THEN: The create_media_manager_item() method should have been called with the mocked settings form # THEN: The create_media_manager_item() method should have been called with the mocked settings form
assert mocked_plugin.create_settings_Tab.call_count == 1, \ assert mocked_plugin.create_settings_tab.call_count == 1, \
u'The create_media_manager_item() method should have been called once.' u'The create_media_manager_item() method should have been called once.'
self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins, self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
u'The plugins on the settings form should be the same as the plugins in the plugin manager') u'The plugins on the settings form should be the same as the plugins in the plugin manager')
@ -135,8 +135,8 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_settings_tabs() # WHEN: We run hook_settings_tabs()
plugin_manager.hook_settings_tabs() plugin_manager.hook_settings_tabs()
# THEN: The create_settings_Tab() method should have been called # THEN: The create_settings_tab() method should have been called
mocked_plugin.create_settings_Tab.assert_called_with(self.mocked_settings_form) mocked_plugin.create_settings_tab.assert_called_with(self.mocked_settings_form)
def hook_import_menu_with_disabled_plugin_test(self): def hook_import_menu_with_disabled_plugin_test(self):
""" """

View File

@ -11,7 +11,9 @@ from PyQt4 import QtGui
class TestSettings(TestCase): class TestSettings(TestCase):
"""
Test the functions in the Settings module
"""
def setUp(self): def setUp(self):
""" """
Create the UI Create the UI

View File

@ -6,6 +6,7 @@ from unittest import TestCase
from openlp.core.lib import UiStrings from openlp.core.lib import UiStrings
class TestUiStrings(TestCase): class TestUiStrings(TestCase):
def check_same_instance_test(self): def check_same_instance_test(self):

View File

@ -30,8 +30,10 @@ class TestAppLocation(TestCase):
mocked_get_directory.return_value = u'test/dir' mocked_get_directory.return_value = u'test/dir'
mocked_check_directory_exists.return_value = True mocked_check_directory_exists.return_value = True
mocked_os.path.normpath.return_value = u'test/dir' mocked_os.path.normpath.return_value = u'test/dir'
# WHEN: we call AppLocation.get_data_path() # WHEN: we call AppLocation.get_data_path()
data_path = AppLocation.get_data_path() data_path = AppLocation.get_data_path()
# THEN: check that all the correct methods were called, and the result is correct # THEN: check that all the correct methods were called, and the result is correct
mocked_settings.contains.assert_called_with(u'advanced/data path') mocked_settings.contains.assert_called_with(u'advanced/data path')
mocked_get_directory.assert_called_with(AppLocation.DataDir) mocked_get_directory.assert_called_with(AppLocation.DataDir)
@ -49,8 +51,10 @@ class TestAppLocation(TestCase):
mocked_settings.contains.return_value = True mocked_settings.contains.return_value = True
mocked_settings.value.return_value.toString.return_value = u'custom/dir' mocked_settings.value.return_value.toString.return_value = u'custom/dir'
mocked_os.path.normpath.return_value = u'custom/dir' mocked_os.path.normpath.return_value = u'custom/dir'
# WHEN: we call AppLocation.get_data_path() # WHEN: we call AppLocation.get_data_path()
data_path = AppLocation.get_data_path() data_path = AppLocation.get_data_path()
# THEN: the mocked Settings methods were called and the value returned was our set up value # THEN: the mocked Settings methods were called and the value returned was our set up value
mocked_settings.contains.assert_called_with(u'advanced/data path') mocked_settings.contains.assert_called_with(u'advanced/data path')
mocked_settings.value.assert_called_with(u'advanced/data path') mocked_settings.value.assert_called_with(u'advanced/data path')
@ -100,8 +104,10 @@ class TestAppLocation(TestCase):
# GIVEN: A mocked out AppLocation.get_data_path() # GIVEN: A mocked out AppLocation.get_data_path()
mocked_get_data_path.return_value = u'test/dir' mocked_get_data_path.return_value = u'test/dir'
mocked_check_directory_exists.return_value = True mocked_check_directory_exists.return_value = True
# WHEN: we call AppLocation.get_data_path() # WHEN: we call AppLocation.get_data_path()
data_path = AppLocation.get_section_data_path(u'section') data_path = AppLocation.get_section_data_path(u'section')
# THEN: check that all the correct methods were called, and the result is correct # THEN: check that all the correct methods were called, and the result is correct
mocked_check_directory_exists.assert_called_with(u'test/dir/section') mocked_check_directory_exists.assert_called_with(u'test/dir/section')
assert data_path == u'test/dir/section', u'Result should be "test/dir/section"' assert data_path == u'test/dir/section', u'Result should be "test/dir/section"'
@ -112,8 +118,10 @@ class TestAppLocation(TestCase):
""" """
with patch(u'openlp.core.utils.applocation._get_frozen_path') as mocked_get_frozen_path: with patch(u'openlp.core.utils.applocation._get_frozen_path') as mocked_get_frozen_path:
mocked_get_frozen_path.return_value = u'app/dir' mocked_get_frozen_path.return_value = u'app/dir'
# WHEN: We call AppLocation.get_directory # WHEN: We call AppLocation.get_directory
directory = AppLocation.get_directory(AppLocation.AppDir) directory = AppLocation.get_directory(AppLocation.AppDir)
# THEN: # THEN:
assert directory == u'app/dir', u'Directory should be "app/dir"' assert directory == u'app/dir', u'Directory should be "app/dir"'
@ -130,8 +138,10 @@ class TestAppLocation(TestCase):
mocked_get_frozen_path.return_value = u'plugins/dir' mocked_get_frozen_path.return_value = u'plugins/dir'
mocked_sys.frozen = 1 mocked_sys.frozen = 1
mocked_sys.argv = ['openlp'] mocked_sys.argv = ['openlp']
# WHEN: We call AppLocation.get_directory # WHEN: We call AppLocation.get_directory
directory = AppLocation.get_directory(AppLocation.PluginsDir) directory = AppLocation.get_directory(AppLocation.PluginsDir)
# THEN: # THEN:
assert directory == u'plugins/dir', u'Directory should be "plugins/dir"' assert directory == u'plugins/dir', u'Directory should be "plugins/dir"'

View File

@ -0,0 +1,108 @@
"""
This module contains tests for the lib submodule of the Remotes plugin.
"""
import os
from unittest import TestCase
from tempfile import mkstemp
from mock import patch
from openlp.core.lib import Settings
from openlp.plugins.remotes.lib.remotetab import RemoteTab
from PyQt4 import QtGui
__default_settings__ = {
u'remotes/twelve hour': True,
u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0'
}
ZERO_URL = u'0.0.0.0'
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'..', u'resources'))
class TestRemoteTab(TestCase):
"""
Test the functions in the :mod:`lib` module.
"""
def setUp(self):
"""
Create the UI
"""
fd, self.ini_file = mkstemp(u'.ini')
Settings().set_filename(self.ini_file)
self.application = QtGui.QApplication.instance()
Settings().extend_default_settings(__default_settings__)
self.parent = QtGui.QMainWindow()
self.form = RemoteTab(self.parent, u'Remotes', None, None)
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.application
del self.parent
del self.form
os.unlink(self.ini_file)
def set_basic_urls_test(self):
"""
Test the set_urls function with standard defaults
"""
# GIVEN: A mocked location
with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
patch(u'openlp.core.utils.applocation.os') as mocked_os:
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
mocked_settings = mocked_class.return_value
mocked_settings.contains.return_value = False
mocked_get_directory.return_value = u'test/dir'
mocked_check_directory_exists.return_value = True
mocked_os.path.normpath.return_value = u'test/dir'
# WHEN: when the set_urls is called having reloaded the form.
self.form.load()
self.form.set_urls()
# THEN: the following screen values should be set
self.assertEqual(self.form.address_edit.text(), ZERO_URL, u'The default URL should be set on the screen')
self.assertEqual(self.form.https_settings_group_box.isEnabled(), False,
u'The Https box should not be enabled')
self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
u'The Https checked box should note be Checked')
self.assertEqual(self.form.user_login_group_box.isChecked(), False,
u'The authentication box should not be enabled')
def set_certificate_urls_test(self):
"""
Test the set_urls function with certificate available
"""
# GIVEN: A mocked location
with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
patch(u'openlp.core.utils.applocation.os') as mocked_os:
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
mocked_settings = mocked_class.return_value
mocked_settings.contains.return_value = False
mocked_get_directory.return_value = TEST_PATH
mocked_check_directory_exists.return_value = True
mocked_os.path.normpath.return_value = TEST_PATH
# WHEN: when the set_urls is called having reloaded the form.
self.form.load()
self.form.set_urls()
# THEN: the following screen values should be set
self.assertEqual(self.form.http_settings_group_box.isEnabled(), True,
u'The Http group box should be enabled')
self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
u'The Https checked box should be Checked')
self.assertEqual(self.form.https_settings_group_box.isEnabled(), True,
u'The Https box should be enabled')

View File

@ -0,0 +1,99 @@
"""
This module contains tests for the lib submodule of the Remotes plugin.
"""
import os
from unittest import TestCase
from tempfile import mkstemp
from mock import MagicMock
from openlp.core.lib import Settings
from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash
from PyQt4 import QtGui
__default_settings__ = {
u'remotes/twelve hour': True,
u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0'
}
class TestRouter(TestCase):
"""
Test the functions in the :mod:`lib` module.
"""
def setUp(self):
"""
Create the UI
"""
fd, self.ini_file = mkstemp(u'.ini')
Settings().set_filename(self.ini_file)
self.application = QtGui.QApplication.instance()
Settings().extend_default_settings(__default_settings__)
self.router = HttpRouter()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.application
os.unlink(self.ini_file)
def fetch_password_unknown_test(self):
"""
Test the fetch password code with an unknown userid
"""
# GIVEN: A default configuration
# WHEN: called with the defined userid
password = fetch_password(u'itwinkle')
# THEN: the function should return None
self.assertEqual(password, None, u'The result for fetch_password should be None')
def fetch_password_known_test(self):
"""
Test the fetch password code with the defined userid
"""
# GIVEN: A default configuration
# WHEN: called with the defined userid
password = fetch_password(u'openlp')
required_password = make_sha_hash(u'password')
# THEN: the function should return the correct password
self.assertEqual(password, required_password, u'The result for fetch_password should be the defined password')
def sha_password_encrypter_test(self):
"""
Test hash password function
"""
# GIVEN: A default configuration
# WHEN: called with the defined userid
required_password = make_sha_hash(u'password')
test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
# THEN: the function should return the correct password
self.assertEqual(required_password, test_value,
u'The result for make_sha_hash should return the correct encrypted password')
def process_http_request_test(self):
"""
Test the router control functionality
"""
# GIVEN: A testing set of Routes
mocked_function = MagicMock()
test_route = [
(r'^/stage/api/poll$', mocked_function),
]
self.router.routes = test_route
# WHEN: called with a poll route
self.router.process_http_request(u'/stage/api/poll', None)
# THEN: the function should have been called only once
assert mocked_function.call_count == 1, \
u'The mocked function should have been matched and called once.'

View File

@ -0,0 +1,138 @@
"""
This module contains tests for the lib submodule of the Remotes plugin.
"""
import os
from unittest import TestCase
from tempfile import mkstemp
from mock import MagicMock
import urllib2
import cherrypy
from BeautifulSoup import BeautifulSoup
from openlp.core.lib import Settings
from openlp.plugins.remotes.lib.httpserver import HttpServer
from PyQt4 import QtGui
__default_settings__ = {
u'remotes/twelve hour': True,
u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0'
}
class TestRouter(TestCase):
"""
Test the functions in the :mod:`lib` module.
"""
def setUp(self):
"""
Create the UI
"""
fd, self.ini_file = mkstemp(u'.ini')
Settings().set_filename(self.ini_file)
self.application = QtGui.QApplication.instance()
Settings().extend_default_settings(__default_settings__)
self.server = HttpServer()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.application
os.unlink(self.ini_file)
self.server.close()
def start_server(self):
"""
Common function to start server then mock out the router. CherryPy crashes if you mock before you start
"""
self.server.start_server()
self.server.router = MagicMock()
self.server.router.process_http_request = process_http_request
def start_default_server_test(self):
"""
Test the default server serves the correct initial page
"""
# GIVEN: A default configuration
Settings().setValue(u'remotes/authentication enabled', False)
self.start_server()
# WHEN: called the route location
code, page = call_remote_server(u'http://localhost:4316')
# THEN: default title will be returned
self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
u'The default menu should be returned')
def start_authenticating_server_test(self):
"""
Test the default server serves the correctly with authentication
"""
# GIVEN: A default authorised configuration
Settings().setValue(u'remotes/authentication enabled', True)
self.start_server()
# WHEN: called the route location with no user details
code, page = call_remote_server(u'http://localhost:4316')
# THEN: then server will ask for details
self.assertEqual(code, 401, u'The basic authorisation request should be returned')
# WHEN: called the route location with user details
code, page = call_remote_server(u'http://localhost:4316', u'openlp', u'password')
# THEN: default title will be returned
self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
u'The default menu should be returned')
# WHEN: called the route location with incorrect user details
code, page = call_remote_server(u'http://localhost:4316', u'itwinkle', u'password')
# THEN: then server will ask for details
self.assertEqual(code, 401, u'The basic authorisation request should be returned')
def call_remote_server(url, username=None, password=None):
"""
Helper function
``username``
The username.
``password``
The password.
"""
if username:
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
passman.add_password(None, url, username, password)
authhandler = urllib2.HTTPBasicAuthHandler(passman)
opener = urllib2.build_opener(authhandler)
urllib2.install_opener(opener)
try:
page = urllib2.urlopen(url)
return 0, page.read()
except urllib2.HTTPError, e:
return e.code, u''
def process_http_request(url_path, *args):
"""
Override function to make the Mock work but does nothing.
``Url_path``
The url_path.
``*args``
Some args.
"""
cherrypy.response.status = 200
return None

View File

View File