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.auto_select_id = -1
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):
"""
@ -481,6 +484,15 @@ class MediaManagerItem(QtGui.QWidget):
else:
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):
"""
Make the currently selected item go live.
@ -523,6 +535,15 @@ class MediaManagerItem(QtGui.QWidget):
for item in items:
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):
"""
Add this item to the current service.

View File

@ -103,7 +103,7 @@ class Plugin(QtCore.QObject):
``add_export_menu_Item(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
dialog.
@ -252,7 +252,7 @@ class Plugin(QtCore.QObject):
"""
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
for this plugin to the user.

View File

@ -153,7 +153,7 @@ class PluginManager(object):
"""
for plugin in self.plugins:
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):
"""

View File

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

View File

@ -77,6 +77,11 @@ try:
ICU_VERSION = u'OK'
except ImportError:
ICU_VERSION = u'-'
try:
import cherrypy
CHERRYPY_VERSION = cherrypy.__version__
except ImportError:
CHERRYPY_VERSION = u'-'
try:
import uno
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'PySQLite: %s\n' % SQLITE_VERSION + \
u'Mako: %s\n' % MAKO_VERSION + \
u'CherryPy: %s\n' % CHERRYPY_VERSION + \
u'pyICU: %s\n' % ICU_VERSION + \
u'pyUNO bridge: %s\n' % UNO_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'theme_update_global', self.theme_change)
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):
"""
@ -315,6 +314,8 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
self.layout.setSpacing(0)
self.layout.setMargin(0)
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):
"""
@ -993,7 +994,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
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))

View File

@ -96,6 +96,7 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog):
"""
Process the form saving the settings
"""
log.debug(u'Processing settings exit')
for tabIndex in range(self.stacked_layout.count()):
self.stacked_layout.widget(tabIndex).save()
# 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
self.preview_list_widget.clicked.connect(self.onSlideSelected)
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_toggle_display', self.toggle_display)
self.toolbar.set_widget_visible(self.loop_list, False)
self.toolbar.set_widget_visible(self.wide_menu, False)
else:
@ -373,13 +374,16 @@ class SlideController(DisplayController):
else:
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_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_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_unblank' % self.type_prefix, self.on_slide_unblank)
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):
"""

View File

@ -49,10 +49,12 @@ class AlertsManager(QtCore.QObject):
def __init__(self, parent):
QtCore.QObject.__init__(self, parent)
Registry().register(u'alerts_manager', self)
self.timer_id = 0
self.alert_list = []
Registry().register_function(u'live_display_active', self.generate_alert)
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):
"""

View File

@ -55,6 +55,7 @@ UGLY_CHARS = {
log = logging.getLogger(__name__)
class BGExtract(object):
"""
Extract verses from BibleGateway
@ -671,6 +672,7 @@ class HTTPBible(BibleDB):
application = property(_get_application)
def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=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()
return soup
def send_error_message(error_type):
"""
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
self.dnd_id = u'Media'
def create_settings_Tab(self, parent):
def create_settings_tab(self, parent):
"""
Create the settings Tab
"""

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ the remotes.
``/files/{filename}``
Serve a static file.
``/api/poll``
``/stage/api/poll``
Poll to see if there are any changes. Returns a JSON-encoded dict of
any changes that occurred::
@ -119,122 +119,198 @@ import os
import re
import urllib
import urlparse
import cherrypy
from PyQt4 import QtCore, QtNetwork
from mako.template import Template
from PyQt4 import QtCore
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
from openlp.core.utils import AppLocation, translate
from cherrypy._cpcompat import sha, ntob
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'
content = ''
headers = {
'Content-Type': 'text/html; charset="utf-8"\r\n'
}
return sha(ntob(password)).hexdigest()
def __init__(self, content='', headers=None, code=None):
if headers is None:
headers = {}
self.content = content
for key, value in headers.iteritems():
self.headers[key] = value
if code:
self.code = code
def fetch_password(username):
"""
Fetch the password for a provided user.
"""
if username != Settings().value(u'remotes/user id'):
return None
return make_sha_hash(Settings().value(u'remotes/password'))
class HttpServer(object):
"""
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 http server, and start the server.
"""
log.debug(u'Initialise httpserver')
self.plugin = plugin
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
self.connections = []
self.start_tcp()
self.settings_section = u'remotes'
self.router = HttpRouter()
def start_tcp(self):
def start_server(self):
"""
Start the http server, use the port in the settings default to 4316.
Listen out for slide and song changes so they can be broadcast to
clients. Listen out for socket connections.
Start the http server based on configuration.
"""
log.debug(u'Start TCP server')
port = Settings().value(self.plugin.settings_section + u'/port')
address = Settings().value(self.plugin.settings_section + u'/ip address')
self.server = QtNetwork.QTcpServer()
self.server.listen(QtNetwork.QHostAddress(address), port)
self.server.newConnection.connect(self.new_connection)
log.debug(u'TCP listening on port %d' % port)
log.debug(u'Start CherryPy server')
# Define to security levels and inject the router code
self.root = self.Public()
self.root.files = self.Files()
self.root.stage = self.Stage()
self.root.router = self.router
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
communication.
Define the configuration of the server.
"""
log.debug(u'new http connection')
socket = self.server.nextPendingConnection()
if socket:
self.connections.append(HttpConnection(self, socket))
if Settings().value(self.settings_section + u'/https enabled'):
port = Settings().value(self.settings_section + u'/https port')
address = Settings().value(self.settings_section + u'/ip address')
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')
if connection in self.connections:
self.connections.remove(connection)
@cherrypy.expose
def default(self, *args, **kwargs):
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):
"""
Close down the 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
and the client.
This code is called by the HttpServer upon a request and it processes it based on the routing table.
"""
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 = [
(u'^/$', self.serve_file),
(u'^/(stage)$', self.serve_file),
(r'^/files/(.*)$', self.serve_file),
(r'^/api/poll$', self.poll),
(r'^/stage/api/poll$', self.poll),
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/stage/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service),
(r'^/stage/api/service/(.*)$', self.service),
(r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
(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/(.*)/live$', self.go_live),
(r'^/api/(.*)/add$', self.add_to_service)
]
self.socket.readyRead.connect(self.ready_read)
self.socket.disconnected.connect(self.disconnected)
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):
"""
Read the service item in use and return the data as a json object
"""
service_items = []
if self.live_controller.service_item:
current_unique_identifier = self.live_controller.service_item.unique_identifier
@ -281,40 +357,6 @@ class HttpConnection(object):
'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):
"""
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'
elif filename == u'stage':
filename = u'stage.html'
path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
if not path.startswith(self.parent.html_dir):
return HttpResponse(code=u'404 Not Found')
path = os.path.normpath(os.path.join(self.html_dir, filename))
if not path.startswith(self.html_dir):
return self._http_not_found()
ext = os.path.splitext(filename)[1]
html = None
if ext == u'.html':
@ -360,11 +402,12 @@ class HttpConnection(object):
content = file_handle.read()
except IOError:
log.exception(u'Failed to open %s' % path)
return HttpResponse(code=u'404 Not Found')
return self._http_not_found()
finally:
if file_handle:
file_handle.close()
return HttpResponse(content, {u'Content-Type': mimetype})
cherrypy.response.headers['Content-Type'] = mimetype
return content
def poll(self):
"""
@ -379,18 +422,20 @@ class HttpConnection(object):
u'theme': self.live_controller.theme_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):
"""
Hide or show the display screen.
This is a cross Thread call and UI is updated so Events need to be used.
``action``
This is the action, either ``hide`` or ``show``.
"""
Registry().execute(u'slidecontroller_toggle_display', action)
return HttpResponse(json.dumps({u'results': {u'success': True}}),
{u'Content-Type': u'application/json'})
self.live_controller.emit(QtCore.SIGNAL(u'slidecontroller_toggle_display'), action)
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'success': True}})
def alert(self):
"""
@ -399,16 +444,16 @@ class HttpConnection(object):
plugin = self.plugin_manager.get_plugin_by_name("alerts")
if plugin.status == PluginStatus.Active:
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:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
text = urllib.unquote(text)
Registry().execute(u'alerts_text', [text])
self.alerts_manager.emit(QtCore.SIGNAL(u'alerts_text'), [text])
success = True
else:
success = False
return HttpResponse(json.dumps({u'results': {u'success': success}}),
{u'Content-Type': u'application/json'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'success': success}})
def controller(self, display_type, action):
"""
@ -444,44 +489,44 @@ class HttpConnection(object):
if current_item:
json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier
else:
if self.url_params and self.url_params.get(u'data'):
if self.request_data:
try:
data = json.loads(self.url_params[u'data'][0])
data = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
log.info(data)
# This slot expects an int within a list.
id = data[u'request'][u'id']
Registry().execute(event, [id])
self.live_controller.emit(QtCore.SIGNAL(event), [data])
else:
Registry().execute(event)
self.live_controller.emit(QtCore.SIGNAL(event))
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):
"""
Handles requests for service items
Handles requests for service items in the service manager
``action``
The action to perform.
"""
event = u'servicemanager_%s' % action
if action == u'list':
return HttpResponse(json.dumps({u'results': {u'items': self._get_service_items()}}),
{u'Content-Type': u'application/json'})
else:
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': self._get_service_items()}})
event += u'_item'
if self.url_params and self.url_params.get(u'data'):
if self.request_data:
try:
data = json.loads(self.url_params[u'data'][0])
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
Registry().execute(event, data[u'request'][u'id'])
data = json.loads(self.request_data)[u'request'][u'id']
except KeyError:
return self._http_bad_request()
self.service_manager.emit(QtCore.SIGNAL(event), data)
else:
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.
@ -493,8 +538,9 @@ class HttpConnection(object):
searches = []
for plugin in self.plugin_manager.plugins:
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'])])
return HttpResponse(json.dumps({u'results': {u'items': searches}}), {u'Content-Type': u'application/json'})
searches.append([plugin.name, unicode(plugin.text_strings[StringContent.Name][u'plural'])])
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': searches}})
def search(self, plugin_name):
"""
@ -504,69 +550,63 @@ class HttpConnection(object):
The plugin name to search in.
"""
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:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
text = urllib.unquote(text)
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:
results = plugin.media_item.search(text, False)
else:
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):
"""
Go live on an item of type ``plugin``.
"""
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:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
plugin.media_item.go_live(id, remote=True)
return HttpResponse(code=u'200 OK')
plugin.media_item.emit(QtCore.SIGNAL(u'%s_go_live' % plugin_name), [id, True])
return self._http_success()
def add_to_service(self, plugin_name):
"""
Add item of type ``plugin_name`` to the end of the service.
"""
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:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
item_id = plugin.media_item.createItemFromId(id)
plugin.media_item.add_to_service(item_id, remote=True)
return HttpResponse(code=u'200 OK')
item_id = plugin.media_item.create_item_from_id(id)
plugin.media_item.emit(QtCore.SIGNAL(u'%s_add_to_service' % plugin_name), [item_id, True])
self._http_success()
def send_response(self, response):
http = u'HTTP/1.1 %s\r\n' % response.code
for header, value in response.headers.iteritems():
http += '%s: %s\r\n' % (header, value)
http += '\r\n'
self.socket.write(http)
self.socket.write(response.content)
def _http_success(self):
"""
Set the HTTP success return code.
"""
cherrypy.response.status = 200
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')
self.close()
cherrypy.response.status = 400
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:
return
log.debug(u'close socket')
self.socket.close()
self.socket = None
self.parent.close_connection(self)
cherrypy.response.status = 404
cherrypy.response.body = ["<html><body>Sorry, an error occurred </body></html>"]
def _get_service_manager(self):
"""
@ -597,3 +637,13 @@ class HttpConnection(object):
return self._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 #
###############################################################################
import os.path
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'
@ -53,32 +56,84 @@ class RemoteTab(SettingsTab):
self.address_label.setObjectName(u'address_label')
self.address_edit = QtGui.QLineEdit(self.server_settings_group_box)
self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(
u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self))
self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'),
self))
self.address_edit.setObjectName(u'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.setObjectName(u'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_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.setObjectName(u'port_spin_box')
self.server_settings_layout.addRow(self.port_label, self.port_spin_box)
self.remote_url_label = QtGui.QLabel(self.server_settings_group_box)
self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
self.remote_url_label = QtGui.QLabel(self.http_settings_group_box)
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.setOpenExternalLinks(True)
self.server_settings_layout.addRow(self.remote_url_label, self.remote_url)
self.stage_url_label = QtGui.QLabel(self.server_settings_group_box)
self.http_setting_layout.addRow(self.remote_url_label, self.remote_url)
self.stage_url_label = QtGui.QLabel(self.http_settings_group_box)
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.setOpenExternalLinks(True)
self.server_settings_layout.addRow(self.stage_url_label, self.stage_url)
self.left_layout.addWidget(self.server_settings_group_box)
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
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.setObjectName(u'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.left_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.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):
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/'
'apps/details?id=org.openlp.android">download</a> to install the '
'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):
"""
Update the display based on the data input on the screen
"""
ip_address = u'localhost'
if self.address_edit.text() == ZERO_URL:
interfaces = QtNetwork.QNetworkInterface.allInterfaces()
@ -129,31 +199,73 @@ class RemoteTab(SettingsTab):
break
else:
ip_address = self.address_edit.text()
url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
self.remote_url.setText(u'<a href="%s">%s</a>' % (url, url))
url += u'stage'
self.stage_url.setText(u'<a href="%s">%s</a>' % (url, url))
http_url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_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):
"""
Load the configuration and update the server configuration if necessary
"""
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.twelve_hour = Settings().value(self.settings_section + u'/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.https_changed()
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 \
Settings().value(self.settings_section + u'/port') != self.port_spin_box.value():
changed = True
Settings().value(self.settings_section + u'/port') != self.port_spin_box.value() or \
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'/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'/twelve hour', self.twelve_hour)
if changed:
Registry().execute(u'remotes_config_updated')
Settings().setValue(self.settings_section + u'/authentication enabled', self.user_login_group_box.isChecked())
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
# we have a set value convert to True/False
if check_state == QtCore.Qt.Checked:
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
from PyQt4 import QtGui
from openlp.core.lib import Plugin, StringContent, translate, build_icon
from openlp.plugins.remotes.lib import RemoteTab, HttpServer
@ -37,6 +39,11 @@ log = logging.getLogger(__name__)
__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'
}
@ -60,7 +67,8 @@ class RemotesPlugin(Plugin):
"""
log.debug(u'initialise')
Plugin.initialise(self)
self.server = HttpServer(self)
self.server = HttpServer()
self.server.start_server()
def finalise(self):
"""
@ -70,6 +78,7 @@ class RemotesPlugin(Plugin):
Plugin.finalise(self)
if self.server:
self.server.close()
self.server = None
def about(self):
"""
@ -99,5 +108,6 @@ class RemotesPlugin(Plugin):
"""
Called when Config is changed to restart the server on new address or port
"""
self.finalise()
self.initialise()
log.debug(u'remote config changed')
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',
'BeautifulSoup',
'mako',
'cherrypy',
'migrate',
'uno',
'icu',

View File

@ -74,7 +74,7 @@ class TestPluginManager(TestCase):
# WHEN: We run 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, \
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()
plugin_manager.hook_settings_tabs()
# 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, \
# 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, \
u'The create_media_manager_item() method should not have been called.'
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')
@ -117,7 +117,7 @@ class TestPluginManager(TestCase):
plugin_manager.hook_settings_tabs()
# 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.'
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')
@ -135,8 +135,8 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_settings_tabs()
plugin_manager.hook_settings_tabs()
# THEN: The create_settings_Tab() method should have been called
mocked_plugin.create_settings_Tab.assert_called_with(self.mocked_settings_form)
# THEN: The create_settings_tab() method should have been called
mocked_plugin.create_settings_tab.assert_called_with(self.mocked_settings_form)
def hook_import_menu_with_disabled_plugin_test(self):
"""

View File

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

View File

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

View File

@ -30,8 +30,10 @@ class TestAppLocation(TestCase):
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: we call AppLocation.get_data_path()
data_path = AppLocation.get_data_path()
# 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_get_directory.assert_called_with(AppLocation.DataDir)
@ -49,8 +51,10 @@ class TestAppLocation(TestCase):
mocked_settings.contains.return_value = True
mocked_settings.value.return_value.toString.return_value = u'custom/dir'
mocked_os.path.normpath.return_value = u'custom/dir'
# WHEN: we call 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
mocked_settings.contains.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()
mocked_get_data_path.return_value = u'test/dir'
mocked_check_directory_exists.return_value = True
# WHEN: we call AppLocation.get_data_path()
data_path = AppLocation.get_section_data_path(u'section')
# 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')
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:
mocked_get_frozen_path.return_value = u'app/dir'
# WHEN: We call AppLocation.get_directory
directory = AppLocation.get_directory(AppLocation.AppDir)
# THEN:
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_sys.frozen = 1
mocked_sys.argv = ['openlp']
# WHEN: We call AppLocation.get_directory
directory = AppLocation.get_directory(AppLocation.PluginsDir)
# THEN:
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