diff --git a/nose2.cfg b/nose2.cfg index 1f2e01126..ae73407d7 100644 --- a/nose2.cfg +++ b/nose2.cfg @@ -1,5 +1,5 @@ [unittest] -verbose = true +verbose = True plugins = nose2.plugins.mp [log-capture] @@ -9,14 +9,19 @@ filter = -nose log-level = ERROR [test-result] -always-on = true -descriptions = true +always-on = True +descriptions = True [coverage] -always-on = true +always-on = True coverage = openlp coverage-report = html [multiprocess] -always-on = false +always-on = False processes = 4 + +[output-buffer] +always-on = True +stderr = True +stdout = False diff --git a/openlp.py b/openlp.py index 02287cd5e..9bccc526f 100755 --- a/openlp.py +++ b/openlp.py @@ -46,6 +46,7 @@ if __name__ == '__main__': """ Instantiate and run the application. """ + faulthandler.enable() set_up_fault_handling() # Add support for using multiprocessing from frozen Windows executable (built using PyInstaller), # see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support diff --git a/openlp/core/api/deploy.py b/openlp/core/api/deploy.py index 0419b45db..a42f83f0c 100644 --- a/openlp/core/api/deploy.py +++ b/openlp/core/api/deploy.py @@ -22,7 +22,6 @@ """ Download and "install" the remote web client """ -import os from zipfile import ZipFile from openlp.core.common.applocation import AppLocation @@ -30,18 +29,18 @@ from openlp.core.common.registry import Registry from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size -def deploy_zipfile(app_root, zip_name): +def deploy_zipfile(app_root_path, zip_name): """ Process the downloaded zip file and add to the correct directory - :param zip_name: the zip file to be processed - :param app_root: the directory where the zip get expanded to + :param str zip_name: the zip file name to be processed + :param openlp.core.common.path.Path app_root_path: The directory to expand the zip to :return: None """ - zip_file = os.path.join(app_root, zip_name) - web_zip = ZipFile(zip_file) - web_zip.extractall(app_root) + zip_path = app_root_path / zip_name + web_zip = ZipFile(str(zip_path)) + web_zip.extractall(str(app_root_path)) def download_sha256(): @@ -53,6 +52,8 @@ def download_sha256(): web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent}) except ConnectionError: return False + if not web_config: + return None file_bits = web_config.split() return file_bits[0], file_bits[2] @@ -67,4 +68,4 @@ def download_and_check(callback=None): if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip', AppLocation.get_section_data_path('remotes') / 'site.zip', sha256=sha256): - deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') + deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip') diff --git a/openlp/core/api/endpoint/controller.py b/openlp/core/api/endpoint/controller.py index 8ecfdb732..13e8d1681 100644 --- a/openlp/core/api/endpoint/controller.py +++ b/openlp/core/api/endpoint/controller.py @@ -28,6 +28,7 @@ import json from openlp.core.api.http.endpoint import Endpoint from openlp.core.api.http import requires_auth from openlp.core.common.applocation import AppLocation +from openlp.core.common.path import Path from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities, create_thumb @@ -66,12 +67,12 @@ def controller_text(request): elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'): item['tag'] = str(index + 1) thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) - full_thumbnail_path = str(AppLocation.get_data_path() / thumbnail_path) + full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path # Create thumbnail if it doesn't exists - if not os.path.exists(full_thumbnail_path): - create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False) - Registry().get('image_manager').add_image(full_thumbnail_path, frame['title'], None, 88, 88) - item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path) + if not full_thumbnail_path.exists(): + create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False) + Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88) + item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path)) item['text'] = str(frame['title']) item['html'] = str(frame['title']) else: diff --git a/openlp/core/api/endpoint/core.py b/openlp/core/api/endpoint/core.py index 2988e03aa..8b9bcc4c0 100644 --- a/openlp/core/api/endpoint/core.py +++ b/openlp/core/api/endpoint/core.py @@ -172,15 +172,3 @@ def main_image(request): 'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image)) } return {'results': result} - - -def get_content_type(file_name): - """ - Examines the extension of the file and determines what the content_type should be, defaults to text/plain - Returns the extension and the content_type - - :param file_name: name of file - """ - ext = os.path.splitext(file_name)[1] - content_type = FILE_TYPES.get(ext, 'text/plain') - return ext, content_type diff --git a/openlp/core/api/endpoint/pluginhelpers.py b/openlp/core/api/endpoint/pluginhelpers.py index 9377bde6a..b8f606fbc 100644 --- a/openlp/core/api/endpoint/pluginhelpers.py +++ b/openlp/core/api/endpoint/pluginhelpers.py @@ -19,7 +19,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import os import json import re import urllib @@ -103,7 +102,7 @@ def display_thumbnails(request, controller_name, log, dimensions, file_name, sli :param controller_name: which controller is requesting the image :param log: the logger object :param dimensions: the image size eg 88x88 - :param file_name: the file name of the image + :param str file_name: the file name of the image :param slide: the individual image name :return: """ @@ -124,12 +123,10 @@ def display_thumbnails(request, controller_name, log, dimensions, file_name, sli if controller_name and file_name: file_name = urllib.parse.unquote(file_name) if '..' not in file_name: # no hacking please + full_path = AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name if slide: - full_path = str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name / slide) - else: - full_path = str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name) - if os.path.exists(full_path): - path, just_file_name = os.path.split(full_path) - Registry().get('image_manager').add_image(full_path, just_file_name, None, width, height) - image = Registry().get('image_manager').get_image(full_path, just_file_name, width, height) + full_path = full_path / slide + if full_path.exists(): + Registry().get('image_manager').add_image(full_path, full_path.name, None, width, height) + image = Registry().get('image_manager').get_image(full_path, full_path.name, width, height) return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8') diff --git a/openlp/core/api/endpoint/remote.py b/openlp/core/api/endpoint/remote.py index 4741ada15..3a408f74e 100644 --- a/openlp/core/api/endpoint/remote.py +++ b/openlp/core/api/endpoint/remote.py @@ -27,7 +27,7 @@ from openlp.core.api.endpoint.core import TRANSLATED_STRINGS log = logging.getLogger(__name__) -remote_endpoint = Endpoint('remote', template_dir='remotes', static_dir='remotes') +remote_endpoint = Endpoint('remote', template_dir='remotes') @remote_endpoint.route('{view}') diff --git a/openlp/core/api/http/endpoint.py b/openlp/core/api/http/endpoint.py index fe2b11d9a..011b7e73a 100644 --- a/openlp/core/api/http/endpoint.py +++ b/openlp/core/api/http/endpoint.py @@ -22,8 +22,6 @@ """ The Endpoint class, which provides plugins with a way to serve their own portion of the API """ -import os - from mako.template import Template from openlp.core.common.applocation import AppLocation @@ -67,13 +65,17 @@ class Endpoint(object): def render_template(self, filename, **kwargs): """ Render a mako template + + :param str filename: The file name of the template to render. + :return: The rendered template object. + :rtype: mako.template.Template """ - root = str(AppLocation.get_section_data_path('remotes')) + root_path = AppLocation.get_section_data_path('remotes') if not self.template_dir: raise Exception('No template directory specified') - path = os.path.join(root, self.template_dir, filename) + path = root_path / self.template_dir / filename if self.static_dir: kwargs['static_url'] = '/{prefix}/static'.format(prefix=self.url_prefix) kwargs['static_url'] = kwargs['static_url'].replace('//', '/') kwargs['assets_url'] = '/assets' - return Template(filename=path, input_encoding='utf-8').render(**kwargs) + return Template(filename=str(path), input_encoding='utf-8').render(**kwargs) diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index fad135f2b..c80275801 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -39,9 +39,9 @@ from openlp.core.api.http import application from openlp.core.api.poll import Poller from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import create_paths -from openlp.core.common.registry import RegistryProperties, Registry +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.common.i18n import translate @@ -67,13 +67,16 @@ class HttpWorker(QtCore.QObject): address = Settings().value('api/ip address') port = Settings().value('api/port') Registry().execute('get_website_version') - serve(application, host=address, port=port) + try: + serve(application, host=address, port=port) + except OSError: + log.exception('An error occurred when serving the application.') def stop(self): pass -class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): +class HttpServer(RegistryBase, RegistryProperties, LogMixin): """ Wrapper round a server instance """ @@ -82,13 +85,14 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): Initialise the http server, and start the http server """ super(HttpServer, self).__init__(parent) - self.worker = HttpWorker() - self.thread = QtCore.QThread() - self.worker.moveToThread(self.thread) - self.thread.started.connect(self.worker.run) - self.thread.start() - Registry().register_function('download_website', self.first_time) - Registry().register_function('get_website_version', self.website_version) + if Registry().get_flag('no_web_server'): + self.worker = HttpWorker() + self.thread = QtCore.QThread() + self.worker.moveToThread(self.thread) + self.thread.started.connect(self.worker.run) + self.thread.start() + Registry().register_function('download_website', self.first_time) + Registry().register_function('get_website_version', self.website_version) Registry().set_flag('website_version', '0.0') def bootstrap_post_set_up(self): diff --git a/openlp/core/api/http/wsgiapp.py b/openlp/core/api/http/wsgiapp.py index f948d4096..4c863b7cb 100644 --- a/openlp/core/api/http/wsgiapp.py +++ b/openlp/core/api/http/wsgiapp.py @@ -25,7 +25,6 @@ App stuff """ import json import logging -import os import re from webob import Request, Response @@ -138,12 +137,11 @@ class WSGIApplication(object): Add a static directory as a route """ if route not in self.static_routes: - root = str(AppLocation.get_section_data_path('remotes')) - static_path = os.path.abspath(os.path.join(root, static_dir)) - if not os.path.exists(static_path): + static_path = AppLocation.get_section_data_path('remotes') / static_dir + if not static_path.exists(): log.error('Static path "%s" does not exist. Skipping creating static route/', static_path) return - self.static_routes[route] = DirectoryApp(static_path) + self.static_routes[route] = DirectoryApp(str(static_path.resolve())) def dispatch(self, request): """ diff --git a/openlp/core/api/poll.py b/openlp/core/api/poll.py index 5b3fb33c4..d2d36f60a 100644 --- a/openlp/core/api/poll.py +++ b/openlp/core/api/poll.py @@ -23,7 +23,7 @@ import json from openlp.core.common.httputils import get_web_page -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings diff --git a/openlp/core/api/websockets.py b/openlp/core/api/websockets.py index d64fdf3cc..3417eb74d 100644 --- a/openlp/core/api/websockets.py +++ b/openlp/core/api/websockets.py @@ -31,8 +31,8 @@ import time from PyQt5 import QtCore -from openlp.core.common.mixins import OpenLPMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings log = logging.getLogger(__name__) @@ -61,7 +61,7 @@ class WebSocketWorker(QtCore.QObject): self.ws_server.stop = True -class WebSocketServer(RegistryProperties, OpenLPMixin): +class WebSocketServer(RegistryProperties, LogMixin): """ Wrapper round a server instance """ @@ -70,12 +70,13 @@ class WebSocketServer(RegistryProperties, OpenLPMixin): Initialise and start the WebSockets server """ super(WebSocketServer, self).__init__() - self.settings_section = 'api' - self.worker = WebSocketWorker(self) - self.thread = QtCore.QThread() - self.worker.moveToThread(self.thread) - self.thread.started.connect(self.worker.run) - self.thread.start() + if Registry().get_flag('no_web_server'): + self.settings_section = 'api' + self.worker = WebSocketWorker(self) + self.thread = QtCore.QThread() + self.worker.moveToThread(self.thread) + self.thread.started.connect(self.worker.run) + self.thread.start() def start_server(self): """ diff --git a/openlp/core/app.py b/openlp/core/app.py index 7c3938cfe..114a62807 100644 --- a/openlp/core/app.py +++ b/openlp/core/app.py @@ -38,7 +38,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import is_macosx, is_win from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import LanguageManager, UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin +from openlp.core.common.mixins import LogMixin from openlp.core.common.path import create_paths, copytree from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings @@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main'] log = logging.getLogger() -class OpenLP(OpenLPMixin, QtWidgets.QApplication): +class OpenLP(QtWidgets.QApplication, LogMixin): """ The core application class. This class inherits from Qt's QApplication class in order to provide the core of the application. @@ -403,8 +403,8 @@ def main(args=None): .format(back_up_path=back_up_path)) QtWidgets.QMessageBox.information( None, translate('OpenLP', 'Settings Upgrade'), - translate('OpenLP', 'Your settings are about to upgraded. A backup will be created at {back_up_path}') - .format(back_up_path=back_up_path)) + translate('OpenLP', 'Your settings are about to be upgraded. A backup will be created at ' + '{back_up_path}').format(back_up_path=back_up_path)) settings.export(back_up_path) settings.upgrade_settings() # First time checks in settings diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index f8017fdbd..99e222041 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -43,9 +43,13 @@ log = logging.getLogger(__name__ + '.__init__') FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)') SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])') -CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE) -INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE) +CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]') +INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]') IMAGES_FILTER = None +REPLACMENT_CHARS_MAP = str.maketrans({'\u2018': '\'', '\u2019': '\'', '\u201c': '"', '\u201d': '"', '\u2026': '...', + '\u2013': '-', '\u2014': '-', '\v': '\n\n', '\f': '\n\n'}) +NEW_LINE_REGEX = re.compile(r' ?(\r\n?|\n) ?') +WHITESPACE_REGEX = re.compile(r'[ \t]+') def trace_error_handler(logger): @@ -314,17 +318,6 @@ def get_filesystem_encoding(): return encoding -def split_filename(path): - """ - Return a list of the parts in a given path. - """ - path = os.path.abspath(path) - if not os.path.isfile(path): - return path, '' - else: - return os.path.split(path) - - def delete_file(file_path): """ Deletes a file from the system. @@ -339,7 +332,7 @@ def delete_file(file_path): if file_path.exists(): file_path.unlink() return True - except (IOError, OSError): + except OSError: log.exception('Unable to delete file {file_path}'.format(file_path=file_path)) return False @@ -436,3 +429,17 @@ def get_file_encoding(file_path): return detector.result except OSError: log.exception('Error detecting file encoding') + + +def normalize_str(irreg_str): + """ + Normalize the supplied string. Remove unicode control chars and tidy up white space. + + :param str irreg_str: The string to normalize. + :return: The normalized string + :rtype: str + """ + irreg_str = irreg_str.translate(REPLACMENT_CHARS_MAP) + irreg_str = CONTROL_CHARS.sub('', irreg_str) + irreg_str = NEW_LINE_REGEX.sub('\n', irreg_str) + return WHITESPACE_REGEX.sub(' ', irreg_str) diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index cf70edfc1..279c49002 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -83,7 +83,7 @@ class AppLocation(object): """ # Check if we have a different data location. if Settings().contains('advanced/data path'): - path = Settings().value('advanced/data path') + path = Path(Settings().value('advanced/data path')) else: path = AppLocation.get_directory(AppLocation.DataDir) create_paths(path) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 11ae7b563..21b778b80 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -97,8 +97,8 @@ def get_web_page(url, headers=None, update_openlp=False, proxies=None): response = requests.get(url, headers=headers, proxies=proxies, timeout=float(CONNECTION_TIMEOUT)) log.debug('Downloaded page {url}'.format(url=response.url)) break - except IOError: - # For now, catch IOError. All requests errors inherit from IOError + except OSError: + # For now, catch OSError. All requests errors inherit from OSError log.exception('Unable to connect to {url}'.format(url=url)) response = None if retries >= CONNECTION_RETRIES: @@ -127,7 +127,7 @@ def get_url_file_size(url): try: response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True) return int(response.headers['Content-Length']) - except IOError: + except OSError: if retries > CONNECTION_RETRIES: raise ConnectionError('Unable to download {url}'.format(url=url)) else: @@ -173,7 +173,7 @@ def url_get_file(callback, url, file_path, sha256=None): file_path.unlink() return False break - except IOError: + except OSError: trace_error_handler(log) if retries > CONNECTION_RETRIES: if file_path.exists(): diff --git a/openlp/core/common/i18n.py b/openlp/core/common/i18n.py index 1f4357808..9149f3fe6 100644 --- a/openlp/core/common/i18n.py +++ b/openlp/core/common/i18n.py @@ -53,7 +53,7 @@ def translate(context, text, comment=None, qt_translate=QtCore.QCoreApplication. Language = namedtuple('Language', ['id', 'name', 'code']) ICU_COLLATOR = None -DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE) +DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+') LANGUAGES = sorted([ Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'), Language(2, translate('common.languages', 'Abkhazian', 'Language code: ab'), 'ab'), diff --git a/openlp/core/common/mixins.py b/openlp/core/common/mixins.py index 6a39b8f2c..a07940e10 100644 --- a/openlp/core/common/mixins.py +++ b/openlp/core/common/mixins.py @@ -25,25 +25,29 @@ Provide Error Handling and login Services import logging import inspect -from openlp.core.common import trace_error_handler, de_hump +from openlp.core.common import is_win, trace_error_handler from openlp.core.common.registry import Registry DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed', 'preview_size_changed', 'resizeEvent'] -class OpenLPMixin(object): +class LogMixin(object): """ Base Calling object for OpenLP classes. """ - def __init__(self, *args, **kwargs): - super(OpenLPMixin, self).__init__(*args, **kwargs) - self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) - if self.logger.getEffectiveLevel() == logging.DEBUG: - for name, m in inspect.getmembers(self, inspect.ismethod): - if name not in DO_NOT_TRACE_EVENTS: - if not name.startswith("_") and not name.startswith("log"): - setattr(self, name, self.logging_wrapper(m, self)) + @property + def logger(self): + if hasattr(self, '_logger') and self._logger: + return self._logger + else: + self._logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) + if self._logger.getEffectiveLevel() == logging.DEBUG: + for name, m in inspect.getmembers(self, inspect.ismethod): + if name not in DO_NOT_TRACE_EVENTS: + if not name.startswith("_") and not name.startswith("log"): + setattr(self, name, self.logging_wrapper(m, self)) + return self._logger def logging_wrapper(self, func, parent): """ @@ -93,30 +97,141 @@ class OpenLPMixin(object): self.logger.exception(message) -class RegistryMixin(object): +class RegistryProperties(object): """ This adds registry components to classes to use at run time. """ - def __init__(self, parent): - """ - Register the class and bootstrap hooks. - """ - try: - super(RegistryMixin, self).__init__(parent) - except TypeError: - super(RegistryMixin, self).__init__() - Registry().register(de_hump(self.__class__.__name__), self) - Registry().register_function('bootstrap_initialise', self.bootstrap_initialise) - Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) + _application = None + _plugin_manager = None + _image_manager = None + _media_controller = None + _service_manager = None + _preview_controller = None + _live_controller = None + _main_window = None + _renderer = None + _theme_manager = None + _settings_form = None + _alerts_manager = None + _projector_manager = None - def bootstrap_initialise(self): + @property + def application(self): """ - Dummy method to be overridden + Adds the openlp to the class dynamically. + Windows needs to access the application in a dynamic manner. """ - pass + if is_win(): + return Registry().get('application') + else: + if not hasattr(self, '_application') or not self._application: + self._application = Registry().get('application') + return self._application - def bootstrap_post_set_up(self): + @property + def plugin_manager(self): """ - Dummy method to be overridden + Adds the plugin manager to the class dynamically """ - pass + if not hasattr(self, '_plugin_manager') or not self._plugin_manager: + self._plugin_manager = Registry().get('plugin_manager') + return self._plugin_manager + + @property + def image_manager(self): + """ + Adds the image manager to the class dynamically + """ + if not hasattr(self, '_image_manager') or not self._image_manager: + self._image_manager = Registry().get('image_manager') + return self._image_manager + + @property + def media_controller(self): + """ + Adds the media controller to the class dynamically + """ + if not hasattr(self, '_media_controller') or not self._media_controller: + self._media_controller = Registry().get('media_controller') + return self._media_controller + + @property + def service_manager(self): + """ + Adds the service manager to the class dynamically + """ + if not hasattr(self, '_service_manager') or not self._service_manager: + self._service_manager = Registry().get('service_manager') + return self._service_manager + + @property + def preview_controller(self): + """ + Adds the preview controller to the class dynamically + """ + if not hasattr(self, '_preview_controller') or not self._preview_controller: + self._preview_controller = Registry().get('preview_controller') + return self._preview_controller + + @property + def live_controller(self): + """ + Adds the live controller to the class dynamically + """ + if not hasattr(self, '_live_controller') or not self._live_controller: + self._live_controller = Registry().get('live_controller') + return self._live_controller + + @property + def main_window(self): + """ + Adds the main window to the class dynamically + """ + if not hasattr(self, '_main_window') or not self._main_window: + self._main_window = Registry().get('main_window') + return self._main_window + + @property + def renderer(self): + """ + Adds the Renderer to the class dynamically + """ + if not hasattr(self, '_renderer') or not self._renderer: + self._renderer = Registry().get('renderer') + return self._renderer + + @property + def theme_manager(self): + """ + Adds the theme manager to the class dynamically + """ + if not hasattr(self, '_theme_manager') or not self._theme_manager: + self._theme_manager = Registry().get('theme_manager') + return self._theme_manager + + @property + def settings_form(self): + """ + Adds the settings form to the class dynamically + """ + if not hasattr(self, '_settings_form') or not self._settings_form: + self._settings_form = Registry().get('settings_form') + return self._settings_form + + @property + def alerts_manager(self): + """ + Adds the alerts manager to the class dynamically + """ + if not hasattr(self, '_alerts_manager') or not self._alerts_manager: + self._alerts_manager = Registry().get('alerts_manager') + return self._alerts_manager + + @property + def projector_manager(self): + """ + Adds the projector manager to the class dynamically + """ + if not hasattr(self, '_projector_manager') or not self._projector_manager: + self._projector_manager = Registry().get('projector_manager') + return self._projector_manager diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index 19e17470b..0e4b45c2a 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -69,6 +69,16 @@ class Path(PathVariant): path = path.relative_to(base_path) return {'__Path__': path.parts} + def rmtree(self, ignore_errors=False, onerror=None): + """ + Provide an interface to :func:`shutil.rmtree` + + :param bool ignore_errors: Ignore errors + :param onerror: Handler function to handle any errors + :rtype: None + """ + shutil.rmtree(str(self), ignore_errors, onerror) + def replace_params(args, kwargs, params): """ @@ -153,23 +163,6 @@ def copytree(*args, **kwargs): return str_to_path(shutil.copytree(*args, **kwargs)) -def rmtree(*args, **kwargs): - """ - Wraps :func:shutil.rmtree` so that we can accept Path objects. - - :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object - :return: Passes the return from :func:`shutil.rmtree` back - :rtype: None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.rmtree - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) - - return shutil.rmtree(*args, **kwargs) - - def which(*args, **kwargs): """ Wraps :func:shutil.which` so that it return a Path objects. @@ -233,7 +226,7 @@ def create_paths(*paths, **kwargs): try: if not path.exists(): path.mkdir(parents=True) - except IOError: + except OSError: if not kwargs.get('do_not_log', False): log.exception('failed to check if directory exists or create directory') diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index f01c41b0b..45d41b083 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -25,7 +25,7 @@ Provide Registry Services import logging import sys -from openlp.core.common import is_win, trace_error_handler +from openlp.core.common import de_hump, trace_error_handler log = logging.getLogger(__name__) @@ -61,6 +61,15 @@ class Registry(object): registry.initialising = True return registry + @classmethod + def destroy(cls): + """ + Destroy the Registry. + """ + if cls.__instance__.running_under_test: + del cls.__instance__ + cls.__instance__ = None + def get(self, key): """ Extracts the registry value from the list based on the key passed in @@ -119,7 +128,7 @@ class Registry(object): :param event: The function description.. :param function: The function to be called when the event happens. """ - if event in self.functions_list: + if event in self.functions_list and function in self.functions_list[event]: self.functions_list[event].remove(function) def execute(self, event, *args, **kwargs): @@ -142,8 +151,9 @@ class Registry(object): trace_error_handler(log) log.exception('Exception for function {function}'.format(function=function)) else: - trace_error_handler(log) - log.exception('Event {event} called but not registered'.format(event=event)) + if log.getEffectiveLevel() == logging.DEBUG: + trace_error_handler(log) + log.exception('Event {event} called but not registered'.format(event=event)) return results def get_flag(self, key): @@ -178,128 +188,30 @@ class Registry(object): del self.working_flags[key] -class RegistryProperties(object): +class RegistryBase(object): """ This adds registry components to classes to use at run time. """ + def __init__(self, *args, **kwargs): + """ + Register the class and bootstrap hooks. + """ + try: + super().__init__(*args, **kwargs) + except TypeError: + super().__init__() + Registry().register(de_hump(self.__class__.__name__), self) + Registry().register_function('bootstrap_initialise', self.bootstrap_initialise) + Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) - @property - def application(self): + def bootstrap_initialise(self): """ - Adds the openlp to the class dynamically. - Windows needs to access the application in a dynamic manner. + Dummy method to be overridden """ - if is_win(): - return Registry().get('application') - else: - if not hasattr(self, '_application') or not self._application: - self._application = Registry().get('application') - return self._application + pass - @property - def plugin_manager(self): + def bootstrap_post_set_up(self): """ - Adds the plugin manager to the class dynamically + Dummy method to be overridden """ - if not hasattr(self, '_plugin_manager') or not self._plugin_manager: - self._plugin_manager = Registry().get('plugin_manager') - return self._plugin_manager - - @property - def image_manager(self): - """ - Adds the image manager to the class dynamically - """ - if not hasattr(self, '_image_manager') or not self._image_manager: - self._image_manager = Registry().get('image_manager') - return self._image_manager - - @property - def media_controller(self): - """ - Adds the media controller to the class dynamically - """ - if not hasattr(self, '_media_controller') or not self._media_controller: - self._media_controller = Registry().get('media_controller') - return self._media_controller - - @property - def service_manager(self): - """ - Adds the service manager to the class dynamically - """ - if not hasattr(self, '_service_manager') or not self._service_manager: - self._service_manager = Registry().get('service_manager') - return self._service_manager - - @property - def preview_controller(self): - """ - Adds the preview controller to the class dynamically - """ - if not hasattr(self, '_preview_controller') or not self._preview_controller: - self._preview_controller = Registry().get('preview_controller') - return self._preview_controller - - @property - def live_controller(self): - """ - Adds the live controller to the class dynamically - """ - if not hasattr(self, '_live_controller') or not self._live_controller: - self._live_controller = Registry().get('live_controller') - return self._live_controller - - @property - def main_window(self): - """ - Adds the main window to the class dynamically - """ - if not hasattr(self, '_main_window') or not self._main_window: - self._main_window = Registry().get('main_window') - return self._main_window - - @property - def renderer(self): - """ - Adds the Renderer to the class dynamically - """ - if not hasattr(self, '_renderer') or not self._renderer: - self._renderer = Registry().get('renderer') - return self._renderer - - @property - def theme_manager(self): - """ - Adds the theme manager to the class dynamically - """ - if not hasattr(self, '_theme_manager') or not self._theme_manager: - self._theme_manager = Registry().get('theme_manager') - return self._theme_manager - - @property - def settings_form(self): - """ - Adds the settings form to the class dynamically - """ - if not hasattr(self, '_settings_form') or not self._settings_form: - self._settings_form = Registry().get('settings_form') - return self._settings_form - - @property - def alerts_manager(self): - """ - Adds the alerts manager to the class dynamically - """ - if not hasattr(self, '_alerts_manager') or not self._alerts_manager: - self._alerts_manager = Registry().get('alerts_manager') - return self._alerts_manager - - @property - def projector_manager(self): - """ - Adds the projector manager to the class dynamically - """ - if not hasattr(self, '_projector_manager') or not self._projector_manager: - self._projector_manager = Registry().get('projector_manager') - return self._projector_manager + pass diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index a6bc549f1..54f1d9b2a 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -40,7 +40,7 @@ __version__ = 2 # Fix for bug #1014422. X11_BYPASS_DEFAULT = True -if is_linux(): +if is_linux(): # pragma: no cover # Default to False on Gnome. X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID')) # Default to False on Xfce. @@ -206,11 +206,14 @@ class Settings(QtCore.QSettings): 'projector/source dialog type': 0 # Source select dialog box type } __file_path__ = '' + # Settings upgrades prior to 3.0 __setting_upgrade_1__ = [ - # Changed during 2.2.x development. ('songs/search as type', 'advanced/search as type', []), ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system ('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting + ] + # Settings upgrades for 3.0 (aka 2.6) + __setting_upgrade_2__ = [ ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4. ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4. ('remotes/https enabled', '', []), @@ -231,11 +234,9 @@ class Settings(QtCore.QSettings): # Last search type was renamed to last used search type in 2.6 since Bible search value type changed in 2.6. ('songs/last search type', 'songs/last used search type', []), ('bibles/last search type', '', []), - ('custom/last search type', 'custom/last used search type', [])] - - __setting_upgrade_2__ = [ + ('custom/last search type', 'custom/last used search type', []), # The following changes are being made for the conversion to using Path objects made in 2.6 development - ('advanced/data path', 'advanced/data path', [(str_to_path, None)]), + ('advanced/data path', 'advanced/data path', [(lambda p: Path(p) if p is not None else None, None)]), ('crashreport/last directory', 'crashreport/last directory', [(str_to_path, None)]), ('servicemanager/last directory', 'servicemanager/last directory', [(str_to_path, None)]), ('servicemanager/last file', 'servicemanager/last file', [(str_to_path, None)]), @@ -255,7 +256,10 @@ class Settings(QtCore.QSettings): ('core/logo file', 'core/logo file', [(str_to_path, None)]), ('presentations/last directory', 'presentations/last directory', [(str_to_path, None)]), ('images/last directory', 'images/last directory', [(str_to_path, None)]), - ('media/last directory', 'media/last directory', [(str_to_path, None)]) + ('media/last directory', 'media/last directory', [(str_to_path, None)]), + ('songuasge/db password', 'songusage/db password', []), + ('songuasge/db hostname', 'songusage/db hostname', []), + ('songuasge/db database', 'songusage/db database', []) ] @staticmethod @@ -464,32 +468,38 @@ class Settings(QtCore.QSettings): for version in range(current_version, __version__): version += 1 upgrade_list = getattr(self, '__setting_upgrade_{version}__'.format(version=version)) - for old_key, new_key, rules in upgrade_list: + for old_keys, new_key, rules in upgrade_list: # Once removed we don't have to do this again. - Can be removed once fully switched to the versioning # system. - if not self.contains(old_key): + if not isinstance(old_keys, (tuple, list)): + old_keys = [old_keys] + if any([not self.contains(old_key) for old_key in old_keys]): + log.warning('One of {} does not exist, skipping upgrade'.format(old_keys)) continue if new_key: # Get the value of the old_key. - old_value = super(Settings, self).value(old_key) + old_values = [super(Settings, self).value(old_key) for old_key in old_keys] # When we want to convert the value, we have to figure out the default value (because we cannot get # the default value from the central settings dict. if rules: - default_value = rules[0][1] - old_value = self._convert_value(old_value, default_value) + default_values = rules[0][1] + if not isinstance(default_values, (list, tuple)): + default_values = [default_values] + old_values = [self._convert_value(old_value, default_value) + for old_value, default_value in zip(old_values, default_values)] # Iterate over our rules and check what the old_value should be "converted" to. - for new, old in rules: + new_value = None + for new_rule, old_rule in rules: # If the value matches with the condition (rule), then use the provided value. This is used to # convert values. E. g. an old value 1 results in True, and 0 in False. - if callable(new): - old_value = new(old_value) - elif old == old_value: - old_value = new + if callable(new_rule): + new_value = new_rule(*old_values) + elif old_rule in old_values: + new_value = new_rule break - self.setValue(new_key, old_value) - if new_key != old_key: - self.remove(old_key) - self.setValue('settings/version', version) + self.setValue(new_key, new_value) + [self.remove(old_key) for old_key in old_keys if old_key != new_key] + self.setValue('settings/version', version) def value(self, key): """ diff --git a/openlp/core/display/renderer.py b/openlp/core/display/renderer.py index fd040d9c6..836e5edea 100644 --- a/openlp/core/display/renderer.py +++ b/openlp/core/display/renderer.py @@ -25,9 +25,9 @@ import re from string import Template from PyQt5 import QtGui, QtCore, QtWebKitWidgets -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import path_to_str -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ServiceItem, expand_tags, build_chords_css, \ @@ -46,7 +46,7 @@ VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100))) FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456'] -class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): +class Renderer(RegistryBase, LogMixin, RegistryProperties): """ Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but this class will provide display defense code. @@ -198,6 +198,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): :param theme_data: The theme to generated a preview for. :param force_page: Flag to tell message lines per page need to be generated. + :rtype: QtGui.QPixmap """ # save value for use in format_slide self.force_page = force_page @@ -222,8 +223,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): self.display.build_html(service_item) raw_html = service_item.get_rendered_frame(0) self.display.text(raw_html, False) - preview = self.display.preview() - return preview + return self.display.preview() self.force_page = False def format_slide(self, text, item): diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 0f4078420..0c6e64546 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -104,7 +104,7 @@ def get_text_file_string(text_file_path): # no BOM was found file_handle.seek(0) content = file_handle.read() - except (IOError, UnicodeError): + except (OSError, UnicodeError): log.exception('Failed to open text file {text}'.format(text=text_file_path)) return content @@ -179,8 +179,9 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None): height of 88 is used. :return: The final icon. """ - ext = os.path.splitext(thumb_path)[1].lower() - reader = QtGui.QImageReader(image_path) + # TODO: To path object + thumb_path = Path(thumb_path) + reader = QtGui.QImageReader(str(image_path)) if size is None: # No size given; use default height of 88 if reader.size().isEmpty(): @@ -207,10 +208,10 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None): # Invalid; use default height of 88 reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88)) thumb = reader.read() - thumb.save(thumb_path, ext[1:]) + thumb.save(str(thumb_path), thumb_path.suffix[1:].lower()) if not return_icon: return - if os.path.exists(thumb_path): + if thumb_path.exists(): return build_icon(thumb_path) # Fallback for files with animation support. return build_icon(image_path) @@ -620,6 +621,3 @@ from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css from .imagemanager import ImageManager from .mediamanageritem import MediaManagerItem -from .projector.db import ProjectorDB, Projector -from .projector.pjlink import PJLink -from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 698b1d73e..9e3c5aff9 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -350,6 +350,7 @@ class Manager(object): resulting in the plugin_name being used. :param upgrade_mod: The upgrade_schema function for this database """ + super().__init__() self.is_dirty = False self.session = None self.db_url = None diff --git a/tests/interfaces/openlp_core_api/__init__.py b/openlp/core/lib/json/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_api/__init__.py rename to openlp/core/lib/json/__init__.py diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index dda12e10e..55306267c 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -30,14 +30,15 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.path import Path, path_to_str, str_to_path -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext -from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.ui.lib.filedialog import FileDialog -from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD -from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.edits import SearchEdit +from openlp.core.widgets.toolbar import OpenLPToolbar +from openlp.core.widgets.views import ListWidgetWithDnD log = logging.getLogger(__name__) @@ -91,7 +92,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): Run some initial setup. This method is separate from __init__ in order to mock it out in tests. """ self.hide() - self.whitespace = re.compile(r'[\W_]+', re.UNICODE) + self.whitespace = re.compile(r'[\W_]+') visible_title = self.plugin.get_string(StringContent.VisibleName) self.title = str(visible_title['title']) Registry().register(self.plugin.name, self) @@ -317,10 +318,10 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): self, self.on_new_prompt, Settings().value(self.settings_section + '/last directory'), self.on_new_file_masks) - log.info('New files(s) {file_paths}'.format(file_paths=file_paths)) + log.info('New file(s) {file_paths}'.format(file_paths=file_paths)) if file_paths: self.application.set_busy_cursor() - self.validate_and_load([path_to_str(path) for path in file_paths]) + self.validate_and_load(file_paths) self.application.set_normal_cursor() def load_file(self, data): @@ -329,21 +330,24 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): :param data: A dictionary containing the list of files to be loaded and the target """ - new_files = [] + new_file_paths = [] error_shown = False for file_name in data['files']: - file_type = file_name.split('.')[-1] - if file_type.lower() not in self.on_new_file_masks: + file_path = str_to_path(file_name) + if file_path.suffix[1:].lower() not in self.on_new_file_masks: if not error_shown: - critical_error_message_box(translate('OpenLP.MediaManagerItem', 'Invalid File Type'), - translate('OpenLP.MediaManagerItem', - 'Invalid File {name}.\n' - 'Suffix not supported').format(name=file_name)) + critical_error_message_box( + translate('OpenLP.MediaManagerItem', 'Invalid File Type'), + translate('OpenLP.MediaManagerItem', + 'Invalid File {file_path}.\nFile extension not supported').format( + file_path=file_path)) error_shown = True else: - new_files.append(file_name) - if new_files: - self.validate_and_load(new_files, data['target']) + new_file_paths.append(file_path) + if new_file_paths: + if 'target' in data: + self.validate_and_load(new_file_paths, data['target']) + self.validate_and_load(new_file_paths) def dnd_move_internal(self, target): """ @@ -353,12 +357,12 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): """ pass - def validate_and_load(self, files, target_group=None): + def validate_and_load(self, file_paths, target_group=None): """ Process a list for files either from the File Dialog or from Drag and Drop - :param files: The files to be loaded. + :param list[openlp.core.common.path.Path] file_paths: The files to be loaded. :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files """ full_list = [] @@ -366,18 +370,17 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole)) duplicates_found = False files_added = False - for file_path in files: - if file_path in full_list: + for file_path in file_paths: + if path_to_str(file_path) in full_list: duplicates_found = True else: files_added = True - full_list.append(file_path) + full_list.append(path_to_str(file_path)) if full_list and files_added: if target_group is None: self.list_view.clear() self.load_list(full_list, target_group) - last_dir = os.path.split(files[0])[0] - Settings().setValue(self.settings_section + '/last directory', Path(last_dir)) + Settings().setValue(self.settings_section + '/last directory', file_paths[0].parent) Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list()) if duplicates_found: critical_error_message_box(UiStrings().Duplicate, diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index 059b7a314..7e3b2e416 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -26,9 +26,10 @@ import logging from PyQt5 import QtCore -from openlp.core.common.registry import Registry, RegistryProperties -from openlp.core.common.settings import Settings from openlp.core.common.i18n import UiStrings +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings from openlp.core.version import get_version log = logging.getLogger(__name__) @@ -138,10 +139,6 @@ class Plugin(QtCore.QObject, RegistryProperties): self.text_strings = {} self.set_plugin_text_strings() self.name_strings = self.text_strings[StringContent.Name] - if version: - self.version = version - else: - self.version = get_version()['version'] self.settings_section = self.name self.icon = None self.media_item_class = media_item_class @@ -161,6 +158,19 @@ class Plugin(QtCore.QObject, RegistryProperties): Settings.extend_default_settings(default_settings) Registry().register_function('{name}_add_service_item'.format(name=self.name), self.process_add_service_event) Registry().register_function('{name}_config_updated'.format(name=self.name), self.config_update) + self._setup(version) + + def _setup(self, version): + """ + Run some initial setup. This method is separate from __init__ in order to mock it out in tests. + + :param version: Defaults to *None*, which means that the same version number is used as OpenLP's version number. + :rtype: None + """ + if version: + self.version = version + else: + self.version = get_version()['version'] def check_pre_conditions(self): """ diff --git a/openlp/core/lib/pluginmanager.py b/openlp/core/lib/pluginmanager.py index 1bdb020aa..d4a02f8c0 100644 --- a/openlp/core/lib/pluginmanager.py +++ b/openlp/core/lib/pluginmanager.py @@ -26,12 +26,12 @@ import os from openlp.core.common import extension_loader from openlp.core.common.applocation import AppLocation -from openlp.core.common.registry import RegistryProperties -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import RegistryBase from openlp.core.lib import Plugin, PluginStatus -class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties): +class PluginManager(RegistryBase, LogMixin, RegistryProperties): """ This is the Plugin manager, which loads all the plugins, and executes all the hooks, as and when necessary. @@ -43,8 +43,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties): """ super(PluginManager, self).__init__(parent) self.log_info('Plugin manager Initialising') - self.base_path = os.path.abspath(str(AppLocation.get_directory(AppLocation.PluginsDir))) - self.log_debug('Base path {path}'.format(path=self.base_path)) + self.log_debug('Base path {path}'.format(path=AppLocation.get_directory(AppLocation.PluginsDir))) self.plugins = [] self.log_info('Plugin manager Initialised') diff --git a/openlp/core/lib/searchedit.py b/openlp/core/lib/searchedit.py deleted file mode 100644 index db45486f9..000000000 --- a/openlp/core/lib/searchedit.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### - -import logging - -from PyQt5 import QtCore, QtWidgets - -from openlp.core.common.settings import Settings -from openlp.core.lib import build_icon -from openlp.core.lib.ui import create_widget_action - -log = logging.getLogger(__name__) - - -class SearchEdit(QtWidgets.QLineEdit): - """ - This is a specialised QLineEdit with a "clear" button inside for searches. - """ - searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant) - cleared = QtCore.pyqtSignal() - - def __init__(self, parent, settings_section): - """ - Constructor. - """ - super().__init__(parent) - self.settings_section = settings_section - self._current_search_type = -1 - self.clear_button = QtWidgets.QToolButton(self) - self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png')) - self.clear_button.setCursor(QtCore.Qt.ArrowCursor) - self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }') - self.clear_button.resize(18, 18) - self.clear_button.hide() - self.clear_button.clicked.connect(self._on_clear_button_clicked) - self.textChanged.connect(self._on_search_edit_text_changed) - self._update_style_sheet() - self.setAcceptDrops(False) - - def _update_style_sheet(self): - """ - Internal method to update the stylesheet depending on which widgets are available and visible. - """ - frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) - right_padding = self.clear_button.width() + frame_width - if hasattr(self, 'menu_button'): - left_padding = self.menu_button.width() - stylesheet = 'QLineEdit {{ padding-left:{left}px; padding-right: {right}px; }} '.format(left=left_padding, - right=right_padding) - else: - stylesheet = 'QLineEdit {{ padding-right: {right}px; }} '.format(right=right_padding) - self.setStyleSheet(stylesheet) - msz = self.minimumSizeHint() - self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2), - max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2)) - - def resizeEvent(self, event): - """ - Reimplemented method to react to resizing of the widget. - - :param event: The event that happened. - """ - size = self.clear_button.size() - frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) - self.clear_button.move(self.rect().right() - frame_width - size.width(), - (self.rect().bottom() + 1 - size.height()) // 2) - if hasattr(self, 'menu_button'): - size = self.menu_button.size() - self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2) - - def current_search_type(self): - """ - Readonly property to return the current search type. - """ - return self._current_search_type - - def set_current_search_type(self, identifier): - """ - Set a new current search type. - - :param identifier: The search type identifier (int). - """ - menu = self.menu_button.menu() - for action in menu.actions(): - if identifier == action.data(): - self.setPlaceholderText(action.placeholder_text) - self.menu_button.setDefaultAction(action) - self._current_search_type = identifier - Settings().setValue('{section}/last used search type'.format(section=self.settings_section), identifier) - self.searchTypeChanged.emit(identifier) - return True - - def set_search_types(self, items): - """ - A list of tuples to be used in the search type menu. The first item in the list will be preselected as the - default. - - :param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon - instance or string) and a title for the item in the menu. In short, they should look like this:: - - (, , , <place holder text>) - - For instance:: - - (1, <QIcon instance>, "Titles", "Search Song Titles...") - - Or:: - - (2, ":/songs/authors.png", "Authors", "Search Authors...") - """ - menu = QtWidgets.QMenu(self) - for identifier, icon, title, placeholder in items: - action = create_widget_action( - menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered) - action.placeholder_text = placeholder - if not hasattr(self, 'menu_button'): - self.menu_button = QtWidgets.QToolButton(self) - self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png')) - self.menu_button.setCursor(QtCore.Qt.ArrowCursor) - self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup) - self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }') - self.menu_button.resize(QtCore.QSize(28, 18)) - self.menu_button.setMenu(menu) - self.set_current_search_type( - Settings().value('{section}/last used search type'.format(section=self.settings_section))) - self.menu_button.show() - self._update_style_sheet() - - def _on_search_edit_text_changed(self, text): - """ - Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide - the clear button. - - :param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit. - """ - self.clear_button.setVisible(bool(text)) - - def _on_clear_button_clicked(self): - """ - Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has - cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the - line edit. - """ - self.clear() - self.cleared.emit() - - def _on_menu_action_triggered(self): - """ - Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the - correct action on the button, and set the current search type (using the list of identifiers provided by the - developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier. - """ - for action in self.menu_button.menu().actions(): - # Why is this needed? - action.setChecked(False) - self.set_current_search_type(self.sender().data()) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 22b1f9b5f..3a824d424 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -35,7 +35,7 @@ from PyQt5 import QtGui from openlp.core.common import md5_hash from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords diff --git a/openlp/core/lib/settingstab.py b/openlp/core/lib/settingstab.py index 4b259465f..06009ee3d 100644 --- a/openlp/core/lib/settingstab.py +++ b/openlp/core/lib/settingstab.py @@ -25,7 +25,7 @@ own tab to the settings dialog. """ from PyQt5 import QtWidgets -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties class SettingsTab(QtWidgets.QWidget, RegistryProperties): diff --git a/openlp/core/lib/projector/__init__.py b/openlp/core/projectors/__init__.py similarity index 94% rename from openlp/core/lib/projector/__init__.py rename to openlp/core/projectors/__init__.py index dc7d2c89d..66cfd6080 100644 --- a/openlp/core/lib/projector/__init__.py +++ b/openlp/core/projectors/__init__.py @@ -20,9 +20,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ - :mod:`openlp.core.ui.projector` + :mod:`openlp.core.projectors` - Initialization for the openlp.core.ui.projector modules. + Initialization for the openlp.core.projectors modules. """ diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/projectors/constants.py similarity index 97% rename from openlp/core/lib/projector/constants.py rename to openlp/core/projectors/constants.py index 715896133..a9410d109 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/projectors/constants.py @@ -144,6 +144,24 @@ PJLINK_VALID_CMD = { } } +# QAbstractSocketState enums converted to string +S_QSOCKET_STATE = { + 0: 'QSocketState - UnconnectedState', + 1: 'QSocketState - HostLookupState', + 2: 'QSocketState - ConnectingState', + 3: 'QSocketState - ConnectedState', + 4: 'QSocketState - BoundState', + 5: 'QSocketState - ListeningState (internal use only)', + 6: 'QSocketState - ClosingState', + 'UnconnectedState': 0, + 'HostLookupState': 1, + 'ConnectingState': 2, + 'ConnectedState': 3, + 'BoundState': 4, + 'ListeningState': 5, + 'ClosingState': 6 +} + # Error and status codes S_OK = E_OK = 0 # E_OK included since I sometimes forget # Error codes. Start at 200 so we don't duplicate system error codes. diff --git a/openlp/core/lib/projector/db.py b/openlp/core/projectors/db.py similarity index 98% rename from openlp/core/lib/projector/db.py rename to openlp/core/projectors/db.py index 223159a51..fe8785861 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/projectors/db.py @@ -43,13 +43,13 @@ from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.orm import relationship from openlp.core.lib.db import Manager, init_db, init_url -from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES -from openlp.core.lib.projector import upgrade +from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES +from openlp.core.projectors import upgrade Base = declarative_base(MetaData()) -class CommonBase(object): +class CommonMixin(object): """ Base class to automate table name and ID column. """ @@ -60,7 +60,7 @@ class CommonBase(object): id = Column(Integer, primary_key=True) -class Manufacturer(CommonBase, Base): +class Manufacturer(Base, CommonMixin): """ Projector manufacturer table. @@ -85,7 +85,7 @@ class Manufacturer(CommonBase, Base): lazy='joined') -class Model(CommonBase, Base): +class Model(Base, CommonMixin): """ Projector model table. @@ -113,7 +113,7 @@ class Model(CommonBase, Base): lazy='joined') -class Source(CommonBase, Base): +class Source(Base, CommonMixin): """ Projector video source table. @@ -140,7 +140,7 @@ class Source(CommonBase, Base): text = Column(String(30)) -class Projector(CommonBase, Base): +class Projector(Base, CommonMixin): """ Projector table. @@ -213,7 +213,7 @@ class Projector(CommonBase, Base): lazy='joined') -class ProjectorSource(CommonBase, Base): +class ProjectorSource(Base, CommonMixin): """ Projector local source table This table allows mapping specific projector source input to a local @@ -415,7 +415,7 @@ class ProjectorDB(Manager): for key in projector.source_available: item = self.get_object_filtered(ProjectorSource, and_(ProjectorSource.code == key, - ProjectorSource.projector_id == projector.dbid)) + ProjectorSource.projector_id == projector.id)) if item is None: source_dict[key] = PJLINK_DEFAULT_CODES[key] else: diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/projectors/editform.py similarity index 99% rename from openlp/core/ui/projector/editform.py rename to openlp/core/projectors/editform.py index bd3267665..4ae1f96d9 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/projectors/editform.py @@ -30,8 +30,8 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import verify_ip_address from openlp.core.common.i18n import translate from openlp.core.lib import build_icon -from openlp.core.lib.projector.db import Projector -from openlp.core.lib.projector.constants import PJLINK_PORT +from openlp.core.projectors.db import Projector +from openlp.core.projectors.constants import PJLINK_PORT log = logging.getLogger(__name__) log.debug('editform loaded') diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/projectors/manager.py similarity index 97% rename from openlp/core/ui/projector/manager.py rename to openlp/core/projectors/manager.py index 47bbb832e..b352858b1 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/projectors/manager.py @@ -20,9 +20,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ - :mod: openlp.core.ui.projector.manager` module +:mod: openlp.core.ui.projector.manager` module - Provides the functions for the display/control of Projectors. +Provides the functions for the display/control of Projectors. """ import logging @@ -30,19 +30,19 @@ import logging from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib.ui import create_widget_action -from openlp.core.lib.projector import DialogSourceStyle -from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHENTICATION, E_ERROR, \ +from openlp.core.projectors import DialogSourceStyle +from openlp.core.projectors.constants import ERROR_MSG, ERROR_STRING, E_AUTHENTICATION, E_ERROR, \ E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \ S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP -from openlp.core.lib.projector.db import ProjectorDB -from openlp.core.lib.projector.pjlink import PJLink, PJLinkUDP -from openlp.core.ui.lib import OpenLPToolbar -from openlp.core.ui.projector.editform import ProjectorEditForm -from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle +from openlp.core.projectors.db import ProjectorDB +from openlp.core.projectors.pjlink import PJLink, PJLinkUDP +from openlp.core.projectors.editform import ProjectorEditForm +from openlp.core.projectors.sourceselectform import SourceSelectTabs, SourceSelectSingle +from openlp.core.widgets.toolbar import OpenLPToolbar log = logging.getLogger(__name__) log.debug('projectormanager loaded') @@ -276,7 +276,7 @@ class UiProjectorManager(object): self.update_icons() -class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjectorManager, RegistryProperties): +class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogMixin, RegistryProperties): """ Manage the projectors. """ @@ -288,7 +288,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto :param projectordb: Database session inherited from superclass. """ log.debug('__init__()') - super().__init__(parent) + super(ProjectorManager, self).__init__(parent) self.settings_section = 'projector' self.projectordb = projectordb self.projector_list = [] @@ -518,7 +518,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto projector.thread.quit() new_list = [] for item in self.projector_list: - if item.link.dbid == projector.link.dbid: + if item.link.db_item.id == projector.link.db_item.id: continue new_list.append(item) self.projector_list = new_list @@ -672,14 +672,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto data=projector.model_filter) count = 1 for item in projector.link.lamp: + if item['On'] is None: + status = translate('OpenLP.ProjectorManager', 'Unavailable') + elif item['On']: + status = translate('OpenLP.ProjectorManager', 'ON') + else: + status = translate('OpenLP.ProjectorManager', 'OFF') message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager', 'Lamp'), count=count, - status=translate('OpenLP.ProjectorManager', - 'ON') - if item['On'] - else translate('OpenLP.ProjectorManager', - 'OFF')) + status=status) message += '<b>{title}</b>: {hours}<br />'.format(title=translate('OpenLP.ProjectorManager', 'Hours'), hours=item['Hours']) @@ -730,7 +732,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto thread.started.connect(item.link.thread_started) thread.finished.connect(item.link.thread_stopped) thread.finished.connect(thread.deleteLater) - item.link.projectorNetwork.connect(self.update_status) item.link.changeStatus.connect(self.update_status) item.link.projectorAuthentication.connect(self.authentication_error) item.link.projectorNoAuthentication.connect(self.no_authentication_error) diff --git a/openlp/core/lib/projector/pjlink.py b/openlp/core/projectors/pjlink.py similarity index 77% rename from openlp/core/lib/projector/pjlink.py rename to openlp/core/projectors/pjlink.py index 2eb4da32c..12ae7e1d0 100644 --- a/openlp/core/lib/projector/pjlink.py +++ b/openlp/core/projectors/pjlink.py @@ -54,12 +54,11 @@ from PyQt5 import QtCore, QtNetwork from openlp.core.common import qmd5_hash from openlp.core.common.i18n import translate -from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG, ERROR_STRING, \ +from openlp.core.projectors.constants import CONNECTION_ERRORS, CR, ERROR_MSG, ERROR_STRING, \ E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, E_OK, \ E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, PJLINK_ERST_DATA, \ PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \ - STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \ - S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS + STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_QSOCKET_STATE, S_STATUS log = logging.getLogger(__name__) log.debug('pjlink loaded') @@ -111,7 +110,7 @@ class PJLinkCommands(object): """ log.debug('PJlinkCommands(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs)) super().__init__() - # Map command to function + # Map PJLink command to method self.pjlink_functions = { 'AVMT': self.process_avmt, 'CLSS': self.process_clss, @@ -123,7 +122,9 @@ class PJLinkCommands(object): 'INST': self.process_inst, 'LAMP': self.process_lamp, 'NAME': self.process_name, - 'PJLINK': self.check_login, + 'PJLINK': self.process_pjlink, + # TODO: Part of check_login refactor - remove when done + # 'PJLINK': self.check_login, 'POWR': self.process_powr, 'SNUM': self.process_snum, 'SVER': self.process_sver, @@ -135,7 +136,8 @@ class PJLinkCommands(object): """ Initialize instance variables. Also used to reset projector-specific information to default. """ - log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, state=self.state())) + log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, + state=S_QSOCKET_STATE[self.state()])) self.fan = None # ERST self.filter_time = None # FILT self.lamp = None # LAMP @@ -165,6 +167,7 @@ class PJLinkCommands(object): self.socket_timer.stop() self.send_busy = False self.send_queue = [] + self.priority_queue = [] def process_command(self, cmd, data): """ @@ -176,18 +179,19 @@ class PJLinkCommands(object): log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip, cmd=cmd, data=data)) - # Check if we have a future command not available yet - _cmd = cmd.upper() + # cmd should already be in uppercase, but data may be in mixed-case. + # Due to some replies should stay as mixed-case, validate using separate uppercase check _data = data.upper() - if _cmd not in PJLINK_VALID_CMD: - log.error("({ip}) Ignoring command='{cmd}' (Invalid/Unknown)".format(ip=self.ip, cmd=cmd)) + # Check if we have a future command not available yet + if cmd not in PJLINK_VALID_CMD: + log.error('({ip}) Ignoring command="{cmd}" (Invalid/Unknown)'.format(ip=self.ip, cmd=cmd)) return elif _data == 'OK': log.debug('({ip}) Command "{cmd}" returned OK'.format(ip=self.ip, cmd=cmd)) - # A command returned successfully, no further processing needed - return - elif _cmd not in self.pjlink_functions: - log.warning("({ip}) Unable to process command='{cmd}' (Future option)".format(ip=self.ip, cmd=cmd)) + # A command returned successfully, so do a query on command to verify status + return self.send_command(cmd=cmd) + elif cmd not in self.pjlink_functions: + log.warning('({ip}) Unable to process command="{cmd}" (Future option?)'.format(ip=self.ip, cmd=cmd)) return elif _data in PJLINK_ERRORS: # Oops - projector error @@ -211,12 +215,10 @@ class PJLinkCommands(object): elif _data == PJLINK_ERRORS[E_PROJECTOR]: # Projector/display error self.change_status(E_PROJECTOR) - self.receive_data_signal() return # Command checks already passed log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd)) - self.receive_data_signal() - self.pjlink_functions[_cmd](data) + self.pjlink_functions[cmd](data) def process_avmt(self, data): """ @@ -259,19 +261,19 @@ class PJLinkCommands(object): # : Received: '%1CLSS=Class 1' (Optoma) # : Received: '%1CLSS=Version1' (BenQ) if len(data) > 1: - log.warning("({ip}) Non-standard CLSS reply: '{data}'".format(ip=self.ip, data=data)) + log.warning('({ip}) Non-standard CLSS reply: "{data}"'.format(ip=self.ip, data=data)) # Due to stupid projectors not following standards (Optoma, BenQ comes to mind), # AND the different responses that can be received, the semi-permanent way to # fix the class reply is to just remove all non-digit characters. try: clss = re.findall('\d', data)[0] # Should only be the first match except IndexError: - log.error("({ip}) No numbers found in class version reply '{data}' - " - "defaulting to class '1'".format(ip=self.ip, data=data)) + log.error('({ip}) No numbers found in class version reply "{data}" - ' + 'defaulting to class "1"'.format(ip=self.ip, data=data)) clss = '1' elif not data.isdigit(): - log.error("({ip}) NAN clss version reply '{data}' - " - "defaulting to class '1'".format(ip=self.ip, data=data)) + log.error('({ip}) NAN CLSS version reply "{data}" - ' + 'defaulting to class "1"'.format(ip=self.ip, data=data)) clss = '1' else: clss = data @@ -289,7 +291,7 @@ class PJLinkCommands(object): """ if len(data) != PJLINK_ERST_DATA['DATA_LENGTH']: count = PJLINK_ERST_DATA['DATA_LENGTH'] - log.warning("{ip}) Invalid error status response '{data}': length != {count}".format(ip=self.ip, + log.warning('{ip}) Invalid error status response "{data}": length != {count}'.format(ip=self.ip, data=data, count=count)) return @@ -297,7 +299,7 @@ class PJLinkCommands(object): datacheck = int(data) except ValueError: # Bad data - ignore - log.warning("({ip}) Invalid error status response '{data}'".format(ip=self.ip, data=data)) + log.warning('({ip}) Invalid error status response "{data}"'.format(ip=self.ip, data=data)) return if datacheck == 0: self.projector_errors = None @@ -402,17 +404,20 @@ class PJLinkCommands(object): :param data: Lamp(s) status. """ lamps = [] - data_dict = data.split() - while data_dict: - try: - fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True} - except ValueError: - # In case of invalid entry - log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data)) - return - lamps.append(fill) - data_dict.pop(0) # Remove lamp hours - data_dict.pop(0) # Remove lamp on/off + lamp_list = data.split() + if len(lamp_list) < 2: + lamps.append({'Hours': int(lamp_list[0]), 'On': None}) + else: + while lamp_list: + try: + fill = {'Hours': int(lamp_list[0]), 'On': False if lamp_list[1] == '0' else True} + except ValueError: + # In case of invalid entry + log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data)) + return + lamps.append(fill) + lamp_list.pop(0) # Remove lamp hours + lamp_list.pop(0) # Remove lamp on/off self.lamp = lamps return @@ -427,6 +432,51 @@ class PJLinkCommands(object): log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name)) return + def process_pjlink(self, data): + """ + Process initial socket connection to terminal. + + :param data: Initial packet with authentication scheme + """ + log.debug('({ip}) Processing PJLINK command'.format(ip=self.ip)) + chk = data.split(' ') + if len(chk[0]) != 1: + # Invalid - after splitting, first field should be 1 character, either '0' or '1' only + log.error('({ip}) Invalid initial authentication scheme - aborting'.format(ip=self.ip)) + return self.disconnect_from_host() + elif chk[0] == '0': + # Normal connection no authentication + if len(chk) > 1: + # Invalid data - there should be nothing after a normal authentication scheme + log.error('({ip}) Normal connection with extra information - aborting'.format(ip=self.ip)) + return self.disconnect_from_host() + elif self.pin: + log.error('({ip}) Normal connection but PIN set - aborting'.format(ip=self.ip)) + return self.disconnect_from_host() + else: + data_hash = None + elif chk[0] == '1': + if len(chk) < 2: + # Not enough information for authenticated connection + log.error('({ip}) Authenticated connection but not enough info - aborting'.format(ip=self.ip)) + return self.disconnect_from_host() + elif not self.pin: + log.error('({ip}) Authenticate connection but no PIN - aborting'.format(ip=self.ip)) + return self.disconnect_from_host() + else: + data_hash = str(qmd5_hash(salt=chk[1].encode('utf-8'), data=self.pin.encode('utf-8')), + encoding='ascii') + # Passed basic checks, so start connection + self.readyRead.connect(self.get_socket) + if not self.no_poll: + log.debug('({ip}) process_pjlink(): Starting timer'.format(ip=self.ip)) + self.timer.setInterval(2000) # Set 2 seconds for initial information + self.timer.start() + self.change_status(S_CONNECTED) + log.debug('({ip}) process_pjlink(): Sending "CLSS" initial command'.format(ip=self.ip)) + # Since this is an initial connection, make it a priority just in case + return self.send_command(cmd="CLSS", salt=data_hash, priority=True) + def process_powr(self, data): """ Power status. See PJLink specification for format. @@ -447,7 +497,7 @@ class PJLinkCommands(object): self.send_command('INST') else: # Log unknown status response - log.warning('({ip}) Unknown power response: {data}'.format(ip=self.ip, data=data)) + log.warning('({ip}) Unknown power response: "{data}"'.format(ip=self.ip, data=data)) return def process_rfil(self, data): @@ -457,9 +507,9 @@ class PJLinkCommands(object): if self.model_filter is None: self.model_filter = data else: - log.warning("({ip}) Filter model already set".format(ip=self.ip)) - log.warning("({ip}) Saved model: '{old}'".format(ip=self.ip, old=self.model_filter)) - log.warning("({ip}) New model: '{new}'".format(ip=self.ip, new=data)) + log.warning('({ip}) Filter model already set'.format(ip=self.ip)) + log.warning('({ip}) Saved model: "{old}"'.format(ip=self.ip, old=self.model_filter)) + log.warning('({ip}) New model: "{new}"'.format(ip=self.ip, new=data)) def process_rlmp(self, data): """ @@ -468,9 +518,9 @@ class PJLinkCommands(object): if self.model_lamp is None: self.model_lamp = data else: - log.warning("({ip}) Lamp model already set".format(ip=self.ip)) - log.warning("({ip}) Saved lamp: '{old}'".format(ip=self.ip, old=self.model_lamp)) - log.warning("({ip}) New lamp: '{new}'".format(ip=self.ip, new=data)) + log.warning('({ip}) Lamp model already set'.format(ip=self.ip)) + log.warning('({ip}) Saved lamp: "{old}"'.format(ip=self.ip, old=self.model_lamp)) + log.warning('({ip}) New lamp: "{new}"'.format(ip=self.ip, new=data)) def process_snum(self, data): """ @@ -479,16 +529,16 @@ class PJLinkCommands(object): :param data: Serial number from projector. """ if self.serial_no is None: - log.debug("({ip}) Setting projector serial number to '{data}'".format(ip=self.ip, data=data)) + log.debug('({ip}) Setting projector serial number to "{data}"'.format(ip=self.ip, data=data)) self.serial_no = data self.db_update = False else: # Compare serial numbers and see if we got the same projector if self.serial_no != data: - log.warning("({ip}) Projector serial number does not match saved serial number".format(ip=self.ip)) - log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.serial_no)) - log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data)) - log.warning("({ip}) NOT saving serial number".format(ip=self.ip)) + log.warning('({ip}) Projector serial number does not match saved serial number'.format(ip=self.ip)) + log.warning('({ip}) Saved: "{old}"'.format(ip=self.ip, old=self.serial_no)) + log.warning('({ip}) Received: "{new}"'.format(ip=self.ip, new=data)) + log.warning('({ip}) NOT saving serial number'.format(ip=self.ip)) self.serial_no_received = data def process_sver(self, data): @@ -497,30 +547,29 @@ class PJLinkCommands(object): """ if len(data) > 32: # Defined in specs max version is 32 characters - log.warning("Invalid software version - too long") + log.warning('Invalid software version - too long') return elif self.sw_version is None: - log.debug("({ip}) Setting projector software version to '{data}'".format(ip=self.ip, data=data)) + log.debug('({ip}) Setting projector software version to "{data}"'.format(ip=self.ip, data=data)) self.sw_version = data self.db_update = True else: # Compare software version and see if we got the same projector if self.serial_no != data: - log.warning("({ip}) Projector software version does not match saved " - "software version".format(ip=self.ip)) - log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.sw_version)) - log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data)) - log.warning("({ip}) Saving new serial number as sw_version_received".format(ip=self.ip)) + log.warning('({ip}) Projector software version does not match saved ' + 'software version'.format(ip=self.ip)) + log.warning('({ip}) Saved: "{old}"'.format(ip=self.ip, old=self.sw_version)) + log.warning('({ip}) Received: "{new}"'.format(ip=self.ip, new=data)) + log.warning('({ip}) Saving new serial number as sw_version_received'.format(ip=self.ip)) self.sw_version_received = data -class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): +class PJLink(QtNetwork.QTcpSocket, PJLinkCommands): """ Socket service for PJLink TCP socket. """ # Signals sent by this module changeStatus = QtCore.pyqtSignal(str, int, str) - projectorNetwork = QtCore.pyqtSignal(int) # Projector network activity projectorStatus = QtCore.pyqtSignal(int) # Status update projectorAuthentication = QtCore.pyqtSignal(str) # Authentication error projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed @@ -538,9 +587,9 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): :param poll_time: Time (in seconds) to poll connected projector :param socket_timeout: Time (in seconds) to abort the connection if no response """ - log.debug('PJlink(projector={projector}, args={args} kwargs={kwargs})'.format(projector=projector, - args=args, - kwargs=kwargs)) + log.debug('PJlink(projector="{projector}", args="{args}" kwargs="{kwargs}")'.format(projector=projector, + args=args, + kwargs=kwargs)) super().__init__() self.entry = projector self.ip = self.entry.ip @@ -571,6 +620,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): self.widget = None # QListBox entry self.timer = None # Timer that calls the poll_loop self.send_queue = [] + self.priority_queue = [] self.send_busy = False # Socket timer for some possible brain-dead projectors or network cable pulled self.socket_timer = None @@ -584,6 +634,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): self.connected.connect(self.check_login) self.disconnected.connect(self.disconnect_from_host) self.error.connect(self.get_error) + self.projectorReceivedData.connect(self._send_command) def thread_stopped(self): """ @@ -606,6 +657,10 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): self.projectorReceivedData.disconnect(self._send_command) except TypeError: pass + try: + self.readyRead.disconnect(self.get_socket) # Set in process_pjlink + except TypeError: + pass self.disconnect_from_host() self.deleteLater() self.i_am_running = False @@ -623,10 +678,10 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): Retrieve information from projector that changes. Normally called by timer(). """ - if self.state() != self.ConnectedState: - log.warning("({ip}) poll_loop(): Not connected - returning".format(ip=self.ip)) + if self.state() != S_QSOCKET_STATE['ConnectedState']: + log.warning('({ip}) poll_loop(): Not connected - returning'.format(ip=self.ip)) return - log.debug('({ip}) Updating projector status'.format(ip=self.ip)) + log.debug('({ip}) poll_loop(): Updating projector status'.format(ip=self.ip)) # Reset timer in case we were called from a set command if self.timer.interval() < self.poll_time: # Reset timer to 5 seconds @@ -638,28 +693,28 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): if self.pjlink_class == '2': check_list.extend(['FILT', 'FREZ']) for command in check_list: - self.send_command(command, queue=True) + self.send_command(command) # The following commands do not change, so only check them once if self.power == S_ON and self.source_available is None: - self.send_command('INST', queue=True) + self.send_command('INST') if self.other_info is None: - self.send_command('INFO', queue=True) + self.send_command('INFO') if self.manufacturer is None: - self.send_command('INF1', queue=True) + self.send_command('INF1') if self.model is None: - self.send_command('INF2', queue=True) + self.send_command('INF2') if self.pjlink_name is None: - self.send_command('NAME', queue=True) + self.send_command('NAME') if self.pjlink_class == '2': # Class 2 specific checks if self.serial_no is None: - self.send_command('SNUM', queue=True) + self.send_command('SNUM') if self.sw_version is None: - self.send_command('SVER', queue=True) + self.send_command('SVER') if self.model_filter is None: - self.send_command('RFIL', queue=True) + self.send_command('RFIL') if self.model_lamp is None: - self.send_command('RLMP', queue=True) + self.send_command('RLMP') def _get_status(self, status): """ @@ -711,14 +766,12 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): code=status_code, message=status_message if msg is None else msg)) self.changeStatus.emit(self.ip, status, message) + self.projectorUpdateIcons.emit() @QtCore.pyqtSlot() def check_login(self, data=None): """ - Processes the initial connection and authentication (if needed). - Starts poll timer if connection is established. - - NOTE: Qt md5 hash function doesn't work with projector authentication. Use the python md5 hash function. + Processes the initial connection and convert to a PJLink packet if valid initial connection :param data: Optional data if called from another routine """ @@ -731,12 +784,12 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): self.change_status(E_SOCKET_TIMEOUT) return read = self.readLine(self.max_size) - self.readLine(self.max_size) # Clean out the trailing \r\n + self.readLine(self.max_size) # Clean out any trailing whitespace if read is None: log.warning('({ip}) read is None - socket error?'.format(ip=self.ip)) return elif len(read) < 8: - log.warning('({ip}) Not enough data read)'.format(ip=self.ip)) + log.warning('({ip}) Not enough data read - skipping'.format(ip=self.ip)) return data = decode(read, 'utf-8') # Possibility of extraneous data on input when reading. @@ -748,9 +801,16 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): # PJLink initial login will be: # 'PJLink 0' - Unauthenticated login - no extra steps required. # 'PJLink 1 XXXXXX' Authenticated login - extra processing required. - if not data.upper().startswith('PJLINK'): - # Invalid response + if not data.startswith('PJLINK'): + # Invalid initial packet - close socket + log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.ip)) return self.disconnect_from_host() + log.debug('({ip}) check_login(): Formatting initial connection prompt to PJLink packet'.format(ip=self.ip)) + return self.get_data('{start}{clss}{data}'.format(start=PJLINK_PREFIX, + clss='1', + data=data.replace(' ', '=', 1)).encode('utf-8')) + # TODO: The below is replaced by process_pjlink() - remove when working properly + """ if '=' in data: # Processing a login reply data_check = data.strip().split('=') @@ -799,18 +859,19 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): log.debug('({ip}) Starting timer'.format(ip=self.ip)) self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.start() + """ def _trash_buffer(self, msg=None): """ Clean out extraneous stuff in the buffer. """ - log.warning("({ip}) {message}".format(ip=self.ip, message='Invalid packet' if msg is None else msg)) + log.warning('({ip}) {message}'.format(ip=self.ip, message='Invalid packet' if msg is None else msg)) self.send_busy = False trash_count = 0 while self.bytesAvailable() > 0: trash = self.read(self.max_size) trash_count += len(trash) - log.debug("({ip}) Finished cleaning buffer - {count} bytes dropped".format(ip=self.ip, + log.debug('({ip}) Finished cleaning buffer - {count} bytes dropped'.format(ip=self.ip, count=trash_count)) return @@ -822,7 +883,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): :param data: Data to process. buffer must be formatted as a proper PJLink packet. :param ip: Destination IP for buffer. """ - log.debug("({ip}) get_buffer(data='{buff}' ip='{ip_in}'".format(ip=self.ip, buff=data, ip_in=ip)) + log.debug('({ip}) get_buffer(data="{buff}" ip="{ip_in}"'.format(ip=self.ip, buff=data, ip_in=ip)) if ip is None: log.debug("({ip}) get_buffer() Don't know who data is for - exiting".format(ip=self.ip)) return @@ -840,39 +901,52 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): return # Although we have a packet length limit, go ahead and use a larger buffer read = self.readLine(1024) - log.debug("({ip}) get_socket(): '{buff}'".format(ip=self.ip, buff=read)) + log.debug('({ip}) get_socket(): "{buff}"'.format(ip=self.ip, buff=read)) if read == -1: # No data available log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.ip)) return self.receive_data_signal() self.socket_timer.stop() - self.projectorNetwork.emit(S_NETWORK_RECEIVED) - return self.get_data(buff=read, ip=self.ip) + self.get_data(buff=read, ip=self.ip) + return self.receive_data_signal() - def get_data(self, buff, ip): + def get_data(self, buff, ip=None): """ Process received data :param buff: Data to process. :param ip: (optional) Destination IP. """ - log.debug("({ip}) get_data(ip='{ip_in}' buffer='{buff}'".format(ip=self.ip, ip_in=ip, buff=buff)) + # Since "self" is not available to options and the "ip" keyword is a "maybe I'll use in the future", + # set to default here + if ip is None: + ip = self.ip + log.debug('({ip}) get_data(ip="{ip_in}" buffer="{buff}"'.format(ip=self.ip, ip_in=ip, buff=buff)) # NOTE: Class2 has changed to some values being UTF-8 data_in = decode(buff, 'utf-8') data = data_in.strip() - if (len(data) < 7) or (not data.startswith(PJLINK_PREFIX)): - return self._trash_buffer(msg='get_data(): Invalid packet - length or prefix') + # Initial packet checks + if (len(data) < 7): + return self._trash_buffer(msg='get_data(): Invalid packet - length') elif len(data) > self.max_size: return self._trash_buffer(msg='get_data(): Invalid packet - too long') + elif not data.startswith(PJLINK_PREFIX): + return self._trash_buffer(msg='get_data(): Invalid packet - PJLink prefix missing') elif '=' not in data: - return self._trash_buffer(msg='get_data(): Invalid packet does not have equal') + return self._trash_buffer(msg='get_data(): Invalid reply - Does not have "="') log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data)) header, data = data.split('=') + # At this point, the header should contain: + # "PVCCCC" + # Where: + # P = PJLINK_PREFIX + # V = PJLink class or version + # C = PJLink command try: - version, cmd = header[1], header[2:] + version, cmd = header[1], header[2:].upper() except ValueError as e: self.change_status(E_INVALID_DATA) - log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip())) + log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in)) return self._trash_buffer('get_data(): Expected header + command + data') if cmd not in PJLINK_VALID_CMD: log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd)) @@ -880,6 +954,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): if int(self.pjlink_class) < int(version): log.warning('({ip}) get_data(): Projector returned class reply higher ' 'than projector stated class'.format(ip=self.ip)) + self.send_busy = False return self.process_command(cmd, data) @QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError) @@ -909,23 +984,21 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): self.reset_information() return - def send_command(self, cmd, opts='?', salt=None, queue=False): + def send_command(self, cmd, opts='?', salt=None, priority=False): """ Add command to output queue if not already in queue. :param cmd: Command to send :param opts: Command option (if any) - defaults to '?' (get information) :param salt: Optional salt for md5 hash initial authentication - :param queue: Option to force add to queue rather than sending directly + :param priority: Option to send packet now rather than queue it up """ if self.state() != self.ConnectedState: log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip)) - self.send_queue = [] - return + return self.reset_information() if cmd not in PJLINK_VALID_CMD: log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.ip)) return - self.projectorNetwork.emit(S_NETWORK_SENDING) log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip, command=cmd, data=opts, @@ -939,28 +1012,26 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): header = PJLINK_HEADER.format(linkclass=cmd_ver[0]) else: # NOTE: Once we get to version 3 then think about looping - log.error('({ip}): send_command(): PJLink class check issue? aborting'.format(ip=self.ip)) + log.error('({ip}): send_command(): PJLink class check issue? Aborting'.format(ip=self.ip)) return out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt, header=header, command=cmd, options=opts, suffix=CR) - if out in self.send_queue: - # Already there, so don't add - log.debug('({ip}) send_command(out="{data}") Already in queue - skipping'.format(ip=self.ip, - data=out.strip())) - elif not queue and len(self.send_queue) == 0: - # Nothing waiting to send, so just send it - log.debug('({ip}) send_command(out="{data}") Sending data'.format(ip=self.ip, data=out.strip())) - return self._send_command(data=out) + if out in self.priority_queue: + log.debug('({ip}) send_command(): Already in priority queue - skipping'.format(ip=self.ip)) + elif out in self.send_queue: + log.debug('({ip}) send_command(): Already in normal queue - skipping'.format(ip=self.ip)) else: - log.debug('({ip}) send_command(out="{data}") adding to queue'.format(ip=self.ip, data=out.strip())) - self.send_queue.append(out) - self.projectorReceivedData.emit() - log.debug('({ip}) send_command(): send_busy is {data}'.format(ip=self.ip, data=self.send_busy)) - if not self.send_busy: - log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip)) + if priority: + log.debug('({ip}) send_command(): Adding to priority queue'.format(ip=self.ip)) + self.priority_queue.append(out) + else: + log.debug('({ip}) send_command(): Adding to normal queue'.format(ip=self.ip)) + self.send_queue.append(out) + if self.priority_queue or self.send_queue: + # May be some initial connection setup so make sure we send data self._send_command() @QtCore.pyqtSlot() @@ -971,44 +1042,53 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): :param data: Immediate data to send :param utf8: Send as UTF-8 string otherwise send as ASCII string """ - log.debug('({ip}) _send_string()'.format(ip=self.ip)) - log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state())) + # Funny looking data check, but it's a quick check for data=None + log.debug('({ip}) _send_command(data="{data}")'.format(ip=self.ip, data=data.strip() if data else data)) + log.debug('({ip}) _send_command(): Connection status: {data}'.format(ip=self.ip, + data=S_QSOCKET_STATE[self.state()])) if self.state() != self.ConnectedState: - log.debug('({ip}) _send_string() Not connected - abort'.format(ip=self.ip)) - self.send_queue = [] + log.debug('({ip}) _send_command() Not connected - abort'.format(ip=self.ip)) self.send_busy = False - return + return self.disconnect_from_host() + if data and data not in self.priority_queue: + log.debug('({ip}) _send_command(): Priority packet - adding to priority queue'.format(ip=self.ip)) + self.priority_queue.append(data) + if self.send_busy: # Still waiting for response from last command sent + log.debug('({ip}) _send_command(): Still busy, returning'.format(ip=self.ip)) + log.debug('({ip}) _send_command(): Priority queue = {data}'.format(ip=self.ip, data=self.priority_queue)) + log.debug('({ip}) _send_command(): Normal queue = {data}'.format(ip=self.ip, data=self.send_queue)) return - if data is not None: - out = data - log.debug('({ip}) _send_string(data="{data}")'.format(ip=self.ip, data=out.strip())) + + if len(self.priority_queue) != 0: + out = self.priority_queue.pop(0) + log.debug('({ip}) _send_command(): Getting priority queued packet'.format(ip=self.ip)) elif len(self.send_queue) != 0: out = self.send_queue.pop(0) - log.debug('({ip}) _send_string(queued data="{data}"%s)'.format(ip=self.ip, data=out.strip())) + log.debug('({ip}) _send_command(): Getting normal queued packet'.format(ip=self.ip)) else: # No data to send - log.debug('({ip}) _send_string(): No data to send'.format(ip=self.ip)) + log.debug('({ip}) _send_command(): No data to send'.format(ip=self.ip)) self.send_busy = False return self.send_busy = True - log.debug('({ip}) _send_string(): Sending "{data}"'.format(ip=self.ip, data=out.strip())) - log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue)) + log.debug('({ip}) _send_command(): Sending "{data}"'.format(ip=self.ip, data=out.strip())) self.socket_timer.start() - self.projectorNetwork.emit(S_NETWORK_SENDING) sent = self.write(out.encode('{string_encoding}'.format(string_encoding='utf-8' if utf8 else 'ascii'))) self.waitForBytesWritten(2000) # 2 seconds should be enough if sent == -1: # Network error? - log.warning("({ip}) _send_command(): -1 received".format(ip=self.ip)) + log.warning('({ip}) _send_command(): -1 received - disconnecting from host'.format(ip=self.ip)) self.change_status(E_NETWORK, translate('OpenLP.PJLink', 'Error while sending data to projector')) + self.disconnect_from_host() def connect_to_host(self): """ Initiate connection to projector. """ + log.debug('{ip}) connect_to_host(): Starting connection'.format(ip=self.ip)) if self.state() == self.ConnectedState: log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip)) return @@ -1024,22 +1104,19 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): if abort: log.warning('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.ip)) else: - log.warning('({ip}) disconnect_from_host(): Not connected - returning'.format(ip=self.ip)) - self.reset_information() + log.warning('({ip}) disconnect_from_host(): Not connected'.format(ip=self.ip)) self.disconnectFromHost() try: self.readyRead.disconnect(self.get_socket) except TypeError: pass + log.debug('({ip}) disconnect_from_host() ' + 'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0])) if abort: self.change_status(E_NOT_CONNECTED) else: - log.debug('({ip}) disconnect_from_host() ' - 'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0])) - if self.status_connect != E_NOT_CONNECTED: - self.change_status(S_NOT_CONNECTED) + self.change_status(S_NOT_CONNECTED) self.reset_information() - self.projectorUpdateIcons.emit() def get_av_mute_status(self): """ diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/projectors/sourceselectform.py similarity index 99% rename from openlp/core/ui/projector/sourceselectform.py rename to openlp/core/projectors/sourceselectform.py index 0c150d25b..aaf8170ac 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/projectors/sourceselectform.py @@ -31,8 +31,8 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import is_macosx from openlp.core.common.i18n import translate from openlp.core.lib import build_icon -from openlp.core.lib.projector.db import ProjectorSource -from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES +from openlp.core.projectors.db import ProjectorSource +from openlp.core.projectors.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES log = logging.getLogger(__name__) diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/projectors/tab.py similarity index 99% rename from openlp/core/ui/projector/tab.py rename to openlp/core/projectors/tab.py index b7c2e5dda..29e6a9511 100644 --- a/openlp/core/ui/projector/tab.py +++ b/openlp/core/projectors/tab.py @@ -29,7 +29,7 @@ from PyQt5 import QtWidgets from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab -from openlp.core.lib.projector import DialogSourceStyle +from openlp.core.projectors import DialogSourceStyle log = logging.getLogger(__name__) log.debug('projectortab module loaded') diff --git a/openlp/core/lib/projector/upgrade.py b/openlp/core/projectors/upgrade.py similarity index 100% rename from openlp/core/lib/projector/upgrade.py rename to openlp/core/projectors/upgrade.py diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 2b8383755..5c2463e5d 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -115,9 +115,10 @@ from .formattingtagcontroller import FormattingTagController from .shortcutlistform import ShortcutListForm from .servicemanager import ServiceManager from .thememanager import ThemeManager -from .projector.manager import ProjectorManager -from .projector.tab import ProjectorTab -from .projector.editform import ProjectorEditForm + +from openlp.core.projectors.editform import ProjectorEditForm +from openlp.core.projectors.manager import ProjectorManager +from openlp.core.projectors.tab import ProjectorTab __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', 'ThemeManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'AudioPlayer', diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 9581dc6f9..2d434c0c6 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -32,8 +32,9 @@ from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, format_time, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab, build_icon -from openlp.core.ui.lib import PathEdit, PathType from openlp.core.ui.style import HAS_DARK_STYLE +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType log = logging.getLogger(__name__) @@ -122,7 +123,7 @@ class AdvancedTab(SettingsTab): self.data_directory_layout.setObjectName('data_directory_layout') self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box) self.data_directory_new_label.setObjectName('data_directory_current_label') - self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories, + self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathEditType.Directories, default_path=AppLocation.get_directory(AppLocation.DataDir)) self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit) self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 1ceeed989..45124dac8 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -72,10 +72,10 @@ except ImportError: from openlp.core.common import is_linux from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.ui.exceptiondialog import Ui_ExceptionDialog -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog from openlp.core.version import get_version @@ -155,7 +155,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties): try: with file_path.open('w') as report_file: report_file.write(report_text) - except IOError: + except OSError: log.exception('Failed to write crash report') def on_send_report_button_clicked(self): diff --git a/openlp/core/ui/filerenameform.py b/openlp/core/ui/filerenameform.py index d6a519240..249ea1f10 100644 --- a/openlp/core/ui/filerenameform.py +++ b/openlp/core/ui/filerenameform.py @@ -25,7 +25,8 @@ The file rename dialog. from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.ui.filerenamedialog import Ui_FileRenameDialog diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index ea98577c8..37fac2dd4 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -38,7 +38,8 @@ from openlp.core.common import clean_button_text, trace_error_handler from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate from openlp.core.common.path import Path, create_paths -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index be893cd5e..5212479ef 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -261,8 +261,8 @@ class UiFirstTimeWizard(object): self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Alerts – Display informative messages while showing other slides')) self.projectors_check_box.setText(translate('OpenLP.FirstTimeWizard', - 'Projectors – Control PJLink compatible projects on your network' - ' from OpenLP')) + 'Projector Controller – Control PJLink compatible projects on your' + ' network from OpenLP')) self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection')) self.no_internet_page.setSubTitle( translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.')) diff --git a/openlp/core/ui/formattingtagcontroller.py b/openlp/core/ui/formattingtagcontroller.py index e92173fed..4b9d75fee 100644 --- a/openlp/core/ui/formattingtagcontroller.py +++ b/openlp/core/ui/formattingtagcontroller.py @@ -43,7 +43,7 @@ class FormattingTagController(object): r'(?P<tag>[^\s/!\?>]+)(?:\s+[^\s=]+="[^"]*")*\s*(?P<empty>/)?' r'|(?P<cdata>!\[CDATA\[(?:(?!\]\]>).)*\]\])' r'|(?P<procinst>\?(?:(?!\?>).)*\?)' - r'|(?P<comment>!--(?:(?!-->).)*--))>', re.UNICODE) + r'|(?P<comment>!--(?:(?!-->).)*--))>') self.html_regex = re.compile(r'^(?:[^<>]*%s)*[^<>]*$' % self.html_tag_regex.pattern) def pre_save(self): diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 8488f13a1..a908ac91d 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -33,7 +33,8 @@ from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import SettingsTab -from openlp.core.ui.lib import ColorButton, PathEdit +from openlp.core.widgets.buttons import ColorButton +from openlp.core.widgets.edits import PathEdit log = logging.getLogger(__name__) diff --git a/openlp/core/ui/lib/historycombobox.py b/openlp/core/ui/lib/historycombobox.py deleted file mode 100644 index 6320bc383..000000000 --- a/openlp/core/ui/lib/historycombobox.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -The :mod:`~openlp.core.ui.lib.historycombobox` module contains the HistoryComboBox widget -""" - -from PyQt5 import QtCore, QtWidgets - - -class HistoryComboBox(QtWidgets.QComboBox): - """ - The :class:`~openlp.core.common.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed`` - signal for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit - box into its list. - """ - returnPressed = QtCore.pyqtSignal() - - def __init__(self, parent=None): - """ - Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top. - - :param parent: The parent widget - """ - super().__init__(parent) - self.setDuplicatesEnabled(False) - self.setEditable(True) - self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - self.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop) - - def keyPressEvent(self, event): - """ - Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to - the dropdown list. - - :param event: The keyboard event - """ - # Handle Enter and Return ourselves - if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return: - # Emit the returnPressed signal - self.returnPressed.emit() - # Save the current text to the dropdown list - if self.currentText() and self.findText(self.currentText()) == -1: - self.insertItem(0, self.currentText()) - # Let the parent handle any keypress events - super().keyPressEvent(event) - - def focusOutEvent(self, event): - """ - Override the inherited focusOutEvent to save the current text to the dropdown list. - - :param event: The focus event - """ - # Save the current text to the dropdown list - if self.currentText() and self.findText(self.currentText()) == -1: - self.insertItem(0, self.currentText()) - # Let the parent handle any keypress events - super().focusOutEvent(event) - - def getItems(self): - """ - Get all the items from the history - - :return: A list of strings - """ - return [self.itemText(i) for i in range(self.count())] diff --git a/openlp/core/ui/lib/listwidgetwithdnd.py b/openlp/core/ui/lib/listwidgetwithdnd.py deleted file mode 100755 index 5648ff8f4..000000000 --- a/openlp/core/ui/lib/listwidgetwithdnd.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Extend QListWidget to handle drag and drop functionality -""" -import os - -from PyQt5 import QtCore, QtGui, QtWidgets - -from openlp.core.common.i18n import UiStrings -from openlp.core.common.registry import Registry - - -class ListWidgetWithDnD(QtWidgets.QListWidget): - """ - Provide a list widget to store objects and handle drag and drop events - """ - def __init__(self, parent=None, name=''): - """ - Initialise the list widget - """ - super().__init__(parent) - self.mime_data_text = name - self.no_results_text = UiStrings().NoResults - self.setSpacing(1) - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - self.setAlternatingRowColors(True) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - def activateDnD(self): - """ - Activate DnD of widget - """ - self.setAcceptDrops(True) - self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) - Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) - - def clear(self, search_while_typing=False): - """ - Re-implement clear, so that we can customise feedback when using 'Search as you type' - - :param search_while_typing: True if we want to display the customised message - :return: None - """ - if search_while_typing: - self.no_results_text = UiStrings().ShortResults - else: - self.no_results_text = UiStrings().NoResults - super().clear() - - def mouseMoveEvent(self, event): - """ - Drag and drop event does not care what data is selected as the recipient will use events to request the data - move just tell it what plugin to call - """ - if event.buttons() != QtCore.Qt.LeftButton: - event.ignore() - return - if not self.selectedItems(): - event.ignore() - return - drag = QtGui.QDrag(self) - mime_data = QtCore.QMimeData() - drag.setMimeData(mime_data) - mime_data.setText(self.mime_data_text) - drag.exec(QtCore.Qt.CopyAction) - - def dragEnterEvent(self, event): - """ - When something is dragged into this object, check if you should be able to drop it in here. - """ - if event.mimeData().hasUrls(): - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """ - Make an object droppable, and set it to copy the contents of the object, not move it. - """ - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - else: - event.ignore() - - def dropEvent(self, event): - """ - Receive drop event check if it is a file and process it if it is. - - :param event: Handle of the event pint passed - """ - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - files = [] - for url in event.mimeData().urls(): - local_file = os.path.normpath(url.toLocalFile()) - if os.path.isfile(local_file): - files.append(local_file) - elif os.path.isdir(local_file): - listing = os.listdir(local_file) - for file in listing: - files.append(os.path.join(local_file, file)) - Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text), - {'files': files, 'target': self.itemAt(event.pos())}) - else: - event.ignore() - - def allItems(self): - """ - An generator to list all the items in the widget - - :return: a generator - """ - for row in range(self.count()): - yield self.item(row) - - def paintEvent(self, event): - """ - Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty. - - :param event: A QPaintEvent - :return: None - """ - super().paintEvent(event) - if not self.count(): - viewport = self.viewport() - painter = QtGui.QPainter(viewport) - font = QtGui.QFont() - font.setItalic(True) - painter.setFont(font) - painter.drawText(QtCore.QRect(0, 0, viewport.width(), viewport.height()), - (QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap), self.no_results_text) diff --git a/openlp/core/ui/lib/pathedit.py b/openlp/core/ui/lib/pathedit.py deleted file mode 100644 index 7b28c47ba..000000000 --- a/openlp/core/ui/lib/pathedit.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -from enum import Enum - -from PyQt5 import QtCore, QtWidgets - -from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.path import Path, path_to_str, str_to_path -from openlp.core.lib import build_icon -from openlp.core.ui.lib.filedialog import FileDialog - - -class PathType(Enum): - Files = 1 - Directories = 2 - - -class PathEdit(QtWidgets.QWidget): - """ - The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when - a file or directory needs to be selected. - """ - pathChanged = QtCore.pyqtSignal(Path) - - def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True): - """ - Initialise the PathEdit widget - - :param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method. - :param str dialog_caption: Used to customise the caption in the QFileDialog. - :param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert - button is clicked - :param bool show_revert: Used to determine if the 'revert button' should be visible. - :rtype: None - """ - super().__init__(parent) - self.default_path = default_path - self.dialog_caption = dialog_caption - self._path_type = path_type - self._path = None - self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles) - self._setup(show_revert) - - def _setup(self, show_revert): - """ - Set up the widget - :param bool show_revert: Show or hide the revert button - :rtype: None - """ - widget_layout = QtWidgets.QHBoxLayout() - widget_layout.setContentsMargins(0, 0, 0, 0) - self.line_edit = QtWidgets.QLineEdit(self) - widget_layout.addWidget(self.line_edit) - self.browse_button = QtWidgets.QToolButton(self) - self.browse_button.setIcon(build_icon(':/general/general_open.png')) - widget_layout.addWidget(self.browse_button) - self.revert_button = QtWidgets.QToolButton(self) - self.revert_button.setIcon(build_icon(':/general/general_revert.png')) - self.revert_button.setVisible(show_revert) - widget_layout.addWidget(self.revert_button) - self.setLayout(widget_layout) - # Signals and Slots - self.browse_button.clicked.connect(self.on_browse_button_clicked) - self.revert_button.clicked.connect(self.on_revert_button_clicked) - self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished) - self.update_button_tool_tips() - - @property - def path(self): - """ - A property getter method to return the selected path. - - :return: The selected path - :rtype: openlp.core.common.path.Path - """ - return self._path - - @path.setter - def path(self, path): - """ - A Property setter method to set the selected path - - :param openlp.core.common.path.Path path: The path to set the widget to - :rtype: None - """ - self._path = path - text = path_to_str(path) - self.line_edit.setText(text) - self.line_edit.setToolTip(text) - - @property - def path_type(self): - """ - A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to - selecting a file or directory. - - :return: The type selected - :rtype: PathType - """ - return self._path_type - - @path_type.setter - def path_type(self, path_type): - """ - A Property setter method to set the path type - - :param PathType path_type: The type of path to select - :rtype: None - """ - self._path_type = path_type - self.update_button_tool_tips() - - def update_button_tool_tips(self): - """ - Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised - - :rtype: None - """ - if self._path_type == PathType.Directories: - self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.')) - self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.')) - else: - self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.')) - self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.')) - - def on_browse_button_clicked(self): - """ - A handler to handle a click on the browse button. - - Show the QFileDialog and process the input from the user - - :rtype: None - """ - caption = self.dialog_caption - path = None - if self._path_type == PathType.Directories: - if not caption: - caption = translate('OpenLP.PathEdit', 'Select Directory') - path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly) - elif self._path_type == PathType.Files: - if not caption: - caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File') - path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters) - if path: - self.on_new_path(path) - - def on_revert_button_clicked(self): - """ - A handler to handle a click on the revert button. - - Set the new path to the value of the default_path instance variable. - - :rtype: None - """ - self.on_new_path(self.default_path) - - def on_line_edit_editing_finished(self): - """ - A handler to handle when the line edit has finished being edited. - - :rtype: None - """ - path = str_to_path(self.line_edit.text()) - self.on_new_path(path) - - def on_new_path(self, path): - """ - A method called to validate and set a new path. - - Emits the pathChanged Signal - - :param openlp.core.common.path.Path path: The new path - :rtype: None - """ - if self._path != path: - self.path = path - self.pathChanged.emit(path) diff --git a/openlp/core/ui/lib/spelltextedit.py b/openlp/core/ui/lib/spelltextedit.py deleted file mode 100644 index d0fc25af2..000000000 --- a/openlp/core/ui/lib/spelltextedit.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -The :mod:`~openlp.core.lib.spelltextedit` module contains a classes to add spell checking to an edit widget. -""" - -import logging -import re - -try: - import enchant - from enchant import DictNotFoundError - from enchant.errors import Error - ENCHANT_AVAILABLE = True -except ImportError: - ENCHANT_AVAILABLE = False - -# based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check - -from PyQt5 import QtCore, QtGui, QtWidgets - -from openlp.core.common.i18n import translate -from openlp.core.lib import FormattingTags -from openlp.core.lib.ui import create_action - -log = logging.getLogger(__name__) - - -class SpellTextEdit(QtWidgets.QPlainTextEdit): - """ - Spell checking widget based on QPlanTextEdit. - """ - def __init__(self, parent=None, formatting_tags_allowed=True): - """ - Constructor. - """ - global ENCHANT_AVAILABLE - super(SpellTextEdit, self).__init__(parent) - self.formatting_tags_allowed = formatting_tags_allowed - # Default dictionary based on the current locale. - if ENCHANT_AVAILABLE: - try: - self.dictionary = enchant.Dict() - self.highlighter = Highlighter(self.document()) - self.highlighter.spelling_dictionary = self.dictionary - except (Error, DictNotFoundError): - ENCHANT_AVAILABLE = False - log.debug('Could not load default dictionary') - - def mousePressEvent(self, event): - """ - Handle mouse clicks within the text edit region. - """ - if event.button() == QtCore.Qt.RightButton: - # Rewrite the mouse event to a left button event so the cursor is moved to the location of the pointer. - event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, - event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier) - QtWidgets.QPlainTextEdit.mousePressEvent(self, event) - - def contextMenuEvent(self, event): - """ - Provide the context menu for the text edit region. - """ - popup_menu = self.createStandardContextMenu() - # Select the word under the cursor. - cursor = self.textCursor() - # only select text if not already selected - if not cursor.hasSelection(): - cursor.select(QtGui.QTextCursor.WordUnderCursor) - self.setTextCursor(cursor) - # Add menu with available languages. - if ENCHANT_AVAILABLE: - lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:')) - for lang in enchant.list_languages(): - action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag) - lang_menu.addAction(action) - popup_menu.insertSeparator(popup_menu.actions()[0]) - popup_menu.insertMenu(popup_menu.actions()[0], lang_menu) - lang_menu.triggered.connect(self.set_language) - # Check if the selected word is misspelled and offer spelling suggestions if it is. - if ENCHANT_AVAILABLE and self.textCursor().hasSelection(): - text = self.textCursor().selectedText() - if not self.dictionary.check(text): - spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions')) - for word in self.dictionary.suggest(text): - action = SpellAction(word, spell_menu) - action.correct.connect(self.correct_word) - spell_menu.addAction(action) - # Only add the spelling suggests to the menu if there are suggestions. - if spell_menu.actions(): - popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) - tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags')) - if self.formatting_tags_allowed: - for html in FormattingTags.get_html_tags(): - action = SpellAction(html['desc'], tag_menu) - action.correct.connect(self.html_tag) - tag_menu.addAction(action) - popup_menu.insertSeparator(popup_menu.actions()[0]) - popup_menu.insertMenu(popup_menu.actions()[0], tag_menu) - popup_menu.exec(event.globalPos()) - - def set_language(self, action): - """ - Changes the language for this spelltextedit. - - :param action: The action. - """ - self.dictionary = enchant.Dict(action.text()) - self.highlighter.spelling_dictionary = self.dictionary - self.highlighter.highlightBlock(self.toPlainText()) - self.highlighter.rehighlight() - - def correct_word(self, word): - """ - Replaces the selected text with word. - """ - cursor = self.textCursor() - cursor.beginEditBlock() - cursor.removeSelectedText() - cursor.insertText(word) - cursor.endEditBlock() - - def html_tag(self, tag): - """ - Replaces the selected text with word. - """ - tag = tag.replace('&', '') - for html in FormattingTags.get_html_tags(): - if tag == html['desc']: - cursor = self.textCursor() - if self.textCursor().hasSelection(): - text = cursor.selectedText() - cursor.beginEditBlock() - cursor.removeSelectedText() - cursor.insertText(html['start tag']) - cursor.insertText(text) - cursor.insertText(html['end tag']) - cursor.endEditBlock() - else: - cursor = self.textCursor() - cursor.insertText(html['start tag']) - cursor.insertText(html['end tag']) - - -class Highlighter(QtGui.QSyntaxHighlighter): - """ - Provides a text highlighter for pointing out spelling errors in text. - """ - WORDS = r'(?iu)[\w\']+' - - def __init__(self, *args): - """ - Constructor - """ - super(Highlighter, self).__init__(*args) - self.spelling_dictionary = None - - def highlightBlock(self, text): - """ - Highlight mis spelt words in a block of text. - - Note, this is a Qt hook. - """ - if not self.spelling_dictionary: - return - text = str(text) - char_format = QtGui.QTextCharFormat() - char_format.setUnderlineColor(QtCore.Qt.red) - char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline) - for word_object in re.finditer(self.WORDS, text): - if not self.spelling_dictionary.check(word_object.group()): - self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format) - - -class SpellAction(QtWidgets.QAction): - """ - A special QAction that returns the text in a signal. - """ - correct = QtCore.pyqtSignal(str) - - def __init__(self, *args): - """ - Constructor - """ - super(SpellAction, self).__init__(*args) - self.triggered.connect(lambda x: self.correct.emit(self.text())) diff --git a/openlp/core/ui/lib/treewidgetwithdnd.py b/openlp/core/ui/lib/treewidgetwithdnd.py deleted file mode 100644 index 792fa8ab8..000000000 --- a/openlp/core/ui/lib/treewidgetwithdnd.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Extend QTreeWidget to handle drag and drop functionality -""" -import os - -from PyQt5 import QtCore, QtGui, QtWidgets - -from openlp.core.common import is_win -from openlp.core.common.registry import Registry - - -class TreeWidgetWithDnD(QtWidgets.QTreeWidget): - """ - Provide a tree widget to store objects and handle drag and drop events - """ - def __init__(self, parent=None, name=''): - """ - Initialise the tree widget - """ - super(TreeWidgetWithDnD, self).__init__(parent) - self.mime_data_text = name - self.allow_internal_dnd = False - self.header().close() - self.default_indentation = self.indentation() - self.setIndentation(0) - self.setAnimated(True) - - def activateDnD(self): - """ - Activate DnD of widget - """ - self.setAcceptDrops(True) - self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) - Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) - Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal) - - def mouseMoveEvent(self, event): - """ - Drag and drop event does not care what data is selected as the recipient will use events to request the data - move just tell it what plugin to call - - :param event: The event that occurred - """ - if event.buttons() != QtCore.Qt.LeftButton: - event.ignore() - return - if not self.selectedItems(): - event.ignore() - return - drag = QtGui.QDrag(self) - mime_data = QtCore.QMimeData() - drag.setMimeData(mime_data) - mime_data.setText(self.mime_data_text) - drag.exec(QtCore.Qt.CopyAction) - - def dragEnterEvent(self, event): - """ - Receive drag enter event, check if it is a file or internal object and allow it if it is. - - :param event: The event that occurred - """ - if event.mimeData().hasUrls(): - event.accept() - elif self.allow_internal_dnd: - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """ - Receive drag move event, check if it is a file or internal object and allow it if it is. - - :param event: The event that occurred - """ - QtWidgets.QTreeWidget.dragMoveEvent(self, event) - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - elif self.allow_internal_dnd: - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - else: - event.ignore() - - def dropEvent(self, event): - """ - Receive drop event, check if it is a file or internal object and process it if it is. - - :param event: Handle of the event pint passed - """ - # If we are on Windows, OpenLP window will not be set on top. For example, user can drag images to Library and - # the folder stays on top of the group creation box. This piece of code fixes this issue. - if is_win(): - self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) - self.setWindowState(QtCore.Qt.WindowNoState) - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - files = [] - for url in event.mimeData().urls(): - local_file = url.toLocalFile() - if os.path.isfile(local_file): - files.append(local_file) - elif os.path.isdir(local_file): - listing = os.listdir(local_file) - for file_name in listing: - files.append(os.path.join(local_file, file_name)) - Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())}) - elif self.allow_internal_dnd: - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos())) - else: - event.ignore() - - # Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple. - def addItem(self, item): - self.addTopLevelItem(item) - - def count(self): - return self.topLevelItemCount() - - def item(self, index): - return self.topLevelItem(index) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index d7f2264ee..80cccdb4c 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -29,16 +29,15 @@ Some of the code for this form is based on the examples at: """ import html import logging -import os from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultimedia from openlp.core.common import is_macosx, is_win from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate -from openlp.core.common.mixins import OpenLPMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import path_to_str -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import ServiceItem, ImageSource, build_html, expand_tags, image_to_byte @@ -131,7 +130,7 @@ class Display(QtWidgets.QGraphicsView): self.web_loaded = True -class MainDisplay(OpenLPMixin, Display, RegistryProperties): +class MainDisplay(Display, LogMixin, RegistryProperties): """ This is the display screen as a specialized class from the Display class """ @@ -398,6 +397,8 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): def preview(self): """ Generates a preview of the image displayed. + + :rtype: QtGui.QPixmap """ was_visible = self.isVisible() self.application.process_events() @@ -488,8 +489,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): service_item = ServiceItem() service_item.title = 'webkit' service_item.processor = 'webkit' - path = os.path.join(str(AppLocation.get_section_data_path('themes')), - self.service_item.theme_data.theme_name) + path = str(AppLocation.get_section_data_path('themes') / self.service_item.theme_data.theme_name) service_item.add_from_command(path, path_to_str(self.service_item.theme_data.background_filename), ':/media/slidecontroller_multimedia.png') @@ -603,7 +603,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): self.web_view.setGeometry(0, 0, self.width(), self.height()) -class AudioPlayer(OpenLPMixin, QtCore.QObject): +class AudioPlayer(LogMixin, QtCore.QObject): """ This Class will play audio only allowing components to work with a soundtrack independent of the user interface. """ diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 26a8b921a..ee07cbd69 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -40,22 +40,22 @@ from openlp.core.common import is_win, is_macosx, add_actions from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import LanguageManager, UiStrings, translate -from openlp.core.common.path import Path, copyfile, create_paths, path_to_str, str_to_path -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.path import Path, copyfile, create_paths +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.display.renderer import Renderer from openlp.core.lib import PluginManager, ImageManager, PluginStatus, build_icon from openlp.core.lib.ui import create_action +from openlp.core.projectors.manager import ProjectorManager from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController from openlp.core.ui.firsttimeform import FirstTimeForm -from openlp.core.ui.lib.dockwidget import OpenLPDockWidget -from openlp.core.ui.lib.filedialog import FileDialog -from openlp.core.ui.lib.mediadockmanager import MediaDockManager +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.docks import OpenLPDockWidget, MediaDockManager from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm -from openlp.core.ui.projector.manager import ProjectorManager from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet from openlp.core.version import get_version @@ -180,7 +180,7 @@ class Ui_MainWindow(object): triggers=self.service_manager_contents.on_load_service_clicked) self.file_save_item = create_action(main_window, 'fileSaveItem', icon=':/general/general_save.png', can_shortcuts=True, category=UiStrings().File, - triggers=self.service_manager_contents.save_file) + triggers=self.service_manager_contents.decide_save_method) self.file_save_as_item = create_action(main_window, 'fileSaveAsItem', can_shortcuts=True, category=UiStrings().File, triggers=self.service_manager_contents.save_file_as) @@ -296,10 +296,9 @@ class Ui_MainWindow(object): # Give QT Extra Hint that this is an About Menu Item self.about_item.setMenuRole(QtWidgets.QAction.AboutRole) if is_win(): - self.local_help_file = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), 'OpenLP.chm') + self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / 'OpenLP.chm' elif is_macosx(): - self.local_help_file = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), - '..', 'Resources', 'OpenLP.help') + self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / '..' / 'Resources' / 'OpenLP.help' self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png', can_shortcuts=True, category=UiStrings().Help, triggers=self.on_help_clicked) @@ -375,7 +374,7 @@ class Ui_MainWindow(object): self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library')) self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service')) self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Themes')) - self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projectors')) + self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projector Controller')) self.file_new_item.setText(translate('OpenLP.MainWindow', '&New Service')) self.file_new_item.setToolTip(UiStrings().NewService) self.file_new_item.setStatusTip(UiStrings().CreateService) @@ -407,7 +406,7 @@ class Ui_MainWindow(object): translate('OpenLP.MainWindow', 'Import settings from a *.config file previously exported from ' 'this or another machine.')) self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings')) - self.view_projector_manager_item.setText(translate('OpenLP.MainWindow', '&Projectors')) + self.view_projector_manager_item.setText(translate('OpenLP.MainWindow', '&Projector Controller')) self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Hide or show Projectors.')) self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow', 'Toggle visibility of the Projectors.')) @@ -505,9 +504,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): Settings().set_up_default_values() self.about_form = AboutForm(self) MediaController() - if Registry().get_flag('no_web_server'): - websockets.WebSocketServer() - server.HttpServer() + websockets.WebSocketServer() + server.HttpServer() SettingsForm(self) self.formatting_tag_form = FormattingTagForm(self) self.shortcut_form = ShortcutListForm(self) @@ -650,8 +648,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.application.process_events() plugin.first_time() self.application.process_events() - temp_dir = os.path.join(str(gettempdir()), 'openlp') - shutil.rmtree(temp_dir, True) + temp_path = Path(gettempdir(), 'openlp') + temp_path.rmtree(True) def on_first_time_wizard_clicked(self): """ @@ -760,7 +758,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): Use the Online manual in other cases. (Linux) """ if is_macosx() or is_win(): - QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(self.local_help_file)) + QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(str(self.local_help_file))) else: import webbrowser webbrowser.open_new('http://manual.openlp.org/') @@ -1220,7 +1218,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): settings.remove('custom slide') settings.remove('service') settings.beginGroup(self.general_settings_section) - self.recent_files = [path_to_str(file_path) for file_path in settings.value('recent files')] + self.recent_files = settings.value('recent files') settings.endGroup() settings.beginGroup(self.ui_settings_section) self.move(settings.value('main window position')) @@ -1244,7 +1242,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): log.debug('Saving QSettings') settings = Settings() settings.beginGroup(self.general_settings_section) - settings.setValue('recent files', [str_to_path(file) for file in self.recent_files]) + settings.setValue('recent files', self.recent_files) settings.endGroup() settings.beginGroup(self.ui_settings_section) settings.setValue('main window position', self.pos()) @@ -1260,26 +1258,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): Updates the recent file menu with the latest list of service files accessed. """ recent_file_count = Settings().value('advanced/recent file count') - existing_recent_files = [recentFile for recentFile in self.recent_files if os.path.isfile(str(recentFile))] - recent_files_to_display = existing_recent_files[0:recent_file_count] self.recent_files_menu.clear() - for file_id, filename in enumerate(recent_files_to_display): - log.debug('Recent file name: {name}'.format(name=filename)) + count = 0 + for recent_path in self.recent_files: + if not recent_path.is_file(): + continue + count += 1 + log.debug('Recent file name: {name}'.format(name=recent_path)) action = create_action(self, '', - text='&{n} {name}'.format(n=file_id + 1, - name=os.path.splitext(os.path.basename(str(filename)))[0]), - data=filename, - triggers=self.service_manager_contents.on_recent_service_clicked) + text='&{n} {name}'.format(n=count, name=recent_path.name), + data=recent_path, triggers=self.service_manager_contents.on_recent_service_clicked) self.recent_files_menu.addAction(action) - clear_recent_files_action = create_action(self, '', - text=translate('OpenLP.MainWindow', 'Clear List', 'Clear List of ' - 'recent files'), - statustip=translate('OpenLP.MainWindow', 'Clear the list of recent ' - 'files.'), - enabled=bool(self.recent_files), - triggers=self.clear_recent_file_menu) + if count == recent_file_count: + break + clear_recent_files_action = \ + create_action(self, '', text=translate('OpenLP.MainWindow', 'Clear List', 'Clear List of recent files'), + statustip=translate('OpenLP.MainWindow', 'Clear the list of recent files.'), + enabled=bool(self.recent_files), triggers=self.clear_recent_file_menu) add_actions(self.recent_files_menu, (None, clear_recent_files_action)) - clear_recent_files_action.setEnabled(bool(self.recent_files)) def add_recent_file(self, filename): """ @@ -1291,20 +1287,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): # actually stored in the settings therefore the default value of 20 will # always be used. max_recent_files = Settings().value('advanced/max recent files') - if filename: - # Add some cleanup to reduce duplication in the recent file list - filename = os.path.abspath(filename) - # abspath() only capitalises the drive letter if it wasn't provided - # in the given filename which then causes duplication. - if filename[1:3] == ':\\': - filename = filename[0].upper() + filename[1:] - if filename in self.recent_files: - self.recent_files.remove(filename) - if not isinstance(self.recent_files, list): - self.recent_files = [self.recent_files] - self.recent_files.insert(0, filename) - while len(self.recent_files) > max_recent_files: - self.recent_files.pop() + file_path = Path(filename) + # Some cleanup to reduce duplication in the recent file list + file_path = file_path.resolve() + if file_path in self.recent_files: + self.recent_files.remove(file_path) + self.recent_files.insert(0, file_path) + self.recent_files = self.recent_files[:max_recent_files] def clear_recent_file_menu(self): """ @@ -1367,7 +1356,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): '- Please wait for copy to finish').format(path=self.new_data_path)) dir_util.copy_tree(str(old_data_path), str(self.new_data_path)) log.info('Copy successful') - except (IOError, os.error, DistutilsFileError) as why: + except (OSError, DistutilsFileError) as why: self.application.set_normal_cursor() log.exception('Data copy failed {err}'.format(err=str(why))) err_text = translate('OpenLP.MainWindow', diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index bdc315556..10c384b0c 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -31,8 +31,8 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.api.http import register_endpoint from openlp.core.common import extension_loader from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box @@ -42,7 +42,7 @@ from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ parse_optical_path -from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.widgets.toolbar import OpenLPToolbar log = logging.getLogger(__name__) @@ -91,7 +91,7 @@ class MediaSlider(QtWidgets.QSlider): QtWidgets.QSlider.mouseReleaseEvent(self, event) -class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): +class MediaController(RegistryBase, LogMixin, RegistryProperties): """ The implementation of the Media Controller. The Media Controller adds an own class for every Player. Currently these are QtWebkit, Phonon and Vlc. display_controllers are an array of controllers keyed on the @@ -498,8 +498,6 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param controller: The media controller. :return: True if setup succeeded else False. """ - if controller is None: - controller = self.display_controllers[DisplayControllerType.Plugin] # stop running videos self.media_reset(controller) # Setup media info @@ -509,9 +507,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info.media_type = MediaType.CD else: controller.media_info.media_type = MediaType.DVD - controller.media_info.start_time = start // 1000 - controller.media_info.end_time = end // 1000 - controller.media_info.length = (end - start) // 1000 + controller.media_info.start_time = start + controller.media_info.end_time = end + controller.media_info.length = (end - start) controller.media_info.title_track = title controller.media_info.audio_track = audio_track controller.media_info.subtitle_track = subtitle_track diff --git a/openlp/core/ui/media/mediaplayer.py b/openlp/core/ui/media/mediaplayer.py index e4d210513..77d089c89 100644 --- a/openlp/core/ui/media/mediaplayer.py +++ b/openlp/core/ui/media/mediaplayer.py @@ -22,7 +22,7 @@ """ The :mod:`~openlp.core.ui.media.mediaplayer` module contains the MediaPlayer class. """ -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.ui.media import MediaState diff --git a/openlp/core/ui/media/playertab.py b/openlp/core/ui/media/playertab.py index f719a167b..28d7798ee 100644 --- a/openlp/core/ui/media/playertab.py +++ b/openlp/core/ui/media/playertab.py @@ -23,6 +23,7 @@ The :mod:`~openlp.core.ui.media.playertab` module holds the configuration tab for the media stuff. """ import platform + from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate @@ -31,7 +32,7 @@ from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab from openlp.core.lib.ui import create_button from openlp.core.ui.media import get_media_players, set_media_players -from openlp.core.ui.lib.colorbutton import ColorButton +from openlp.core.widgets.buttons import ColorButton class MediaQCheckBox(QtWidgets.QCheckBox): diff --git a/openlp/core/ui/media/vendor/mediainfoWrapper.py b/openlp/core/ui/media/vendor/mediainfoWrapper.py index 6f270d46e..97d936d6b 100644 --- a/openlp/core/ui/media/vendor/mediainfoWrapper.py +++ b/openlp/core/ui/media/vendor/mediainfoWrapper.py @@ -25,10 +25,8 @@ information related to the rwquested media. """ import json import os -from subprocess import Popen -from tempfile import mkstemp +from subprocess import check_output -import six from bs4 import BeautifulSoup, NavigableString ENV_DICT = os.environ @@ -80,7 +78,7 @@ class Track(object): def to_data(self): data = {} - for k, v in six.iteritems(self.__dict__): + for k, v in self.__dict__.items(): if k != 'xml_dom_fragment': data[k] = v return data @@ -100,20 +98,10 @@ class MediaInfoWrapper(object): @staticmethod def parse(filename, environment=ENV_DICT): - command = ["mediainfo", "-f", "--Output=XML", filename] - fileno_out, fname_out = mkstemp(suffix=".xml", prefix="media-") - fileno_err, fname_err = mkstemp(suffix=".err", prefix="media-") - fp_out = os.fdopen(fileno_out, 'r+b') - fp_err = os.fdopen(fileno_err, 'r+b') - p = Popen(command, stdout=fp_out, stderr=fp_err, env=environment) - p.wait() - fp_out.seek(0) - - xml_dom = MediaInfoWrapper.parse_xml_data_into_dom(fp_out.read()) - fp_out.close() - fp_err.close() - os.unlink(fname_out) - os.unlink(fname_err) + xml = check_output(['mediainfo', '-f', '--Output=XML', '--Inform=OLDXML', filename]) + if not xml.startswith(b'<?xml'): + xml = check_output(['mediainfo', '-f', '--Output=XML', filename]) + xml_dom = MediaInfoWrapper.parse_xml_data_into_dom(xml) return MediaInfoWrapper(xml_dom) def _populate_tracks(self): diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 605f6a8a1..840471cfe 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -280,7 +280,8 @@ class VlcPlayer(MediaPlayer): start_time = controller.media_info.start_time log.debug('mediatype: ' + str(controller.media_info.media_type)) # Set tracks for the optical device - if controller.media_info.media_type == MediaType.DVD: + if controller.media_info.media_type == MediaType.DVD and \ + self.get_live_state() != MediaState.Paused and self.get_preview_state() != MediaState.Paused: log.debug('vlc play, playing started') if controller.media_info.title_track > 0: log.debug('vlc play, title_track set: ' + str(controller.media_info.title_track)) @@ -350,7 +351,7 @@ class VlcPlayer(MediaPlayer): """ if display.controller.media_info.media_type == MediaType.CD \ or display.controller.media_info.media_type == MediaType.DVD: - seek_value += int(display.controller.media_info.start_time * 1000) + seek_value += int(display.controller.media_info.start_time) if display.vlc_media_player.is_seekable(): display.vlc_media_player.set_time(seek_value) @@ -386,15 +387,15 @@ class VlcPlayer(MediaPlayer): self.stop(display) controller = display.controller if controller.media_info.end_time > 0: - if display.vlc_media_player.get_time() > controller.media_info.end_time * 1000: + if display.vlc_media_player.get_time() > controller.media_info.end_time: self.stop(display) self.set_visible(display, False) if not controller.seek_slider.isSliderDown(): controller.seek_slider.blockSignals(True) if display.controller.media_info.media_type == MediaType.CD \ or display.controller.media_info.media_type == MediaType.DVD: - controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time() - - int(display.controller.media_info.start_time * 1000)) + controller.seek_slider.setSliderPosition( + display.vlc_media_player.get_time() - int(display.controller.media_info.start_time)) else: controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) controller.seek_slider.blockSignals(False) diff --git a/openlp/core/ui/pluginform.py b/openlp/core/ui/pluginform.py index b34d79714..43fa5e68d 100644 --- a/openlp/core/ui/pluginform.py +++ b/openlp/core/ui/pluginform.py @@ -27,7 +27,7 @@ import logging from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.lib import PluginStatus from openlp.core.ui.plugindialog import Ui_PluginViewDialog diff --git a/openlp/core/ui/printservicedialog.py b/openlp/core/ui/printservicedialog.py index d71b42cdb..ee3b07080 100644 --- a/openlp/core/ui/printservicedialog.py +++ b/openlp/core/ui/printservicedialog.py @@ -26,7 +26,7 @@ from PyQt5 import QtCore, QtWidgets, QtPrintSupport from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.widgets.edits import SpellTextEdit class ZoomSize(object): diff --git a/openlp/core/ui/printserviceform.py b/openlp/core/ui/printserviceform.py index 482d0f084..07ab24496 100644 --- a/openlp/core/ui/printserviceform.py +++ b/openlp/core/ui/printserviceform.py @@ -30,7 +30,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets, QtPrintSupport from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import get_text_file_string from openlp.core.ui.printservicedialog import Ui_PrintServiceDialog, ZoomSize diff --git a/openlp/core/ui/serviceitemeditform.py b/openlp/core/ui/serviceitemeditform.py index 17f648ecc..0a4b7cab6 100644 --- a/openlp/core/ui/serviceitemeditform.py +++ b/openlp/core/ui/serviceitemeditform.py @@ -24,7 +24,8 @@ The service item edit dialog """ from PyQt5 import QtCore, QtWidgets -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.ui.serviceitemeditdialog import Ui_ServiceItemEditDialog diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index ff6ab9a47..29718e09a 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -32,19 +32,19 @@ from tempfile import mkstemp from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import ThemeLevel, split_filename, delete_file +from openlp.core.common import ThemeLevel, delete_file from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, format_time, translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.path import Path, create_paths, path_to_str, str_to_path -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.path import Path, create_paths, str_to_path +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm -from openlp.core.ui.lib import OpenLPToolbar -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.toolbar import OpenLPToolbar class ServiceManagerList(QtWidgets.QTreeWidget): @@ -56,8 +56,24 @@ class ServiceManagerList(QtWidgets.QTreeWidget): Constructor """ super(ServiceManagerList, self).__init__(parent) + self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) + self.setAlternatingRowColors(True) + self.setHeaderHidden(True) + self.setExpandsOnDoubleClick(False) self.service_manager = service_manager + def dragEnterEvent(self, event): + """ + React to a drag enter event + """ + event.accept() + + def dragMoveEvent(self, event): + """ + React to a drage move event + """ + event.accept() + def keyPressEvent(self, event): """ Capture Key press and respond accordingly. @@ -117,7 +133,7 @@ class Ui_ServiceManager(object): self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) # Create the top toolbar - self.toolbar = OpenLPToolbar(widget) + self.toolbar = OpenLPToolbar(self) self.toolbar.add_toolbar_action('newService', text=UiStrings().NewService, icon=':/general/general_new.png', tooltip=UiStrings().CreateService, triggers=self.on_new_service_clicked) self.toolbar.add_toolbar_action('openService', text=UiStrings().OpenService, @@ -147,78 +163,60 @@ class Ui_ServiceManager(object): QtWidgets.QAbstractItemView.CurrentChanged | QtWidgets.QAbstractItemView.DoubleClicked | QtWidgets.QAbstractItemView.EditKeyPressed) - self.service_manager_list.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) - self.service_manager_list.setAlternatingRowColors(True) - self.service_manager_list.setHeaderHidden(True) - self.service_manager_list.setExpandsOnDoubleClick(False) self.service_manager_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.service_manager_list.customContextMenuRequested.connect(self.context_menu) self.service_manager_list.setObjectName('service_manager_list') # enable drop - self.service_manager_list.__class__.dragEnterEvent = lambda x, event: event.accept() - self.service_manager_list.__class__.dragMoveEvent = lambda x, event: event.accept() - self.service_manager_list.__class__.dropEvent = self.drop_event + self.service_manager_list.dropEvent = self.drop_event self.layout.addWidget(self.service_manager_list) # Add the bottom toolbar self.order_toolbar = OpenLPToolbar(widget) action_list = ActionList.get_instance() action_list.add_category(UiStrings().Service, CategoryOrder.standard_toolbar) - self.service_manager_list.move_top = self.order_toolbar.add_toolbar_action( + self.move_top_action = self.order_toolbar.add_toolbar_action( 'moveTop', text=translate('OpenLP.ServiceManager', 'Move to &top'), icon=':/services/service_top.png', tooltip=translate('OpenLP.ServiceManager', 'Move item to the top of the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_top) - self.service_manager_list.move_up = self.order_toolbar.add_toolbar_action( + self.move_up_action = self.order_toolbar.add_toolbar_action( 'moveUp', text=translate('OpenLP.ServiceManager', 'Move &up'), icon=':/services/service_up.png', tooltip=translate('OpenLP.ServiceManager', 'Move item up one position in the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_up) - self.service_manager_list.move_down = self.order_toolbar.add_toolbar_action( + self.move_down_action = self.order_toolbar.add_toolbar_action( 'moveDown', text=translate('OpenLP.ServiceManager', 'Move &down'), icon=':/services/service_down.png', tooltip=translate('OpenLP.ServiceManager', 'Move item down one position in the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_down) - self.service_manager_list.move_bottom = self.order_toolbar.add_toolbar_action( + self.move_bottom_action = self.order_toolbar.add_toolbar_action( 'moveBottom', text=translate('OpenLP.ServiceManager', 'Move to &bottom'), icon=':/services/service_bottom.png', tooltip=translate('OpenLP.ServiceManager', 'Move item to the end of the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_end) - self.service_manager_list.down = self.order_toolbar.add_toolbar_action( - 'down', - text=translate('OpenLP.ServiceManager', 'Move &down'), can_shortcuts=True, - tooltip=translate('OpenLP.ServiceManager', 'Moves the selection down the window.'), visible=False, - triggers=self.on_move_selection_down) - action_list.add_action(self.service_manager_list.down) - self.service_manager_list.up = self.order_toolbar.add_toolbar_action( - 'up', - text=translate('OpenLP.ServiceManager', 'Move up'), can_shortcuts=True, - tooltip=translate('OpenLP.ServiceManager', 'Moves the selection up the window.'), visible=False, - triggers=self.on_move_selection_up) - action_list.add_action(self.service_manager_list.up) self.order_toolbar.addSeparator() - self.service_manager_list.delete = self.order_toolbar.add_toolbar_action( + self.delete_action = self.order_toolbar.add_toolbar_action( 'delete', can_shortcuts=True, text=translate('OpenLP.ServiceManager', '&Delete From Service'), icon=':/general/general_delete.png', tooltip=translate('OpenLP.ServiceManager', 'Delete the selected item from the service.'), triggers=self.on_delete_from_service) self.order_toolbar.addSeparator() - self.service_manager_list.expand = self.order_toolbar.add_toolbar_action( + self.expand_action = self.order_toolbar.add_toolbar_action( 'expand', can_shortcuts=True, text=translate('OpenLP.ServiceManager', '&Expand all'), icon=':/services/service_expand_all.png', tooltip=translate('OpenLP.ServiceManager', 'Expand all the service items.'), category=UiStrings().Service, triggers=self.on_expand_all) - self.service_manager_list.collapse = self.order_toolbar.add_toolbar_action( + self.collapse_action = self.order_toolbar.add_toolbar_action( 'collapse', can_shortcuts=True, text=translate('OpenLP.ServiceManager', '&Collapse all'), icon=':/services/service_collapse_all.png', tooltip=translate('OpenLP.ServiceManager', 'Collapse all the service items.'), category=UiStrings().Service, triggers=self.on_collapse_all) self.order_toolbar.addSeparator() - self.service_manager_list.make_live = self.order_toolbar.add_toolbar_action( + self.make_live_action = self.order_toolbar.add_toolbar_action( 'make_live', can_shortcuts=True, text=translate('OpenLP.ServiceManager', 'Go Live'), icon=':/general/general_live.png', tooltip=translate('OpenLP.ServiceManager', 'Send the selected item to Live.'), category=UiStrings().Service, - triggers=self.make_live) + triggers=self.on_make_live_action_triggered) self.layout.addWidget(self.order_toolbar) # Connect up our signals and slots self.theme_combo_box.activated.connect(self.on_theme_combo_box_selected) @@ -254,7 +252,7 @@ class Ui_ServiceManager(object): icon=':/media/auto-start_active.png', triggers=self.on_auto_start) # Add already existing delete action to the menu. - self.menu.addAction(self.service_manager_list.delete) + self.menu.addAction(self.delete_action) self.create_custom_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Create New &Custom ' 'Slide'), @@ -285,28 +283,20 @@ class Ui_ServiceManager(object): self.preview_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'), icon=':/general/general_preview.png', triggers=self.make_preview) # Add already existing make live action to the menu. - self.menu.addAction(self.service_manager_list.make_live) + self.menu.addAction(self.make_live_action) self.menu.addSeparator() self.theme_menu = QtWidgets.QMenu(translate('OpenLP.ServiceManager', '&Change Item Theme')) self.menu.addMenu(self.theme_menu) - self.service_manager_list.addActions( - [self.service_manager_list.move_down, - self.service_manager_list.move_up, - self.service_manager_list.make_live, - self.service_manager_list.move_top, - self.service_manager_list.move_bottom, - self.service_manager_list.up, - self.service_manager_list.down, - self.service_manager_list.expand, - self.service_manager_list.collapse - ]) + self.service_manager_list.addActions([self.move_down_action, self.move_up_action, self.make_live_action, + self.move_top_action, self.move_bottom_action, self.expand_action, + self.collapse_action]) Registry().register_function('theme_update_list', self.update_theme_list) Registry().register_function('config_screen_changed', self.regenerate_service_items) Registry().register_function('theme_update_global', self.theme_change) Registry().register_function('mediaitem_suffix_reset', self.reset_supported_suffixes) -class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceManager, RegistryProperties): +class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixin, RegistryProperties): """ Manages the services. This involves taking text strings from plugins and adding them to the service. This service can then be zipped up with all the resources used into one OSZ or oszl file for use on any OpenLP v2 installation. @@ -320,7 +310,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ Sets up the service manager, toolbars, list view, et al. """ - super(ServiceManager, self).__init__(parent) + super().__init__(parent) self.active = build_icon(':/media/auto-start_active.png') self.inactive = build_icon(':/media/auto-start_inactive.png') self.service_items = [] @@ -329,7 +319,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa self.service_id = 0 # is a new service and has not been saved self._modified = False - self._file_name = '' + self._service_path = None self.service_has_all_original_files = True self.list_double_clicked = False @@ -360,7 +350,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa if modified: self.service_id += 1 self._modified = modified - service_file = self.short_file_name() or translate('OpenLP.ServiceManager', 'Untitled Service') + if self._service_path: + service_file = self._service_path.name + else: + service_file = translate('OpenLP.ServiceManager', 'Untitled Service') self.main_window.set_service_modified(modified, service_file) def is_modified(self): @@ -376,8 +369,8 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa :param openlp.core.common.path.Path file_path: The service file name :rtype: None """ - self._file_name = path_to_str(file_path) - self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) + self._service_path = file_path + self.main_window.set_service_modified(self.is_modified(), file_path.name) Settings().setValue('servicemanager/last file', file_path) if file_path and file_path.suffix == '.oszl': self._save_lite = True @@ -387,14 +380,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa def file_name(self): """ Return the current file name including path. + + :rtype: openlp.core.common.path.Path """ - return self._file_name + return self._service_path def short_file_name(self): """ Return the current file name, excluding the path. """ - return split_filename(self._file_name)[1] + if self._service_path: + return self._service_path.name def reset_supported_suffixes(self): """ @@ -472,6 +468,12 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa Load a recent file as the service triggered by mainwindow recent service list. :param field: """ + if self.is_modified(): + result = self.save_modified_service() + if result == QtWidgets.QMessageBox.Cancel: + return False + elif result == QtWidgets.QMessageBox.Save: + self.decide_save_method() sender = self.sender() self.load_file(sender.data()) @@ -601,7 +603,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa if not os.path.exists(save_file): shutil.copy(audio_from, save_file) zip_file.write(audio_from, audio_to) - except IOError: + except OSError: self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name)) self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'), translate('OpenLP.ServiceManager', 'There was an error saving your file.')) @@ -662,7 +664,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa zip_file = zipfile.ZipFile(temp_file_name, 'w', zipfile.ZIP_STORED, True) # First we add service contents. zip_file.writestr(service_file_name, service_content) - except IOError: + except OSError: self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name)) self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'), translate('OpenLP.ServiceManager', 'There was an error saving your file.')) @@ -708,20 +710,29 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory') if directory_path: default_file_path = directory_path / default_file_path + lite_filter = translate('OpenLP.ServiceManager', 'OpenLP Service Files - lite (*.oszl)') + packaged_filter = translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz)') + if self._service_path and self._service_path.suffix == '.oszl': + default_filter = lite_filter + else: + default_filter = packaged_filter # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # the long term. - if self._file_name.endswith('oszl') or self.service_has_all_original_files: + if self._service_path and self._service_path.suffix == '.oszl' or self.service_has_all_original_files: file_path, filter_used = FileDialog.getSaveFileName( self.main_window, UiStrings().SaveService, default_file_path, - translate('OpenLP.ServiceManager', - 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) + '{packaged};; {lite}'.format(packaged=packaged_filter, lite=lite_filter), + default_filter) else: file_path, filter_used = FileDialog.getSaveFileName( - self.main_window, UiStrings().SaveService, file_path, - translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;')) + self.main_window, UiStrings().SaveService, default_file_path, + '{packaged};;'.format(packaged=packaged_filter)) if not file_path: return False - file_path.with_suffix('.osz') + if filter_used == lite_filter: + file_path = file_path.with_suffix('.oszl') + else: + file_path = file_path.with_suffix('.osz') self.set_file_name(file_path) self.decide_save_method() @@ -789,11 +800,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa else: critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.')) self.log_error('File contains no service data') - except (IOError, NameError): + except (OSError, NameError): self.log_exception('Problem loading service file {name}'.format(name=file_name)) critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File could not be opened because it is corrupt.')) - except zipfile.BadZipfile: + except zipfile.BadZipFile: if os.path.getsize(file_name) == 0: self.log_exception('Service file is zero sized: {name}'.format(name=file_name)) QtWidgets.QMessageBox.information(self, translate('OpenLP.ServiceManager', 'Empty File'), @@ -1655,14 +1666,15 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa if start_pos == -1: return if item is None: - end_pos = len(self.service_items) + end_pos = len(self.service_items) - 1 else: end_pos = get_parent_item_data(item) - 1 service_item = self.service_items[start_pos] - self.service_items.remove(service_item) - self.service_items.insert(end_pos, service_item) - self.repaint_service_list(end_pos, child) - self.set_modified() + if start_pos != end_pos: + self.service_items.remove(service_item) + self.service_items.insert(end_pos, service_item) + self.repaint_service_list(end_pos, child) + self.set_modified() else: # we are not over anything so drop replace = False @@ -1728,6 +1740,15 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa self.service_items[item]['service_item'].update_theme(theme) self.regenerate_service_items(True) + def on_make_live_action_triggered(self, checked): + """ + Handle `make_live_action` when the action is triggered. + + :param bool checked: Not Used. + :rtype: None + """ + self.make_live() + def get_drop_position(self): """ Getter for drop_position. Used in: MediaManagerItem diff --git a/openlp/core/ui/servicenoteform.py b/openlp/core/ui/servicenoteform.py index 998431636..179248570 100644 --- a/openlp/core/ui/servicenoteform.py +++ b/openlp/core/ui/servicenoteform.py @@ -25,9 +25,10 @@ The :mod:`~openlp.core.ui.servicenoteform` module contains the `ServiceNoteForm` from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import create_button_box +from openlp.core.widgets.edits import SpellTextEdit class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties): diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index 5524b89ad..4d7c4ff6c 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -27,11 +27,12 @@ import logging from PyQt5 import QtCore, QtWidgets from openlp.core.api import ApiTab -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib import build_icon +from openlp.core.projectors.tab import ProjectorTab from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab from openlp.core.ui.media import PlayerTab -from openlp.core.ui.projector.tab import ProjectorTab from openlp.core.ui.settingsdialog import Ui_SettingsDialog log = logging.getLogger(__name__) diff --git a/openlp/core/ui/shortcutlistform.py b/openlp/core/ui/shortcutlistform.py index 3db127d75..92a0e789f 100644 --- a/openlp/core/ui/shortcutlistform.py +++ b/openlp/core/ui/shortcutlistform.py @@ -29,7 +29,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common.actions import ActionList from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.ui.shortcutlistdialog import Ui_ShortcutListDialog diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 062adbd71..16c710754 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -22,7 +22,6 @@ """ The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller """ - import copy import os from collections import deque @@ -33,15 +32,15 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import SlideLimits from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, build_icon, build_html from openlp.core.lib.ui import create_action -from openlp.core.ui.lib.toolbar import OpenLPToolbar -from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType +from openlp.core.widgets.toolbar import OpenLPToolbar +from openlp.core.widgets.views import ListPreviewWidget # Threshold which has to be trespassed to toggle. @@ -82,11 +81,11 @@ class DisplayController(QtWidgets.QWidget): """ Controller is a general display controller widget. """ - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the general Controller. """ - super(DisplayController, self).__init__(parent) + super().__init__(*args, **kwargs) self.is_live = False self.display = None self.controller_type = None @@ -133,16 +132,16 @@ class InfoLabel(QtWidgets.QLabel): super().setText(text) -class SlideController(DisplayController, RegistryProperties): +class SlideController(DisplayController, LogMixin, RegistryProperties): """ SlideController is the slide controller widget. This widget is what the user uses to control the displaying of verses/slides/etc on the screen. """ - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the Slide Controller. """ - super(SlideController, self).__init__(parent) + super().__init__(*args, **kwargs) def post_set_up(self): """ @@ -237,6 +236,9 @@ class SlideController(DisplayController, RegistryProperties): self.hide_menu.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) self.hide_menu.setMenu(QtWidgets.QMenu(translate('OpenLP.SlideController', 'Hide'), self.toolbar)) self.toolbar.add_toolbar_widget(self.hide_menu) + self.toolbar.add_toolbar_action('goPreview', icon=':/general/general_live.png', + tooltip=translate('OpenLP.SlideController', 'Move to preview.'), + triggers=self.on_go_preview) # The order of the blank to modes in Shortcuts list comes from here. self.desktop_screen_enable = create_action(self, 'desktopScreenEnable', text=translate('OpenLP.SlideController', 'Show Desktop'), @@ -1429,6 +1431,15 @@ class SlideController(DisplayController, RegistryProperties): self.live_controller.add_service_manager_item(self.service_item, row) self.live_controller.preview_widget.setFocus() + def on_go_preview(self, field=None): + """ + If live copy slide item to preview controller from live Controller + """ + row = self.preview_widget.current_slide_number() + if -1 < row < self.preview_widget.slide_count(): + self.preview_controller.add_service_manager_item(self.service_item, row) + self.preview_controller.preview_widget.setFocus() + def on_media_start(self, item): """ Respond to the arrival of a media service item @@ -1513,7 +1524,7 @@ class SlideController(DisplayController, RegistryProperties): self.display.audio_player.go_to(action.data()) -class PreviewController(RegistryMixin, OpenLPMixin, SlideController): +class PreviewController(RegistryBase, SlideController): """ Set up the Preview Controller. """ @@ -1521,11 +1532,12 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController): slidecontroller_preview_next = QtCore.pyqtSignal() slidecontroller_preview_previous = QtCore.pyqtSignal() - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the base Controller as a preview. """ - super(PreviewController, self).__init__(parent) + self.__registry_name = 'preview_slidecontroller' + super().__init__(*args, **kwargs) self.split = 0 self.type_prefix = 'preview' self.category = 'Preview Toolbar' @@ -1537,7 +1549,7 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController): self.post_set_up() -class LiveController(RegistryMixin, OpenLPMixin, SlideController): +class LiveController(RegistryBase, SlideController): """ Set up the Live Controller. """ @@ -1549,11 +1561,11 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController): mediacontroller_live_pause = QtCore.pyqtSignal() mediacontroller_live_stop = QtCore.pyqtSignal() - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the base Controller as a live. """ - super(LiveController, self).__init__(parent) + super().__init__(*args, **kwargs) self.is_live = True self.split = 1 self.type_prefix = 'live' diff --git a/openlp/core/ui/starttimeform.py b/openlp/core/ui/starttimeform.py index e1ad9d9a6..00d0bff40 100644 --- a/openlp/core/ui/starttimeform.py +++ b/openlp/core/ui/starttimeform.py @@ -25,7 +25,8 @@ The actual start time form. from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.starttimedialog import Ui_StartTimeDialog diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index c15110569..f8449986a 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -28,7 +28,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import get_images_filter, is_not_image_file from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import ThemeLayoutForm diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index f27eb6abb..fae75f81d 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -31,17 +31,17 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import delete_file from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate, get_locale_key -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.path import Path, copyfile, create_paths, path_to_str, rmtree -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.path import Path, copyfile, create_paths, path_to_str +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import Theme, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm -from openlp.core.ui.lib import OpenLPToolbar -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.toolbar import OpenLPToolbar class Ui_ThemeManager(object): @@ -125,7 +125,7 @@ class Ui_ThemeManager(object): self.theme_list_widget.currentItemChanged.connect(self.check_list_state) -class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManager, RegistryProperties): +class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, RegistryProperties): """ Manages the orders of Theme. """ @@ -376,7 +376,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage delete_file(self.theme_path / thumb) delete_file(self.thumb_path / thumb) try: - rmtree(self.theme_path / theme) + (self.theme_path / theme).rmtree() except OSError: self.log_exception('Error deleting theme {name}'.format(name=theme)) @@ -431,7 +431,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage 'The theme_name export failed because this error occurred: {err}') .format(err=ose.strerror)) if theme_path.exists(): - rmtree(theme_path, True) + theme_path.rmtree(ignore_errors=True) return False def on_import_theme(self, checked=None): @@ -497,12 +497,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name) else: name = text_name - thumb = self.thumb_path / '{name}.png'.format(name=text_name) + thumb_path = self.thumb_path / '{name}.png'.format(name=text_name) item_name = QtWidgets.QListWidgetItem(name) - if validate_thumb(theme_path, thumb): - icon = build_icon(thumb) + if validate_thumb(theme_path, thumb_path): + icon = build_icon(thumb_path) else: - icon = create_thumb(str(theme_path), str(thumb)) + icon = create_thumb(theme_path, thumb_path) item_name.setIcon(icon) item_name.setData(QtCore.Qt.UserRole, text_name) self.theme_list_widget.addItem(item_name) @@ -604,7 +604,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage else: with full_name.open('wb') as out_file: out_file.write(theme_zip.read(zipped_file)) - except (IOError, zipfile.BadZipfile): + except (OSError, zipfile.BadZipFile): self.log_exception('Importing theme from zip failed {name}'.format(name=file_path)) raise ValidationError except ValidationError: @@ -667,7 +667,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage theme_path = theme_dir / '{file_name}.json'.format(file_name=name) try: theme_path.write_text(theme_pretty) - except IOError: + except OSError: self.log_exception('Saving theme to file failed') if image_source_path and image_destination_path: if self.old_background_image_path and image_destination_path != self.old_background_image_path: @@ -675,7 +675,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage if image_source_path != image_destination_path: try: copyfile(image_source_path, image_destination_path) - except IOError: + except OSError: self.log_exception('Failed to save theme image') self.generate_and_save_image(name, theme) @@ -692,7 +692,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage sample_path_name.unlink() frame.save(str(sample_path_name), 'png') thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name) - create_thumb(str(sample_path_name), str(thumb_path), False) + create_thumb(sample_path_name, thumb_path, False) def update_preview_images(self): """ @@ -711,6 +711,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage :param theme_data: The theme to generated a preview for. :param force_page: Flag to tell message lines per page need to be generated. + :rtype: QtGui.QPixmap """ return self.renderer.generate_preview(theme_data, force_page) diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index c50cd1a85..d7bc5f822 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -29,7 +29,8 @@ from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets -from openlp.core.ui.lib import ColorButton, PathEdit +from openlp.core.widgets.buttons import ColorButton +from openlp.core.widgets.edits import PathEdit class Ui_ThemeWizard(object): diff --git a/openlp/core/version.py b/openlp/core/version.py index 6d038a3d9..6ba1b9ecc 100644 --- a/openlp/core/version.py +++ b/openlp/core/version.py @@ -23,7 +23,6 @@ The :mod:`openlp.core.version` module downloads the version details for OpenLP. """ import logging -import os import platform import sys import time @@ -96,7 +95,7 @@ class VersionWorker(QtCore.QObject): remote_version = response.text log.debug('New version found: %s', remote_version) break - except IOError: + except OSError: log.exception('Unable to connect to OpenLP server to download version file') retries += 1 else: @@ -176,18 +175,12 @@ def get_version(): full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip()) else: # We're not running the development version, let's use the file. - file_path = str(AppLocation.get_directory(AppLocation.VersionDir)) - file_path = os.path.join(file_path, '.version') - version_file = None + file_path = AppLocation.get_directory(AppLocation.VersionDir) / '.version' try: - version_file = open(file_path, 'r') - full_version = str(version_file.read()).rstrip() - except IOError: + full_version = file_path.read_text().rstrip() + except OSError: log.exception('Error in version file.') full_version = '0.0.0-bzr000' - finally: - if version_file: - version_file.close() bits = full_version.split('-') APPLICATION_VERSION = { 'full': full_version, diff --git a/tests/interfaces/openlp_core_common/__init__.py b/openlp/core/widgets/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_common/__init__.py rename to openlp/core/widgets/__init__.py diff --git a/openlp/core/ui/lib/colorbutton.py b/openlp/core/widgets/buttons.py similarity index 100% rename from openlp/core/ui/lib/colorbutton.py rename to openlp/core/widgets/buttons.py diff --git a/openlp/core/ui/lib/filedialog.py b/openlp/core/widgets/dialogs.py similarity index 100% rename from openlp/core/ui/lib/filedialog.py rename to openlp/core/widgets/dialogs.py diff --git a/openlp/core/ui/lib/mediadockmanager.py b/openlp/core/widgets/docks.py similarity index 76% rename from openlp/core/ui/lib/mediadockmanager.py rename to openlp/core/widgets/docks.py index 8fee1d55d..a1b4e9789 100644 --- a/openlp/core/ui/lib/mediadockmanager.py +++ b/openlp/core/widgets/docks.py @@ -20,15 +20,41 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The media manager dock. +The :mod:`~openlp.core.widgets.docks` module contains a customised base dock widget and dock widgets """ import logging -from openlp.core.lib import StringContent +from PyQt5 import QtWidgets + +from openlp.core.display.screens import ScreenList +from openlp.core.lib import StringContent, build_icon log = logging.getLogger(__name__) +class OpenLPDockWidget(QtWidgets.QDockWidget): + """ + Custom DockWidget class to handle events + """ + def __init__(self, parent=None, name=None, icon=None): + """ + Initialise the DockWidget + """ + log.debug('Initialise the %s widget' % name) + super(OpenLPDockWidget, self).__init__(parent) + if name: + self.setObjectName(name) + if icon: + self.setWindowIcon(build_icon(icon)) + # Sort out the minimum width. + screens = ScreenList() + main_window_docbars = screens.current['size'].width() // 5 + if main_window_docbars > 300: + self.setMinimumWidth(300) + else: + self.setMinimumWidth(main_window_docbars) + + class MediaDockManager(object): """ Provide a repository for MediaManagerItems diff --git a/openlp/core/widgets/edits.py b/openlp/core/widgets/edits.py new file mode 100644 index 000000000..89310a713 --- /dev/null +++ b/openlp/core/widgets/edits.py @@ -0,0 +1,583 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~openlp.core.widgets.edits` module contains all the customised edit widgets used in OpenLP +""" +import logging +import re + +from PyQt5 import QtCore, QtGui, QtWidgets + +from openlp.core.common import CONTROL_CHARS +from openlp.core.common.i18n import UiStrings, translate +from openlp.core.common.path import Path, path_to_str, str_to_path +from openlp.core.common.settings import Settings +from openlp.core.lib import FormattingTags, build_icon +from openlp.core.lib.ui import create_widget_action, create_action +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.enums import PathEditType + +try: + import enchant + from enchant import DictNotFoundError + from enchant.errors import Error + ENCHANT_AVAILABLE = True +except ImportError: + ENCHANT_AVAILABLE = False + +log = logging.getLogger(__name__) + + +class SearchEdit(QtWidgets.QLineEdit): + """ + This is a specialised QLineEdit with a "clear" button inside for searches. + """ + searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant) + cleared = QtCore.pyqtSignal() + + def __init__(self, parent, settings_section): + """ + Constructor. + """ + super().__init__(parent) + self.settings_section = settings_section + self._current_search_type = -1 + self.clear_button = QtWidgets.QToolButton(self) + self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png')) + self.clear_button.setCursor(QtCore.Qt.ArrowCursor) + self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }') + self.clear_button.resize(18, 18) + self.clear_button.hide() + self.clear_button.clicked.connect(self._on_clear_button_clicked) + self.textChanged.connect(self._on_search_edit_text_changed) + self._update_style_sheet() + self.setAcceptDrops(False) + + def _update_style_sheet(self): + """ + Internal method to update the stylesheet depending on which widgets are available and visible. + """ + frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) + right_padding = self.clear_button.width() + frame_width + if hasattr(self, 'menu_button'): + left_padding = self.menu_button.width() + stylesheet = 'QLineEdit {{ padding-left:{left}px; padding-right: {right}px; }} '.format(left=left_padding, + right=right_padding) + else: + stylesheet = 'QLineEdit {{ padding-right: {right}px; }} '.format(right=right_padding) + self.setStyleSheet(stylesheet) + msz = self.minimumSizeHint() + self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2), + max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2)) + + def resizeEvent(self, event): + """ + Reimplemented method to react to resizing of the widget. + + :param event: The event that happened. + """ + size = self.clear_button.size() + frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) + self.clear_button.move(self.rect().right() - frame_width - size.width(), + (self.rect().bottom() + 1 - size.height()) // 2) + if hasattr(self, 'menu_button'): + size = self.menu_button.size() + self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2) + + def current_search_type(self): + """ + Readonly property to return the current search type. + """ + return self._current_search_type + + def set_current_search_type(self, identifier): + """ + Set a new current search type. + + :param identifier: The search type identifier (int). + """ + menu = self.menu_button.menu() + for action in menu.actions(): + if identifier == action.data(): + self.setPlaceholderText(action.placeholder_text) + self.menu_button.setDefaultAction(action) + self._current_search_type = identifier + Settings().setValue('{section}/last used search type'.format(section=self.settings_section), identifier) + self.searchTypeChanged.emit(identifier) + return True + + def set_search_types(self, items): + """ + A list of tuples to be used in the search type menu. The first item in the list will be preselected as the + default. + + :param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon + instance or string) and a title for the item in the menu. In short, they should look like this:: + + (<identifier>, <icon>, <title>, <place holder text>) + + For instance:: + + (1, <QIcon instance>, "Titles", "Search Song Titles...") + + Or:: + + (2, ":/songs/authors.png", "Authors", "Search Authors...") + """ + menu = QtWidgets.QMenu(self) + for identifier, icon, title, placeholder in items: + action = create_widget_action( + menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered) + action.placeholder_text = placeholder + if not hasattr(self, 'menu_button'): + self.menu_button = QtWidgets.QToolButton(self) + self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png')) + self.menu_button.setCursor(QtCore.Qt.ArrowCursor) + self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup) + self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }') + self.menu_button.resize(QtCore.QSize(28, 18)) + self.menu_button.setMenu(menu) + self.set_current_search_type( + Settings().value('{section}/last used search type'.format(section=self.settings_section))) + self.menu_button.show() + self._update_style_sheet() + + def _on_search_edit_text_changed(self, text): + """ + Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide + the clear button. + + :param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit. + """ + self.clear_button.setVisible(bool(text)) + + def _on_clear_button_clicked(self): + """ + Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has + cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the + line edit. + """ + self.clear() + self.cleared.emit() + + def _on_menu_action_triggered(self): + """ + Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the + correct action on the button, and set the current search type (using the list of identifiers provided by the + developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier. + """ + for action in self.menu_button.menu().actions(): + # Why is this needed? + action.setChecked(False) + self.set_current_search_type(self.sender().data()) + + +class PathEdit(QtWidgets.QWidget): + """ + The :class:`~openlp.core.widgets.edits.PathEdit` class subclasses QWidget to create a custom widget for use when + a file or directory needs to be selected. + """ + pathChanged = QtCore.pyqtSignal(Path) + + def __init__(self, parent=None, path_type=PathEditType.Files, default_path=None, dialog_caption=None, + show_revert=True): + """ + Initialise the PathEdit widget + + :param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method. + :param str dialog_caption: Used to customise the caption in the QFileDialog. + :param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert + button is clicked + :param bool show_revert: Used to determine if the 'revert button' should be visible. + :rtype: None + """ + super().__init__(parent) + self.default_path = default_path + self.dialog_caption = dialog_caption + self._path_type = path_type + self._path = None + self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles) + self._setup(show_revert) + + def _setup(self, show_revert): + """ + Set up the widget + :param bool show_revert: Show or hide the revert button + :rtype: None + """ + widget_layout = QtWidgets.QHBoxLayout() + widget_layout.setContentsMargins(0, 0, 0, 0) + self.line_edit = QtWidgets.QLineEdit(self) + widget_layout.addWidget(self.line_edit) + self.browse_button = QtWidgets.QToolButton(self) + self.browse_button.setIcon(build_icon(':/general/general_open.png')) + widget_layout.addWidget(self.browse_button) + self.revert_button = QtWidgets.QToolButton(self) + self.revert_button.setIcon(build_icon(':/general/general_revert.png')) + self.revert_button.setVisible(show_revert) + widget_layout.addWidget(self.revert_button) + self.setLayout(widget_layout) + # Signals and Slots + self.browse_button.clicked.connect(self.on_browse_button_clicked) + self.revert_button.clicked.connect(self.on_revert_button_clicked) + self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished) + self.update_button_tool_tips() + + @QtCore.pyqtProperty('QVariant') + def path(self): + """ + A property getter method to return the selected path. + + :return: The selected path + :rtype: openlp.core.common.path.Path + """ + return self._path + + @path.setter + def path(self, path): + """ + A Property setter method to set the selected path + + :param openlp.core.common.path.Path path: The path to set the widget to + :rtype: None + """ + self._path = path + text = path_to_str(path) + self.line_edit.setText(text) + self.line_edit.setToolTip(text) + + @property + def path_type(self): + """ + A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to + selecting a file or directory. + + :return: The type selected + :rtype: PathType + """ + return self._path_type + + @path_type.setter + def path_type(self, path_type): + """ + A Property setter method to set the path type + + :param PathType path_type: The type of path to select + :rtype: None + """ + self._path_type = path_type + self.update_button_tool_tips() + + def update_button_tool_tips(self): + """ + Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised + + :rtype: None + """ + if self._path_type == PathEditType.Directories: + self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.')) + self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.')) + else: + self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.')) + self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.')) + + def on_browse_button_clicked(self): + """ + A handler to handle a click on the browse button. + + Show the QFileDialog and process the input from the user + + :rtype: None + """ + caption = self.dialog_caption + path = None + if self._path_type == PathEditType.Directories: + if not caption: + caption = translate('OpenLP.PathEdit', 'Select Directory') + path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly) + elif self._path_type == PathEditType.Files: + if not caption: + caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File') + path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters) + if path: + self.on_new_path(path) + + def on_revert_button_clicked(self): + """ + A handler to handle a click on the revert button. + + Set the new path to the value of the default_path instance variable. + + :rtype: None + """ + self.on_new_path(self.default_path) + + def on_line_edit_editing_finished(self): + """ + A handler to handle when the line edit has finished being edited. + + :rtype: None + """ + path = str_to_path(self.line_edit.text()) + self.on_new_path(path) + + def on_new_path(self, path): + """ + A method called to validate and set a new path. + + Emits the pathChanged Signal + + :param openlp.core.common.path.Path path: The new path + :rtype: None + """ + if self._path != path: + self._path = path + self.pathChanged.emit(path) + + +class SpellTextEdit(QtWidgets.QPlainTextEdit): + """ + Spell checking widget based on QPlanTextEdit. + + Based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check + """ + def __init__(self, parent=None, formatting_tags_allowed=True): + """ + Constructor. + """ + global ENCHANT_AVAILABLE + super(SpellTextEdit, self).__init__(parent) + self.formatting_tags_allowed = formatting_tags_allowed + # Default dictionary based on the current locale. + if ENCHANT_AVAILABLE: + try: + self.dictionary = enchant.Dict() + self.highlighter = Highlighter(self.document()) + self.highlighter.spelling_dictionary = self.dictionary + except (Error, DictNotFoundError): + ENCHANT_AVAILABLE = False + log.debug('Could not load default dictionary') + + def mousePressEvent(self, event): + """ + Handle mouse clicks within the text edit region. + """ + if event.button() == QtCore.Qt.RightButton: + # Rewrite the mouse event to a left button event so the cursor is moved to the location of the pointer. + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, + event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier) + QtWidgets.QPlainTextEdit.mousePressEvent(self, event) + + def contextMenuEvent(self, event): + """ + Provide the context menu for the text edit region. + """ + popup_menu = self.createStandardContextMenu() + # Select the word under the cursor. + cursor = self.textCursor() + # only select text if not already selected + if not cursor.hasSelection(): + cursor.select(QtGui.QTextCursor.WordUnderCursor) + self.setTextCursor(cursor) + # Add menu with available languages. + if ENCHANT_AVAILABLE: + lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:')) + for lang in enchant.list_languages(): + action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag) + lang_menu.addAction(action) + popup_menu.insertSeparator(popup_menu.actions()[0]) + popup_menu.insertMenu(popup_menu.actions()[0], lang_menu) + lang_menu.triggered.connect(self.set_language) + # Check if the selected word is misspelled and offer spelling suggestions if it is. + if ENCHANT_AVAILABLE and self.textCursor().hasSelection(): + text = self.textCursor().selectedText() + if not self.dictionary.check(text): + spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions')) + for word in self.dictionary.suggest(text): + action = SpellAction(word, spell_menu) + action.correct.connect(self.correct_word) + spell_menu.addAction(action) + # Only add the spelling suggests to the menu if there are suggestions. + if spell_menu.actions(): + popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) + tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags')) + if self.formatting_tags_allowed: + for html in FormattingTags.get_html_tags(): + action = SpellAction(html['desc'], tag_menu) + action.correct.connect(self.html_tag) + tag_menu.addAction(action) + popup_menu.insertSeparator(popup_menu.actions()[0]) + popup_menu.insertMenu(popup_menu.actions()[0], tag_menu) + popup_menu.exec(event.globalPos()) + + def set_language(self, action): + """ + Changes the language for this spelltextedit. + + :param action: The action. + """ + self.dictionary = enchant.Dict(action.text()) + self.highlighter.spelling_dictionary = self.dictionary + self.highlighter.highlightBlock(self.toPlainText()) + self.highlighter.rehighlight() + + def correct_word(self, word): + """ + Replaces the selected text with word. + """ + cursor = self.textCursor() + cursor.beginEditBlock() + cursor.removeSelectedText() + cursor.insertText(word) + cursor.endEditBlock() + + def html_tag(self, tag): + """ + Replaces the selected text with word. + """ + tag = tag.replace('&', '') + for html in FormattingTags.get_html_tags(): + if tag == html['desc']: + cursor = self.textCursor() + if self.textCursor().hasSelection(): + text = cursor.selectedText() + cursor.beginEditBlock() + cursor.removeSelectedText() + cursor.insertText(html['start tag']) + cursor.insertText(text) + cursor.insertText(html['end tag']) + cursor.endEditBlock() + else: + cursor = self.textCursor() + cursor.insertText(html['start tag']) + cursor.insertText(html['end tag']) + + def insertFromMimeData(self, source): + """ + Reimplement `insertFromMimeData` so that we can remove any control characters + + :param QtCore.QMimeData source: The mime data to insert + :rtype: None + """ + self.insertPlainText(CONTROL_CHARS.sub('', source.text())) + + +class Highlighter(QtGui.QSyntaxHighlighter): + """ + Provides a text highlighter for pointing out spelling errors in text. + """ + WORDS = r'(?i)[\w\']+' + + def __init__(self, *args): + """ + Constructor + """ + super(Highlighter, self).__init__(*args) + self.spelling_dictionary = None + + def highlightBlock(self, text): + """ + Highlight mis spelt words in a block of text. + + Note, this is a Qt hook. + """ + if not self.spelling_dictionary: + return + text = str(text) + char_format = QtGui.QTextCharFormat() + char_format.setUnderlineColor(QtCore.Qt.red) + char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline) + for word_object in re.finditer(self.WORDS, text): + if not self.spelling_dictionary.check(word_object.group()): + self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format) + + +class SpellAction(QtWidgets.QAction): + """ + A special QAction that returns the text in a signal. + """ + correct = QtCore.pyqtSignal(str) + + def __init__(self, *args): + """ + Constructor + """ + super(SpellAction, self).__init__(*args) + self.triggered.connect(lambda x: self.correct.emit(self.text())) + + +class HistoryComboBox(QtWidgets.QComboBox): + """ + The :class:`~openlp.core.common.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed`` + signal for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit + box into its list. + """ + returnPressed = QtCore.pyqtSignal() + + def __init__(self, parent=None): + """ + Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top. + + :param parent: The parent widget + """ + super().__init__(parent) + self.setDuplicatesEnabled(False) + self.setEditable(True) + self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + self.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop) + + def keyPressEvent(self, event): + """ + Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to + the dropdown list. + + :param event: The keyboard event + """ + # Handle Enter and Return ourselves + if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return: + # Emit the returnPressed signal + self.returnPressed.emit() + # Save the current text to the dropdown list + if self.currentText() and self.findText(self.currentText()) == -1: + self.insertItem(0, self.currentText()) + # Let the parent handle any keypress events + super().keyPressEvent(event) + + def focusOutEvent(self, event): + """ + Override the inherited focusOutEvent to save the current text to the dropdown list. + + :param event: The focus event + """ + # Save the current text to the dropdown list + if self.currentText() and self.findText(self.currentText()) == -1: + self.insertItem(0, self.currentText()) + # Let the parent handle any keypress events + super().focusOutEvent(event) + + def getItems(self): + """ + Get all the items from the history + + :return: A list of strings + """ + return [self.itemText(i) for i in range(self.count())] diff --git a/openlp/core/ui/lib/__init__.py b/openlp/core/widgets/enums.py similarity index 69% rename from openlp/core/ui/lib/__init__.py rename to openlp/core/widgets/enums.py index cf55b9d20..f79dd775c 100644 --- a/openlp/core/ui/lib/__init__.py +++ b/openlp/core/widgets/enums.py @@ -19,18 +19,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.core.widgets.enums` module contains enumerations used by the widgets +""" +from enum import Enum -from .colorbutton import ColorButton -from .listpreviewwidget import ListPreviewWidget -from .listwidgetwithdnd import ListWidgetWithDnD -from .mediadockmanager import MediaDockManager -from .dockwidget import OpenLPDockWidget -from .toolbar import OpenLPToolbar -from .wizard import OpenLPWizard, WizardStrings -from .pathedit import PathEdit, PathType -from .spelltextedit import SpellTextEdit -from .treewidgetwithdnd import TreeWidgetWithDnD -__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'MediaDockManager', 'OpenLPDockWidget', - 'OpenLPToolbar', 'OpenLPWizard', 'PathEdit', 'PathType', 'SpellTextEdit', 'TreeWidgetWithDnD', - 'WizardStrings'] +class PathEditType(Enum): + Files = 1 + Directories = 2 diff --git a/openlp/core/ui/lib/toolbar.py b/openlp/core/widgets/toolbar.py similarity index 98% rename from openlp/core/ui/lib/toolbar.py rename to openlp/core/widgets/toolbar.py index 68343889f..8c76ce50e 100644 --- a/openlp/core/ui/lib/toolbar.py +++ b/openlp/core/widgets/toolbar.py @@ -40,7 +40,7 @@ class OpenLPToolbar(QtWidgets.QToolBar): """ Initialise the toolbar. """ - super(OpenLPToolbar, self).__init__(parent) + super().__init__(parent) # useful to be able to reuse button icons... self.setIconSize(QtCore.QSize(20, 20)) self.actions = {} diff --git a/openlp/core/ui/lib/listpreviewwidget.py b/openlp/core/widgets/views.py similarity index 56% rename from openlp/core/ui/lib/listpreviewwidget.py rename to openlp/core/widgets/views.py index d03261e8c..219dd145f 100644 --- a/openlp/core/ui/lib/listpreviewwidget.py +++ b/openlp/core/widgets/views.py @@ -23,10 +23,14 @@ The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller. It is based on a QTableWidget but represents its contents in list form. """ +import os from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common.registry import RegistryProperties +from openlp.core.common import is_win +from openlp.core.common.i18n import UiStrings +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ImageSource, ItemCapabilities, ServiceItem @@ -238,3 +242,241 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): Returns the number of slides this widget holds. """ return super(ListPreviewWidget, self).rowCount() + + +class ListWidgetWithDnD(QtWidgets.QListWidget): + """ + Provide a list widget to store objects and handle drag and drop events + """ + def __init__(self, parent=None, name=''): + """ + Initialise the list widget + """ + super().__init__(parent) + self.mime_data_text = name + self.no_results_text = UiStrings().NoResults + self.setSpacing(1) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setAlternatingRowColors(True) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + def activateDnD(self): + """ + Activate DnD of widget + """ + self.setAcceptDrops(True) + self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) + Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) + + def clear(self, search_while_typing=False): + """ + Re-implement clear, so that we can customise feedback when using 'Search as you type' + + :param search_while_typing: True if we want to display the customised message + :return: None + """ + if search_while_typing: + self.no_results_text = UiStrings().ShortResults + else: + self.no_results_text = UiStrings().NoResults + super().clear() + + def mouseMoveEvent(self, event): + """ + Drag and drop event does not care what data is selected as the recipient will use events to request the data + move just tell it what plugin to call + """ + if event.buttons() != QtCore.Qt.LeftButton: + event.ignore() + return + if not self.selectedItems(): + event.ignore() + return + drag = QtGui.QDrag(self) + mime_data = QtCore.QMimeData() + drag.setMimeData(mime_data) + mime_data.setText(self.mime_data_text) + drag.exec(QtCore.Qt.CopyAction) + + def dragEnterEvent(self, event): + """ + When something is dragged into this object, check if you should be able to drop it in here. + """ + if event.mimeData().hasUrls(): + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """ + Make an object droppable, and set it to copy the contents of the object, not move it. + """ + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + """ + Receive drop event check if it is a file and process it if it is. + + :param event: Handle of the event pint passed + """ + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + files = [] + for url in event.mimeData().urls(): + local_file = os.path.normpath(url.toLocalFile()) + if os.path.isfile(local_file): + files.append(local_file) + elif os.path.isdir(local_file): + listing = os.listdir(local_file) + for file in listing: + files.append(os.path.join(local_file, file)) + Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text), + {'files': files}) + else: + event.ignore() + + def allItems(self): + """ + An generator to list all the items in the widget + + :return: a generator + """ + for row in range(self.count()): + yield self.item(row) + + def paintEvent(self, event): + """ + Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty. + + :param event: A QPaintEvent + :return: None + """ + super().paintEvent(event) + if not self.count(): + viewport = self.viewport() + painter = QtGui.QPainter(viewport) + font = QtGui.QFont() + font.setItalic(True) + painter.setFont(font) + painter.drawText(QtCore.QRect(0, 0, viewport.width(), viewport.height()), + (QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap), self.no_results_text) + + +class TreeWidgetWithDnD(QtWidgets.QTreeWidget): + """ + Provide a tree widget to store objects and handle drag and drop events + """ + def __init__(self, parent=None, name=''): + """ + Initialise the tree widget + """ + super(TreeWidgetWithDnD, self).__init__(parent) + self.mime_data_text = name + self.allow_internal_dnd = False + self.header().close() + self.default_indentation = self.indentation() + self.setIndentation(0) + self.setAnimated(True) + + def activateDnD(self): + """ + Activate DnD of widget + """ + self.setAcceptDrops(True) + self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) + Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) + Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal) + + def mouseMoveEvent(self, event): + """ + Drag and drop event does not care what data is selected as the recipient will use events to request the data + move just tell it what plugin to call + + :param event: The event that occurred + """ + if event.buttons() != QtCore.Qt.LeftButton: + event.ignore() + return + if not self.selectedItems(): + event.ignore() + return + drag = QtGui.QDrag(self) + mime_data = QtCore.QMimeData() + drag.setMimeData(mime_data) + mime_data.setText(self.mime_data_text) + drag.exec(QtCore.Qt.CopyAction) + + def dragEnterEvent(self, event): + """ + Receive drag enter event, check if it is a file or internal object and allow it if it is. + + :param event: The event that occurred + """ + if event.mimeData().hasUrls(): + event.accept() + elif self.allow_internal_dnd: + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """ + Receive drag move event, check if it is a file or internal object and allow it if it is. + + :param event: The event that occurred + """ + QtWidgets.QTreeWidget.dragMoveEvent(self, event) + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + elif self.allow_internal_dnd: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + """ + Receive drop event, check if it is a file or internal object and process it if it is. + + :param event: Handle of the event pint passed + """ + # If we are on Windows, OpenLP window will not be set on top. For example, user can drag images to Library and + # the folder stays on top of the group creation box. This piece of code fixes this issue. + if is_win(): + self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + self.setWindowState(QtCore.Qt.WindowNoState) + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + files = [] + for url in event.mimeData().urls(): + local_file = url.toLocalFile() + if os.path.isfile(local_file): + files.append(local_file) + elif os.path.isdir(local_file): + listing = os.listdir(local_file) + for file_name in listing: + files.append(os.path.join(local_file, file_name)) + Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())}) + elif self.allow_internal_dnd: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos())) + else: + event.ignore() + + # Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple. + def addItem(self, item): + self.addTopLevelItem(item) + + def count(self): + return self.topLevelItemCount() + + def item(self, index): + return self.topLevelItem(index) diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/widgets/wizard.py similarity index 98% rename from openlp/core/ui/lib/wizard.py rename to openlp/core/widgets/wizard.py index 1da24eab3..dc8288bc9 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/widgets/wizard.py @@ -28,11 +28,12 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import is_macosx from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog log = logging.getLogger(__name__) diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index 6eb26ff57..e5ae93093 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -26,12 +26,12 @@ displaying of alerts. from PyQt5 import QtCore from openlp.core.common.i18n import translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings -class AlertsManager(OpenLPMixin, RegistryMixin, QtCore.QObject, RegistryProperties): +class AlertsManager(QtCore.QObject, RegistryBase, LogMixin, RegistryProperties): """ AlertsManager manages the settings of Alerts. """ diff --git a/openlp/plugins/alerts/lib/alertstab.py b/openlp/plugins/alerts/lib/alertstab.py index 406e35607..51d92cf06 100644 --- a/openlp/plugins/alerts/lib/alertstab.py +++ b/openlp/plugins/alerts/lib/alertstab.py @@ -26,7 +26,7 @@ from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab from openlp.core.lib.ui import create_valign_selection_widgets -from openlp.core.ui.lib.colorbutton import ColorButton +from openlp.core.widgets.buttons import ColorButton class AlertsTab(SettingsTab): diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 4cb9ee453..35dd82eeb 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -23,7 +23,6 @@ The bible import functions for OpenLP """ import logging -import os import urllib.error from lxml import etree @@ -41,7 +40,8 @@ from openlp.core.common.settings import Settings from openlp.core.lib.db import delete_database from openlp.core.lib.exceptions import ValidationError from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.bibles.lib.db import clean_filename from openlp.plugins.bibles.lib.importers.http import CWExtract, BGExtract, BSExtract from openlp.plugins.bibles.lib.manager import BibleFormat @@ -122,15 +122,9 @@ class BibleImportForm(OpenLPWizard): Set up the signals used in the bible importer. """ self.web_source_combo_box.currentIndexChanged.connect(self.on_web_source_combo_box_index_changed) - self.osis_browse_button.clicked.connect(self.on_osis_browse_button_clicked) - self.csv_books_button.clicked.connect(self.on_csv_books_browse_button_clicked) - self.csv_verses_button.clicked.connect(self.on_csv_verses_browse_button_clicked) - self.open_song_browse_button.clicked.connect(self.on_open_song_browse_button_clicked) - self.zefania_browse_button.clicked.connect(self.on_zefania_browse_button_clicked) - self.wordproject_browse_button.clicked.connect(self.on_wordproject_browse_button_clicked) self.web_update_button.clicked.connect(self.on_web_update_button_clicked) - self.sword_browse_button.clicked.connect(self.on_sword_browse_button_clicked) - self.sword_zipbrowse_button.clicked.connect(self.on_sword_zipbrowse_button_clicked) + self.sword_folder_path_edit.pathChanged.connect(self.on_sword_folder_path_edit_path_changed) + self.sword_zipfile_path_edit.pathChanged.connect(self.on_sword_zipfile_path_edit_path_changed) def add_custom_pages(self): """ @@ -161,17 +155,12 @@ class BibleImportForm(OpenLPWizard): self.osis_layout.setObjectName('OsisLayout') self.osis_file_label = QtWidgets.QLabel(self.osis_widget) self.osis_file_label.setObjectName('OsisFileLabel') - self.osis_file_layout = QtWidgets.QHBoxLayout() - self.osis_file_layout.setObjectName('OsisFileLayout') - self.osis_file_edit = QtWidgets.QLineEdit(self.osis_widget) - self.osis_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - self.osis_file_edit.setObjectName('OsisFileEdit') - self.osis_file_layout.addWidget(self.osis_file_edit) - self.osis_browse_button = QtWidgets.QToolButton(self.osis_widget) - self.osis_browse_button.setIcon(self.open_icon) - self.osis_browse_button.setObjectName('OsisBrowseButton') - self.osis_file_layout.addWidget(self.osis_browse_button) - self.osis_layout.addRow(self.osis_file_label, self.osis_file_layout) + self.osis_path_edit = PathEdit( + self.osis_widget, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.OSIS), + show_revert=False) + self.osis_layout.addRow(self.osis_file_label, self.osis_path_edit) self.osis_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer) self.select_stack.addWidget(self.osis_widget) self.csv_widget = QtWidgets.QWidget(self.select_page) @@ -181,30 +170,27 @@ class BibleImportForm(OpenLPWizard): self.csv_layout.setObjectName('CsvLayout') self.csv_books_label = QtWidgets.QLabel(self.csv_widget) self.csv_books_label.setObjectName('CsvBooksLabel') - self.csv_books_layout = QtWidgets.QHBoxLayout() - self.csv_books_layout.setObjectName('CsvBooksLayout') - self.csv_books_edit = QtWidgets.QLineEdit(self.csv_widget) - self.csv_books_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - self.csv_books_edit.setObjectName('CsvBooksEdit') - self.csv_books_layout.addWidget(self.csv_books_edit) - self.csv_books_button = QtWidgets.QToolButton(self.csv_widget) - self.csv_books_button.setIcon(self.open_icon) - self.csv_books_button.setObjectName('CsvBooksButton') - self.csv_books_layout.addWidget(self.csv_books_button) - self.csv_layout.addRow(self.csv_books_label, self.csv_books_layout) + self.csv_books_path_edit = PathEdit( + self.csv_widget, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.CSV), + show_revert=False, + ) + self.csv_books_path_edit.filters = \ + '{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File')) + self.csv_layout.addRow(self.csv_books_label, self.csv_books_path_edit) self.csv_verses_label = QtWidgets.QLabel(self.csv_widget) self.csv_verses_label.setObjectName('CsvVersesLabel') - self.csv_verses_layout = QtWidgets.QHBoxLayout() - self.csv_verses_layout.setObjectName('CsvVersesLayout') - self.csv_verses_edit = QtWidgets.QLineEdit(self.csv_widget) - self.csv_verses_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - self.csv_verses_edit.setObjectName('CsvVersesEdit') - self.csv_verses_layout.addWidget(self.csv_verses_edit) - self.csv_verses_button = QtWidgets.QToolButton(self.csv_widget) - self.csv_verses_button.setIcon(self.open_icon) - self.csv_verses_button.setObjectName('CsvVersesButton') - self.csv_verses_layout.addWidget(self.csv_verses_button) - self.csv_layout.addRow(self.csv_verses_label, self.csv_verses_layout) + self.csv_verses_path_edit = PathEdit( + self.csv_widget, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.CSV), + show_revert=False, + ) + self.csv_verses_path_edit.filters = \ + '{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File')) + self.csv_layout.addRow(self.csv_books_label, self.csv_books_path_edit) + self.csv_layout.addRow(self.csv_verses_label, self.csv_verses_path_edit) self.csv_layout.setItem(3, QtWidgets.QFormLayout.LabelRole, self.spacer) self.select_stack.addWidget(self.csv_widget) self.open_song_widget = QtWidgets.QWidget(self.select_page) @@ -214,17 +200,13 @@ class BibleImportForm(OpenLPWizard): self.open_song_layout.setObjectName('OpenSongLayout') self.open_song_file_label = QtWidgets.QLabel(self.open_song_widget) self.open_song_file_label.setObjectName('OpenSongFileLabel') - self.open_song_file_layout = QtWidgets.QHBoxLayout() - self.open_song_file_layout.setObjectName('OpenSongFileLayout') - self.open_song_file_edit = QtWidgets.QLineEdit(self.open_song_widget) - self.open_song_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - self.open_song_file_edit.setObjectName('OpenSongFileEdit') - self.open_song_file_layout.addWidget(self.open_song_file_edit) - self.open_song_browse_button = QtWidgets.QToolButton(self.open_song_widget) - self.open_song_browse_button.setIcon(self.open_icon) - self.open_song_browse_button.setObjectName('OpenSongBrowseButton') - self.open_song_file_layout.addWidget(self.open_song_browse_button) - self.open_song_layout.addRow(self.open_song_file_label, self.open_song_file_layout) + self.open_song_path_edit = PathEdit( + self.open_song_widget, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.OS), + show_revert=False, + ) + self.open_song_layout.addRow(self.open_song_file_label, self.open_song_path_edit) self.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer) self.select_stack.addWidget(self.open_song_widget) self.web_tab_widget = QtWidgets.QTabWidget(self.select_page) @@ -292,17 +274,14 @@ class BibleImportForm(OpenLPWizard): self.zefania_layout.setObjectName('ZefaniaLayout') self.zefania_file_label = QtWidgets.QLabel(self.zefania_widget) self.zefania_file_label.setObjectName('ZefaniaFileLabel') - self.zefania_file_layout = QtWidgets.QHBoxLayout() - self.zefania_file_layout.setObjectName('ZefaniaFileLayout') - self.zefania_file_edit = QtWidgets.QLineEdit(self.zefania_widget) - self.zefania_file_edit.setObjectName('ZefaniaFileEdit') - self.zefania_file_layout.addWidget(self.zefania_file_edit) - self.zefania_browse_button = QtWidgets.QToolButton(self.zefania_widget) - self.zefania_browse_button.setIcon(self.open_icon) - self.zefania_browse_button.setObjectName('ZefaniaBrowseButton') - self.zefania_file_layout.addWidget(self.zefania_browse_button) - self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout) - self.zefania_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer) + self.zefania_path_edit = PathEdit( + self.zefania_widget, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.ZEF), + show_revert=False, + ) + self.zefania_layout.addRow(self.zefania_file_label, self.zefania_path_edit) + self.zefania_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer) self.select_stack.addWidget(self.zefania_widget) self.sword_widget = QtWidgets.QWidget(self.select_page) self.sword_widget.setObjectName('SwordWidget') @@ -318,16 +297,13 @@ class BibleImportForm(OpenLPWizard): self.sword_folder_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_folder_label.setObjectName('SwordSourceLabel') self.sword_folder_label.setObjectName('SwordFolderLabel') - self.sword_folder_edit = QtWidgets.QLineEdit(self.sword_folder_tab) - self.sword_folder_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - self.sword_folder_edit.setObjectName('SwordFolderEdit') - self.sword_browse_button = QtWidgets.QToolButton(self.sword_folder_tab) - self.sword_browse_button.setIcon(self.open_icon) - self.sword_browse_button.setObjectName('SwordBrowseButton') - self.sword_folder_layout = QtWidgets.QHBoxLayout() - self.sword_folder_layout.addWidget(self.sword_folder_edit) - self.sword_folder_layout.addWidget(self.sword_browse_button) - self.sword_folder_tab_layout.addRow(self.sword_folder_label, self.sword_folder_layout) + self.sword_folder_path_edit = PathEdit( + self.sword_folder_tab, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.SWORD), + show_revert=False, + ) + self.sword_folder_tab_layout.addRow(self.sword_folder_label, self.sword_folder_path_edit) self.sword_bible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_bible_label.setObjectName('SwordBibleLabel') self.sword_bible_combo_box = QtWidgets.QComboBox(self.sword_folder_tab) @@ -342,16 +318,13 @@ class BibleImportForm(OpenLPWizard): self.sword_zip_layout.setObjectName('SwordZipLayout') self.sword_zipfile_label = QtWidgets.QLabel(self.sword_zip_tab) self.sword_zipfile_label.setObjectName('SwordZipFileLabel') - self.sword_zipfile_edit = QtWidgets.QLineEdit(self.sword_zip_tab) - self.sword_zipfile_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - self.sword_zipfile_edit.setObjectName('SwordZipFileEdit') - self.sword_zipbrowse_button = QtWidgets.QToolButton(self.sword_zip_tab) - self.sword_zipbrowse_button.setIcon(self.open_icon) - self.sword_zipbrowse_button.setObjectName('SwordZipBrowseButton') - self.sword_zipfile_layout = QtWidgets.QHBoxLayout() - self.sword_zipfile_layout.addWidget(self.sword_zipfile_edit) - self.sword_zipfile_layout.addWidget(self.sword_zipbrowse_button) - self.sword_zip_layout.addRow(self.sword_zipfile_label, self.sword_zipfile_layout) + self.sword_zipfile_path_edit = PathEdit( + self.sword_zip_tab, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.SWORD), + show_revert=False, + ) + self.sword_zip_layout.addRow(self.sword_zipfile_label, self.sword_zipfile_path_edit) self.sword_zipbible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_zipbible_label.setObjectName('SwordZipBibleLabel') self.sword_zipbible_combo_box = QtWidgets.QComboBox(self.sword_zip_tab) @@ -372,18 +345,13 @@ class BibleImportForm(OpenLPWizard): self.wordproject_layout.setObjectName('WordProjectLayout') self.wordproject_file_label = QtWidgets.QLabel(self.wordproject_widget) self.wordproject_file_label.setObjectName('WordProjectFileLabel') - self.wordproject_file_layout = QtWidgets.QHBoxLayout() - self.wordproject_file_layout.setObjectName('WordProjectFileLayout') - self.wordproject_file_edit = QtWidgets.QLineEdit(self.wordproject_widget) - self.wordproject_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - self.wordproject_file_edit.setObjectName('WordProjectFileEdit') - self.wordproject_file_layout.addWidget(self.wordproject_file_edit) - self.wordproject_browse_button = QtWidgets.QToolButton(self.wordproject_widget) - self.wordproject_browse_button.setIcon(self.open_icon) - self.wordproject_browse_button.setObjectName('WordProjectBrowseButton') - self.wordproject_file_layout.addWidget(self.wordproject_browse_button) - self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_file_layout) - self.wordproject_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer) + self.wordproject_path_edit = PathEdit( + self.wordproject_widget, + default_path=Settings().value('bibles/last directory import'), + dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.WordProject), + show_revert=False) + self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_path_edit) + self.wordproject_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer) self.select_stack.addWidget(self.wordproject_widget) self.select_page_layout.addLayout(self.select_stack) self.addPage(self.select_page) @@ -468,8 +436,6 @@ class BibleImportForm(OpenLPWizard): self.sword_bible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:')) self.sword_folder_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD data folder:')) self.sword_zipfile_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD zip-file:')) - self.sword_folder_edit.setPlaceholderText(translate('BiblesPlugin.ImportWizardForm', - 'Defaults to the standard SWORD data folder')) self.sword_zipbible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:')) self.sword_tab_widget.setTabText(self.sword_tab_widget.indexOf(self.sword_folder_tab), translate('BiblesPlugin.ImportWizardForm', 'Import from folder')) @@ -518,7 +484,7 @@ class BibleImportForm(OpenLPWizard): if self.field('source_format') == BibleFormat.OSIS: if not self.field('osis_location'): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OSIS) - self.osis_file_edit.setFocus() + self.osis_path_edit.setFocus() return False elif self.field('source_format') == BibleFormat.CSV: if not self.field('csv_booksfile'): @@ -538,18 +504,18 @@ class BibleImportForm(OpenLPWizard): elif self.field('source_format') == BibleFormat.OpenSong: if not self.field('opensong_file'): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OS) - self.open_song_file_edit.setFocus() + self.open_song_path_edit.setFocus() return False elif self.field('source_format') == BibleFormat.Zefania: if not self.field('zefania_file'): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.ZEF) - self.zefania_file_edit.setFocus() + self.zefania_path_edit.setFocus() return False elif self.field('source_format') == BibleFormat.WordProject: if not self.field('wordproject_file'): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.WordProject) - self.wordproject_file_edit.setFocus() + self.wordproject_path_edit.setFocus() return False elif self.field('source_format') == BibleFormat.WebDownload: # If count is 0 the bible list has not yet been downloaded @@ -563,7 +529,7 @@ class BibleImportForm(OpenLPWizard): if not self.field('sword_folder_path') and self.sword_bible_combo_box.count() == 0: critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFolder % WizardStrings.SWORD) - self.sword_folder_edit.setFocus() + self.sword_folder_path_edit.setFocus() return False key = self.sword_bible_combo_box.itemData(self.sword_bible_combo_box.currentIndex()) if 'description' in self.pysword_folder_modules_json[key]: @@ -575,7 +541,7 @@ class BibleImportForm(OpenLPWizard): elif self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_zip_tab): if not self.field('sword_zip_path'): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.SWORD) - self.sword_zipfile_edit.setFocus() + self.sword_zipfile_path_edit.setFocus() return False key = self.sword_zipbible_combo_box.itemData(self.sword_zipbible_combo_box.currentIndex()) if 'description' in self.pysword_zip_modules_json[key]: @@ -586,7 +552,6 @@ class BibleImportForm(OpenLPWizard): elif self.currentPage() == self.license_details_page: license_version = self.field('license_version') license_copyright = self.field('license_copyright') - path = str(AppLocation.get_section_data_path('bibles')) if not license_version: critical_error_message_box( UiStrings().EmptyField, @@ -608,7 +573,7 @@ class BibleImportForm(OpenLPWizard): 'existing one.')) self.version_name_edit.setFocus() return False - elif os.path.exists(os.path.join(path, clean_filename(license_version))): + elif (AppLocation.get_section_data_path('bibles') / clean_filename(license_version)).exists(): critical_error_message_box( translate('BiblesPlugin.ImportWizardForm', 'Bible Exists'), translate('BiblesPlugin.ImportWizardForm', 'This Bible already exists. Please import ' @@ -631,57 +596,6 @@ class BibleImportForm(OpenLPWizard): bibles.sort(key=get_locale_key) self.web_translation_combo_box.addItems(bibles) - def on_osis_browse_button_clicked(self): - """ - Show the file open dialog for the OSIS file. - """ - self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.OSIS, self.osis_file_edit, - 'last directory import') - - def on_csv_books_browse_button_clicked(self): - """ - Show the file open dialog for the books CSV file. - """ - # TODO: Verify format() with varible template - self.get_file_name( - WizardStrings.OpenTypeFile % WizardStrings.CSV, - self.csv_books_edit, - 'last directory import', - '{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File'))) - - def on_csv_verses_browse_button_clicked(self): - """ - Show the file open dialog for the verses CSV file. - """ - # TODO: Verify format() with variable template - self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.CSV, self.csv_verses_edit, - 'last directory import', - '{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File'))) - - def on_open_song_browse_button_clicked(self): - """ - Show the file open dialog for the OpenSong file. - """ - # TODO: Verify format() with variable template - self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.OS, self.open_song_file_edit, - 'last directory import') - - def on_zefania_browse_button_clicked(self): - """ - Show the file open dialog for the Zefania file. - """ - # TODO: Verify format() with variable template - self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.ZEF, self.zefania_file_edit, - 'last directory import') - - def on_wordproject_browse_button_clicked(self): - """ - Show the file open dialog for the WordProject file. - """ - # TODO: Verify format() with variable template - self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.WordProject, self.wordproject_file_edit, - 'last directory import') - def on_web_update_button_clicked(self): """ Download list of bibles from Crosswalk, BibleServer and BibleGateway. @@ -718,15 +632,13 @@ class BibleImportForm(OpenLPWizard): self.web_update_button.setEnabled(True) self.web_progress_bar.setVisible(False) - def on_sword_browse_button_clicked(self): + def on_sword_folder_path_edit_path_changed(self, new_path): """ Show the file open dialog for the SWORD folder. """ - self.get_folder(WizardStrings.OpenTypeFolder % WizardStrings.SWORD, self.sword_folder_edit, - 'last directory import') - if self.sword_folder_edit.text(): + if new_path: try: - self.pysword_folder_modules = modules.SwordModules(self.sword_folder_edit.text()) + self.pysword_folder_modules = modules.SwordModules(str(new_path)) self.pysword_folder_modules_json = self.pysword_folder_modules.parse_modules() bible_keys = self.pysword_folder_modules_json.keys() self.sword_bible_combo_box.clear() @@ -735,15 +647,13 @@ class BibleImportForm(OpenLPWizard): except: self.sword_bible_combo_box.clear() - def on_sword_zipbrowse_button_clicked(self): + def on_sword_zipfile_path_edit_path_changed(self, new_path): """ Show the file open dialog for a SWORD zip-file. """ - self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.SWORD, self.sword_zipfile_edit, - 'last directory import') - if self.sword_zipfile_edit.text(): + if new_path: try: - self.pysword_zip_modules = modules.SwordModules(self.sword_zipfile_edit.text()) + self.pysword_zip_modules = modules.SwordModules(str(new_path)) self.pysword_zip_modules_json = self.pysword_zip_modules.parse_modules() bible_keys = self.pysword_zip_modules_json.keys() self.sword_zipbible_combo_box.clear() @@ -757,16 +667,16 @@ class BibleImportForm(OpenLPWizard): Register the bible import wizard fields. """ self.select_page.registerField('source_format', self.format_combo_box) - self.select_page.registerField('osis_location', self.osis_file_edit) - self.select_page.registerField('csv_booksfile', self.csv_books_edit) - self.select_page.registerField('csv_versefile', self.csv_verses_edit) - self.select_page.registerField('opensong_file', self.open_song_file_edit) - self.select_page.registerField('zefania_file', self.zefania_file_edit) - self.select_page.registerField('wordproject_file', self.wordproject_file_edit) + self.select_page.registerField('osis_location', self.osis_path_edit, 'path', PathEdit.pathChanged) + self.select_page.registerField('csv_booksfile', self.csv_books_path_edit, 'path', PathEdit.pathChanged) + self.select_page.registerField('csv_versefile', self.csv_verses_path_edit, 'path', PathEdit.pathChanged) + self.select_page.registerField('opensong_file', self.open_song_path_edit, 'path', PathEdit.pathChanged) + self.select_page.registerField('zefania_file', self.zefania_path_edit, 'path', PathEdit.pathChanged) + self.select_page.registerField('wordproject_file', self.wordproject_path_edit, 'path', PathEdit.pathChanged) self.select_page.registerField('web_location', self.web_source_combo_box) self.select_page.registerField('web_biblename', self.web_translation_combo_box) - self.select_page.registerField('sword_folder_path', self.sword_folder_edit) - self.select_page.registerField('sword_zip_path', self.sword_zipfile_edit) + self.select_page.registerField('sword_folder_path', self.sword_folder_path_edit, 'path', PathEdit.pathChanged) + self.select_page.registerField('sword_zip_path', self.sword_zipfile_path_edit, 'path', PathEdit.pathChanged) self.select_page.registerField('proxy_server', self.web_server_edit) self.select_page.registerField('proxy_username', self.web_user_edit) self.select_page.registerField('proxy_password', self.web_password_edit) @@ -785,13 +695,13 @@ class BibleImportForm(OpenLPWizard): self.finish_button.setVisible(False) self.cancel_button.setVisible(True) self.setField('source_format', 0) - self.setField('osis_location', '') - self.setField('csv_booksfile', '') - self.setField('csv_versefile', '') - self.setField('opensong_file', '') - self.setField('zefania_file', '') - self.setField('sword_folder_path', '') - self.setField('sword_zip_path', '') + self.setField('osis_location', None) + self.setField('csv_booksfile', None) + self.setField('csv_versefile', None) + self.setField('opensong_file', None) + self.setField('zefania_file', None) + self.setField('sword_folder_path', None) + self.setField('sword_zip_path', None) self.setField('web_location', WebDownload.Crosswalk) self.setField('web_biblename', self.web_translation_combo_box.currentIndex()) self.setField('proxy_server', settings.value('proxy address')) @@ -833,16 +743,16 @@ class BibleImportForm(OpenLPWizard): if bible_type == BibleFormat.OSIS: # Import an OSIS bible. importer = self.manager.import_bible(BibleFormat.OSIS, name=license_version, - filename=self.field('osis_location')) + file_path=self.field('osis_location')) elif bible_type == BibleFormat.CSV: # Import a CSV bible. importer = self.manager.import_bible(BibleFormat.CSV, name=license_version, - booksfile=self.field('csv_booksfile'), - versefile=self.field('csv_versefile')) + books_path=self.field('csv_booksfile'), + verse_path=self.field('csv_versefile')) elif bible_type == BibleFormat.OpenSong: # Import an OpenSong bible. importer = self.manager.import_bible(BibleFormat.OpenSong, name=license_version, - filename=self.field('opensong_file')) + file_path=self.field('opensong_file')) elif bible_type == BibleFormat.WebDownload: # Import a bible from the web. self.progress_bar.setMaximum(1) @@ -861,11 +771,11 @@ class BibleImportForm(OpenLPWizard): elif bible_type == BibleFormat.Zefania: # Import a Zefania bible. importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version, - filename=self.field('zefania_file')) + file_path=self.field('zefania_file')) elif bible_type == BibleFormat.WordProject: # Import a WordProject bible. importer = self.manager.import_bible(BibleFormat.WordProject, name=license_version, - filename=self.field('wordproject_file')) + file_path=self.field('wordproject_file')) elif bible_type == BibleFormat.SWORD: # Import a SWORD bible. if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab): diff --git a/openlp/plugins/bibles/forms/booknameform.py b/openlp/plugins/bibles/forms/booknameform.py index f78559ce5..7c8a2c3cd 100644 --- a/openlp/plugins/bibles/forms/booknameform.py +++ b/openlp/plugins/bibles/forms/booknameform.py @@ -113,8 +113,7 @@ class BookNameForm(QDialog, Ui_BookNameDialog): cor_book = self.corresponding_combo_box.currentText() for character in '\\.^$*+?{}[]()': cor_book = cor_book.replace(character, '\\' + character) - books = [key for key in list(self.book_names.keys()) if re.match(cor_book, str(self.book_names[key]), - re.UNICODE)] + books = [key for key in list(self.book_names.keys()) if re.match(cor_book, str(self.book_names[key]))] books = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f] if books: self.book_id = books[0]['id'] diff --git a/openlp/plugins/bibles/forms/editbibleform.py b/openlp/plugins/bibles/forms/editbibleform.py index 365d98a4e..8899087ba 100644 --- a/openlp/plugins/bibles/forms/editbibleform.py +++ b/openlp/plugins/bibles/forms/editbibleform.py @@ -26,7 +26,7 @@ import re from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.lib.ui import critical_error_message_box from .editbibledialog import Ui_EditBibleDialog from openlp.plugins.bibles.lib import BibleStrings diff --git a/openlp/plugins/bibles/lib/__init__.py b/openlp/plugins/bibles/lib/__init__.py index 9247485c1..f9d93a43e 100644 --- a/openlp/plugins/bibles/lib/__init__.py +++ b/openlp/plugins/bibles/lib/__init__.py @@ -224,13 +224,13 @@ def update_reference_separators(): range_regex = '(?:(?P<from_chapter>[0-9]+){sep_v})?' \ '(?P<from_verse>[0-9]+)(?P<range_to>{sep_r}(?:(?:(?P<to_chapter>' \ '[0-9]+){sep_v})?(?P<to_verse>[0-9]+)|{sep_e})?)?'.format_map(REFERENCE_SEPARATORS) - REFERENCE_MATCHES['range'] = re.compile(r'^\s*{range}\s*$'.format(range=range_regex), re.UNICODE) - REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l'], re.UNICODE) + REFERENCE_MATCHES['range'] = re.compile(r'^\s*{range}\s*$'.format(range=range_regex)) + REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l']) # full reference match: <book>(<range>(,(?!$)|(?=$)))+ REFERENCE_MATCHES['full'] = \ re.compile(r'^\s*(?!\s)(?P<book>[\d]*[.]?[^\d\.]+)\.*(?<!\s)\s*' r'(?P<ranges>(?:{range_regex}(?:{sep_l}(?!\s*$)|(?=\s*$)))+)\s*$'.format( - range_regex=range_regex, sep_l=REFERENCE_SEPARATORS['sep_l']), re.UNICODE) + range_regex=range_regex, sep_l=REFERENCE_SEPARATORS['sep_l'])) def get_reference_separator(separator_type): diff --git a/openlp/plugins/bibles/lib/bibleimport.py b/openlp/plugins/bibles/lib/bibleimport.py index 61aa35a3e..7efb46f5d 100644 --- a/openlp/plugins/bibles/lib/bibleimport.py +++ b/openlp/plugins/bibles/lib/bibleimport.py @@ -23,37 +23,37 @@ from lxml import etree, objectify from zipfile import is_zipfile -from openlp.core.common.mixins import OpenLPMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.i18n import get_language, translate from openlp.core.lib import ValidationError from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.db import AlternativeBookNamesDB, BibleDB, BiblesResourcesDB -class BibleImport(OpenLPMixin, RegistryProperties, BibleDB): +class BibleImport(BibleDB, LogMixin, RegistryProperties): """ Helper class to import bibles from a third party source into OpenLP """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.filename = kwargs['filename'] if 'filename' in kwargs else None + self.file_path = kwargs.get('file_path') self.wizard = None self.stop_import_flag = False Registry().register_function('openlp_stop_wizard', self.stop_import) @staticmethod - def is_compressed(file): + def is_compressed(file_path): """ Check if the supplied file is compressed - :param file: A path to the file to check + :param file_path: A path to the file to check """ - if is_zipfile(file): + if is_zipfile(str(file_path)): critical_error_message_box( message=translate('BiblesPlugin.BibleImport', 'The file "{file}" you supplied is compressed. You must decompress it before import.' - ).format(file=file)) + ).format(file=file_path)) return True return False @@ -96,6 +96,8 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB): if language_form.exec(bible_name): combo_box = language_form.language_combo_box language_id = combo_box.itemData(combo_box.currentIndex()) + else: + return False if not language_id: return None self.save_meta('language_id', language_id) @@ -141,24 +143,24 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB): self.log_debug('No book name supplied. Falling back to guess_id') book_ref_id = guess_id if not book_ref_id: - raise ValidationError(msg='Could not resolve book_ref_id in "{}"'.format(self.filename)) + raise ValidationError(msg='Could not resolve book_ref_id in "{}"'.format(self.file_path)) book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) if book_details is None: raise ValidationError(msg='book_ref_id: {book_ref} Could not be found in the BibleResourcesDB while ' - 'importing {file}'.format(book_ref=book_ref_id, file=self.filename)) + 'importing {file}'.format(book_ref=book_ref_id, file=self.file_path)) return self.create_book(name, book_ref_id, book_details['testament_id']) - def parse_xml(self, filename, use_objectify=False, elements=None, tags=None): + def parse_xml(self, file_path, use_objectify=False, elements=None, tags=None): """ Parse and clean the supplied file by removing any elements or tags we don't use. - :param filename: The filename of the xml file to parse. Str + :param file_path: The filename of the xml file to parse. Str :param use_objectify: Use the objectify parser rather than the etree parser. (Bool) :param elements: A tuple of element names (Str) to remove along with their content. :param tags: A tuple of element names (Str) to remove, preserving their content. :return: The root element of the xml document """ try: - with open(filename, 'rb') as import_file: + with file_path.open('rb') as import_file: # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own # encoding detection, and the two mechanisms together interfere with each other. if not use_objectify: @@ -205,17 +207,17 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB): self.log_debug('Stopping import') self.stop_import_flag = True - def validate_xml_file(self, filename, tag): + def validate_xml_file(self, file_path, tag): """ Validate the supplied file - :param filename: The supplied file + :param file_path: The supplied file :param tag: The expected root tag type :return: True if valid. ValidationError is raised otherwise. """ - if BibleImport.is_compressed(filename): + if BibleImport.is_compressed(file_path): raise ValidationError(msg='Compressed file') - bible = self.parse_xml(filename, use_objectify=True) + bible = self.parse_xml(file_path, use_objectify=True) if bible is None: raise ValidationError(msg='Error when opening file') root_tag = bible.tag.lower() diff --git a/openlp/plugins/bibles/lib/biblestab.py b/openlp/plugins/bibles/lib/biblestab.py index 21970c106..c5e67840a 100644 --- a/openlp/plugins/bibles/lib/biblestab.py +++ b/openlp/plugins/bibles/lib/biblestab.py @@ -41,11 +41,11 @@ class BiblesTab(SettingsTab): """ log.info('Bible Tab loaded') - def _init_(self, parent, title, visible_title, icon_path): + def _init_(self, *args, **kwargs): self.paragraph_style = True self.show_new_chapters = False self.display_style = 0 - super(BiblesTab, self).__init__(parent, title, visible_title, icon_path) + super().__init__(*args, **kwargs) def setupUi(self): self.setObjectName('BiblesTab') diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index 5d82fe613..13aeadfae 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -19,7 +19,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import chardet import logging import os @@ -36,6 +35,7 @@ from sqlalchemy.orm.exc import UnmappedClassError from openlp.core.common import clean_filename from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate +from openlp.core.common.path import Path from openlp.core.lib.db import BaseModel, init_db, Manager from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib import BibleStrings, LanguageSelection, upgrade @@ -130,10 +130,15 @@ class BibleDB(Manager): :param parent: :param kwargs: ``path`` - The path to the bible database file. + The path to the bible database file. Type: openlp.core.common.path.Path ``name`` The name of the database. This is also used as the file name for SQLite databases. + + ``file`` + Type: openlp.core.common.path.Path + + :rtype: None """ log.info('BibleDB loaded') self._setup(parent, **kwargs) @@ -146,20 +151,20 @@ class BibleDB(Manager): self.session = None if 'path' not in kwargs: raise KeyError('Missing keyword argument "path".') + self.path = kwargs['path'] if 'name' not in kwargs and 'file' not in kwargs: raise KeyError('Missing keyword argument "name" or "file".') if 'name' in kwargs: self.name = kwargs['name'] if not isinstance(self.name, str): self.name = str(self.name, 'utf-8') - self.file = clean_filename(self.name) + '.sqlite' + # TODO: To path object + file_path = Path(clean_filename(self.name) + '.sqlite') if 'file' in kwargs: - self.file = kwargs['file'] - Manager.__init__(self, 'bibles', init_schema, self.file, upgrade) + file_path = kwargs['file'] + Manager.__init__(self, 'bibles', init_schema, file_path, upgrade) if self.session and 'file' in kwargs: self.get_name() - if 'path' in kwargs: - self.path = kwargs['path'] self._is_web_bible = None def get_name(self): @@ -308,8 +313,7 @@ class BibleDB(Manager): book_escaped = book for character in RESERVED_CHARACTERS: book_escaped = book_escaped.replace(character, '\\' + character) - regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), - re.UNICODE | re.IGNORECASE) + regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), re.IGNORECASE) if language_selection == LanguageSelection.Bible: db_book = self.get_book(book) if db_book: @@ -472,9 +476,9 @@ class BiblesResourcesDB(QtCore.QObject, Manager): Return the cursor object. Instantiate one if it doesn't exist yet. """ if BiblesResourcesDB.cursor is None: - file_path = os.path.join(str(AppLocation.get_directory(AppLocation.PluginsDir)), - 'bibles', 'resources', 'bibles_resources.sqlite') - conn = sqlite3.connect(file_path) + file_path = \ + AppLocation.get_directory(AppLocation.PluginsDir) / 'bibles' / 'resources' / 'bibles_resources.sqlite' + conn = sqlite3.connect(str(file_path)) BiblesResourcesDB.cursor = conn.cursor() return BiblesResourcesDB.cursor @@ -760,17 +764,13 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager): If necessary loads up the database and creates the tables if the database doesn't exist. """ if AlternativeBookNamesDB.cursor is None: - file_path = os.path.join( - str(AppLocation.get_directory(AppLocation.DataDir)), 'bibles', 'alternative_book_names.sqlite') - if not os.path.exists(file_path): + file_path = AppLocation.get_directory(AppLocation.DataDir) / 'bibles' / 'alternative_book_names.sqlite' + AlternativeBookNamesDB.conn = sqlite3.connect(str(file_path)) + if not file_path.exists(): # create new DB, create table alternative_book_names - AlternativeBookNamesDB.conn = sqlite3.connect(file_path) AlternativeBookNamesDB.conn.execute( 'CREATE TABLE alternative_book_names(id INTEGER NOT NULL, ' 'book_reference_id INTEGER, language_id INTEGER, name VARCHAR(50), PRIMARY KEY (id))') - else: - # use existing DB - AlternativeBookNamesDB.conn = sqlite3.connect(file_path) AlternativeBookNamesDB.cursor = AlternativeBookNamesDB.conn.cursor() return AlternativeBookNamesDB.cursor diff --git a/openlp/plugins/bibles/lib/importers/__init__.py b/openlp/plugins/bibles/lib/importers/__init__.py new file mode 100644 index 000000000..f83bbd595 --- /dev/null +++ b/openlp/plugins/bibles/lib/importers/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~openlp.plugins.bibles.lib.importers` module contains importers for the Bibles plugin. +""" diff --git a/openlp/plugins/bibles/lib/importers/csvbible.py b/openlp/plugins/bibles/lib/importers/csvbible.py index e897c4803..46124fd5e 100644 --- a/openlp/plugins/bibles/lib/importers/csvbible.py +++ b/openlp/plugins/bibles/lib/importers/csvbible.py @@ -73,8 +73,8 @@ class CSVBible(BibleImport): """ super().__init__(*args, **kwargs) self.log_info(self.__class__.__name__) - self.books_file = kwargs['booksfile'] - self.verses_file = kwargs['versefile'] + self.books_path = kwargs['books_path'] + self.verses_path = kwargs['verse_path'] @staticmethod def get_book_name(name, books): @@ -92,21 +92,22 @@ class CSVBible(BibleImport): return book_name @staticmethod - def parse_csv_file(filename, results_tuple): + def parse_csv_file(file_path, results_tuple): """ Parse the supplied CSV file. - :param filename: The name of the file to parse. Str - :param results_tuple: The namedtuple to use to store the results. namedtuple - :return: An iterable yielding namedtuples of type results_tuple + :param openlp.core.common.path.Path file_path: The name of the file to parse. + :param namedtuple results_tuple: The namedtuple to use to store the results. + :return: An list of namedtuples of type results_tuple + :rtype: list[namedtuple] """ try: - encoding = get_file_encoding(Path(filename))['encoding'] - with open(filename, 'r', encoding=encoding, newline='') as csv_file: + encoding = get_file_encoding(file_path)['encoding'] + with file_path.open('r', encoding=encoding, newline='') as csv_file: csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"') return [results_tuple(*line) for line in csv_reader] except (OSError, csv.Error): - raise ValidationError(msg='Parsing "{file}" failed'.format(file=filename)) + raise ValidationError(msg='Parsing "{file}" failed'.format(file=file_path)) def process_books(self, books): """ @@ -159,12 +160,12 @@ class CSVBible(BibleImport): self.language_id = self.get_language(bible_name) if not self.language_id: return False - books = self.parse_csv_file(self.books_file, Book) + books = self.parse_csv_file(self.books_path, Book) self.wizard.progress_bar.setValue(0) self.wizard.progress_bar.setMinimum(0) self.wizard.progress_bar.setMaximum(len(books)) book_list = self.process_books(books) - verses = self.parse_csv_file(self.verses_file, Verse) + verses = self.parse_csv_file(self.verses_path, Verse) self.wizard.progress_bar.setValue(0) self.wizard.progress_bar.setMaximum(len(books) + 1) self.process_verses(verses, book_list) diff --git a/openlp/plugins/bibles/lib/importers/http.py b/openlp/plugins/bibles/lib/importers/http.py index 5d3098b4d..b88dbe7a9 100644 --- a/openlp/plugins/bibles/lib/importers/http.py +++ b/openlp/plugins/bibles/lib/importers/http.py @@ -32,7 +32,8 @@ from bs4 import BeautifulSoup, NavigableString, Tag from openlp.core.common.httputils import get_web_page from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib.bibleimport import BibleImport diff --git a/openlp/plugins/bibles/lib/importers/opensong.py b/openlp/plugins/bibles/lib/importers/opensong.py index 2ac72503d..8400c9bf7 100644 --- a/openlp/plugins/bibles/lib/importers/opensong.py +++ b/openlp/plugins/bibles/lib/importers/opensong.py @@ -46,7 +46,8 @@ def parse_chapter_number(number, previous_number): :param number: The raw data from the xml :param previous_number: The previous chapter number - :return: Number of current chapter. (Int) + :return: Number of current chapter. + :rtype: int """ if number: return int(number.split()[-1]) @@ -132,13 +133,13 @@ class OpenSongBible(BibleImport): :param bible_name: The name of the bible being imported :return: True if import completed, False if import was unsuccessful """ - self.log_debug('Starting OpenSong import from "{name}"'.format(name=self.filename)) - self.validate_xml_file(self.filename, 'bible') - bible = self.parse_xml(self.filename, use_objectify=True) + self.log_debug('Starting OpenSong import from "{name}"'.format(name=self.file_path)) + self.validate_xml_file(self.file_path, 'bible') + bible = self.parse_xml(self.file_path, use_objectify=True) if bible is None: return False # No language info in the opensong format, so ask the user - self.language_id = self.get_language_id(bible_name=self.filename) + self.language_id = self.get_language_id(bible_name=str(self.file_path)) if not self.language_id: return False self.process_books(bible.b) diff --git a/openlp/plugins/bibles/lib/importers/osis.py b/openlp/plugins/bibles/lib/importers/osis.py index f10e084bc..cde57954a 100644 --- a/openlp/plugins/bibles/lib/importers/osis.py +++ b/openlp/plugins/bibles/lib/importers/osis.py @@ -159,14 +159,14 @@ class OSISBible(BibleImport): """ Loads a Bible from file. """ - self.log_debug('Starting OSIS import from "{name}"'.format(name=self.filename)) - self.validate_xml_file(self.filename, '{http://www.bibletechnologies.net/2003/osis/namespace}osis') - bible = self.parse_xml(self.filename, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS) + self.log_debug('Starting OSIS import from "{name}"'.format(name=self.file_path)) + self.validate_xml_file(self.file_path, '{http://www.bibletechnologies.net/2003/osis/namespace}osis') + bible = self.parse_xml(self.file_path, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS) if bible is None: return False # Find bible language language = bible.xpath("//ns:osisText/@xml:lang", namespaces=NS) - self.language_id = self.get_language_id(language[0] if language else None, bible_name=self.filename) + self.language_id = self.get_language_id(language[0] if language else None, bible_name=str(self.file_path)) if not self.language_id: return False self.process_books(bible) diff --git a/openlp/plugins/bibles/lib/importers/sword.py b/openlp/plugins/bibles/lib/importers/sword.py index fd7b91a44..dcb392754 100644 --- a/openlp/plugins/bibles/lib/importers/sword.py +++ b/openlp/plugins/bibles/lib/importers/sword.py @@ -60,7 +60,7 @@ class SwordBible(BibleImport): bible = pysword_modules.get_bible_from_module(self.sword_key) language = pysword_module_json['lang'] language = language[language.find('.') + 1:] - language_id = self.get_language_id(language, bible_name=self.filename) + language_id = self.get_language_id(language, bible_name=str(self.file_path)) if not language_id: return False books = bible.get_structure().get_books() diff --git a/openlp/plugins/bibles/lib/importers/wordproject.py b/openlp/plugins/bibles/lib/importers/wordproject.py index bf5c5fc75..90d23a2b5 100644 --- a/openlp/plugins/bibles/lib/importers/wordproject.py +++ b/openlp/plugins/bibles/lib/importers/wordproject.py @@ -19,15 +19,14 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import os -import re import logging -from codecs import open as copen +import re from tempfile import TemporaryDirectory from zipfile import ZipFile from bs4 import BeautifulSoup, Tag, NavigableString +from openlp.core.common.path import Path from openlp.plugins.bibles.lib.bibleimport import BibleImport BOOK_NUMBER_PATTERN = re.compile(r'\[(\d+)\]') @@ -51,9 +50,9 @@ class WordProjectBible(BibleImport): Unzip the file to a temporary directory """ self.tmp = TemporaryDirectory() - zip_file = ZipFile(os.path.abspath(self.filename)) - zip_file.extractall(self.tmp.name) - self.base_dir = os.path.join(self.tmp.name, os.path.splitext(os.path.basename(self.filename))[0]) + with ZipFile(str(self.file_path)) as zip_file: + zip_file.extractall(self.tmp.name) + self.base_path = Path(self.tmp.name, self.file_path.stem) def process_books(self): """ @@ -62,8 +61,7 @@ class WordProjectBible(BibleImport): :param bible_data: parsed xml :return: None """ - with copen(os.path.join(self.base_dir, 'index.htm'), encoding='utf-8', errors='ignore') as index_file: - page = index_file.read() + page = (self.base_path / 'index.htm').read_text(encoding='utf-8', errors='ignore') soup = BeautifulSoup(page, 'lxml') bible_books = soup.find('div', 'textOptions').find_all('li') book_count = len(bible_books) @@ -93,9 +91,7 @@ class WordProjectBible(BibleImport): :return: None """ log.debug(book_link) - book_file = os.path.join(self.base_dir, os.path.normpath(book_link)) - with copen(book_file, encoding='utf-8', errors='ignore') as f: - page = f.read() + page = (self.base_path / book_link).read_text(encoding='utf-8', errors='ignore') soup = BeautifulSoup(page, 'lxml') header_div = soup.find('div', 'textHeader') chapters_p = header_div.find('p') @@ -114,9 +110,8 @@ class WordProjectBible(BibleImport): """ Get the verses for a particular book """ - chapter_file_name = os.path.join(self.base_dir, '{:02d}'.format(book_number), '{}.htm'.format(chapter_number)) - with copen(chapter_file_name, encoding='utf-8', errors='ignore') as chapter_file: - page = chapter_file.read() + chapter_file_path = self.base_path / '{:02d}'.format(book_number) / '{}.htm'.format(chapter_number) + page = chapter_file_path.read_text(encoding='utf-8', errors='ignore') soup = BeautifulSoup(page, 'lxml') text_body = soup.find('div', 'textBody') if text_body: @@ -158,9 +153,9 @@ class WordProjectBible(BibleImport): """ Loads a Bible from file. """ - self.log_debug('Starting WordProject import from "{name}"'.format(name=self.filename)) + self.log_debug('Starting WordProject import from "{name}"'.format(name=self.file_path)) self._unzip_file() - self.language_id = self.get_language_id(None, bible_name=self.filename) + self.language_id = self.get_language_id(None, bible_name=str(self.file_path)) result = False if self.language_id: self.process_books() diff --git a/openlp/plugins/bibles/lib/importers/zefania.py b/openlp/plugins/bibles/lib/importers/zefania.py index a3849a7a0..2b5eb68b8 100644 --- a/openlp/plugins/bibles/lib/importers/zefania.py +++ b/openlp/plugins/bibles/lib/importers/zefania.py @@ -45,13 +45,13 @@ class ZefaniaBible(BibleImport): """ Loads a Bible from file. """ - log.debug('Starting Zefania import from "{name}"'.format(name=self.filename)) + log.debug('Starting Zefania import from "{name}"'.format(name=self.file_path)) success = True try: - xmlbible = self.parse_xml(self.filename, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS) + xmlbible = self.parse_xml(self.file_path, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS) # Find bible language language = xmlbible.xpath("/XMLBIBLE/INFORMATION/language/text()") - language_id = self.get_language_id(language[0] if language else None, bible_name=self.filename) + language_id = self.get_language_id(language[0] if language else None, bible_name=str(self.file_path)) if not language_id: return False no_of_books = int(xmlbible.xpath('count(//BIBLEBOOK)')) @@ -69,7 +69,7 @@ class ZefaniaBible(BibleImport): log.debug('Could not find a name, will use number, basically a guess.') book_ref_id = int(bnumber) if not book_ref_id: - log.error('Importing books from "{name}" failed'.format(name=self.filename)) + log.error('Importing books from "{name}" failed'.format(name=self.file_path)) return False book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) db_book = self.create_book(book_details['name'], book_ref_id, book_details['testament_id']) diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 55444f080..37586b4be 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -19,15 +19,13 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging from openlp.core.common import delete_file from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import Path -from openlp.core.common.registry import RegistryProperties from openlp.core.common.settings import Settings from openlp.plugins.bibles.lib import LanguageSelection, parse_reference from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta @@ -98,7 +96,7 @@ class BibleFormat(object): ] -class BibleManager(OpenLPMixin, RegistryProperties): +class BibleManager(LogMixin, RegistryProperties): """ The Bible manager which holds and manages all the Bibles. """ @@ -116,7 +114,7 @@ class BibleManager(OpenLPMixin, RegistryProperties): self.settings_section = 'bibles' self.web = 'Web' self.db_cache = None - self.path = str(AppLocation.get_section_data_path(self.settings_section)) + self.path = AppLocation.get_section_data_path(self.settings_section) self.proxy_name = Settings().value(self.settings_section + '/proxy name') self.suffix = '.sqlite' self.import_wizard = None @@ -129,20 +127,20 @@ class BibleManager(OpenLPMixin, RegistryProperties): of HTTPBible is loaded instead of the BibleDB class. """ log.debug('Reload bibles') - files = [str(file) for file in AppLocation.get_files(self.settings_section, self.suffix)] - if 'alternative_book_names.sqlite' in files: - files.remove('alternative_book_names.sqlite') - log.debug('Bible Files {text}'.format(text=files)) + file_paths = AppLocation.get_files(self.settings_section, self.suffix) + if Path('alternative_book_names.sqlite') in file_paths: + file_paths.remove(Path('alternative_book_names.sqlite')) + log.debug('Bible Files {text}'.format(text=file_paths)) self.db_cache = {} - for filename in files: - bible = BibleDB(self.parent, path=self.path, file=filename) + for file_path in file_paths: + bible = BibleDB(self.parent, path=self.path, file=file_path) if not bible.session: continue name = bible.get_name() # Remove corrupted files. if name is None: bible.session.close_all() - delete_file(Path(self.path, filename)) + delete_file(self.path / file_path) continue log.debug('Bible Name: "{name}"'.format(name=name)) self.db_cache[name] = bible @@ -151,7 +149,7 @@ class BibleManager(OpenLPMixin, RegistryProperties): source = self.db_cache[name].get_object(BibleMeta, 'download_source') download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server') - web_bible = HTTPBible(self.parent, path=self.path, file=filename, download_source=source.value, + web_bible = HTTPBible(self.parent, path=self.path, file=file_path, download_source=source.value, download_name=download_name) if meta_proxy: web_bible.proxy_server = meta_proxy.value diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 10bb8d6ec..fdfc4b51d 100755 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -30,9 +30,9 @@ from openlp.core.common.i18n import UiStrings, translate, get_locale_key from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext -from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \ critical_error_message_box, find_and_set_in_combo_box, build_icon +from openlp.core.widgets.edits import SearchEdit from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm from openlp.plugins.bibles.forms.editbibleform import EditBibleForm from openlp.plugins.bibles.lib import DisplayStyle, LayoutStyle, VerseReferenceList, \ diff --git a/openlp/plugins/custom/forms/editcustomslidedialog.py b/openlp/plugins/custom/forms/editcustomslidedialog.py index 452146feb..5ccc94cc6 100644 --- a/openlp/plugins/custom/forms/editcustomslidedialog.py +++ b/openlp/plugins/custom/forms/editcustomslidedialog.py @@ -25,7 +25,7 @@ from PyQt5 import QtWidgets from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon from openlp.core.lib.ui import create_button, create_button_box -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.widgets.edits import SpellTextEdit class Ui_CustomSlideEditDialog(object): diff --git a/openlp/plugins/custom/lib/db.py b/openlp/plugins/custom/lib/db.py index dc1f74567..6581ee5ae 100644 --- a/openlp/plugins/custom/lib/db.py +++ b/openlp/plugins/custom/lib/db.py @@ -26,7 +26,7 @@ the Custom plugin from sqlalchemy import Column, Table, types from sqlalchemy.orm import mapper -from openlp.core.common.i18n import get_locale_key +from openlp.core.common.i18n import get_natural_key from openlp.core.lib.db import BaseModel, init_db @@ -36,10 +36,10 @@ class CustomSlide(BaseModel): """ # By default sort the customs by its title considering language specific characters. def __lt__(self, other): - return get_locale_key(self.title) < get_locale_key(other.title) + return get_natural_key(self.title) < get_natural_key(other.title) def __eq__(self, other): - return get_locale_key(self.title) == get_locale_key(other.title) + return get_natural_key(self.title) == get_natural_key(other.title) def __hash__(self): """ diff --git a/openlp/plugins/images/lib/imagetab.py b/openlp/plugins/images/lib/imagetab.py index 23b742bfb..e2b64a6cc 100644 --- a/openlp/plugins/images/lib/imagetab.py +++ b/openlp/plugins/images/lib/imagetab.py @@ -25,7 +25,7 @@ from PyQt5 import QtWidgets from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab -from openlp.core.ui.lib.colorbutton import ColorButton +from openlp.core.widgets.buttons import ColorButton class ImageTab(SettingsTab): diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 134dbe2eb..25a8d2353 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -26,14 +26,14 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import delete_file, get_images_filter from openlp.core.common.applocation import AppLocation -from openlp.core.common.i18n import UiStrings, translate, get_locale_key +from openlp.core.common.i18n import UiStrings, translate, get_natural_key from openlp.core.common.path import Path, create_paths from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.ui.lib.treewidgetwithdnd import TreeWidgetWithDnD +from openlp.core.widgets.views import TreeWidgetWithDnD from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups @@ -81,8 +81,12 @@ class ImageMediaItem(MediaManagerItem): self.add_group_action.setToolTip(UiStrings().AddGroupDot) self.replace_action.setText(UiStrings().ReplaceBG) self.replace_action.setToolTip(UiStrings().ReplaceLiveBG) + self.replace_action_context.setText(UiStrings().ReplaceBG) + self.replace_action_context.setToolTip(UiStrings().ReplaceLiveBG) self.reset_action.setText(UiStrings().ResetBG) self.reset_action.setToolTip(UiStrings().ResetLiveBG) + self.reset_action_context.setText(UiStrings().ResetBG) + self.reset_action_context.setToolTip(UiStrings().ResetLiveBG) def required_icons(self): """ @@ -184,6 +188,13 @@ class ImageMediaItem(MediaManagerItem): self.list_view, text=translate('ImagePlugin', 'Add new image(s)'), icon=':/general/general_open.png', triggers=self.on_file_click) + create_widget_action(self.list_view, separator=True) + self.replace_action_context = create_widget_action( + self.list_view, text=UiStrings().ReplaceBG, icon=':/slides/slide_theme.png', + triggers=self.on_replace_click) + self.reset_action_context = create_widget_action( + self.list_view, text=UiStrings().ReplaceLiveBG, icon=':/system/system_close.png', + visible=False, triggers=self.on_reset_click) def add_start_header_bar(self): """ @@ -271,7 +282,7 @@ class ImageMediaItem(MediaManagerItem): :param parent_group_id: The ID of the group that will be added recursively. """ image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id) - image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name)) + image_groups.sort(key=lambda group_object: get_natural_key(group_object.group_name)) folder_icon = build_icon(':/images/image_group.png') for image_group in image_groups: group = QtWidgets.QTreeWidgetItem() @@ -298,7 +309,7 @@ class ImageMediaItem(MediaManagerItem): combobox.clear() combobox.top_level_group_added = False image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id) - image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name)) + image_groups.sort(key=lambda group_object: get_natural_key(group_object.group_name)) for image_group in image_groups: combobox.addItem(prefix + image_group.group_name, image_group.id) self.fill_groups_combobox(combobox, image_group.id, prefix + ' ') @@ -355,7 +366,7 @@ class ImageMediaItem(MediaManagerItem): self.expand_group(open_group.id) # Sort the images by its filename considering language specific. # characters. - images.sort(key=lambda image_object: get_locale_key(image_object.file_path.name)) + images.sort(key=lambda image_object: get_natural_key(image_object.file_path.name)) for image in images: log.debug('Loading image: {name}'.format(name=image.file_path)) file_name = image.file_path.name @@ -390,6 +401,7 @@ class ImageMediaItem(MediaManagerItem): :param files: A List of strings containing the filenames of the files to be loaded :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files """ + file_paths = [Path(file) for file in file_paths] self.application.set_normal_cursor() self.load_list(file_paths, target_group) last_dir = file_paths[0].parent @@ -532,9 +544,9 @@ class ImageMediaItem(MediaManagerItem): group_items.append(item) if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames): image_items.append(item) - group_items.sort(key=lambda item: get_locale_key(item.text(0))) + group_items.sort(key=lambda item: get_natural_key(item.text(0))) target_group.addChildren(group_items) - image_items.sort(key=lambda item: get_locale_key(item.text(0))) + image_items.sort(key=lambda item: get_natural_key(item.text(0))) target_group.addChildren(image_items) def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, @@ -658,6 +670,7 @@ class ImageMediaItem(MediaManagerItem): Called to reset the Live background with the image selected. """ self.reset_action.setVisible(False) + self.reset_action_context.setVisible(False) self.live_controller.display.reset_image() def live_theme_changed(self): @@ -665,6 +678,7 @@ class ImageMediaItem(MediaManagerItem): Triggered by the change of theme in the slide controller. """ self.reset_action.setVisible(False) + self.reset_action_context.setVisible(False) def on_replace_click(self): """ @@ -682,6 +696,7 @@ class ImageMediaItem(MediaManagerItem): if file_path.exists(): if self.live_controller.display.direct_image(str(file_path), background): self.reset_action.setVisible(True) + self.reset_action_context.setVisible(True) else: critical_error_message_box( UiStrings().LiveBGError, diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index f48e9b09e..be9e69fbd 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -28,8 +28,9 @@ from datetime import datetime from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import is_win, is_linux, is_macosx +from openlp.core.common.path import Path from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.media.vlcplayer import get_vlc @@ -109,7 +110,7 @@ class MediaClipSelectorForm(QtWidgets.QDialog, Ui_MediaClipSelector, RegistryPro self.subtitle_tracks_combobox.clear() self.audio_tracks_combobox.clear() self.titles_combo_box.clear() - time = QtCore.QTime() + time = QtCore.QTime(0, 0, 0) self.start_position_edit.setTime(time) self.end_timeedit.setTime(time) self.position_timeedit.setTime(time) @@ -294,7 +295,7 @@ class MediaClipSelectorForm(QtWidgets.QDialog, Ui_MediaClipSelector, RegistryPro :param clicked: Given from signal, not used. """ vlc_ms_pos = self.vlc_media_player.get_time() - time = QtCore.QTime() + time = QtCore.QTime(0, 0, 0) new_pos_time = time.addMSecs(vlc_ms_pos) self.start_position_edit.setTime(new_pos_time) # If start time is after end time, update end time. @@ -310,7 +311,7 @@ class MediaClipSelectorForm(QtWidgets.QDialog, Ui_MediaClipSelector, RegistryPro :param clicked: Given from signal, not used. """ vlc_ms_pos = self.vlc_media_player.get_time() - time = QtCore.QTime() + time = QtCore.QTime(0, 0, 0) new_pos_time = time.addMSecs(vlc_ms_pos) self.end_timeedit.setTime(new_pos_time) # If start time is after end time, update start time. @@ -447,7 +448,7 @@ class MediaClipSelectorForm(QtWidgets.QDialog, Ui_MediaClipSelector, RegistryPro self.position_slider.setMaximum(self.playback_length) # setup start and end time rounded_vlc_ms_length = int(round(self.playback_length / 100.0) * 100.0) - time = QtCore.QTime() + time = QtCore.QTime(0, 0, 0) playback_length_time = time.addMSecs(rounded_vlc_ms_length) self.start_position_edit.setMaximumTime(playback_length_time) self.end_timeedit.setMaximumTime(playback_length_time) @@ -505,7 +506,7 @@ class MediaClipSelectorForm(QtWidgets.QDialog, Ui_MediaClipSelector, RegistryPro if self.vlc_media_player: vlc_ms_pos = self.vlc_media_player.get_time() rounded_vlc_ms_pos = int(round(vlc_ms_pos / 100.0) * 100.0) - time = QtCore.QTime() + time = QtCore.QTime(0, 0, 0) new_pos_time = time.addMSecs(rounded_vlc_ms_pos) self.position_timeedit.setTime(new_pos_time) self.position_slider.setSliderPosition(vlc_ms_pos) @@ -615,7 +616,7 @@ class MediaClipSelectorForm(QtWidgets.QDialog, Ui_MediaClipSelector, RegistryPro break # Append the new name to the optical string and the path optical += new_optical_name + ':' + path - self.media_item.add_optical_clip(optical) + self.media_item.add_optical_clip(Path(optical)) def media_state_wait(self, media_state): """ diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index ab0cf4968..1c7556aa9 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -26,9 +26,10 @@ import os from PyQt5 import QtCore, QtWidgets from openlp.core.common.applocation import AppLocation -from openlp.core.common.i18n import UiStrings, translate, get_locale_key +from openlp.core.common.i18n import UiStrings, translate, get_natural_key from openlp.core.common.path import Path, path_to_str, create_paths -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, ServiceItem, ServiceItemContext, \ build_icon, check_item_selected @@ -175,7 +176,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): def add_custom_context_actions(self): create_widget_action(self.list_view, separator=True) self.replace_action_context = create_widget_action( - self.list_view, text=UiStrings().ReplaceBG, icon=':/slides/slide_blank.png', + self.list_view, text=UiStrings().ReplaceBG, icon=':/slides/slide_theme.png', triggers=self.on_replace_click) self.reset_action_context = create_widget_action( self.list_view, text=UiStrings().ReplaceLiveBG, icon=':/system/system_close.png', @@ -268,10 +269,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): service_item.add_from_command(filename, name, CLAPPERBOARD) service_item.title = clip_name # Set the length - self.media_controller.media_setup_optical(name, title, audio_track, subtitle_track, start, end, None, None) - service_item.set_media_length((end - start) / 1000) - service_item.start_time = start / 1000 - service_item.end_time = end / 1000 + service_item.set_media_length(end - start) + service_item.start_time = start + service_item.end_time = end service_item.add_capability(ItemCapabilities.IsOptical) else: if not os.path.exists(filename): @@ -361,7 +361,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :param media: The media :param target_group: """ - media.sort(key=lambda file_name: get_locale_key(os.path.split(str(file_name))[1])) + media.sort(key=lambda file_name: get_natural_key(os.path.split(str(file_name))[1])) for track in media: track_info = QtCore.QFileInfo(track) item_name = None @@ -403,7 +403,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :return: The media list """ media_file_paths = Settings().value(self.settings_section + '/media files') - media_file_paths.sort(key=lambda file_path: get_locale_key(file_path.name)) + media_file_paths.sort(key=lambda file_path: get_natural_key(file_path.name)) if media_type == MediaType.Audio: extension = self.media_controller.audio_extensions_list else: @@ -454,5 +454,5 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return # Append the optical string to the media list file_paths.append(optical) - self.load_list([optical]) + self.load_list([str(optical)]) Settings().setValue(self.settings_section + '/media files', file_paths) diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index 5efe8c910..461e78746 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -78,10 +78,10 @@ class MediaPlugin(Plugin): """ log.debug('check_installed Mediainfo') # Try to find mediainfo in the path - exists = process_check_binary('mediainfo') + exists = process_check_binary(Path('mediainfo')) # If mediainfo is not in the path, try to find it in the application folder if not exists: - exists = process_check_binary(os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), 'mediainfo')) + exists = process_check_binary(AppLocation.get_directory(AppLocation.AppDir) / 'mediainfo') return exists def app_startup(self): @@ -165,10 +165,11 @@ def process_check_binary(program_path): """ Function that checks whether a binary MediaInfo is present - :param program_path:The full path to the binary to check. + :param openlp.core.common.path.Path program_path:The full path to the binary to check. :return: If exists or not + :rtype: bool """ - runlog = check_binary_exists(Path(program_path)) + runlog = check_binary_exists(program_path) # Analyse the output to see it the program is mediainfo for line in runlog.splitlines(): decoded_line = line.decode() diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index b801597b1..923953040 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -23,7 +23,7 @@ import logging from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common.i18n import UiStrings, translate, get_locale_key +from openlp.core.common.i18n import UiStrings, translate, get_natural_key from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings @@ -165,7 +165,7 @@ class PresentationMediaItem(MediaManagerItem): if not initial_load: self.main_window.display_progress_bar(len(file_paths)) # Sort the presentations by its filename considering language specific characters. - file_paths.sort(key=lambda file_path: get_locale_key(file_path.name)) + file_paths.sort(key=lambda file_path: get_natural_key(file_path.name)) for file_path in file_paths: if not initial_load: self.main_window.increment_progress_bar() @@ -198,10 +198,10 @@ class PresentationMediaItem(MediaManagerItem): if not (preview_path and preview_path.exists()): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(Path(preview_path), Path(thumbnail_path)): + if validate_thumb(preview_path, thumbnail_path): icon = build_icon(thumbnail_path) else: - icon = create_thumb(str(preview_path), str(thumbnail_path)) + icon = create_thumb(preview_path, thumbnail_path) else: if initial_load: icon = build_icon(':/general/general_delete.png') @@ -243,7 +243,7 @@ class PresentationMediaItem(MediaManagerItem): """ Clean up the files created such as thumbnails - :param openlp.core.common.path.Path file_path: File path of the presention to clean up after + :param openlp.core.common.path.Path file_path: File path of the presentation to clean up after :param bool clean_for_update: Only clean thumbnails if update is needed :rtype: None """ diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index 26eb87d85..715e4e3e7 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -19,8 +19,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -import os import logging import re from subprocess import check_output, CalledProcessError diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index daa73f48f..40d9b2c65 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -70,7 +70,7 @@ class PptviewController(PresentationController): try: self.start_process() return self.process.CheckInstalled() - except WindowsError: + except OSError: return False def start_process(self): diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index b19d91837..d364f4e48 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -25,7 +25,7 @@ from PyQt5 import QtCore from openlp.core.common import md5_hash from openlp.core.common.applocation import AppLocation -from openlp.core.common.path import Path, create_paths, rmtree +from openlp.core.common.path import Path, create_paths from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import create_thumb, validate_thumb @@ -126,9 +126,9 @@ class PresentationDocument(object): thumbnail_folder_path = self.get_thumbnail_folder() temp_folder_path = self.get_temp_folder() if thumbnail_folder_path.exists(): - rmtree(thumbnail_folder_path) + thumbnail_folder_path.rmtree() if temp_folder_path.exists(): - rmtree(temp_folder_path) + temp_folder_path.rmtree() except OSError: log.exception('Failed to delete presentation controller files') @@ -139,7 +139,8 @@ class PresentationDocument(object): :return: The path to the thumbnail :rtype: openlp.core.common.path.Path """ - # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed + # TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in + # get_temp_folder and PresentationPluginapp_startup is removed if Settings().value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(bytes(self.file_path)) else: @@ -153,7 +154,8 @@ class PresentationDocument(object): :return: The path to the temporary file folder :rtype: openlp.core.common.path.Path """ - # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed + # TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in + # get_thumbnail_folder and PresentationPluginapp_startup is removed if Settings().value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(bytes(self.file_path)) else: @@ -265,7 +267,7 @@ class PresentationDocument(object): return if image_path.is_file(): thumb_path = self.get_thumbnail_path(index, False) - create_thumb(str(image_path), str(thumb_path), False, QtCore.QSize(-1, 360)) + create_thumb(image_path, thumb_path, False, QtCore.QSize(-1, 360)) def get_thumbnail_path(self, slide_no, check_exists=False): """ diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index 96fcc573a..d6a4f5190 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -26,7 +26,7 @@ from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib import PathEdit +from openlp.core.widgets.edits import PathEdit from openlp.plugins.presentations.lib.pdfcontroller import PdfController diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 7f3333049..a68a22176 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -31,6 +31,7 @@ from PyQt5 import QtCore from openlp.core.api.http import register_endpoint from openlp.core.common import extension_loader from openlp.core.common.i18n import translate +from openlp.core.common.settings import Settings from openlp.core.lib import Plugin, StringContent, build_icon from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab @@ -136,6 +137,20 @@ class PresentationPlugin(Plugin): self.register_controllers(controller) return bool(self.controllers) + def app_startup(self): + """ + Perform tasks on application startup. + """ + # TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in + # PresentationDocument.get_thumbnail_folder and PresentationDocument.get_temp_folder is removed + super().app_startup() + presentation_paths = Settings().value('presentations/presentations files') + for path in presentation_paths: + self.media_item.clean_up_thumbnails(path, clean_for_update=True) + self.media_item.list_view.clear() + Settings().setValue('presentations/thumbnail_scheme', 'md5') + self.media_item.validate_and_load(presentation_paths) + @staticmethod def about(): """ diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index d2238d4b7..8e7724808 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -29,8 +29,9 @@ import multiprocessing from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib import delete_song from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index bdb1a9353..6e0772418 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -31,10 +31,11 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate, get_natural_key from openlp.core.common.path import create_paths, copyfile -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib import PluginStatus, MediaType, create_separated_list from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog from openlp.plugins.songs.forms.editverseform import EditVerseForm from openlp.plugins.songs.forms.mediafilesform import MediaFilesForm @@ -104,9 +105,9 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.topics_list_view.setSortingEnabled(False) self.topics_list_view.setAlternatingRowColors(True) self.audio_list_widget.setAlternatingRowColors(True) - self.find_verse_split = re.compile('---\[\]---\n', re.UNICODE) - self.whitespace = re.compile(r'\W+', re.UNICODE) - self.find_tags = re.compile(u'\{/?\w+\}', re.UNICODE) + self.find_verse_split = re.compile('---\[\]---\n') + self.whitespace = re.compile(r'\W+') + self.find_tags = re.compile(r'\{/?\w+\}') def _load_objects(self, cls, combo, cache): """ diff --git a/openlp/plugins/songs/forms/editversedialog.py b/openlp/plugins/songs/forms/editversedialog.py index 63ca0cf27..76dc70c17 100644 --- a/openlp/plugins/songs/forms/editversedialog.py +++ b/openlp/plugins/songs/forms/editversedialog.py @@ -26,7 +26,7 @@ from openlp.core.common.settings import Settings from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon from openlp.core.lib.ui import create_button_box -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.widgets.edits import SpellTextEdit from openlp.plugins.songs.lib import VerseType diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 28ca8054c..c6446d74c 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -32,8 +32,9 @@ from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import create_separated_list from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib import PathEdit, PathType -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport @@ -124,7 +125,7 @@ class SongExportForm(OpenLPWizard): self.selected_list_widget.setObjectName('selected_list_widget') self.grid_layout.addWidget(self.selected_list_widget, 1, 0, 1, 2) self.output_directory_path_edit = PathEdit( - self.export_song_page, PathType.Directories, + self.export_song_page, PathEditType.Directories, dialog_caption=translate('SongsPlugin.ExportWizardForm', 'Select Destination Folder'), show_revert=False) self.output_directory_path_edit.path = Settings().value('songs/last directory export') self.directory_label = QtWidgets.QLabel(self.export_song_page) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index acfa4b5b8..af88bfb35 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -27,12 +27,13 @@ import logging from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib import PathEdit, PathType -from openlp.core.ui.lib.filedialog import FileDialog -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect log = logging.getLogger(__name__) @@ -383,10 +384,10 @@ class SongImportForm(OpenLPWizard, RegistryProperties): file_path_label = QtWidgets.QLabel(import_widget) file_path_layout.addWidget(file_path_label) if select_mode == SongFormatSelect.SingleFile: - path_type = PathType.Files + path_type = PathEditType.Files dialog_caption = WizardStrings.OpenTypeFile.format(file_type=format_name) else: - path_type = PathType.Directories + path_type = PathEditType.Directories dialog_caption = WizardStrings.OpenTypeFolder.format(folder_name=format_name) path_edit = PathEdit( parent=import_widget, path_type=path_type, dialog_caption=dialog_caption, show_revert=False) diff --git a/openlp/plugins/songs/forms/songmaintenanceform.py b/openlp/plugins/songs/forms/songmaintenanceform.py index 4dc485e24..7f0236d24 100644 --- a/openlp/plugins/songs/forms/songmaintenanceform.py +++ b/openlp/plugins/songs/forms/songmaintenanceform.py @@ -25,7 +25,8 @@ from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_ from openlp.core.common.i18n import UiStrings, translate, get_natural_key -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.songs.forms.authorsform import AuthorsForm from openlp.plugins.songs.forms.topicsform import TopicsForm diff --git a/openlp/plugins/songs/forms/songselectdialog.py b/openlp/plugins/songs/forms/songselectdialog.py index b784071bd..bc0bd7415 100644 --- a/openlp/plugins/songs/forms/songselectdialog.py +++ b/openlp/plugins/songs/forms/songselectdialog.py @@ -28,7 +28,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate from openlp.core.lib import build_icon from openlp.core.ui import SingleColumnTableWidget -from openlp.core.ui.lib.historycombobox import HistoryComboBox +from openlp.core.widgets.edits import HistoryComboBox class Ui_SongSelectDialog(object): diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index f88aa8678..74334ef0d 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -24,7 +24,6 @@ The :mod:`~openlp.plugins.songs.lib` module contains a number of library functio """ import logging -import os import re from PyQt5 import QtWidgets @@ -39,8 +38,8 @@ from openlp.plugins.songs.lib.ui import SongStrings log = logging.getLogger(__name__) -WHITESPACE = re.compile(r'[\W_]+', re.UNICODE) -APOSTROPHE = re.compile('[\'`’ʻ′]', re.UNICODE) +WHITESPACE = re.compile(r'[\W_]+') +APOSTROPHE = re.compile(r'[\'`’ʻ′]') # PATTERN will look for the next occurence of one of these symbols: # \controlword - optionally preceded by \*, optionally followed by a number # \'## - where ## is a pair of hex digits, representing a single character diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 3b8081999..e306dae87 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -26,7 +26,7 @@ import logging from openlp.core.common import is_win from openlp.core.common.i18n import UiStrings, translate -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from .importers.opensong import OpenSongImport from .importers.easyslides import EasySlidesImport from .importers.openlp import OpenLPSongImport diff --git a/openlp/plugins/songs/lib/importers/easyslides.py b/openlp/plugins/songs/lib/importers/easyslides.py index a1ffb7b7c..6d717bdb4 100644 --- a/openlp/plugins/songs/lib/importers/easyslides.py +++ b/openlp/plugins/songs/lib/importers/easyslides.py @@ -25,6 +25,7 @@ import re from lxml import etree, objectify +from openlp.core.common import normalize_str from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib.importers.songimport import SongImport @@ -225,7 +226,7 @@ class EasySlidesImport(SongImport): verses[reg].setdefault(vt, {}) verses[reg][vt].setdefault(vn, {}) verses[reg][vt][vn].setdefault(inst, []) - verses[reg][vt][vn][inst].append(self.tidy_text(line)) + verses[reg][vt][vn][inst].append(normalize_str(line)) # done parsing versetags = [] # we use our_verse_order to ensure, we insert lyrics in the same order diff --git a/openlp/plugins/songs/lib/importers/easyworship.py b/openlp/plugins/songs/lib/importers/easyworship.py index c82a7e6ad..8f096099f 100644 --- a/openlp/plugins/songs/lib/importers/easyworship.py +++ b/openlp/plugins/songs/lib/importers/easyworship.py @@ -44,7 +44,7 @@ NOTE_REGEX = re.compile(r'\(.*?\)') log = logging.getLogger(__name__) -class FieldDescEntry: +class FieldDescEntry(object): def __init__(self, name, field_type, size): self.name = name self.field_type = field_type diff --git a/openlp/plugins/songs/lib/importers/foilpresenter.py b/openlp/plugins/songs/lib/importers/foilpresenter.py index 860177172..f164dfda2 100644 --- a/openlp/plugins/songs/lib/importers/foilpresenter.py +++ b/openlp/plugins/songs/lib/importers/foilpresenter.py @@ -88,7 +88,7 @@ import re from lxml import etree, objectify from openlp.core.common.i18n import translate -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.db import Author, Book, Song, Topic diff --git a/openlp/plugins/songs/lib/importers/mediashout.py b/openlp/plugins/songs/lib/importers/mediashout.py index 67cf0d0fb..9df9baa0f 100644 --- a/openlp/plugins/songs/lib/importers/mediashout.py +++ b/openlp/plugins/songs/lib/importers/mediashout.py @@ -101,7 +101,7 @@ class MediaShoutImport(SongImport): self.song_book_name = song.SongID for verse in verses: tag = VERSE_TAGS[verse.Type] + str(verse.Number) if verse.Type < len(VERSE_TAGS) else 'O' - self.add_verse(self.tidy_text(verse.Text), tag) + self.add_verse(verse.Text, tag) for order in verse_order: if order.Type < len(VERSE_TAGS): self.verse_order_list.append(VERSE_TAGS[order.Type] + str(order.Number)) diff --git a/openlp/plugins/songs/lib/importers/openlp.py b/openlp/plugins/songs/lib/importers/openlp.py index 252b8fd8b..80a981d02 100644 --- a/openlp/plugins/songs/lib/importers/openlp.py +++ b/openlp/plugins/songs/lib/importers/openlp.py @@ -31,7 +31,7 @@ from sqlalchemy.orm.exc import UnmappedClassError from openlp.core.common.i18n import translate from openlp.core.lib.db import BaseModel -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song from openlp.plugins.songs.lib.db import Author, Book, Song, Topic, MediaFile from .songimport import SongImport diff --git a/openlp/plugins/songs/lib/importers/openlyrics.py b/openlp/plugins/songs/lib/importers/openlyrics.py index 44f5f96bf..f443cfb34 100644 --- a/openlp/plugins/songs/lib/importers/openlyrics.py +++ b/openlp/plugins/songs/lib/importers/openlyrics.py @@ -27,7 +27,7 @@ import logging from lxml import etree -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, OpenLyricsError diff --git a/openlp/plugins/songs/lib/importers/openoffice.py b/openlp/plugins/songs/lib/importers/openoffice.py index a097d8b85..f2a8b2147 100644 --- a/openlp/plugins/songs/lib/importers/openoffice.py +++ b/openlp/plugins/songs/lib/importers/openoffice.py @@ -24,7 +24,7 @@ import time from PyQt5 import QtCore -from openlp.core.common import is_win, get_uno_command, get_uno_instance +from openlp.core.common import get_uno_command, get_uno_instance, is_win, normalize_str from openlp.core.common.i18n import translate from .songimport import SongImport @@ -241,7 +241,7 @@ class OpenOfficeImport(SongImport): :param text: The text. """ - song_texts = self.tidy_text(text).split('\f') + song_texts = normalize_str(text).split('\f') self.set_defaults() for song_text in song_texts: if song_text.strip(): diff --git a/openlp/plugins/songs/lib/importers/opensong.py b/openlp/plugins/songs/lib/importers/opensong.py index e6924e7b2..6cd690562 100644 --- a/openlp/plugins/songs/lib/importers/opensong.py +++ b/openlp/plugins/songs/lib/importers/opensong.py @@ -25,6 +25,7 @@ import re from lxml import objectify from lxml.etree import Error, LxmlError +from openlp.core.common import normalize_str from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings from openlp.plugins.songs.lib import VerseType @@ -262,7 +263,7 @@ class OpenSongImport(SongImport): post=this_line[offset + column:]) offset += len(chord) + 2 # Tidy text and remove the ____s from extended words - this_line = self.tidy_text(this_line) + this_line = normalize_str(this_line) this_line = this_line.replace('_', '') this_line = this_line.replace('||', '\n[---]\n') this_line = this_line.strip() diff --git a/openlp/plugins/songs/lib/importers/powerpraise.py b/openlp/plugins/songs/lib/importers/powerpraise.py index a08652e3f..fd6f64360 100644 --- a/openlp/plugins/songs/lib/importers/powerpraise.py +++ b/openlp/plugins/songs/lib/importers/powerpraise.py @@ -25,8 +25,8 @@ Powerpraise song files into the current database. """ from lxml import objectify -from openlp.core.ui.lib.wizard import WizardStrings -from .songimport import SongImport +from openlp.core.widgets.wizard import WizardStrings +from openlp.plugins.songs.lib.importers.songimport import SongImport class PowerPraiseImport(SongImport): diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index e7fec2a6c..c8f9a16f9 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -29,8 +29,8 @@ from lxml import objectify, etree from openlp.core.common.i18n import translate from openlp.core.common import get_file_encoding -from openlp.core.ui.lib.wizard import WizardStrings -from .songimport import SongImport +from openlp.core.widgets.wizard import WizardStrings +from openlp.plugins.songs.lib.importers.songimport import SongImport class PresentationManagerImport(SongImport): diff --git a/openlp/plugins/songs/lib/importers/propresenter.py b/openlp/plugins/songs/lib/importers/propresenter.py index 582b1a6ee..0de0e5300 100644 --- a/openlp/plugins/songs/lib/importers/propresenter.py +++ b/openlp/plugins/songs/lib/importers/propresenter.py @@ -27,9 +27,9 @@ import base64 import logging from lxml import objectify -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import strip_rtf -from .songimport import SongImport +from openlp.plugins.songs.lib.importers.songimport import SongImport log = logging.getLogger(__name__) diff --git a/openlp/plugins/songs/lib/importers/songimport.py b/openlp/plugins/songs/lib/importers/songimport.py index e0cc5220e..2bd8c0e56 100644 --- a/openlp/plugins/songs/lib/importers/songimport.py +++ b/openlp/plugins/songs/lib/importers/songimport.py @@ -25,11 +25,12 @@ import re from PyQt5 import QtCore +from openlp.core.common import normalize_str from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate from openlp.core.common.path import copyfile, create_paths from openlp.core.common.registry import Registry -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile from openlp.plugins.songs.lib.ui import SongStrings @@ -130,26 +131,6 @@ class SongImport(QtCore.QObject): def register(self, import_wizard): self.import_wizard = import_wizard - def tidy_text(self, text): - """ - Get rid of some dodgy unicode and formatting characters we're not interested in. Some can be converted to ascii. - """ - text = text.replace('\u2018', '\'') - text = text.replace('\u2019', '\'') - text = text.replace('\u201c', '"') - text = text.replace('\u201d', '"') - text = text.replace('\u2026', '...') - text = text.replace('\u2013', '-') - text = text.replace('\u2014', '-') - # Replace vertical tab with 2 linebreaks - text = text.replace('\v', '\n\n') - # Replace form feed (page break) with 2 linebreaks - text = text.replace('\f', '\n\n') - # Remove surplus blank lines, spaces, trailing/leading spaces - text = re.sub(r'[ \t]+', ' ', text) - text = re.sub(r' ?(\r\n?|\n) ?', '\n', text) - return text - def process_song_text(self, text): """ Process the song text from import @@ -368,7 +349,7 @@ class SongImport(QtCore.QObject): verse_tag = VerseType.tags[VerseType.Other] log.info('Versetype {old} changing to {new}'.format(old=verse_def, new=new_verse_def)) verse_def = new_verse_def - sxml.add_verse_to_lyrics(verse_tag, verse_def[1:], verse_text, lang) + sxml.add_verse_to_lyrics(verse_tag, verse_def[1:], normalize_str(verse_text), lang) song.lyrics = str(sxml.extract_xml(), 'utf-8') if not self.verse_order_list and self.verse_order_list_generated_useful: self.verse_order_list = self.verse_order_list_generated diff --git a/openlp/plugins/songs/lib/importers/songshowplus.py b/openlp/plugins/songs/lib/importers/songshowplus.py index 2fcf414dd..3109fae1d 100644 --- a/openlp/plugins/songs/lib/importers/songshowplus.py +++ b/openlp/plugins/songs/lib/importers/songshowplus.py @@ -27,7 +27,7 @@ import logging import re import struct -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding from openlp.plugins.songs.lib.importers.songimport import SongImport diff --git a/openlp/plugins/songs/lib/importers/songsoffellowship.py b/openlp/plugins/songs/lib/importers/songsoffellowship.py index 13e073cc1..bbba654c9 100644 --- a/openlp/plugins/songs/lib/importers/songsoffellowship.py +++ b/openlp/plugins/songs/lib/importers/songsoffellowship.py @@ -194,7 +194,6 @@ class SongsOfFellowshipImport(OpenOfficeImport): :param text_portion: A Piece of text """ text = text_portion.getString() - text = self.tidy_text(text) if text.strip() == '': return text if text_portion.CharWeight == BOLD: diff --git a/openlp/plugins/songs/lib/importers/zionworx.py b/openlp/plugins/songs/lib/importers/zionworx.py index 5cfc0576d..23817c31a 100644 --- a/openlp/plugins/songs/lib/importers/zionworx.py +++ b/openlp/plugins/songs/lib/importers/zionworx.py @@ -30,9 +30,6 @@ from openlp.plugins.songs.lib.importers.songimport import SongImport log = logging.getLogger(__name__) -# Used to strip control chars (except 10=LF, 13=CR) -CONTROL_CHARS_MAP = dict.fromkeys(list(range(10)) + [11, 12] + list(range(14, 32)) + [127]) - class ZionWorxImport(SongImport): """ @@ -95,12 +92,12 @@ class ZionWorxImport(SongImport): return self.set_defaults() try: - self.title = self._decode(record['Title1']) + self.title = record['Title1'] if record['Title2']: - self.alternate_title = self._decode(record['Title2']) - self.parse_author(self._decode(record['Writer'])) - self.add_copyright(self._decode(record['Copyright'])) - lyrics = self._decode(record['Lyrics']) + self.alternate_title = record['Title2'] + self.parse_author(record['Writer']) + self.add_copyright(record['Copyright']) + lyrics = record['Lyrics'] except UnicodeDecodeError as e: self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record {index}').format(index=index), translate('SongsPlugin.ZionWorxImport', 'Decoding error: {error}').format(error=e)) @@ -122,10 +119,3 @@ class ZionWorxImport(SongImport): if not self.finish(): self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index + (': "' + title + '"' if title else '')) - - def _decode(self, str): - """ - Strips all control characters (except new lines). - """ - # ZionWorx has no option for setting the encoding for its songs, so we assume encoding is always the same. - return str.translate(CONTROL_CHARS_MAP) diff --git a/openlp/plugins/songs/lib/openlyricsexport.py b/openlp/plugins/songs/lib/openlyricsexport.py index ca7fe0bc7..b07298253 100644 --- a/openlp/plugins/songs/lib/openlyricsexport.py +++ b/openlp/plugins/songs/lib/openlyricsexport.py @@ -30,7 +30,7 @@ from lxml import etree from openlp.core.common import clean_filename from openlp.core.common.i18n import translate from openlp.core.common.path import create_paths -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics log = logging.getLogger(__name__) diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py index 74d91068c..ef47fa77b 100644 --- a/openlp/plugins/songs/lib/openlyricsxml.py +++ b/openlp/plugins/songs/lib/openlyricsxml.py @@ -281,7 +281,7 @@ class OpenLyrics(object): # Process the formatting tags. # Have we any tags in song lyrics? tags_element = None - match = re.search('\{/?\w+\}', song.lyrics, re.UNICODE) + match = re.search(r'\{/?\w+\}', song.lyrics) if match: # Named 'format_' - 'format' is built-in function in Python. format_ = etree.SubElement(song_xml, 'format') diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index 01fb0af6c..808c05b57 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -29,7 +29,7 @@ from openlp.core.common.i18n import translate from openlp.core.common.path import Path from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog from openlp.plugins.songs.lib.db import Song log = logging.getLogger(__name__) diff --git a/openlp/plugins/songusage/forms/songusagedeleteform.py b/openlp/plugins/songusage/forms/songusagedeleteform.py index 4a619e64a..9042756d6 100644 --- a/openlp/plugins/songusage/forms/songusagedeleteform.py +++ b/openlp/plugins/songusage/forms/songusagedeleteform.py @@ -23,7 +23,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.plugins.songusage.lib.db import SongUsageItem from openlp.plugins.songusage.forms.songusagedeletedialog import Ui_SongUsageDeleteDialog diff --git a/openlp/plugins/songusage/forms/songusagedetaildialog.py b/openlp/plugins/songusage/forms/songusagedetaildialog.py index 9d51b041a..7f6d1c16b 100644 --- a/openlp/plugins/songusage/forms/songusagedetaildialog.py +++ b/openlp/plugins/songusage/forms/songusagedetaildialog.py @@ -24,7 +24,8 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate from openlp.core.lib import build_icon from openlp.core.lib.ui import create_button_box -from openlp.core.ui.lib import PathEdit, PathType +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType class Ui_SongUsageDetailDialog(object): @@ -68,7 +69,7 @@ class Ui_SongUsageDetailDialog(object): self.file_horizontal_layout.setSpacing(8) self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8) self.file_horizontal_layout.setObjectName('file_horizontal_layout') - self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False) + self.report_path_edit = PathEdit(self.file_group_box, path_type=PathEditType.Directories, show_revert=False) self.file_horizontal_layout.addWidget(self.report_path_edit) self.vertical_layout.addWidget(self.file_group_box) self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok']) diff --git a/openlp/plugins/songusage/forms/songusagedetailform.py b/openlp/plugins/songusage/forms/songusagedetailform.py index afc013bbd..dc3393c8d 100644 --- a/openlp/plugins/songusage/forms/songusagedetailform.py +++ b/openlp/plugins/songusage/forms/songusagedetailform.py @@ -25,7 +25,7 @@ from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_ from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.common.path import create_paths from openlp.core.lib.ui import critical_error_message_box @@ -54,8 +54,14 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP """ We need to set up the screen """ - self.from_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/from date')) - self.to_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/to date')) + to_date = Settings().value(self.plugin.settings_section + '/to date') + if not (isinstance(to_date, QtCore.QDate) and to_date.isValid()): + to_date = QtCore.QDate.currentDate() + from_date = Settings().value(self.plugin.settings_section + '/from date') + if not (isinstance(from_date, QtCore.QDate) and from_date.isValid()): + from_date = to_date.addYears(-1) + self.from_date_calendar.setSelectedDate(from_date) + self.to_date_calendar.setSelectedDate(to_date) self.report_path_edit.path = Settings().value(self.plugin.settings_section + '/last directory export') def on_report_path_edit_path_changed(self, file_path): diff --git a/openlp/plugins/songusage/songusageplugin.py b/openlp/plugins/songusage/songusageplugin.py index a9acd791a..79f21a8cf 100644 --- a/openlp/plugins/songusage/songusageplugin.py +++ b/openlp/plugins/songusage/songusageplugin.py @@ -38,20 +38,17 @@ from openlp.plugins.songusage.lib.db import init_schema, SongUsageItem log = logging.getLogger(__name__) -YEAR = QtCore.QDate().currentDate().year() -if QtCore.QDate().currentDate().month() < 9: - YEAR -= 1 - +TODAY = QtCore.QDate.currentDate() __default_settings__ = { 'songusage/db type': 'sqlite', 'songusage/db username': '', - 'songuasge/db password': '', - 'songuasge/db hostname': '', - 'songuasge/db database': '', + 'songusage/db password': '', + 'songusage/db hostname': '', + 'songusage/db database': '', 'songusage/active': False, - 'songusage/to date': QtCore.QDate(YEAR, 8, 31), - 'songusage/from date': QtCore.QDate(YEAR - 1, 9, 1), + 'songusage/to date': TODAY, + 'songusage/from date': TODAY.addYears(-1), 'songusage/last directory export': None } diff --git a/scripts/jenkins_script.py b/scripts/jenkins_script.py index 9a0e9a4ec..c7ca693be 100755 --- a/scripts/jenkins_script.py +++ b/scripts/jenkins_script.py @@ -21,35 +21,35 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -This script helps to trigger builds of branches. To use it you have to install the jenkins-webapi package: +This script helps to trigger builds of branches. To use it you have to install the python-jenkins module. On Fedora +and Ubuntu/Debian, it is available as the ``python3-jenkins`` package:: - pip3 install jenkins-webapi + $ sudo dnf/apt install python3-jenkins -You probably want to create an alias. Add this to your ~/.bashrc file and then logout and login (to apply the alias): +To make it easier to run you may want to create a shell script or an alias. To create an alias, add this to your +``~/.bashrc`` (or ``~/.zshrc``) file and then log out and log back in again (to apply the alias):: - alias ci="python3 ./scripts/jenkins_script.py TOKEN" + alias ci="python3 /path/to/openlp_root/scripts/jenkins_script.py -u USERNAME -p PASSWORD" -You can look up the token in the Branch-01-Pull job configuration or ask in IRC. +To create a shell script, create the following file in a location in your ``$PATH`` (I called mine ``ci``):: + + #!/bin/bash + python3 /path/to/openlp_root/scripts/jenkins_script.py -u USERNAME -p PASSWORD + +``USERNAME`` is your Jenkins username, and ``PASSWORD`` is your Jenkins password or personal token. + +An older version of this script used to use a shared TOKEN, but this has been replaced with the username and password. """ - +import os import re -import sys import time -from optparse import OptionParser +from argparse import ArgumentParser from subprocess import Popen, PIPE -import warnings -from requests.exceptions import HTTPError from jenkins import Jenkins - JENKINS_URL = 'https://ci.openlp.io/' REPO_REGEX = r'(.*/+)(~.*)' -# Allows us to black list token. So when we change the token, we can display a proper message to the user. -OLD_TOKENS = [] - -# Disable the InsecureRequestWarning we get from urllib3, because we're not verifying our own self-signed certificate -warnings.simplefilter('ignore') class OpenLPJobs(object): @@ -63,9 +63,10 @@ class OpenLPJobs(object): Branch_Coverage = 'Branch-04b-Test_Coverage' Branch_Pylint = 'Branch-04c-Code_Analysis2' Branch_AppVeyor = 'Branch-05-AppVeyor-Tests' + Branch_macOS = 'Branch-07-macOS-Tests' Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_PEP, Branch_Coverage, Branch_Pylint, - Branch_AppVeyor] + Branch_AppVeyor, Branch_macOS] class Colour(object): @@ -85,13 +86,23 @@ class JenkinsTrigger(object): :param token: The token we need to trigger the build. If you do not have this token, ask in IRC. """ - def __init__(self, token): + def __init__(self, username, password, can_use_colour): """ Create the JenkinsTrigger instance. """ - self.token = token + self.jobs = {} + self.can_use_colour = can_use_colour and not os.name.startswith('nt') self.repo_name = get_repo_name() - self.jenkins_instance = Jenkins(JENKINS_URL) + self.server = Jenkins(JENKINS_URL, username=username, password=password) + + def fetch_jobs(self): + """ + Get the job info for all the jobs + """ + for job_name in OpenLPJobs.Jobs: + job_info = self.server.get_job_info(job_name) + self.jobs[job_name] = job_info + self.jobs[job_name]['nextBuildUrl'] = '{url}{nextBuildNumber}/'.format(**job_info) def trigger_build(self): """ @@ -102,24 +113,35 @@ class JenkinsTrigger(object): # We just want the name (not the email). name = ' '.join(raw_output.decode().split()[:-1]) cause = 'Build triggered by %s (%s)' % (name, self.repo_name) - self.jenkins_instance.job(OpenLPJobs.Branch_Pull).build({'BRANCH_NAME': self.repo_name, 'cause': cause}, - token=self.token) + self.fetch_jobs() + self.server.build_job(OpenLPJobs.Branch_Pull, {'BRANCH_NAME': self.repo_name, 'cause': cause}) - def print_output(self): + def print_output(self, can_continue=False): """ Print the status information of the build triggered. """ print('Add this to your merge proposal:') - print('--------------------------------') + print('-' * 80) bzr = Popen(('bzr', 'revno'), stdout=PIPE, stderr=PIPE) raw_output, error = bzr.communicate() revno = raw_output.decode().strip() print('%s (revision %s)' % (get_repo_name(), revno)) + failed_builds = [] for job in OpenLPJobs.Jobs: if not self.__print_build_info(job): - print('Stopping after failure') - break + if self.current_build: + failed_builds.append((self.current_build['fullDisplayName'], self.current_build['url'])) + if not can_continue: + print('Stopping after failure') + break + print('') + if failed_builds: + print('Failed builds:') + for build_name, url in failed_builds: + print(' - {}: {}console'.format(build_name, url)) + else: + print('All builds passed') def open_browser(self): """ @@ -129,6 +151,20 @@ class JenkinsTrigger(object): # Open the url Popen(('xdg-open', url), stderr=PIPE) + def _get_build_info(self, job_name, build_number): + """ + Get the build info from the server. This method will check the queue and wait for the build. + """ + queue_info = self.server.get_queue_info() + tries = 0 + while queue_info and tries < 50: + tries += 1 + time.sleep(0.5) + queue_info = self.server.get_queue_info() + if tries >= 50: + raise Exception('Build has not started yet, it may be stuck in the queue.') + return self.server.get_build_info(job_name, build_number) + def __print_build_info(self, job_name): """ This helper method prints the job information of the given ``job_name`` @@ -136,21 +172,24 @@ class JenkinsTrigger(object): :param job_name: The name of the job we want the information from. For example *Branch-01-Pull*. Use the class variables from the :class:`OpenLPJobs` class. """ + job = self.jobs[job_name] + print('{:<70} [WAITING]'.format(job['nextBuildUrl']), end='', flush=True) + self.current_build = self._get_build_info(job_name, job['nextBuildNumber']) + print('\b\b\b\b\b\b\b\b\b[RUNNING]', end='', flush=True) is_success = False - job = self.jenkins_instance.job(job_name) - while job.info['inQueue']: - time.sleep(1) - build = job.last_build - build.wait() - if build.info['result'] == 'SUCCESS': - # Make 'SUCCESS' green. - result_string = '%s%s%s' % (Colour.GREEN_START, build.info['result'], Colour.GREEN_END) - is_success = True - else: - # Make 'FAILURE' red. - result_string = '%s%s%s' % (Colour.RED_START, build.info['result'], Colour.RED_END) - url = build.info['url'] - print('[%s] %s' % (result_string, url)) + while self.current_build['building'] is True: + time.sleep(0.5) + self.current_build = self.server.get_build_info(job_name, job['nextBuildNumber']) + result_string = self.current_build['result'] + is_success = result_string == 'SUCCESS' + if self.can_use_colour: + if is_success: + # Make 'SUCCESS' green. + result_string = '{}{}{}'.format(Colour.GREEN_START, result_string, Colour.GREEN_END) + else: + # Make 'FAILURE' red. + result_string = '{}{}{}'.format(Colour.RED_START, result_string, Colour.RED_END) + print('\b\b\b\b\b\b\b\b\b[{:>7}]'.format(result_string)) return is_success @@ -186,36 +225,30 @@ def get_repo_name(): def main(): - usage = 'Usage: python %prog TOKEN [options]' + """ + Run the script + """ + parser = ArgumentParser() + parser.add_argument('-d', '--disable-output', action='store_true', default=False, help='Disable output') + parser.add_argument('-b', '--open-browser', action='store_true', default=False, + help='Opens the jenkins page in your browser') + parser.add_argument('-n', '--no-colour', action='store_true', default=False, + help='Disable coloured output (always disabled on Windows)') + parser.add_argument('-u', '--username', required=True, help='Your Jenkins username') + parser.add_argument('-p', '--password', required=True, help='Your Jenkins password or personal token') + parser.add_argument('-c', '--always-continue', action='store_true', default=False, help='Continue despite failure') + args = parser.parse_args() - parser = OptionParser(usage=usage) - parser.add_option('-d', '--disable-output', dest='enable_output', action='store_false', default=True, - help='Disable output.') - parser.add_option('-b', '--open-browser', dest='open_browser', action='store_true', default=False, - help='Opens the jenkins page in your browser.') - options, args = parser.parse_args(sys.argv) - - if len(args) == 2: - if not get_repo_name(): - print('Not a branch. Have you pushed it to launchpad? Did you cd to the branch?') - return - token = args[-1] - if token in OLD_TOKENS: - print('Your token is not valid anymore. Get the most recent one.') - return - jenkins_trigger = JenkinsTrigger(token) - try: - jenkins_trigger.trigger_build() - except HTTPError: - print('Wrong token.') - return - # Open the browser before printing the output. - if options.open_browser: - jenkins_trigger.open_browser() - if options.enable_output: - jenkins_trigger.print_output() - else: - parser.print_help() + if not get_repo_name(): + print('Not a branch. Have you pushed it to launchpad? Did you cd to the branch?') + return + jenkins_trigger = JenkinsTrigger(args.username, args.password, not args.no_colour) + jenkins_trigger.trigger_build() + # Open the browser before printing the output. + if args.open_browser: + jenkins_trigger.open_browser() + if not args.disable_output: + jenkins_trigger.print_output(can_continue=args.always_continue) if __name__ == '__main__': diff --git a/tests/functional/openlp_core/api/http/test_error.py b/tests/functional/openlp_core/api/http/test_error.py index 1a694705c..599ecfc03 100644 --- a/tests/functional/openlp_core/api/http/test_error.py +++ b/tests/functional/openlp_core/api/http/test_error.py @@ -42,8 +42,8 @@ class TestApiError(TestCase): raise NotFound() # THEN: we get an error and a status - self.assertEquals('Not Found', context.exception.message, 'A Not Found exception should be thrown') - self.assertEquals(404, context.exception.status, 'A 404 status should be thrown') + assert 'Not Found' == context.exception.message, 'A Not Found exception should be thrown' + assert 404 == context.exception.status, 'A 404 status should be thrown' def test_server_error(self): """ @@ -55,5 +55,5 @@ class TestApiError(TestCase): raise ServerError() # THEN: we get an error and a status - self.assertEquals('Server Error', context.exception.message, 'A Not Found exception should be thrown') - self.assertEquals(500, context.exception.status, 'A 500 status should be thrown') + assert'Server Error' == context.exception.message, 'A Not Found exception should be thrown' + assert 500 == context.exception.status, 'A 500 status should be thrown' diff --git a/tests/functional/openlp_core/api/http/test_http.py b/tests/functional/openlp_core/api/http/test_http.py index d9002b2ec..7dd8418ae 100644 --- a/tests/functional/openlp_core/api/http/test_http.py +++ b/tests/functional/openlp_core/api/http/test_http.py @@ -45,12 +45,28 @@ class TestHttpServer(TestCase): @patch('openlp.core.api.http.server.QtCore.QThread') def test_server_start(self, mock_qthread, mock_thread): """ - Test the starting of the Waitress Server + Test the starting of the Waitress Server with the disable flag set off """ # GIVEN: A new httpserver # WHEN: I start the server + Registry().set_flag('no_web_server', True) HttpServer() # THEN: the api environment should have been created - self.assertEquals(1, mock_qthread.call_count, 'The qthread should have been called once') - self.assertEquals(1, mock_thread.call_count, 'The http thread should have been called once') + assert mock_qthread.call_count == 1, 'The qthread should have been called once' + assert mock_thread.call_count == 1, 'The http thread should have been called once' + + @patch('openlp.core.api.http.server.HttpWorker') + @patch('openlp.core.api.http.server.QtCore.QThread') + def test_server_start_not_required(self, mock_qthread, mock_thread): + """ + Test the starting of the Waitress Server with the disable flag set off + """ + # GIVEN: A new httpserver + # WHEN: I start the server + Registry().set_flag('no_web_server', False) + HttpServer() + + # THEN: the api environment should have been created + assert mock_qthread.call_count == 0, 'The qthread should not have have been called' + assert mock_thread.call_count == 0, 'The http thread should not have been called' diff --git a/tests/functional/openlp_core/api/http/test_wsgiapp.py b/tests/functional/openlp_core/api/http/test_wsgiapp.py index aff27404a..17b3ae8c5 100644 --- a/tests/functional/openlp_core/api/http/test_wsgiapp.py +++ b/tests/functional/openlp_core/api/http/test_wsgiapp.py @@ -61,7 +61,7 @@ class TestRouting(TestCase): application.dispatch(rqst) # THEN: the not found returned - self.assertEqual(context.exception.args[0], 'Not Found', 'URL not found in dispatcher') + assert context.exception.args[0] == 'Not Found', 'URL not found in dispatcher' # WHEN: when the URL is correct and dispatch called rqst = MagicMock() @@ -69,8 +69,8 @@ class TestRouting(TestCase): rqst.method = 'GET' application.dispatch(rqst) # THEN: the not found id called - self.assertEqual(1, application.route_map['^\\/test\\/image$']['GET'].call_count, - 'main_index function should have been called') + assert 1 == application.route_map['^\\/test\\/image$']['GET'].call_count, \ + 'main_index function should have been called' @test_endpoint.route('image') diff --git a/tests/functional/openlp_core/api/test_deploy.py b/tests/functional/openlp_core/api/test_deploy.py index be36fb9c7..0aaf1308b 100644 --- a/tests/functional/openlp_core/api/test_deploy.py +++ b/tests/functional/openlp_core/api/test_deploy.py @@ -19,15 +19,13 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -import os -import shutil from tempfile import mkdtemp from unittest import TestCase from openlp.core.api.deploy import deploy_zipfile +from openlp.core.common.path import Path, copyfile -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources')) +TEST_PATH = (Path(__file__).parent / '..' / '..' / '..' / 'resources').resolve() class TestRemoteDeploy(TestCase): @@ -39,25 +37,25 @@ class TestRemoteDeploy(TestCase): """ Setup for tests """ - self.app_root = mkdtemp() + self.app_root_path = Path(mkdtemp()) def tearDown(self): """ Clean up after tests """ - shutil.rmtree(self.app_root) + self.app_root_path.rmtree() def test_deploy_zipfile(self): """ Remote Deploy tests - test the dummy zip file is processed correctly """ # GIVEN: A new downloaded zip file - aa = TEST_PATH - zip_file = os.path.join(TEST_PATH, 'remotes', 'site.zip') - app_root = os.path.join(self.app_root, 'site.zip') - shutil.copyfile(zip_file, app_root) - # WHEN: I process the zipfile - deploy_zipfile(self.app_root, 'site.zip') + zip_path = TEST_PATH / 'remotes' / 'site.zip' + app_root_path = self.app_root_path / 'site.zip' + copyfile(zip_path, app_root_path) - # THEN test if www directory has been created - self.assertTrue(os.path.isdir(os.path.join(self.app_root, 'www')), 'We should have a www directory') + # WHEN: I process the zipfile + deploy_zipfile(self.app_root_path, 'site.zip') + + # THEN: test if www directory has been created + assert (self.app_root_path / 'www').is_dir(), 'We should have a www directory' diff --git a/tests/functional/openlp_core/api/test_tab.py b/tests/functional/openlp_core/api/test_tab.py index f3c4f31e6..bd701784d 100644 --- a/tests/functional/openlp_core/api/test_tab.py +++ b/tests/functional/openlp_core/api/test_tab.py @@ -81,8 +81,8 @@ class TestApiTab(TestCase, TestMixin): # WHEN: the default ip address is given ip_address = self.form.get_ip_address(ZERO_URL) # THEN: the default ip address will be returned - self.assertTrue(re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ip_address), - 'The return value should be a valid ip address') + assert re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ip_address), \ + 'The return value should be a valid ip address' def test_get_ip_address_with_ip(self): """ @@ -93,7 +93,7 @@ class TestApiTab(TestCase, TestMixin): # WHEN: the default ip address is given ip_address = self.form.get_ip_address(given_ip) # THEN: the default ip address will be returned - self.assertEqual(ip_address, given_ip, 'The return value should be %s' % given_ip) + assert ip_address == given_ip, 'The return value should be %s' % given_ip def test_set_urls(self): """ @@ -104,12 +104,11 @@ class TestApiTab(TestCase, TestMixin): # WHEN: the urls are generated self.form.set_urls() # THEN: the following links are returned - self.assertEqual(self.form.remote_url.text(), - "<a href=\"http://192.168.1.1:4316/\">http://192.168.1.1:4316/</a>", - 'The return value should be a fully formed link') - self.assertEqual(self.form.stage_url.text(), - "<a href=\"http://192.168.1.1:4316/stage\">http://192.168.1.1:4316/stage</a>", - 'The return value should be a fully formed stage link') - self.assertEqual(self.form.live_url.text(), - "<a href=\"http://192.168.1.1:4316/main\">http://192.168.1.1:4316/main</a>", - 'The return value should be a fully formed main link') + assert self.form.remote_url.text() == "<a href=\"http://192.168.1.1:4316/\">http://192.168.1.1:4316/</a>", \ + 'The return value should be a fully formed link' + assert self.form.stage_url.text() == \ + "<a href=\"http://192.168.1.1:4316/stage\">http://192.168.1.1:4316/stage</a>", \ + 'The return value should be a fully formed stage link' + assert self.form.live_url.text() == \ + "<a href=\"http://192.168.1.1:4316/main\">http://192.168.1.1:4316/main</a>", \ + 'The return value should be a fully formed main link' diff --git a/tests/functional/openlp_core/api/test_websockets.py b/tests/functional/openlp_core/api/test_websockets.py index 99abcdbc0..7da90123d 100644 --- a/tests/functional/openlp_core/api/test_websockets.py +++ b/tests/functional/openlp_core/api/test_websockets.py @@ -66,15 +66,31 @@ class TestWSServer(TestCase, TestMixin): @patch('openlp.core.api.websockets.QtCore.QThread') def test_serverstart(self, mock_qthread, mock_worker): """ - Test the starting of the WebSockets Server + Test the starting of the WebSockets Server with the disabled flag set on """ # GIVEN: A new httpserver # WHEN: I start the server + Registry().set_flag('no_web_server', True) WebSocketServer() # THEN: the api environment should have been created - self.assertEquals(1, mock_qthread.call_count, 'The qthread should have been called once') - self.assertEquals(1, mock_worker.call_count, 'The http thread should have been called once') + assert mock_qthread.call_count == 1, 'The qthread should have been called once' + assert mock_worker.call_count == 1, 'The http thread should have been called once' + + @patch('openlp.core.api.websockets.WebSocketWorker') + @patch('openlp.core.api.websockets.QtCore.QThread') + def test_serverstart_not_required(self, mock_qthread, mock_worker): + """ + Test the starting of the WebSockets Server with the disabled flag set off + """ + # GIVEN: A new httpserver and the server is not required + # WHEN: I start the server + Registry().set_flag('no_web_server', False) + WebSocketServer() + + # THEN: the api environment should have been created + assert mock_qthread.call_count == 0, 'The qthread should not have been called' + assert mock_worker.call_count == 0, 'The http thread should not have been called' def test_main_poll(self): """ @@ -86,8 +102,7 @@ class TestWSServer(TestCase, TestMixin): Registry().register('live_controller', mocked_live_controller) # THEN: the live json should be generated main_json = self.poll.main_poll() - self.assertEquals(b'{"results": {"slide_count": 5}}', main_json, - 'The return value should match the defined json') + assert b'{"results": {"slide_count": 5}}' == main_json, 'The return value should match the defined json' def test_poll(self): """ @@ -114,13 +129,13 @@ class TestWSServer(TestCase, TestMixin): mocked_is_chords_active.return_value = True poll_json = self.poll.poll() # THEN: the live json should be generated and match expected results - self.assertTrue(poll_json['results']['blank'], 'The blank return value should be True') - self.assertFalse(poll_json['results']['theme'], 'The theme return value should be False') - self.assertFalse(poll_json['results']['display'], 'The display return value should be False') - self.assertFalse(poll_json['results']['isSecure'], 'The isSecure return value should be False') - self.assertFalse(poll_json['results']['isAuthorised'], 'The isAuthorised return value should be False') - self.assertTrue(poll_json['results']['twelve'], 'The twelve return value should be False') - self.assertEquals(poll_json['results']['version'], 3, 'The version return value should be 3') - self.assertEquals(poll_json['results']['slide'], 5, 'The slide return value should be 5') - self.assertEquals(poll_json['results']['service'], 21, 'The version return value should be 21') - self.assertEquals(poll_json['results']['item'], '23-34-45', 'The item return value should match 23-34-45') + assert poll_json['results']['blank'] is True, 'The blank return value should be True' + assert poll_json['results']['theme'] is False, 'The theme return value should be False' + assert poll_json['results']['display'] is False, 'The display return value should be False' + assert poll_json['results']['isSecure'] is False, 'The isSecure return value should be False' + assert poll_json['results']['isAuthorised'] is False, 'The isAuthorised return value should be False' + assert poll_json['results']['twelve'] is True, 'The twelve return value should be True' + assert poll_json['results']['version'] == 3, 'The version return value should be 3' + assert poll_json['results']['slide'] == 5, 'The slide return value should be 5' + assert poll_json['results']['service'] == 21, 'The version return value should be 21' + assert poll_json['results']['item'] == '23-34-45', 'The item return value should match 23-34-45' diff --git a/tests/functional/openlp_core/common/test_actions.py b/tests/functional/openlp_core/common/test_actions.py index bd59d6577..7d5ac6fca 100644 --- a/tests/functional/openlp_core/common/test_actions.py +++ b/tests/functional/openlp_core/common/test_actions.py @@ -59,8 +59,8 @@ class TestCategoryActionList(TestCase): self.list.append(self.action1) # THEN: The actions should (not) be in the list. - self.assertTrue(self.action1 in self.list) - self.assertFalse(self.action2 in self.list) + assert self.action1 in self.list + assert self.action2 not in self.list def test_len(self): """ @@ -69,14 +69,14 @@ class TestCategoryActionList(TestCase): # GIVEN: The list. # WHEN: Do nothing. # THEN: Check the length. - self.assertEqual(len(self.list), 0, "The length should be 0.") + assert len(self.list) == 0, "The length should be 0." # GIVEN: The list. # WHEN: Append an action. self.list.append(self.action1) # THEN: Check the length. - self.assertEqual(len(self.list), 1, "The length should be 1.") + assert len(self.list) == 1, "The length should be 1." def test_append(self): """ @@ -88,10 +88,10 @@ class TestCategoryActionList(TestCase): self.list.append(self.action2) # THEN: Check if the actions are in the list and check if they have the correct weights. - self.assertTrue(self.action1 in self.list) - self.assertTrue(self.action2 in self.list) - self.assertEqual(self.list.actions[0], (0, self.action1)) - self.assertEqual(self.list.actions[1], (1, self.action2)) + assert self.action1 in self.list + assert self.action2 in self.list + assert self.list.actions[0] == (0, self.action1) + assert self.list.actions[1] == (1, self.action2) def test_add(self): """ @@ -106,11 +106,11 @@ class TestCategoryActionList(TestCase): self.list.add(self.action2, action2_weight) # THEN: Check if they were added and have the specified weights. - self.assertTrue(self.action1 in self.list) - self.assertTrue(self.action2 in self.list) + assert self.action1 in self.list + assert self.action2 in self.list # Now check if action1 is second and action2 is first (due to their weights). - self.assertEqual(self.list.actions[0], (41, self.action2)) - self.assertEqual(self.list.actions[1], (42, self.action1)) + assert self.list.actions[0] == (41, self.action2) + assert self.list.actions[1] == (42, self.action1) def test_iterator(self): """ @@ -121,11 +121,11 @@ class TestCategoryActionList(TestCase): self.list.add(self.action2) # WHEN: Iterating over the list - list = [a for a in self.list] + local_list = [a for a in self.list] # THEN: Make sure they are returned in correct order - self.assertEquals(len(self.list), 2) - self.assertIs(list[0], self.action1) - self.assertIs(list[1], self.action2) + assert len(self.list) == 2 + assert local_list[0] is self.action1 + assert local_list[1] is self.action2 def test_remove(self): """ @@ -138,7 +138,7 @@ class TestCategoryActionList(TestCase): self.list.remove(self.action1) # THEN: Now the element should not be in the list anymore. - self.assertFalse(self.action1 in self.list) + assert self.action1 not in self.list # THEN: Check if an exception is raised when trying to remove a not present action. self.assertRaises(ValueError, self.list.remove, self.action2) @@ -153,6 +153,7 @@ class TestActionList(TestCase, TestMixin): """ Prepare the tests """ + self.setup_application() self.action_list = ActionList.get_instance() self.build_settings() self.settings = Settings() diff --git a/tests/functional/openlp_core/common/test_common.py b/tests/functional/openlp_core/common/test_common.py index 1d817ee90..368705821 100644 --- a/tests/functional/openlp_core/common/test_common.py +++ b/tests/functional/openlp_core/common/test_common.py @@ -48,7 +48,7 @@ class TestCommonFunctions(TestCase): extension_loader('glob', ['file2.py', 'file3.py']) # THEN: `extension_loader` should not try to import any files - self.assertFalse(mocked_import_module.called) + assert mocked_import_module.called is False def test_extension_loader_files_found(self): """ @@ -69,7 +69,8 @@ class TestCommonFunctions(TestCase): # THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the # files listed in the `excluded_files` argument - mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), call('openlp.import_dir.file4')]) + mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), + call('openlp.import_dir.file4')]) def test_extension_loader_import_error(self): """ @@ -87,7 +88,7 @@ class TestCommonFunctions(TestCase): extension_loader('glob') # THEN: The `ImportError` should be caught and logged - self.assertTrue(mocked_logger.warning.called) + assert mocked_logger.warning.called def test_extension_loader_os_error(self): """ @@ -105,7 +106,7 @@ class TestCommonFunctions(TestCase): extension_loader('glob') # THEN: The `OSError` should be caught and logged - self.assertTrue(mocked_logger.warning.called) + assert mocked_logger.warning.called def test_de_hump_conversion(self): """ @@ -118,7 +119,7 @@ class TestCommonFunctions(TestCase): new_string = de_hump(string) # THEN: the new string should be converted to python format - self.assertEqual(new_string, "my_class", 'The class name should have been converted') + assert new_string == "my_class", 'The class name should have been converted' def test_de_hump_static(self): """ @@ -131,7 +132,7 @@ class TestCommonFunctions(TestCase): new_string = de_hump(string) # THEN: the new string should be converted to python format - self.assertEqual(new_string, "my_class", 'The class name should have been preserved') + assert new_string == "my_class", 'The class name should have been preserved' def test_path_to_module(self): """ @@ -144,7 +145,7 @@ class TestCommonFunctions(TestCase): result = path_to_module(path) # THEN: path_to_module should return the module name - self.assertEqual(result, 'openlp.core.ui.media.webkitplayer') + assert result == 'openlp.core.ui.media.webkitplayer' def test_trace_error_handler(self): """ @@ -174,9 +175,9 @@ class TestCommonFunctions(TestCase): mocked_sys.platform = 'win32' # THEN: The three platform functions should perform properly - self.assertTrue(is_win(), 'is_win() should return True') - self.assertFalse(is_macosx(), 'is_macosx() should return False') - self.assertFalse(is_linux(), 'is_linux() should return False') + assert is_win() is True, 'is_win() should return True' + assert is_macosx() is False, 'is_macosx() should return False' + assert is_linux() is False, 'is_linux() should return False' def test_is_macosx(self): """ @@ -190,9 +191,9 @@ class TestCommonFunctions(TestCase): mocked_sys.platform = 'darwin' # THEN: The three platform functions should perform properly - self.assertTrue(is_macosx(), 'is_macosx() should return True') - self.assertFalse(is_win(), 'is_win() should return False') - self.assertFalse(is_linux(), 'is_linux() should return False') + assert is_macosx() is True, 'is_macosx() should return True' + assert is_win() is False, 'is_win() should return False' + assert is_linux() is False, 'is_linux() should return False' def test_is_linux(self): """ @@ -206,9 +207,9 @@ class TestCommonFunctions(TestCase): mocked_sys.platform = 'linux3' # THEN: The three platform functions should perform properly - self.assertTrue(is_linux(), 'is_linux() should return True') - self.assertFalse(is_win(), 'is_win() should return False') - self.assertFalse(is_macosx(), 'is_macosx() should return False') + assert is_linux() is True, 'is_linux() should return True' + assert is_win() is False, 'is_win() should return False' + assert is_macosx() is False, 'is_macosx() should return False' def test_clean_button_text(self): """ @@ -222,4 +223,4 @@ class TestCommonFunctions(TestCase): actual_text = clean_button_text(input_text) # THEN: The text should have been cleaned - self.assertEqual(expected_text, actual_text, 'The text should be clean') + assert expected_text == actual_text, 'The text should be clean' diff --git a/tests/functional/openlp_core/common/test_httputils.py b/tests/functional/openlp_core/common/test_httputils.py index e620fa04e..b7c08993f 100644 --- a/tests/functional/openlp_core/common/test_httputils.py +++ b/tests/functional/openlp_core/common/test_httputils.py @@ -59,7 +59,7 @@ class TestHttpUtils(TestCase, TestMixin): # THEN: The user agent is a Linux (or ChromeOS) user agent result = 'Linux' in user_agent or 'CrOS' in user_agent - self.assertTrue(result, 'The user agent should be a valid Linux user agent') + assert result is True, 'The user agent should be a valid Linux user agent' def test_get_user_agent_windows(self): """ @@ -74,7 +74,7 @@ class TestHttpUtils(TestCase, TestMixin): user_agent = get_user_agent() # THEN: The user agent is a Linux (or ChromeOS) user agent - self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent') + assert 'Windows' in user_agent, 'The user agent should be a valid Windows user agent' def test_get_user_agent_macos(self): """ @@ -89,7 +89,7 @@ class TestHttpUtils(TestCase, TestMixin): user_agent = get_user_agent() # THEN: The user agent is a Linux (or ChromeOS) user agent - self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent') + assert 'Mac OS X' in user_agent, 'The user agent should be a valid OS X user agent' def test_get_user_agent_default(self): """ @@ -104,7 +104,7 @@ class TestHttpUtils(TestCase, TestMixin): user_agent = get_user_agent() # THEN: The user agent is a Linux (or ChromeOS) user agent - self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent') + assert 'NetBSD'in user_agent, 'The user agent should be the default user agent' def test_get_web_page_no_url(self): """ @@ -117,7 +117,7 @@ class TestHttpUtils(TestCase, TestMixin): result = get_web_page(test_url) # THEN: None should be returned - self.assertIsNone(result, 'The return value of get_web_page should be None') + assert result is None, 'The return value of get_web_page should be None' @patch('openlp.core.common.httputils.requests') @patch('openlp.core.common.httputils.get_user_agent') @@ -233,11 +233,11 @@ class TestHttpUtils(TestCase, TestMixin): Test socket timeout gets caught """ # GIVEN: Mocked urlopen to fake a network disconnect in the middle of a download - mocked_requests.get.side_effect = IOError + mocked_requests.get.side_effect = OSError # WHEN: Attempt to retrieve a file url_get_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile)) # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files - assert not os.path.exists(self.tempfile), 'tempfile should have been deleted' + assert os.path.exists(self.tempfile) is False, 'tempfile should have been deleted' diff --git a/tests/functional/openlp_core/common/test_i18n.py b/tests/functional/openlp_core/common/test_i18n.py index d6828fb6f..4f4ca2aec 100644 --- a/tests/functional/openlp_core/common/test_i18n.py +++ b/tests/functional/openlp_core/common/test_i18n.py @@ -22,8 +22,10 @@ """ Package to test the openlp.core.lib.languages package. """ +from unittest import skipIf from unittest.mock import MagicMock, patch +from openlp.core.common import is_macosx from openlp.core.common.i18n import LANGUAGES, Language, UiStrings, get_language, get_locale_key, get_natural_key, \ translate @@ -110,6 +112,7 @@ def test_get_language_invalid_with_none(): assert language is None +@skipIf(is_macosx(), 'This test doesn\'t work on macOS currently') def test_get_locale_key(): """ Test the get_locale_key(string) function @@ -155,7 +158,7 @@ def test_check_same_instance(): assert first_instance is second_instance, 'Two UiStrings objects should be the same instance' -def test_translate(self): +def test_translate(): """ Test the translate() function """ diff --git a/tests/functional/openlp_core/common/test_init.py b/tests/functional/openlp_core/common/test_init.py index 532f11bac..9965a07ee 100644 --- a/tests/functional/openlp_core/common/test_init.py +++ b/tests/functional/openlp_core/common/test_init.py @@ -28,7 +28,7 @@ from unittest import TestCase from unittest.mock import MagicMock, PropertyMock, call, patch from openlp.core.common import add_actions, clean_filename, delete_file, get_file_encoding, get_filesystem_encoding, \ - get_uno_command, get_uno_instance, split_filename + get_uno_command, get_uno_instance from openlp.core.common.path import Path from tests.helpers.testmixin import TestMixin @@ -63,8 +63,8 @@ class TestInit(TestCase, TestMixin): add_actions(mocked_target, empty_list) # THEN: The add method on the mocked target is never called - self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') - self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') + assert mocked_target.addSeparator.call_count == 0, 'addSeparator method should not have been called' + assert mocked_target.addAction.call_count == 0, 'addAction method should not have been called' def test_add_actions_none_action(self): """ @@ -79,7 +79,7 @@ class TestInit(TestCase, TestMixin): # THEN: The addSeparator method is called, but the addAction method is never called mocked_target.addSeparator.assert_called_with() - self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') + assert mocked_target.addAction.call_count == 0, 'addAction method should not have been called' def test_add_actions_add_action(self): """ @@ -93,7 +93,7 @@ class TestInit(TestCase, TestMixin): add_actions(mocked_target, action_list) # THEN: The addSeparator method is not called, and the addAction method is called - self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') + assert mocked_target.addSeparator.call_count == 0, 'addSeparator method should not have been called' mocked_target.addAction.assert_called_with('action') def test_add_actions_action_and_none(self): @@ -150,9 +150,8 @@ class TestInit(TestCase, TestMixin): result = get_uno_command() # THEN: The command 'libreoffice' should be called with the appropriate parameters - self.assertEquals(result, - 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' - ' "--accept=pipe,name=openlp_pipe;urp;"') + assert result == 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' \ + ' "--accept=pipe,name=openlp_pipe;urp;"' def test_get_uno_command_only_soffice_command_exists(self): """ @@ -169,8 +168,8 @@ class TestInit(TestCase, TestMixin): result = get_uno_command() # THEN: The command 'soffice' should be called with the appropriate parameters - self.assertEquals(result, 'soffice --nologo --norestore --minimized --nodefault --nofirststartwizard' - ' "--accept=pipe,name=openlp_pipe;urp;"') + assert result == 'soffice --nologo --norestore --minimized --nodefault --nofirststartwizard' \ + ' "--accept=pipe,name=openlp_pipe;urp;"' def test_get_uno_command_when_no_command_exists(self): """ @@ -198,8 +197,8 @@ class TestInit(TestCase, TestMixin): result = get_uno_command('socket') # THEN: The connection parameters should be set for socket - self.assertEqual(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' - ' "--accept=socket,host=localhost,port=2002;urp;"') + assert result == 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' \ + ' "--accept=socket,host=localhost,port=2002;urp;"' def test_get_filesystem_encoding_sys_function_not_called(self): """ @@ -215,8 +214,8 @@ class TestInit(TestCase, TestMixin): # THEN: getdefaultencoding should have been called mocked_getfilesystemencoding.assert_called_with() - self.assertEqual(0, mocked_getdefaultencoding.called, 'getdefaultencoding should not have been called') - self.assertEqual('cp1252', result, 'The result should be "cp1252"') + assert mocked_getdefaultencoding.called == 0, 'getdefaultencoding should not have been called' + assert 'cp1252' == result, 'The result should be "cp1252"' def test_get_filesystem_encoding_sys_function_is_called(self): """ @@ -234,48 +233,7 @@ class TestInit(TestCase, TestMixin): # THEN: getdefaultencoding should have been called mocked_getfilesystemencoding.assert_called_with() mocked_getdefaultencoding.assert_called_with() - self.assertEqual('utf-8', result, 'The result should be "utf-8"') - - def test_split_filename_with_file_path(self): - """ - Test the split_filename() function with a path to a file - """ - # GIVEN: A path to a file. - if os.name == 'nt': - file_path = 'C:\\home\\user\\myfile.txt' - wanted_result = ('C:\\home\\user', 'myfile.txt') - else: - file_path = '/home/user/myfile.txt' - wanted_result = ('/home/user', 'myfile.txt') - with patch('openlp.core.common.os.path.isfile') as mocked_is_file: - mocked_is_file.return_value = True - - # WHEN: Split the file name. - result = split_filename(file_path) - - # THEN: A tuple should be returned. - self.assertEqual(wanted_result, result, 'A tuple with the dir and file name should have been returned') - - def test_split_filename_with_dir_path(self): - """ - Test the split_filename() function with a path to a directory - """ - # GIVEN: A path to a dir. - if os.name == 'nt': - file_path = 'C:\\home\\user\\mydir' - wanted_result = ('C:\\home\\user\\mydir', '') - else: - file_path = '/home/user/mydir' - wanted_result = ('/home/user/mydir', '') - with patch('openlp.core.common.os.path.isfile') as mocked_is_file: - mocked_is_file.return_value = False - - # WHEN: Split the file name. - result = split_filename(file_path) - - # THEN: A tuple should be returned. - self.assertEqual(wanted_result, result, - 'A two-entry tuple with the directory and file name (empty) should have been returned.') + assert 'utf-8' == result, 'The result should be "utf-8"' def test_clean_filename(self): """ @@ -289,7 +247,7 @@ class TestInit(TestCase, TestMixin): result = clean_filename(invalid_name) # THEN: The file name should be cleaned. - self.assertEqual(wanted_name, result, 'The file name should not contain any special characters.') + assert wanted_name == result, 'The file name should not contain any special characters.' def test_delete_file_no_path(self): """ @@ -300,7 +258,7 @@ class TestInit(TestCase, TestMixin): result = delete_file(None) # THEN: delete_file should return False - self.assertFalse(result, "delete_file should return False when called with None") + assert result is False, "delete_file should return False when called with None" def test_delete_file_path_success(self): """ @@ -313,7 +271,7 @@ class TestInit(TestCase, TestMixin): result = delete_file(Path('path', 'file.ext')) # THEN: delete_file should return True - self.assertTrue(result, 'delete_file should return True when it successfully deletes a file') + assert result is True, 'delete_file should return True when it successfully deletes a file' def test_delete_file_path_no_file_exists(self): """ @@ -327,8 +285,8 @@ class TestInit(TestCase, TestMixin): result = delete_file(Path('path', 'file.ext')) # THEN: The function should not attempt to delete the file and it should return True - self.assertFalse(mocked_unlink.called) - self.assertTrue(result, 'delete_file should return True when the file doesnt exist') + assert mocked_unlink.called is False + assert result is True, 'delete_file should return True when the file doesnt exist' def test_delete_file_path_exception(self): """ @@ -344,10 +302,10 @@ class TestInit(TestCase, TestMixin): result = delete_file(Path('path', 'file.ext')) # THEN: The exception should be logged and `delete_file` should return False - self.assertTrue(mocked_log.exception.called) - self.assertFalse(result, 'delete_file should return False when an OSError is raised') + assert mocked_log.exception.called + assert result is False, 'delete_file should return False when an OSError is raised' - def test_get_file_encoding_done_test(self): + def test_get_file_encoding_done(self): """ Test get_file_encoding when the detector sets done to True """ @@ -364,11 +322,11 @@ class TestInit(TestCase, TestMixin): # THEN: The feed method of UniversalDetector should only br called once before returning a result mocked_open.assert_called_once_with('rb') - self.assertEqual(mocked_universal_detector_inst.feed.mock_calls, [call(b"data" * 256)]) + assert mocked_universal_detector_inst.feed.mock_calls == [call(b"data" * 256)] mocked_universal_detector_inst.close.assert_called_once_with() - self.assertEqual(result, encoding_result) + assert result == encoding_result - def test_get_file_encoding_eof_test(self): + def test_get_file_encoding_eof(self): """ Test get_file_encoding when the end of the file is reached """ @@ -386,11 +344,11 @@ class TestInit(TestCase, TestMixin): # THEN: The feed method of UniversalDetector should have been called twice before returning a result mocked_open.assert_called_once_with('rb') - self.assertEqual(mocked_universal_detector_inst.feed.mock_calls, [call(b"data" * 256), call(b"data" * 4)]) + assert mocked_universal_detector_inst.feed.mock_calls == [call(b"data" * 256), call(b"data" * 4)] mocked_universal_detector_inst.close.assert_called_once_with() - self.assertEqual(result, encoding_result) + assert result == encoding_result - def test_get_file_encoding_oserror_test(self): + def test_get_file_encoding_oserror(self): """ Test get_file_encoding when the end of the file is reached """ @@ -405,4 +363,4 @@ class TestInit(TestCase, TestMixin): # THEN: log.exception should be called and get_file_encoding should return None mocked_log.exception.assert_called_once_with('Error detecting file encoding') - self.assertIsNone(result) + assert result is None diff --git a/tests/functional/openlp_core/common/test_json.py b/tests/functional/openlp_core/common/test_json.py index 3b0631dc4..18cba3b4e 100644 --- a/tests/functional/openlp_core/common/test_json.py +++ b/tests/functional/openlp_core/common/test_json.py @@ -45,7 +45,7 @@ class TestOpenLPJsonDecoder(TestCase): result = instance.object_hook({'__Path__': ['test', 'path']}) # THEN: A Path object should be returned - self.assertEqual(result, Path('test', 'path')) + assert result == Path('test', 'path') def test_object_hook_non_path_object(self): """ @@ -59,8 +59,8 @@ class TestOpenLPJsonDecoder(TestCase): result = instance.object_hook({'key': 'value'}) # THEN: The object should be returned unchanged and a Path object should not have been initiated - self.assertEqual(result, {'key': 'value'}) - self.assertFalse(mocked_path.called) + assert result == {'key': 'value'} + assert mocked_path.called is False def test_json_decode(self): """ @@ -73,7 +73,7 @@ class TestOpenLPJsonDecoder(TestCase): obj = json.loads(json_string, cls=OpenLPJsonDecoder) # THEN: The object returned should be a python version of the JSON string - self.assertEqual(obj, [Path('test', 'path1'), Path('test', 'path2')]) + assert obj == [Path('test', 'path1'), Path('test', 'path2')] class TestOpenLPJsonEncoder(TestCase): @@ -91,7 +91,7 @@ class TestOpenLPJsonEncoder(TestCase): result = instance.default(Path('test', 'path')) # THEN: A dictionary object that can be JSON encoded should be returned - self.assertEqual(result, {'__Path__': ('test', 'path')}) + assert result == {'__Path__': ('test', 'path')} def test_default_non_path_object(self): """ @@ -119,4 +119,4 @@ class TestOpenLPJsonEncoder(TestCase): json_string = json.dumps(obj, cls=OpenLPJsonEncoder) # THEN: The JSON string return should be a representation of the object encoded - self.assertEqual(json_string, '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]') + assert json_string == '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]' diff --git a/tests/functional/openlp_core/common/test_mixins.py b/tests/functional/openlp_core/common/test_mixins.py index 7cb8604af..f49c2b797 100644 --- a/tests/functional/openlp_core/common/test_mixins.py +++ b/tests/functional/openlp_core/common/test_mixins.py @@ -23,45 +23,76 @@ Package to test the openlp.core.common package. """ from unittest import TestCase +from unittest.mock import MagicMock, patch -from openlp.core.common.mixins import RegistryMixin +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.registry import Registry -class PlainStub(object): - def __init__(self): - pass - - -class MixinStub(RegistryMixin): - def __init__(self): - super().__init__(None) - - -class TestRegistryMixin(TestCase): - - def test_registry_mixin_missing(self): +class TestRegistryProperties(TestCase, RegistryProperties): + """ + Test the functions in the ThemeManager module + """ + def setUp(self): """ - Test the registry creation and its usage + Create the Register """ - # GIVEN: A new registry Registry.create() - # WHEN: I create an instance of a class that doesn't inherit from RegistryMixin - PlainStub() - - # THEN: Nothing is registered with the registry - self.assertEqual(len(Registry().functions_list), 0), 'The function should not be in the dict anymore.' - - def test_registry_mixin_present(self): + def test_no_application(self): """ - Test the registry creation and its usage + Test property if no registry value assigned """ - # GIVEN: A new registry - Registry.create() + # GIVEN an Empty Registry + # WHEN there is no Application + # THEN the application should be none + assert self.application is None, 'The application value should be None' - # WHEN: I create an instance of a class that inherits from RegistryMixin - MixinStub() + def test_application(self): + """ + Test property if registry value assigned + """ + # GIVEN an Empty Registry + application = MagicMock() - # THEN: The bootstrap methods should be registered - self.assertEqual(len(Registry().functions_list), 2), 'The bootstrap functions should be in the dict.' + # WHEN the application is registered + Registry().register('application', application) + + # THEN the application should be none + assert self.application == application, 'The application value should match' + + @patch('openlp.core.common.mixins.is_win') + def test_application_on_windows(self, mocked_is_win): + """ + Test property if registry value assigned on Windows + """ + # GIVEN an Empty Registry and we're on Windows + application = MagicMock() + mocked_is_win.return_value = True + + # WHEN the application is registered + Registry().register('application', application) + + # THEN the application should be none + assert self.application == application, 'The application value should match' + + @patch('openlp.core.common.mixins.is_win') + def test_get_application_on_windows(self, mocked_is_win): + """ + Set that getting the application object on Windows happens dynamically + """ + # GIVEN an Empty Registry and we're on Windows + mocked_is_win.return_value = True + mock_application = MagicMock() + reg_props = RegistryProperties() + registry = Registry() + + # WHEN the application is accessed + with patch.object(registry, 'get') as mocked_get: + mocked_get.return_value = mock_application + actual_application = reg_props.application + + # THEN the application should be the mock object, and the correct function should have been called + assert mock_application == actual_application, 'The application value should match' + mocked_is_win.assert_called_with() + mocked_get.assert_called_with('application') diff --git a/tests/functional/openlp_core/common/test_path.py b/tests/functional/openlp_core/common/test_path.py index 4b30bd2cb..b069c251d 100644 --- a/tests/functional/openlp_core/common/test_path.py +++ b/tests/functional/openlp_core/common/test_path.py @@ -26,7 +26,7 @@ import os from unittest import TestCase from unittest.mock import ANY, MagicMock, patch -from openlp.core.common.path import Path, copy, copyfile, copytree, create_paths, path_to_str, replace_params, rmtree, \ +from openlp.core.common.path import Path, copy, copyfile, copytree, create_paths, path_to_str, replace_params, \ str_to_path, which @@ -47,8 +47,8 @@ class TestShutil(TestCase): result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) # THEN: The positional and keyword args should not have changed - self.assertEqual(test_args, result_args) - self.assertEqual(test_kwargs, result_kwargs) + assert test_args == result_args + assert test_kwargs == result_kwargs def test_replace_params_params(self): """ @@ -63,8 +63,8 @@ class TestShutil(TestCase): result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) # THEN: The positional and keyword args should have have changed - self.assertEqual(result_args, (1, '2')) - self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4}) + assert result_args == (1, '2') + assert result_kwargs == {'arg3': '3', 'arg4': 4} def test_copy(self): """ @@ -82,7 +82,7 @@ class TestShutil(TestCase): # :func:`shutil.copy` as a Path object. mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) + assert result == Path('destination', 'test', 'path') def test_copy_follow_optional_params(self): """ @@ -114,7 +114,7 @@ class TestShutil(TestCase): # :func:`shutil.copyfile` as a Path object. mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) + assert result == Path('destination', 'test', 'path') def test_copyfile_optional_params(self): """ @@ -147,7 +147,7 @@ class TestShutil(TestCase): # :func:`shutil.copytree` as a Path object. mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) + assert result == Path('destination', 'test', 'path') def test_copytree_optional_params(self): """ @@ -172,31 +172,34 @@ class TestShutil(TestCase): """ Test :func:`rmtree` """ - # GIVEN: A mocked :func:`shutil.rmtree` + # GIVEN: A mocked :func:`shutil.rmtree` and a test Path object with patch('openlp.core.common.path.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: + path = Path('test', 'path') # WHEN: Calling :func:`openlp.core.common.path.rmtree` with the path parameter as Path object type - result = rmtree(Path('test', 'path')) + path.rmtree() # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object. - mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path')) - self.assertIsNone(result) + mocked_shutil_rmtree.assert_called_once_with( + os.path.join('test', 'path'), False, None) def test_rmtree_optional_params(self): """ Test :func:`openlp.core.common.path.rmtree` when optional parameters are passed """ - # GIVEN: A mocked :func:`shutil.rmtree` - with patch('openlp.core.common.path.shutil.rmtree', return_value='') as mocked_shutil_rmtree: + # GIVEN: A mocked :func:`shutil.rmtree` and a test Path object. + with patch('openlp.core.common.path.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: + path = Path('test', 'path') mocked_on_error = MagicMock() # WHEN: Calling :func:`openlp.core.common.path.rmtree` with :param:`ignore_errors` set to True and # :param:`onerror` set to a mocked object - rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) + path.rmtree(ignore_errors=True, onerror=mocked_on_error) # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the # values being modified - mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) + mocked_shutil_rmtree.assert_called_once_with( + os.path.join('test', 'path'), True, mocked_on_error) def test_which_no_command(self): """ @@ -210,7 +213,7 @@ class TestShutil(TestCase): # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None. mocked_shutil_which.assert_called_once_with('no_command') - self.assertIsNone(result) + assert result is None def test_which_command(self): """ @@ -226,7 +229,7 @@ class TestShutil(TestCase): # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a # Path object equivalent of the command path. mocked_shutil_which.assert_called_once_with('command') - self.assertEqual(result, Path('path', 'to', 'command')) + assert result == Path('path', 'to', 'command') class TestPath(TestCase): @@ -253,7 +256,7 @@ class TestPath(TestCase): result = path_to_str(None) # THEN: `path_to_str` should return an empty string - self.assertEqual(result, '') + assert result == '' def test_path_to_str_path_object(self): """ @@ -264,7 +267,7 @@ class TestPath(TestCase): result = path_to_str(Path('test/path')) # THEN: `path_to_str` should return a string representation of the Path object - self.assertEqual(result, os.path.join('test', 'path')) + assert result == os.path.join('test', 'path') def test_str_to_path_type_error(self): """ @@ -285,7 +288,7 @@ class TestPath(TestCase): result = str_to_path('') # THEN: `path_to_str` should return None - self.assertEqual(result, None) + assert result is None def test_path_encode_json(self): """ @@ -297,7 +300,7 @@ class TestPath(TestCase): path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, extra=1, args=2) # THEN: A Path object should have been returned - self.assertEqual(path, Path('path', 'to', 'fi.le')) + assert path == Path('path', 'to', 'fi.le') def test_path_encode_json_base_path(self): """ @@ -309,7 +312,7 @@ class TestPath(TestCase): path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, base_path=Path('/base')) # THEN: A Path object should have been returned with an absolute path - self.assertEqual(path, Path('/', 'base', 'path', 'to', 'fi.le')) + assert path == Path('/', 'base', 'path', 'to', 'fi.le') def test_path_json_object(self): """ @@ -322,7 +325,7 @@ class TestPath(TestCase): obj = path.json_object(extra=1, args=2) # THEN: A JSON decodable object should have been returned. - self.assertEqual(obj, {'__Path__': ('/', 'base', 'path', 'to', 'fi.le')}) + assert obj == {'__Path__': ('/', 'base', 'path', 'to', 'fi.le')} def test_path_json_object_base_path(self): """ @@ -336,7 +339,7 @@ class TestPath(TestCase): obj = path.json_object(base_path=Path('/', 'base')) # THEN: A JSON decodable object should have been returned. - self.assertEqual(obj, {'__Path__': ('path', 'to', 'fi.le')}) + assert obj == {'__Path__': ('path', 'to', 'fi.le')} def test_create_paths_dir_exists(self): """ @@ -371,13 +374,13 @@ class TestPath(TestCase): @patch('openlp.core.common.path.log') def test_create_paths_dir_io_error(self, mocked_logger): """ - Test the create_paths() when an IOError is raised + Test the create_paths() when an OSError is raised """ # GIVEN: A `Path` to check with patched out mkdir and exists methods mocked_path = MagicMock() - mocked_path.exists.side_effect = IOError('Cannot make directory') + mocked_path.exists.side_effect = OSError('Cannot make directory') - # WHEN: An IOError is raised when checking the if the path exists. + # WHEN: An OSError is raised when checking the if the path exists. create_paths(mocked_path) # THEN: The Error should have been logged @@ -385,7 +388,7 @@ class TestPath(TestCase): def test_create_paths_dir_value_error(self): """ - Test the create_paths() when an error other than IOError is raised + Test the create_paths() when an error other than OSError is raised """ # GIVEN: A `Path` to check with patched out mkdir and exists methods mocked_path = MagicMock() diff --git a/tests/functional/openlp_core/common/test_projector_utilities.py b/tests/functional/openlp_core/common/test_projector_utilities.py index cbdfec238..5a3f886cf 100644 --- a/tests/functional/openlp_core/common/test_projector_utilities.py +++ b/tests/functional/openlp_core/common/test_projector_utilities.py @@ -45,7 +45,7 @@ ip6_link_local = 'fe80::223:14ff:fe99:d315' ip6_bad = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' -class testProjectorUtilities(TestCase): +class TestProjectorUtilities(TestCase): """ Validate functions in the projector utilities module """ @@ -57,7 +57,7 @@ class testProjectorUtilities(TestCase): valid = verify_ip_address(addr=ip4_loopback) # THEN: Verify we received True - self.assertTrue(valid, 'IPv4 loopback address should have been valid') + assert valid, 'IPv4 loopback address should have been valid' def test_ip4_local_valid(self): """ @@ -67,7 +67,7 @@ class testProjectorUtilities(TestCase): valid = verify_ip_address(addr=ip4_local) # THEN: Verify we received True - self.assertTrue(valid, 'IPv4 local address should have been valid') + assert valid is True, 'IPv4 local address should have been valid' def test_ip4_broadcast_valid(self): """ @@ -77,7 +77,7 @@ class testProjectorUtilities(TestCase): valid = verify_ip_address(addr=ip4_broadcast) # THEN: Verify we received True - self.assertTrue(valid, 'IPv4 broadcast address should have been valid') + assert valid is True, 'IPv4 broadcast address should have been valid' def test_ip4_address_invalid(self): """ @@ -87,7 +87,7 @@ class testProjectorUtilities(TestCase): valid = verify_ip_address(addr=ip4_bad) # THEN: Verify we received True - self.assertFalse(valid, 'Bad IPv4 address should not have been valid') + assert valid is False, 'Bad IPv4 address should not have been valid' def test_ip6_loopback_valid(self): """ @@ -97,7 +97,7 @@ class testProjectorUtilities(TestCase): valid = verify_ip_address(addr=ip6_loopback) # THEN: Validate return - self.assertTrue(valid, 'IPv6 loopback address should have been valid') + assert valid is True, 'IPv6 loopback address should have been valid' def test_ip6_local_valid(self): """ @@ -107,7 +107,7 @@ class testProjectorUtilities(TestCase): valid = verify_ip_address(addr=ip6_link_local) # THEN: Validate return - self.assertTrue(valid, 'IPv6 link-local address should have been valid') + assert valid is True, 'IPv6 link-local address should have been valid' def test_ip6_address_invalid(self): """ @@ -117,7 +117,7 @@ class testProjectorUtilities(TestCase): valid = verify_ip_address(addr=ip6_bad) # THEN: Validate bad return - self.assertFalse(valid, 'IPv6 bad address should have been invalid') + assert valid is False, 'IPv6 bad address should have been invalid' def test_md5_hash(self): """ @@ -127,7 +127,7 @@ class testProjectorUtilities(TestCase): hash_ = md5_hash(salt=salt.encode('utf-8'), data=pin.encode('utf-8')) # THEN: Validate return has is same - self.assertEquals(hash_, test_hash, 'MD5 should have returned a good hash') + assert hash_ == test_hash, 'MD5 should have returned a good hash' def test_md5_hash_bad(self): """ @@ -137,7 +137,7 @@ class testProjectorUtilities(TestCase): hash_ = md5_hash(salt=pin.encode('utf-8'), data=salt.encode('utf-8')) # THEN: return data is different - self.assertNotEquals(hash_, test_hash, 'MD5 should have returned a bad hash') + assert hash_ is not test_hash, 'MD5 should have returned a bad hash' def test_qmd5_hash(self): """ @@ -147,7 +147,7 @@ class testProjectorUtilities(TestCase): hash_ = qmd5_hash(salt=salt.encode('utf-8'), data=pin.encode('utf-8')) # THEN: Validate return has is same - self.assertEquals(hash_, test_hash, 'Qt-MD5 should have returned a good hash') + assert hash_ == test_hash, 'Qt-MD5 should have returned a good hash' def test_qmd5_hash_bad(self): """ @@ -157,7 +157,7 @@ class testProjectorUtilities(TestCase): hash_ = qmd5_hash(salt=pin.encode('utf-8'), data=salt.encode('utf-8')) # THEN: return data is different - self.assertNotEquals(hash_, test_hash, 'Qt-MD5 should have returned a bad hash') + assert hash_ is not test_hash, 'Qt-MD5 should have returned a bad hash' def test_md5_non_ascii_string(self): """ @@ -167,7 +167,7 @@ class testProjectorUtilities(TestCase): hash_ = md5_hash(salt=test_non_ascii_string.encode('utf-8'), data=None) # THEN: Valid MD5 hash should be returned - self.assertEqual(hash_, test_non_ascii_hash, 'MD5 should have returned a valid hash') + assert hash_ == test_non_ascii_hash, 'MD5 should have returned a valid hash' def test_qmd5_non_ascii_string(self): """ @@ -177,4 +177,4 @@ class testProjectorUtilities(TestCase): hash_ = md5_hash(data=test_non_ascii_string.encode('utf-8')) # THEN: Valid MD5 hash should be returned - self.assertEqual(hash_, test_non_ascii_hash, 'Qt-MD5 should have returned a valid hash') + assert hash_ == test_non_ascii_hash, 'Qt-MD5 should have returned a valid hash' diff --git a/tests/functional/openlp_core/common/test_registry.py b/tests/functional/openlp_core/common/test_registry.py index 6428a27c3..6f3cc5752 100644 --- a/tests/functional/openlp_core/common/test_registry.py +++ b/tests/functional/openlp_core/common/test_registry.py @@ -24,9 +24,9 @@ Package to test the openlp.core.lib package. """ import os from unittest import TestCase -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../', '..', 'resources')) @@ -51,19 +51,19 @@ class TestRegistry(TestCase): # THEN and I will get an exception with self.assertRaises(KeyError) as context: Registry().register('test1', mock_1) - self.assertEqual(context.exception.args[0], 'Duplicate service exception test1', - 'KeyError exception should have been thrown for duplicate service') + assert context.exception.args[0] == 'Duplicate service exception test1', \ + 'KeyError exception should have been thrown for duplicate service' # WHEN I try to get back a non existent component # THEN I will get an exception temp = Registry().get('test2') - self.assertEqual(temp, None, 'None should have been returned for missing service') + assert temp is None, 'None should have been returned for missing service' # WHEN I try to replace a component I should be allowed Registry().remove('test1') # THEN I will get an exception temp = Registry().get('test1') - self.assertEqual(temp, None, 'None should have been returned for deleted service') + assert temp is None, 'None should have been returned for deleted service' def test_registry_function(self): """ @@ -77,21 +77,21 @@ class TestRegistry(TestCase): return_value = Registry().execute('test1') # THEN: I expect then function to have been called and a return given - self.assertEqual(return_value[0], 'function_1', 'A return value is provided and matches') + assert return_value[0] == 'function_1', 'A return value is provided and matches' # WHEN: I execute the a function with the same reference and execute the function Registry().register_function('test1', self.dummy_function_1) return_value = Registry().execute('test1') # THEN: I expect then function to have been called and a return given - self.assertEqual(return_value, ['function_1', 'function_1'], 'A return value list is provided and matches') + assert return_value == ['function_1', 'function_1'], 'A return value list is provided and matches' # WHEN: I execute the a 2nd function with the different reference and execute the function Registry().register_function('test2', self.dummy_function_2) return_value = Registry().execute('test2') # THEN: I expect then function to have been called and a return given - self.assertEqual(return_value[0], 'function_2', 'A return value is provided and matches') + assert return_value[0] == 'function_2', 'A return value is provided and matches' def test_registry_working_flags(self): """ @@ -107,28 +107,28 @@ class TestRegistry(TestCase): # THEN: we should be able retrieve the saved component temp = Registry().get_flag('test1') - self.assertEquals(temp, my_data, 'The value should have been saved') + assert temp == my_data, 'The value should have been saved' # WHEN: I add a component for the second time I am not mad. # THEN and I will not get an exception Registry().set_flag('test1', my_data2) temp = Registry().get_flag('test1') - self.assertEquals(temp, my_data2, 'The value should have been updated') + assert temp == my_data2, 'The value should have been updated' # WHEN I try to get back a non existent Working Flag # THEN I will get an exception with self.assertRaises(KeyError) as context1: temp = Registry().get_flag('test2') - self.assertEqual(context1.exception.args[0], 'Working Flag test2 not found in list', - 'KeyError exception should have been thrown for missing working flag') + assert context1.exception.args[0] == 'Working Flag test2 not found in list', \ + 'KeyError exception should have been thrown for missing working flag' # WHEN I try to replace a working flag I should be allowed Registry().remove_flag('test1') # THEN I will get an exception with self.assertRaises(KeyError) as context: temp = Registry().get_flag('test1') - self.assertEqual(context.exception.args[0], 'Working Flag test1 not found in list', - 'KeyError exception should have been thrown for duplicate working flag') + assert context.exception.args[0] == 'Working Flag test1 not found in list', \ + 'KeyError exception should have been thrown for duplicate working flag' def test_remove_function(self): """ @@ -142,7 +142,7 @@ class TestRegistry(TestCase): Registry().remove_function('test1', self.dummy_function_1) # THEN: The method should not be available. - assert not Registry().functions_list['test1'], 'The function should not be in the dict anymore.' + assert Registry().functions_list['test1'] == [], 'The function should not be in the dict anymore.' def dummy_function_1(self): return "function_1" @@ -151,70 +151,40 @@ class TestRegistry(TestCase): return "function_2" -class TestRegistryProperties(TestCase, RegistryProperties): - """ - Test the functions in the ThemeManager module - """ - def setUp(self): +class PlainStub(object): + def __init__(self): + pass + + +class RegistryStub(RegistryBase): + def __init__(self): + super().__init__() + + +class TestRegistryBase(TestCase): + + def test_registry_mixin_missing(self): """ - Create the Register + Test the registry creation and its usage """ + # GIVEN: A new registry Registry.create() - def test_no_application(self): + # WHEN: I create an instance of a class that doesn't inherit from RegistryMixin + PlainStub() + + # THEN: Nothing is registered with the registry + assert len(Registry().functions_list) == 0, 'The function should not be in the dict anymore.' + + def test_registry_mixin_present(self): """ - Test property if no registry value assigned + Test the registry creation and its usage """ - # GIVEN an Empty Registry - # WHEN there is no Application - # THEN the application should be none - self.assertEqual(self.application, None, 'The application value should be None') + # GIVEN: A new registry + Registry.create() - def test_application(self): - """ - Test property if registry value assigned - """ - # GIVEN an Empty Registry - application = MagicMock() + # WHEN: I create an instance of a class that inherits from RegistryMixin + RegistryStub() - # WHEN the application is registered - Registry().register('application', application) - - # THEN the application should be none - self.assertEqual(self.application, application, 'The application value should match') - - @patch('openlp.core.common.registry.is_win') - def test_application_on_windows(self, mocked_is_win): - """ - Test property if registry value assigned on Windows - """ - # GIVEN an Empty Registry and we're on Windows - application = MagicMock() - mocked_is_win.return_value = True - - # WHEN the application is registered - Registry().register('application', application) - - # THEN the application should be none - self.assertEqual(self.application, application, 'The application value should match') - - @patch('openlp.core.common.registry.is_win') - def test_get_application_on_windows(self, mocked_is_win): - """ - Set that getting the application object on Windows happens dynamically - """ - # GIVEN an Empty Registry and we're on Windows - mocked_is_win.return_value = True - mock_application = MagicMock() - reg_props = RegistryProperties() - registry = Registry() - - # WHEN the application is accessed - with patch.object(registry, 'get') as mocked_get: - mocked_get.return_value = mock_application - actual_application = reg_props.application - - # THEN the application should be the mock object, and the correct function should have been called - self.assertEqual(mock_application, actual_application, 'The application value should match') - mocked_is_win.assert_called_with() - mocked_get.assert_called_with('application') + # THEN: The bootstrap methods should be registered + assert len(Registry().functions_list) == 2, 'The bootstrap functions should be in the dict.' diff --git a/tests/functional/openlp_core/common/test_settings.py b/tests/functional/openlp_core/common/test_settings.py index d54a6a1e1..4cad58af0 100644 --- a/tests/functional/openlp_core/common/test_settings.py +++ b/tests/functional/openlp_core/common/test_settings.py @@ -22,10 +22,12 @@ """ Package to test the openlp.core.lib.settings package. """ +from pathlib import Path from unittest import TestCase -from unittest.mock import patch +from unittest.mock import call, patch -from openlp.core.common.settings import Settings +from openlp.core.common import settings +from openlp.core.common.settings import Settings, media_players_conv from tests.helpers.testmixin import TestMixin @@ -47,28 +49,58 @@ class TestSettings(TestCase, TestMixin): """ self.destroy_settings() - def test_settings_basic(self): - """ - Test the Settings creation and its default usage - """ - # GIVEN: A new Settings setup + def test_media_players_conv(self): + """Test the media players conversion function""" + # GIVEN: A list of media players + media_players = 'phonon,webkit,vlc' + + # WHEN: The media converter function is called + result = media_players_conv(media_players) + + # THEN: The list should have been converted correctly + assert result == 'system,webkit,vlc' + + def test_default_value(self): + """Test reading a setting that doesn't exist yet""" + # GIVEN: A setting that doesn't exist yet # WHEN reading a setting for the first time default_value = Settings().value('core/has run wizard') # THEN the default value is returned - self.assertFalse(default_value, 'The default value should be False') + assert default_value is False, 'The default value should be False' + def test_save_new_value(self): + """Test saving a new setting""" + # GIVEN: A setting that hasn't been saved yet # WHEN a new value is saved into config Settings().setValue('core/has run wizard', True) # THEN the new value is returned when re-read - self.assertTrue(Settings().value('core/has run wizard'), 'The saved value should have been returned') + assert Settings().value('core/has run wizard') is True, 'The saved value should have been returned' + + def test_set_up_default_values(self): + """Test that the default values are updated""" + # GIVEN: A Settings object with defaults + # WHEN: set_up_default_values() is called + Settings.set_up_default_values() + + # THEN: The default values should have been added to the dictionary + assert 'advanced/default service name' in Settings.__default_settings__ + + def test_get_default_value(self): + """Test that the default value for a setting is returned""" + # GIVEN: A Settings class with a default value + Settings.__default_settings__['test/moo'] = 'baa' + + # WHEN: get_default_value() is called + result = Settings().get_default_value('test/moo') + + # THEN: The correct default value should be returned + assert result == 'baa' def test_settings_override(self): - """ - Test the Settings creation and its override usage - """ + """Test the Settings creation and its override usage""" # GIVEN: an override for the settings screen_settings = { 'test/extend': 'very wide', @@ -79,18 +111,22 @@ class TestSettings(TestCase, TestMixin): extend = Settings().value('test/extend') # THEN the default value is returned - self.assertEqual('very wide', extend, 'The default value of "very wide" should be returned') + assert extend == 'very wide', 'The default value of "very wide" should be returned' + + def test_save_existing_setting(self): + """Test that saving an existing setting returns the new value""" + # GIVEN: An existing setting + Settings().extend_default_settings({'test/existing value': None}) + Settings().setValue('test/existing value', 'old value') # WHEN a new value is saved into config - Settings().setValue('test/extend', 'very short') + Settings().setValue('test/existing value', 'new value') # THEN the new value is returned when re-read - self.assertEqual('very short', Settings().value('test/extend'), 'The saved value should be returned') + assert Settings().value('test/existing value') == 'new value', 'The saved value should be returned' def test_settings_override_with_group(self): - """ - Test the Settings creation and its override usage - with groups - """ + """Test the Settings creation and its override usage - with groups""" # GIVEN: an override for the settings screen_settings = { 'test/extend': 'very wide', @@ -103,30 +139,26 @@ class TestSettings(TestCase, TestMixin): extend = settings.value('extend') # THEN the default value is returned - self.assertEqual('very wide', extend, 'The default value defined should be returned') + assert 'very wide' == extend, 'The default value defined should be returned' # WHEN a new value is saved into config Settings().setValue('test/extend', 'very short') # THEN the new value is returned when re-read - self.assertEqual('very short', Settings().value('test/extend'), 'The saved value should be returned') + assert 'very short' == Settings().value('test/extend'), 'The saved value should be returned' def test_settings_nonexisting(self): - """ - Test the Settings on query for non-existing value - """ + """Test the Settings on query for non-existing value""" # GIVEN: A new Settings setup with self.assertRaises(KeyError) as cm: # WHEN reading a setting that doesn't exists Settings().value('core/does not exists') # THEN: An exception with the non-existing key should be thrown - self.assertEqual(str(cm.exception), "'core/does not exists'", 'We should get an exception') + assert str(cm.exception) == "'core/does not exists'", 'We should get an exception' def test_extend_default_settings(self): - """ - Test that the extend_default_settings method extends the default settings - """ + """Test that the extend_default_settings method extends the default settings""" # GIVEN: A patched __default_settings__ dictionary with patch.dict(Settings.__default_settings__, {'test/setting 1': 1, 'test/setting 2': 2, 'test/setting 3': 3}, True): @@ -135,6 +167,127 @@ class TestSettings(TestCase, TestMixin): Settings.extend_default_settings({'test/setting 3': 4, 'test/extended 1': 1, 'test/extended 2': 2}) # THEN: The _default_settings__ dictionary_ should have the new keys - self.assertEqual( - Settings.__default_settings__, {'test/setting 1': 1, 'test/setting 2': 2, 'test/setting 3': 4, - 'test/extended 1': 1, 'test/extended 2': 2}) + assert Settings.__default_settings__ == {'test/setting 1': 1, 'test/setting 2': 2, 'test/setting 3': 4, + 'test/extended 1': 1, 'test/extended 2': 2} + + @patch('openlp.core.common.settings.QtCore.QSettings.contains') + @patch('openlp.core.common.settings.QtCore.QSettings.value') + @patch('openlp.core.common.settings.QtCore.QSettings.setValue') + @patch('openlp.core.common.settings.QtCore.QSettings.remove') + def test_upgrade_single_setting(self, mocked_remove, mocked_setValue, mocked_value, mocked_contains): + """Test that the upgrade mechanism for settings works correctly for single value upgrades""" + # GIVEN: A settings object with an upgrade step to take (99, so that we don't interfere with real ones) + local_settings = Settings() + local_settings.__setting_upgrade_99__ = [ + ('single/value', 'single/new value', [(str, '')]) + ] + settings.__version__ = 99 + mocked_value.side_effect = [98, 10] + mocked_contains.return_value = True + + # WHEN: upgrade_settings() is called + local_settings.upgrade_settings() + + # THEN: The correct calls should have been made with the correct values + assert mocked_value.call_count == 2, 'Settings().value() should have been called twice' + assert mocked_value.call_args_list == [call('settings/version', 0), call('single/value')] + assert mocked_setValue.call_count == 2, 'Settings().setValue() should have been called twice' + assert mocked_setValue.call_args_list == [call('single/new value', '10'), call('settings/version', 99)] + mocked_contains.assert_called_once_with('single/value') + mocked_remove.assert_called_once_with('single/value') + + @patch('openlp.core.common.settings.QtCore.QSettings.contains') + @patch('openlp.core.common.settings.QtCore.QSettings.value') + @patch('openlp.core.common.settings.QtCore.QSettings.setValue') + @patch('openlp.core.common.settings.QtCore.QSettings.remove') + def test_upgrade_setting_value(self, mocked_remove, mocked_setValue, mocked_value, mocked_contains): + """Test that the upgrade mechanism for settings correctly uses the new value when it's not a function""" + # GIVEN: A settings object with an upgrade step to take (99, so that we don't interfere with real ones) + local_settings = Settings() + local_settings.__setting_upgrade_99__ = [ + ('values/old value', 'values/new value', [(True, 1)]) + ] + settings.__version__ = 99 + mocked_value.side_effect = [98, 1] + mocked_contains.return_value = True + + # WHEN: upgrade_settings() is called + local_settings.upgrade_settings() + + # THEN: The correct calls should have been made with the correct values + assert mocked_value.call_count == 2, 'Settings().value() should have been called twice' + assert mocked_value.call_args_list == [call('settings/version', 0), call('values/old value')] + assert mocked_setValue.call_count == 2, 'Settings().setValue() should have been called twice' + assert mocked_setValue.call_args_list == [call('values/new value', True), call('settings/version', 99)] + mocked_contains.assert_called_once_with('values/old value') + mocked_remove.assert_called_once_with('values/old value') + + @patch('openlp.core.common.settings.QtCore.QSettings.contains') + @patch('openlp.core.common.settings.QtCore.QSettings.value') + @patch('openlp.core.common.settings.QtCore.QSettings.setValue') + @patch('openlp.core.common.settings.QtCore.QSettings.remove') + def test_upgrade_multiple_one_invalid(self, mocked_remove, mocked_setValue, mocked_value, mocked_contains): + """Test that the upgrade mechanism for settings works correctly for multiple values where one is invalid""" + # GIVEN: A settings object with an upgrade step to take + local_settings = Settings() + local_settings.__setting_upgrade_99__ = [ + (['multiple/value 1', 'multiple/value 2'], 'single/new value', []) + ] + settings.__version__ = 99 + mocked_value.side_effect = [98, 10] + mocked_contains.side_effect = [True, False] + + # WHEN: upgrade_settings() is called + local_settings.upgrade_settings() + + # THEN: The correct calls should have been made with the correct values + mocked_value.assert_called_once_with('settings/version', 0) + mocked_setValue.assert_called_once_with('settings/version', 99) + assert mocked_contains.call_args_list == [call('multiple/value 1'), call('multiple/value 2')] + + def test_can_upgrade(self): + """Test the Settings.can_upgrade() method""" + # GIVEN: A Settings object + local_settings = Settings() + + # WHEN: can_upgrade() is run + result = local_settings.can_upgrade() + + # THEN: The result should be True + assert result is True, 'The settings should be upgradeable' + + def test_convert_value_setting_none_str(self): + """Test the Settings._convert_value() method when a setting is None and the default value is a string""" + # GIVEN: A settings object + # WHEN: _convert_value() is run + result = Settings()._convert_value(None, 'string') + + # THEN: The result should be an empty string + assert result == '', 'The result should be an empty string' + + def test_convert_value_setting_none_list(self): + """Test the Settings._convert_value() method when a setting is None and the default value is a list""" + # GIVEN: A settings object + # WHEN: _convert_value() is run + result = Settings()._convert_value(None, [None]) + + # THEN: The result should be an empty list + assert result == [], 'The result should be an empty list' + + def test_convert_value_setting_json_Path(self): + """Test the Settings._convert_value() method when a setting is JSON and represents a Path object""" + # GIVEN: A settings object + # WHEN: _convert_value() is run + result = Settings()._convert_value('{"__Path__": ["openlp", "core"]}', None) + + # THEN: The result should be a Path object + assert isinstance(result, Path), 'The result should be a Path object' + + def test_convert_value_setting_bool_str(self): + """Test the Settings._convert_value() method when a setting is supposed to be a boolean""" + # GIVEN: A settings object + # WHEN: _convert_value() is run + result = Settings()._convert_value('false', True) + + # THEN: The result should be False + assert result is False, 'The result should be False' diff --git a/tests/functional/openlp_core/display/test_renderer.py b/tests/functional/openlp_core/display/test_renderer.py index c30fe083b..b1478c7b9 100644 --- a/tests/functional/openlp_core/display/test_renderer.py +++ b/tests/functional/openlp_core/display/test_renderer.py @@ -113,7 +113,7 @@ class TestRenderer(TestCase): result = get_start_tags(given_raw_text) # THEN: Check if the correct tuple is returned. - self.assertEqual(result, expected_tuple), 'A tuple should be returned containing the text with correct ' \ + assert result == expected_tuple, 'A tuple should be returned containing the text with correct ' \ 'tags, the opening tags, and the opening html tags.' def test_word_split(self): @@ -128,7 +128,7 @@ class TestRenderer(TestCase): result_words = words_split(given_line) # THEN: The word lists should be the same. - self.assertListEqual(result_words, expected_words) + assert result_words == expected_words def test_format_slide_logical_split(self): """ @@ -145,7 +145,7 @@ class TestRenderer(TestCase): result_words = renderer.format_slide(given_line, service_item) # THEN: The word lists should be the same. - self.assertListEqual(result_words, expected_words) + assert result_words == expected_words def test_format_slide_blank_before_split(self): """ @@ -162,7 +162,7 @@ class TestRenderer(TestCase): result_words = renderer.format_slide(given_line, service_item) # THEN: The blanks have been removed. - self.assertListEqual(result_words, expected_words) + assert result_words == expected_words def test_format_slide_blank_after_split(self): """ @@ -179,7 +179,7 @@ class TestRenderer(TestCase): result_words = renderer.format_slide(given_line, service_item) # THEN: The blanks have been removed. - self.assertListEqual(result_words, expected_words) + assert result_words == expected_words @patch('openlp.core.display.renderer.QtWebKitWidgets.QWebView') @patch('openlp.core.display.renderer.build_lyrics_format_css') diff --git a/tests/functional/openlp_core/display/test_screens.py b/tests/functional/openlp_core/display/test_screens.py index 258f3b69a..960f9d0a8 100644 --- a/tests/functional/openlp_core/display/test_screens.py +++ b/tests/functional/openlp_core/display/test_screens.py @@ -75,6 +75,6 @@ class TestScreenList(TestCase): # THEN: The screen should have been added and the screens should be identical new_screen_count = len(self.screens.screen_list) - self.assertEqual(old_screen_count + 1, new_screen_count, 'The new_screens list should be bigger') - self.assertEqual(SCREEN, self.screens.screen_list.pop(), - 'The 2nd screen should be identical to the first screen') + assert old_screen_count + 1 == new_screen_count, 'The new_screens list should be bigger' + assert SCREEN == self.screens.screen_list.pop(), \ + 'The 2nd screen should be identical to the first screen' diff --git a/tests/functional/openlp_core/lib/test_db.py b/tests/functional/openlp_core/lib/test_db.py index 871980498..033508965 100644 --- a/tests/functional/openlp_core/lib/test_db.py +++ b/tests/functional/openlp_core/lib/test_db.py @@ -80,8 +80,8 @@ class TestDB(TestCase): MockedMetaData.assert_called_with(bind=mocked_engine) mocked_sessionmaker.assert_called_with(autoflush=True, autocommit=False, bind=mocked_engine) mocked_scoped_session.assert_called_with(mocked_sessionmaker_object) - self.assertIs(session, mocked_scoped_session_object, 'The ``session`` object should be the mock') - self.assertIs(metadata, mocked_metadata, 'The ``metadata`` object should be the mock') + assert session is mocked_scoped_session_object, 'The ``session`` object should be the mock' + assert metadata is mocked_metadata, 'The ``metadata`` object should be the mock' def test_init_db_defaults(self): """ @@ -94,8 +94,8 @@ class TestDB(TestCase): session, metadata = init_db(db_url) # THEN: Valid session and metadata objects should be returned - self.assertIsInstance(session, ScopedSession, 'The ``session`` object should be a ``ScopedSession`` instance') - self.assertIsInstance(metadata, MetaData, 'The ``metadata`` object should be a ``MetaData`` instance') + assert isinstance(session, ScopedSession), 'The ``session`` object should be a ``ScopedSession`` instance' + assert isinstance(metadata, MetaData), 'The ``metadata`` object should be a ``MetaData`` instance' def test_get_upgrade_op(self): """ @@ -116,7 +116,7 @@ class TestDB(TestCase): op = get_upgrade_op(mocked_session) # THEN: The op object should be mocked_op, and the correction function calls should have been made - self.assertIs(op, mocked_op, 'The return value should be the mocked object') + assert op is mocked_op, 'The return value should be the mocked object' mocked_session.bind.connect.assert_called_with() MockedMigrationContext.configure.assert_called_with(mocked_connection) MockedOperations.assert_called_with(mocked_context) @@ -139,7 +139,7 @@ class TestDB(TestCase): # THEN: The AppLocation.get_section_data_path and delete_file methods should have been called MockedAppLocation.get_section_data_path.assert_called_with(test_plugin) mocked_delete_file.assert_called_with(test_location) - self.assertTrue(result, 'The result of delete_file should be True (was rigged that way)') + assert result is True, 'The result of delete_file should be True (was rigged that way)' def test_delete_database_with_db_file_name(self): """ @@ -160,7 +160,7 @@ class TestDB(TestCase): # THEN: The AppLocation.get_section_data_path and delete_file methods should have been called MockedAppLocation.get_section_data_path.assert_called_with(test_plugin) mocked_delete_file.assert_called_with(test_location) - self.assertFalse(result, 'The result of delete_file should be False (was rigged that way)') + assert result is False, 'The result of delete_file should be False (was rigged that way)' def test_skip_db_upgrade_with_no_database(self): """ @@ -174,4 +174,4 @@ class TestDB(TestCase): upgrade_db(url, mocked_upgrade) # THEN: upgrade should NOT have been called - self.assertFalse(mocked_upgrade.called, 'Database upgrade function should NOT have been called') + assert mocked_upgrade.called is False, 'Database upgrade function should NOT have been called' diff --git a/openlp/core/ui/lib/dockwidget.py b/tests/functional/openlp_core/lib/test_exceptions.py similarity index 62% rename from openlp/core/ui/lib/dockwidget.py rename to tests/functional/openlp_core/lib/test_exceptions.py index 398d1e674..c0de323b7 100644 --- a/openlp/core/ui/lib/dockwidget.py +++ b/tests/functional/openlp_core/lib/test_exceptions.py @@ -19,39 +19,27 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - """ -Provide additional functionality required by OpenLP from the inherited QDockWidget. +Package to test the openlp.core.lib.exceptions package. """ +from unittest import TestCase -import logging - -from PyQt5 import QtWidgets - -from openlp.core.display.screens import ScreenList -from openlp.core.lib import build_icon - -log = logging.getLogger(__name__) +from openlp.core.lib.exceptions import ValidationError -class OpenLPDockWidget(QtWidgets.QDockWidget): +class TestValidationError(TestCase): """ - Custom DockWidget class to handle events + Test the ValidationError Class """ - def __init__(self, parent=None, name=None, icon=None): + def test_validation_error(self): """ - Initialise the DockWidget + Test the creation of a ValidationError """ - log.debug('Initialise the %s widget' % name) - super(OpenLPDockWidget, self).__init__(parent) - if name: - self.setObjectName(name) - if icon: - self.setWindowIcon(build_icon(icon)) - # Sort out the minimum width. - screens = ScreenList() - main_window_docbars = screens.current['size'].width() // 5 - if main_window_docbars > 300: - self.setMinimumWidth(300) - else: - self.setMinimumWidth(main_window_docbars) + # GIVEN: The ValidationError class + + # WHEN: Creating an instance of ValidationError + error = ValidationError('Test ValidationError') + + # THEN: Then calling str on the error should return the correct text and it should be an instance of `Exception` + assert str(error) == 'Test ValidationError' + assert isinstance(error, Exception) diff --git a/tests/functional/openlp_core/lib/test_htmlbuilder.py b/tests/functional/openlp_core/lib/test_htmlbuilder.py index 7c8d7809f..ffd796ecb 100644 --- a/tests/functional/openlp_core/lib/test_htmlbuilder.py +++ b/tests/functional/openlp_core/lib/test_htmlbuilder.py @@ -309,7 +309,7 @@ class Htmbuilder(TestCase, TestMixin): html = build_html(item, screen, is_live, background, plugins=plugins) # THEN: The returned html should match. - self.assertEqual(html, HTML, 'The returned html should match') + assert html == HTML, 'The returned html should match' def test_build_background_css_radial(self): """ @@ -325,7 +325,7 @@ class Htmbuilder(TestCase, TestMixin): css = build_background_css(item, width) # THEN: The returned css should match. - self.assertEqual(BACKGROUND_CSS_RADIAL, css, 'The background css should be equal.') + assert BACKGROUND_CSS_RADIAL == css, 'The background css should be equal.' def test_build_lyrics_css(self): """ @@ -346,7 +346,7 @@ class Htmbuilder(TestCase, TestMixin): css = build_lyrics_css(item) # THEN: The css should be equal. - self.assertEqual(LYRICS_CSS, css, 'The lyrics css should be equal.') + assert LYRICS_CSS == css, 'The lyrics css should be equal.' def test_build_lyrics_outline_css(self): """ @@ -363,7 +363,7 @@ class Htmbuilder(TestCase, TestMixin): css = build_lyrics_outline_css(theme_data) # THEN: The css should be equal. - self.assertEqual(LYRICS_OUTLINE_CSS, css, 'The outline css should be equal.') + assert LYRICS_OUTLINE_CSS == css, 'The outline css should be equal.' def test_build_lyrics_format_css(self): """ @@ -386,7 +386,7 @@ class Htmbuilder(TestCase, TestMixin): css = build_lyrics_format_css(theme_data, width, height) # THEN: They should be equal. - self.assertEqual(LYRICS_FORMAT_CSS, css, 'The lyrics format css should be equal.') + assert LYRICS_FORMAT_CSS == css, 'The lyrics format css should be equal.' def test_build_footer_css(self): """ @@ -404,7 +404,7 @@ class Htmbuilder(TestCase, TestMixin): css = build_footer_css(item, height) # THEN: THE css should be the same. - self.assertEqual(FOOTER_CSS, css, 'The footer strings should be equal.') + assert FOOTER_CSS == css, 'The footer strings should be equal.' def test_build_footer_css_wrap(self): """ @@ -423,7 +423,7 @@ class Htmbuilder(TestCase, TestMixin): css = build_footer_css(item, height) # THEN: Footer should wrap - self.assertEqual(FOOTER_CSS_WRAP, css, 'The footer strings should be equal.') + assert FOOTER_CSS_WRAP == css, 'The footer strings should be equal.' def test_build_footer_invalid(self): """ @@ -443,8 +443,8 @@ class Htmbuilder(TestCase, TestMixin): css.append(build_footer_css(item, height)) # THEN: Footer should wrap - self.assertEqual(FOOTER_CSS_INVALID, css[0], 'The footer strings should be blank.') - self.assertEqual(FOOTER_CSS_INVALID, css[1], 'The footer strings should be blank.') + assert FOOTER_CSS_INVALID == css[0], 'The footer strings should be blank.' + assert FOOTER_CSS_INVALID == css[1], 'The footer strings should be blank.' def test_webkit_version(self): """ @@ -454,7 +454,7 @@ class Htmbuilder(TestCase, TestMixin): webkit_ver = float(QtWebKit.qWebKitVersion()) # WHEN: Retrieving the webkit version # THEN: Webkit versions should match - self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one") + assert webkit_version() == webkit_ver, "The returned webkit version doesn't match the installed one" def test_build_chords_css(self): """ @@ -468,4 +468,4 @@ class Htmbuilder(TestCase, TestMixin): chord_css = build_chords_css() # THEN: The build css should look as expected - self.assertEqual(CHORD_CSS_ENABLED, chord_css, 'The chord CSS should look as expected') + assert CHORD_CSS_ENABLED == chord_css, 'The chord CSS should look as expected' diff --git a/tests/functional/openlp_core/lib/test_image_manager.py b/tests/functional/openlp_core/lib/test_image_manager.py index c39f6e8f4..3c47e81a1 100644 --- a/tests/functional/openlp_core/lib/test_image_manager.py +++ b/tests/functional/openlp_core/lib/test_image_manager.py @@ -84,7 +84,7 @@ class TestImageManager(TestCase, TestMixin): # THEN a KeyError is thrown with self.assertRaises(KeyError) as context: self.image_manager.get_image(TEST_PATH, 'church1.jpg') - self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image') + assert context.exception is not '', 'KeyError exception should have been thrown for missing image' def test_different_dimension_image(self): """ @@ -98,7 +98,7 @@ class TestImageManager(TestCase, TestMixin): image = self.image_manager.get_image(full_path, 'church.jpg', 80, 80) # THEN: The return should be of type image - self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage') + assert isinstance(image, QtGui.QImage), 'The returned object should be a QImage' # WHEN: adding the same image with different dimensions self.image_manager.add_image(full_path, 'church.jpg', None, 100, 100) @@ -116,7 +116,7 @@ class TestImageManager(TestCase, TestMixin): # WHEN: calling with correct image, but wrong dimensions with self.assertRaises(KeyError) as context: self.image_manager.get_image(full_path, 'church.jpg', 120, 120) - self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing dimension') + assert context.exception is not '', 'KeyError exception should have been thrown for missing dimension' def test_process_cache(self): """ @@ -141,10 +141,8 @@ class TestImageManager(TestCase, TestMixin): # is being processed (see mocked methods/functions). # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the # # priority is adjusted to Priority.Lowest). - self.assertEqual(self.get_image_priority(image1), Priority.Normal, - "image1's priority should be 'Priority.Normal'") - self.assertEqual(self.get_image_priority(image2), Priority.Normal, - "image2's priority should be 'Priority.Normal'") + assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'" + assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'" # WHEN: Add more images. self.image_manager.add_image(TEST_PATH, image3, None) @@ -162,15 +160,15 @@ class TestImageManager(TestCase, TestMixin): # Because empty() is not reliable, wait a litte; just to make sure. time.sleep(0.1) # THEN: The images' priority reflect how they were processed. - self.assertEqual(self.image_manager._conversion_queue.qsize(), 0, "The queue should be empty.") - self.assertEqual(self.get_image_priority(image1), Priority.Lowest, - "The image should have not been requested (=Lowest)") - self.assertEqual(self.get_image_priority(image2), Priority.Lowest, - "The image should have not been requested (=Lowest)") - self.assertEqual(self.get_image_priority(image3), Priority.Low, - "Only the QImage should have been requested (=Low).") - self.assertEqual(self.get_image_priority(image4), Priority.Urgent, - "The image bytes should have been requested (=Urgent).") + assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty." + assert self.get_image_priority(image1) == Priority.Lowest, \ + "The image should have not been requested (=Lowest)" + assert self.get_image_priority(image2) == Priority.Lowest, \ + "The image should have not been requested (=Lowest)" + assert self.get_image_priority(image3) == Priority.Low, \ + "Only the QImage should have been requested (=Low)." + assert self.get_image_priority(image4) == Priority.Urgent, \ + "The image bytes should have been requested (=Urgent)." def get_image_priority(self, image): """ diff --git a/tests/functional/openlp_core/lib/test_lib.py b/tests/functional/openlp_core/lib/test_lib.py index f2bfaf79c..7cc44ec4e 100644 --- a/tests/functional/openlp_core/lib/test_lib.py +++ b/tests/functional/openlp_core/lib/test_lib.py @@ -49,8 +49,8 @@ class TestLib(TestCase): true_result = str_to_bool(true_boolean) # THEN: We should get back a True bool - self.assertIsInstance(true_result, bool, 'The result should be a boolean') - self.assertTrue(true_result, 'The result should be True') + assert isinstance(true_result, bool), 'The result should be a boolean' + assert true_result is True, 'The result should be True' def test_str_to_bool_with_bool_false(self): """ @@ -63,8 +63,8 @@ class TestLib(TestCase): false_result = str_to_bool(false_boolean) # THEN: We should get back a True bool - self.assertIsInstance(false_result, bool, 'The result should be a boolean') - self.assertFalse(false_result, 'The result should be True') + assert isinstance(false_result, bool), 'The result should be a boolean' + assert false_result is False, 'The result should be True' def test_str_to_bool_with_integer(self): """ @@ -77,7 +77,7 @@ class TestLib(TestCase): int_result = str_to_bool(int_string) # THEN: we should get back a false - self.assertFalse(int_result, 'The result should be False') + assert int_result is False, 'The result should be False' def test_str_to_bool_with_invalid_string(self): """ @@ -90,7 +90,7 @@ class TestLib(TestCase): str_result = str_to_bool(invalid_string) # THEN: we should get back a false - self.assertFalse(str_result, 'The result should be False') + assert str_result is False, 'The result should be False' def test_str_to_bool_with_string_false(self): """ @@ -103,7 +103,7 @@ class TestLib(TestCase): false_result = str_to_bool(false_string) # THEN: we should get back a false - self.assertFalse(false_result, 'The result should be False') + assert false_result is False, 'The result should be False' def test_str_to_bool_with_string_no(self): """ @@ -116,7 +116,7 @@ class TestLib(TestCase): str_result = str_to_bool(no_string) # THEN: we should get back a false - self.assertFalse(str_result, 'The result should be False') + assert str_result is False, 'The result should be False' def test_str_to_bool_with_true_string_value(self): """ @@ -129,7 +129,7 @@ class TestLib(TestCase): true_result = str_to_bool(true_string) # THEN: we should get back a true - self.assertTrue(true_result, 'The result should be True') + assert true_result is True, 'The result should be True' def test_str_to_bool_with_yes_string_value(self): """ @@ -142,7 +142,7 @@ class TestLib(TestCase): str_result = str_to_bool(yes_string) # THEN: we should get back a true - self.assertTrue(str_result, 'The result should be True') + assert str_result is True, 'The result should be True' def test_get_text_file_string_no_file(self): """ @@ -157,7 +157,7 @@ class TestLib(TestCase): # THEN: The result should be False file_path.is_file.assert_called_with() - self.assertFalse(result, 'False should be returned if no file exists') + assert result is False, 'False should be returned if no file exists' def test_get_text_file_string_read_error(self): """ @@ -168,7 +168,7 @@ class TestLib(TestCase): patch.object(Path, 'open'): file_path = Path('testfile.txt') file_path.is_file.return_value = True - file_path.open.side_effect = IOError() + file_path.open.side_effect = OSError() # WHEN: get_text_file_string is called result = get_text_file_string(file_path) @@ -176,7 +176,7 @@ class TestLib(TestCase): # THEN: None should be returned file_path.is_file.assert_called_once_with() file_path.open.assert_called_once_with('r', encoding='utf-8') - self.assertIsNone(result, 'None should be returned if the file cannot be opened') + assert result is None, 'None should be returned if the file cannot be opened' def test_get_text_file_string_decode_error(self): """ @@ -195,7 +195,7 @@ class TestLib(TestCase): result = build_icon(icon) # THEN: The result should be the same icon as we passed in - self.assertIs(icon, result, 'The result should be the same icon as we passed in') + assert icon is result, 'The result should be the same icon as we passed in' def test_build_icon_with_resource(self): """ @@ -217,7 +217,7 @@ class TestLib(TestCase): MockedQPixmap.assert_called_with(resource_uri) # There really should be more assert statements here but due to type checking and things they all break. The # best we can do is to assert that we get back a MagicMock object. - self.assertIsInstance(result, MagicMock, 'The result should be a MagicMock, because we mocked it out') + assert isinstance(result, MagicMock), 'The result should be a MagicMock, because we mocked it out' def test_image_to_byte(self): """ @@ -240,8 +240,8 @@ class TestLib(TestCase): MockedQtCore.QBuffer.assert_called_with(mocked_byte_array) mocked_buffer.open.assert_called_with('writeonly') mocked_image.save.assert_called_with(mocked_buffer, "PNG") - self.assertFalse(mocked_byte_array.toBase64.called) - self.assertEqual(mocked_byte_array, result, 'The mocked out byte array should be returned') + assert mocked_byte_array.toBase64.called is False + assert mocked_byte_array == result, 'The mocked out byte array should be returned' def test_image_to_byte_base_64(self): """ @@ -266,40 +266,39 @@ class TestLib(TestCase): mocked_buffer.open.assert_called_with('writeonly') mocked_image.save.assert_called_with(mocked_buffer, "PNG") mocked_byte_array.toBase64.assert_called_with() - self.assertEqual('base64mock', result, 'The result should be the return value of the mocked out ' - 'base64 method') + assert 'base64mock' == result, 'The result should be the return value of the mocked out base64 method' def test_create_thumb_with_size(self): """ Test the create_thumb() function with a given size. """ # GIVEN: An image to create a thumb of. - image_path = os.path.join(TEST_PATH, 'church.jpg') - thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg') + image_path = Path(TEST_PATH, 'church.jpg') + thumb_path = Path(TEST_PATH, 'church_thumb.jpg') thumb_size = QtCore.QSize(10, 20) # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the # last test. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass # Only continue when the thumb does not exist. - self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.') + assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.' # WHEN: Create the thumb. icon = create_thumb(image_path, thumb_path, size=thumb_size) # THEN: Check if the thumb was created and scaled to the given size. - self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists') - self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon') - self.assertFalse(icon.isNull(), 'The icon should not be null') - self.assertEqual(thumb_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size') + self.assertTrue(thumb_path.exists(), 'Test was not ran, because the thumb already exists') + assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon' + assert icon.isNull() is False, 'The icon should not be null' + assert thumb_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size' # Remove the thumb so that the test actually tests if the thumb will be created. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass @@ -308,32 +307,32 @@ class TestLib(TestCase): Test the create_thumb() function with no size specified. """ # GIVEN: An image to create a thumb of. - image_path = os.path.join(TEST_PATH, 'church.jpg') - thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg') + image_path = Path(TEST_PATH, 'church.jpg') + thumb_path = Path(TEST_PATH, 'church_thumb.jpg') expected_size = QtCore.QSize(63, 88) # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the # last test. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass # Only continue when the thumb does not exist. - self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.') + assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.' # WHEN: Create the thumb. icon = create_thumb(image_path, thumb_path) # THEN: Check if the thumb was created, retaining its aspect ratio. - self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists') - self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon') - self.assertFalse(icon.isNull(), 'The icon should not be null') - self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size') + self.assertTrue(thumb_path.exists(), 'Test was not ran, because the thumb already exists') + assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon' + assert icon.isNull() is False, 'The icon should not be null' + assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size' # Remove the thumb so that the test actually tests if the thumb will be created. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass @@ -342,33 +341,33 @@ class TestLib(TestCase): Test the create_thumb() function with invalid size specified. """ # GIVEN: An image to create a thumb of. - image_path = os.path.join(TEST_PATH, 'church.jpg') - thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg') + image_path = Path(TEST_PATH, 'church.jpg') + thumb_path = Path(TEST_PATH, 'church_thumb.jpg') thumb_size = QtCore.QSize(-1, -1) expected_size = QtCore.QSize(63, 88) # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the # last test. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass # Only continue when the thumb does not exist. - self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.') + assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.' # WHEN: Create the thumb. icon = create_thumb(image_path, thumb_path, size=thumb_size) # THEN: Check if the thumb was created, retaining its aspect ratio. - self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists') - self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon') - self.assertFalse(icon.isNull(), 'The icon should not be null') - self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size') + assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists' + assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon' + assert icon.isNull() is False, 'The icon should not be null' + assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size' # Remove the thumb so that the test actually tests if the thumb will be created. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass @@ -377,33 +376,33 @@ class TestLib(TestCase): Test the create_thumb() function with a size of only width specified. """ # GIVEN: An image to create a thumb of. - image_path = os.path.join(TEST_PATH, 'church.jpg') - thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg') + image_path = Path(TEST_PATH, 'church.jpg') + thumb_path = Path(TEST_PATH, 'church_thumb.jpg') thumb_size = QtCore.QSize(100, -1) expected_size = QtCore.QSize(100, 137) # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the # last test. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass # Only continue when the thumb does not exist. - self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.') + assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.' # WHEN: Create the thumb. icon = create_thumb(image_path, thumb_path, size=thumb_size) # THEN: Check if the thumb was created, retaining its aspect ratio. - self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists') - self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon') - self.assertFalse(icon.isNull(), 'The icon should not be null') - self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size') + assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists' + assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon' + assert icon.isNull() is False, 'The icon should not be null' + assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size' # Remove the thumb so that the test actually tests if the thumb will be created. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass @@ -412,33 +411,33 @@ class TestLib(TestCase): Test the create_thumb() function with a size of only height specified. """ # GIVEN: An image to create a thumb of. - image_path = os.path.join(TEST_PATH, 'church.jpg') - thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg') + image_path = Path(TEST_PATH, 'church.jpg') + thumb_path = Path(TEST_PATH, 'church_thumb.jpg') thumb_size = QtCore.QSize(-1, 100) expected_size = QtCore.QSize(72, 100) # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the # last test. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass # Only continue when the thumb does not exist. - self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.') + assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.' # WHEN: Create the thumb. icon = create_thumb(image_path, thumb_path, size=thumb_size) # THEN: Check if the thumb was created, retaining its aspect ratio. - self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists') - self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon') - self.assertFalse(icon.isNull(), 'The icon should not be null') - self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size') + self.assertTrue(thumb_path.exists(), 'Test was not ran, because the thumb already exists') + assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon' + assert icon.isNull() is False, 'The icon should not be null' + assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size' # Remove the thumb so that the test actually tests if the thumb will be created. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass @@ -447,8 +446,8 @@ class TestLib(TestCase): Test the create_thumb() function with a size of only height specified. """ # GIVEN: An image to create a thumb of. - image_path = os.path.join(TEST_PATH, 'church.jpg') - thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg') + image_path = Path(TEST_PATH, 'church.jpg') + thumb_path = Path(TEST_PATH, 'church_thumb.jpg') thumb_size = QtCore.QSize(-1, 100) expected_size_1 = QtCore.QSize(88, 88) expected_size_2 = QtCore.QSize(100, 100) @@ -456,12 +455,12 @@ class TestLib(TestCase): # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the # last test. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass # Only continue when the thumb does not exist. - self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.') + assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.' # WHEN: Create the thumb. with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size: @@ -469,10 +468,10 @@ class TestLib(TestCase): icon = create_thumb(image_path, thumb_path, size=None) # THEN: Check if the thumb was created with aspect ratio of 1. - self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists') - self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon') - self.assertFalse(icon.isNull(), 'The icon should not be null') - self.assertEqual(expected_size_1, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size') + self.assertTrue(thumb_path.exists(), 'Test was not ran, because the thumb already exists') + assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon' + assert icon.isNull() is False, 'The icon should not be null' + assert expected_size_1 == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size' # WHEN: Create the thumb. with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size: @@ -480,13 +479,13 @@ class TestLib(TestCase): icon = create_thumb(image_path, thumb_path, size=thumb_size) # THEN: Check if the thumb was created with aspect ratio of 1. - self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon') - self.assertFalse(icon.isNull(), 'The icon should not be null') - self.assertEqual(expected_size_2, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size') + assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon' + assert icon.isNull() is False, 'The icon should not be null' + assert expected_size_2 == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size' # Remove the thumb so that the test actually tests if the thumb will be created. try: - os.remove(thumb_path) + thumb_path.unlink() except: pass @@ -505,7 +504,7 @@ class TestLib(TestCase): # THEN: The selectedIndexes function should have been called and the result should be true mocked_list_widget.selectedIndexes.assert_called_with() - self.assertTrue(result, 'The result should be True') + assert result is True, 'The result should be True' def test_check_item_selected_false(self): """ @@ -526,7 +525,7 @@ class TestLib(TestCase): # THEN: The selectedIndexes function should have been called and the result should be true mocked_list_widget.selectedIndexes.assert_called_with() MockedQtWidgets.QMessageBox.information.assert_called_with('parent', 'mocked translate', 'message') - self.assertFalse(result, 'The result should be False') + assert result is False, 'The result should be False' def test_clean_tags(self): """ @@ -548,7 +547,7 @@ class TestLib(TestCase): result_string = clean_tags(string_to_pass) # THEN: The strings should be identical. - self.assertEqual(wanted_string, result_string, 'The strings should be identical') + assert wanted_string == result_string, 'The strings should be identical' def test_expand_tags(self): """ @@ -587,7 +586,7 @@ class TestLib(TestCase): result_string = expand_tags(string_to_pass) # THEN: The strings should be identical. - self.assertEqual(wanted_string, result_string, 'The strings should be identical.') + assert wanted_string == result_string, 'The strings should be identical.' def test_validate_thumb_file_does_not_exist(self): """ @@ -603,7 +602,7 @@ class TestLib(TestCase): # THEN: we should have called a few functions, and the result should be False thumb_path.exists.assert_called_once_with() - self.assertFalse(result, 'The result should be False') + assert result is False, 'The result should be False' def test_validate_thumb_file_exists_and_newer(self): """ @@ -618,7 +617,7 @@ class TestLib(TestCase): result = validate_thumb(file_path, thumb_path) # THEN: `validate_thumb` should return True - self.assertTrue(result) + assert result is True def test_validate_thumb_file_exists_and_older(self): """ @@ -633,7 +632,7 @@ class TestLib(TestCase): # THEN: `validate_thumb` should return False thumb_path.stat.assert_called_once_with() - self.assertFalse(result, 'The result should be False') + assert result is False, 'The result should be False' def test_resize_thumb(self): """ @@ -652,9 +651,9 @@ class TestLib(TestCase): # THEN: Check if the size is correct and the background was set. result_size = image.size() - self.assertEqual(wanted_height, result_size.height(), 'The image should have the requested height.') - self.assertEqual(wanted_width, result_size.width(), 'The image should have the requested width.') - self.assertEqual(image.pixel(0, 0), wanted_background_rgb, 'The background should be white.') + assert wanted_height == result_size.height(), 'The image should have the requested height.' + assert wanted_width == result_size.width(), 'The image should have the requested width.' + assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.' def test_resize_thumb_ignoring_aspect_ratio(self): """ @@ -673,9 +672,9 @@ class TestLib(TestCase): # THEN: Check if the size is correct and the background was set. result_size = image.size() - self.assertEqual(wanted_height, result_size.height(), 'The image should have the requested height.') - self.assertEqual(wanted_width, result_size.width(), 'The image should have the requested width.') - self.assertEqual(image.pixel(0, 0), wanted_background_rgb, 'The background should be white.') + assert wanted_height == result_size.height(), 'The image should have the requested height.' + assert wanted_width == result_size.width(), 'The image should have the requested width.' + assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.' @patch('openlp.core.lib.QtCore.QLocale.createSeparatedList') def test_create_separated_list_qlocate(self, mocked_createSeparatedList): @@ -690,8 +689,8 @@ class TestLib(TestCase): string_result = create_separated_list(string_list) # THEN: We should have "Author 1, Author 2, and Author 3" - self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, ' - 'Author 2, and Author 3".') + assert string_result == 'Author 1, Author 2 and Author 3', \ + 'The string should be "Author 1, Author 2, and Author 3".' def test_create_separated_list_empty_list(self): """ @@ -704,7 +703,7 @@ class TestLib(TestCase): string_result = create_separated_list(string_list) # THEN: We shoud have an emptry string. - self.assertEqual(string_result, '', 'The string sould be empty.') + assert string_result == '', 'The string sould be empty.' def test_create_separated_list_with_one_item(self): """ @@ -717,7 +716,7 @@ class TestLib(TestCase): string_result = create_separated_list(string_list) # THEN: We should have "Author 1" - self.assertEqual(string_result, 'Author 1', 'The string should be "Author 1".') + assert string_result == 'Author 1', 'The string should be "Author 1".' def test_create_separated_list_with_two_items(self): """ @@ -730,7 +729,7 @@ class TestLib(TestCase): string_result = create_separated_list(string_list) # THEN: We should have "Author 1 and Author 2" - self.assertEqual(string_result, 'Author 1 and Author 2', 'The string should be "Author 1 and Author 2".') + assert string_result == 'Author 1 and Author 2', 'The string should be "Author 1 and Author 2".' def test_create_separated_list_with_three_items(self): """ @@ -743,8 +742,8 @@ class TestLib(TestCase): string_result = create_separated_list(string_list) # THEN: We should have "Author 1, Author 2 and Author 3" - self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, ' - 'Author 2, and Author 3".') + assert string_result == 'Author 1, Author 2 and Author 3', \ + 'The string should be "Author 1, Author 2, and Author 3".' def test_expand_chords(self): """ @@ -760,7 +759,7 @@ class TestLib(TestCase): expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \ '</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \ '  </span> <span class="chord"><span><strong>G</strong></span></span></span>' - self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!') + assert expected_html == text_with_expanded_chords, 'The expanded chords should look as expected!' def test_expand_chords2(self): """ @@ -776,7 +775,7 @@ class TestLib(TestCase): expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \ '</span>'M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \ '</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>' - self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!') + assert expected_html == text_with_expanded_chords, 'The expanded chords should look as expected!' def test_compare_chord_lyric_short_chord(self): """ @@ -804,7 +803,7 @@ class TestLib(TestCase): ret = compare_chord_lyric(chord, lyrics) # THEN: The returned value should 4 because the chord is longer than the lyric - self.assertEquals(4, ret, 'The returned value should 4 because the chord is longer than the lyric') + assert 4 == ret, 'The returned value should 4 because the chord is longer than the lyric' def test_find_formatting_tags(self): """ @@ -819,7 +818,7 @@ class TestLib(TestCase): active_tags = find_formatting_tags(lyrics, tags) # THEN: The list of active tags should contain only 'st' - self.assertListEqual(['st'], active_tags, 'The list of active tags should contain only "st"') + assert ['st'] == active_tags, 'The list of active tags should contain only "st"' def test_expand_chords_for_printing(self): """ @@ -856,4 +855,4 @@ class TestLib(TestCase): '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \ 'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st} </td>' \ '</tr></table></td></tr></table>' - self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!') + assert expected_html == text_with_expanded_chords, 'The expanded chords should look as expected!' diff --git a/tests/functional/openlp_core/lib/test_mediamanageritem.py b/tests/functional/openlp_core/lib/test_mediamanageritem.py index fba8ce36b..6c659a5bc 100644 --- a/tests/functional/openlp_core/lib/test_mediamanageritem.py +++ b/tests/functional/openlp_core/lib/test_mediamanageritem.py @@ -42,8 +42,8 @@ class TestMediaManagerItem(TestCase, TestMixin): self.mocked_setup = self.setup_patcher.start() self.addCleanup(self.setup_patcher.stop) - @patch(u'openlp.core.lib.mediamanageritem.Settings') - @patch(u'openlp.core.lib.mediamanageritem.MediaManagerItem.on_preview_click') + @patch('openlp.core.lib.mediamanageritem.Settings') + @patch('openlp.core.lib.mediamanageritem.MediaManagerItem.on_preview_click') def test_on_double_clicked(self, mocked_on_preview_click, MockedSettings): """ Test that when an item is double-clicked then the item is previewed @@ -69,14 +69,14 @@ class TestMediaManagerItem(TestCase, TestMixin): # WHEN: Object is created mmi.required_icons() # THEN: Default icons should be populated - self.assertFalse(mmi.has_import_icon, 'There should be no import icon by default') - self.assertTrue(mmi.has_new_icon, 'By default a new icon should be present') - self.assertFalse(mmi.has_file_icon, 'There should be no file icon by default') - self.assertTrue(mmi.has_delete_icon, 'By default a delete icon should be present') - self.assertFalse(mmi.add_to_service_item, 'There should be no add_to_service icon by default') + assert mmi.has_import_icon is False, 'There should be no import icon by default' + assert mmi.has_new_icon is True, 'By default a new icon should be present' + assert mmi.has_file_icon is False, 'There should be no file icon by default' + assert mmi.has_delete_icon is True, 'By default a delete icon should be present' + assert mmi.add_to_service_item is False, 'There should be no add_to_service icon by default' - @patch(u'openlp.core.lib.mediamanageritem.Settings') - @patch(u'openlp.core.lib.mediamanageritem.MediaManagerItem.on_live_click') + @patch('openlp.core.lib.mediamanageritem.Settings') + @patch('openlp.core.lib.mediamanageritem.MediaManagerItem.on_live_click') def test_on_double_clicked_go_live(self, mocked_on_live_click, MockedSettings): """ Test that when "Double-click to go live" is enabled that the item goes live @@ -93,9 +93,9 @@ class TestMediaManagerItem(TestCase, TestMixin): # THEN: on_live_click() should have been called mocked_on_live_click.assert_called_with() - @patch(u'openlp.core.lib.mediamanageritem.Settings') - @patch(u'openlp.core.lib.mediamanageritem.MediaManagerItem.on_live_click') - @patch(u'openlp.core.lib.mediamanageritem.MediaManagerItem.on_preview_click') + @patch('openlp.core.lib.mediamanageritem.Settings') + @patch('openlp.core.lib.mediamanageritem.MediaManagerItem.on_live_click') + @patch('openlp.core.lib.mediamanageritem.MediaManagerItem.on_preview_click') def test_on_double_clicked_single_click_preview(self, mocked_on_preview_click, mocked_on_live_click, MockedSettings): """ @@ -111,5 +111,5 @@ class TestMediaManagerItem(TestCase, TestMixin): mmi.on_double_clicked() # THEN: on_live_click() should have been called - self.assertEqual(0, mocked_on_live_click.call_count, u'on_live_click() should not have been called') - self.assertEqual(0, mocked_on_preview_click.call_count, u'on_preview_click() should not have been called') + assert 0 == mocked_on_live_click.call_count, 'on_live_click() should not have been called' + assert 0 == mocked_on_preview_click.call_count, 'on_preview_click() should not have been called' diff --git a/tests/functional/openlp_core/lib/test_pluginmanager.py b/tests/functional/openlp_core/lib/test_pluginmanager.py index 52e9d91bf..163f52877 100644 --- a/tests/functional/openlp_core/lib/test_pluginmanager.py +++ b/tests/functional/openlp_core/lib/test_pluginmanager.py @@ -64,8 +64,8 @@ class TestPluginManager(TestCase): plugin_manager.hook_media_manager() # THEN: The create_media_manager_item() method should have been called - self.assertEqual(0, mocked_plugin.create_media_manager_item.call_count, - 'The create_media_manager_item() method should not have been called.') + assert 0 == mocked_plugin.create_media_manager_item.call_count, \ + 'The create_media_manager_item() method should not have been called.' def test_hook_media_manager_with_active_plugin(self): """ @@ -97,8 +97,8 @@ class TestPluginManager(TestCase): plugin_manager.hook_settings_tabs() # THEN: The hook_settings_tabs() method should have been called - self.assertEqual(0, mocked_plugin.create_media_manager_item.call_count, - 'The create_media_manager_item() method should not have been called.') + assert 0 == mocked_plugin.create_media_manager_item.call_count, \ + 'The create_media_manager_item() method should not have been called.' def test_hook_settings_tabs_with_disabled_plugin_and_mocked_form(self): """ @@ -117,10 +117,10 @@ class TestPluginManager(TestCase): plugin_manager.hook_settings_tabs() # THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same - self.assertEqual(0, mocked_plugin.create_settings_tab.call_count, - 'The create_media_manager_item() method should not have been called.') - self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins, - 'The plugins on the settings form should be the same as the plugins in the plugin manager') + assert 0 == mocked_plugin.create_settings_tab.call_count, \ + 'The create_media_manager_item() method should not have been called.' + assert mocked_settings_form.plugin_manager.plugins == plugin_manager.plugins, \ + 'The plugins on the settings form should be the same as the plugins in the plugin manager' def test_hook_settings_tabs_with_active_plugin_and_mocked_form(self): """ @@ -139,10 +139,10 @@ class TestPluginManager(TestCase): plugin_manager.hook_settings_tabs() # THEN: The create_media_manager_item() method should have been called with the mocked settings form - self.assertEqual(1, mocked_plugin.create_settings_tab.call_count, - 'The create_media_manager_item() method should have been called once.') - self.assertEqual(plugin_manager.plugins, mocked_settings_form.plugin_manager.plugins, - 'The plugins on the settings form should be the same as the plugins in the plugin manager') + assert 1 == mocked_plugin.create_settings_tab.call_count, \ + 'The create_media_manager_item() method should have been called once.' + assert plugin_manager.plugins == mocked_settings_form.plugin_manager.plugins, \ + 'The plugins on the settings form should be the same as the plugins in the plugin manager' def test_hook_settings_tabs_with_active_plugin_and_no_form(self): """ @@ -174,8 +174,8 @@ class TestPluginManager(TestCase): plugin_manager.hook_import_menu() # THEN: The create_media_manager_item() method should have been called - self.assertEqual(0, mocked_plugin.add_import_menu_item.call_count, - 'The add_import_menu_item() method should not have been called.') + assert 0 == mocked_plugin.add_import_menu_item.call_count, \ + 'The add_import_menu_item() method should not have been called.' def test_hook_import_menu_with_active_plugin(self): """ @@ -207,8 +207,8 @@ class TestPluginManager(TestCase): plugin_manager.hook_export_menu() # THEN: The add_export_menu_item() method should not have been called - self.assertEqual(0, mocked_plugin.add_export_menu_item.call_count, - 'The add_export_menu_item() method should not have been called.') + assert 0 == mocked_plugin.add_export_menu_item.call_count, \ + 'The add_export_menu_item() method should not have been called.' def test_hook_export_menu_with_active_plugin(self): """ @@ -241,8 +241,8 @@ class TestPluginManager(TestCase): plugin_manager.hook_upgrade_plugin_settings(settings) # THEN: The upgrade_settings() method should not have been called - self.assertEqual(0, mocked_plugin.upgrade_settings.call_count, - 'The upgrade_settings() method should not have been called.') + assert 0 == mocked_plugin.upgrade_settings.call_count, \ + 'The upgrade_settings() method should not have been called.' def test_hook_upgrade_plugin_settings_with_active_plugin(self): """ @@ -275,8 +275,8 @@ class TestPluginManager(TestCase): plugin_manager.hook_tools_menu() # THEN: The add_tools_menu_item() method should have been called - self.assertEqual(0, mocked_plugin.add_tools_menu_item.call_count, - 'The add_tools_menu_item() method should not have been called.') + assert 0 == mocked_plugin.add_tools_menu_item.call_count, \ + 'The add_tools_menu_item() method should not have been called.' def test_hook_tools_menu_with_active_plugin(self): """ @@ -310,7 +310,7 @@ class TestPluginManager(TestCase): # THEN: The is_active() method should have been called, and initialise() method should NOT have been called mocked_plugin.is_active.assert_called_with() - self.assertEqual(0, mocked_plugin.initialise.call_count, 'The initialise() method should not have been called.') + assert 0 == mocked_plugin.initialise.call_count, 'The initialise() method should not have been called.' def test_initialise_plugins_with_active_plugin(self): """ @@ -346,7 +346,7 @@ class TestPluginManager(TestCase): # THEN: The is_active() method should have been called, and initialise() method should NOT have been called mocked_plugin.is_active.assert_called_with() - self.assertEqual(0, mocked_plugin.finalise.call_count, 'The finalise() method should not have been called.') + assert 0 == mocked_plugin.finalise.call_count, 'The finalise() method should not have been called.' def test_finalise_plugins_with_active_plugin(self): """ @@ -380,7 +380,7 @@ class TestPluginManager(TestCase): result = plugin_manager.get_plugin_by_name('Missing Plugin') # THEN: The is_active() and finalise() methods should have been called - self.assertIsNone(result, 'The result for get_plugin_by_name should be None') + assert result is None, 'The result for get_plugin_by_name should be None' def test_get_plugin_by_name_exists(self): """ @@ -396,7 +396,7 @@ class TestPluginManager(TestCase): result = plugin_manager.get_plugin_by_name('Mocked Plugin') # THEN: The is_active() and finalise() methods should have been called - self.assertEqual(result, mocked_plugin, 'The result for get_plugin_by_name should be the mocked plugin') + assert result == mocked_plugin, 'The result for get_plugin_by_name should be the mocked plugin' def test_new_service_created_with_disabled_plugin(self): """ @@ -414,8 +414,8 @@ class TestPluginManager(TestCase): # THEN: The isActive() method should have been called, and initialise() method should NOT have been called mocked_plugin.is_active.assert_called_with() - self.assertEqual(0, mocked_plugin.new_service_created.call_count, - 'The new_service_created() method should not have been called.') + assert 0 == mocked_plugin.new_service_created.call_count, \ + 'The new_service_created() method should not have been called.' def test_new_service_created_with_active_plugin(self): """ diff --git a/tests/functional/openlp_core/lib/test_projector_pjlink_base.py b/tests/functional/openlp_core/lib/test_projector_pjlink_base.py deleted file mode 100644 index 578f37ede..000000000 --- a/tests/functional/openlp_core/lib/test_projector_pjlink_base.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2015 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Package to test the openlp.core.lib.projector.pjlink base package. -""" -from unittest import TestCase -from unittest.mock import call, patch, MagicMock - -from openlp.core.lib.projector.db import Projector -from openlp.core.lib.projector.pjlink import PJLink -from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_ON, S_CONNECTED - -from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH, TEST1_DATA - -pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True) - - -class TestPJLinkBase(TestCase): - """ - Tests for the PJLink module - """ - @patch.object(pjlink_test, 'readyRead') - @patch.object(pjlink_test, 'send_command') - @patch.object(pjlink_test, 'waitForReadyRead') - @patch('openlp.core.common.qmd5_hash') - def test_authenticated_connection_call(self, - mock_qmd5_hash, - mock_waitForReadyRead, - mock_send_command, - mock_readyRead): - """ - Ticket 92187: Fix for projector connect with PJLink authentication exception. - """ - # GIVEN: Test object - pjlink = pjlink_test - - # WHEN: Calling check_login with authentication request: - pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE) - - # THEN: Should have called qmd5_hash - self.assertTrue(mock_qmd5_hash.called_with(TEST_SALT, - "Connection request should have been called with TEST_SALT")) - self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN, - "Connection request should have been called with TEST_PIN")) - - @patch.object(pjlink_test, 'change_status') - def test_status_change(self, mock_change_status): - """ - Test process_command call with ERR2 (Parameter) status - """ - # GIVEN: Test object - pjlink = pjlink_test - - # WHEN: process_command is called with "ERR2" status from projector - pjlink.process_command('POWR', 'ERR2') - - # THEN: change_status should have called change_status with E_UNDEFINED - # as first parameter - mock_change_status.called_with(E_PARAMETER, - 'change_status should have been called with "{}"'.format( - ERROR_STRING[E_PARAMETER])) - - @patch.object(pjlink_test, 'send_command') - @patch.object(pjlink_test, 'waitForReadyRead') - @patch.object(pjlink_test, 'projectorAuthentication') - @patch.object(pjlink_test, 'timer') - @patch.object(pjlink_test, 'socket_timer') - def test_bug_1593882_no_pin_authenticated_connection(self, - mock_socket_timer, - mock_timer, - mock_authentication, - mock_ready_read, - mock_send_command): - """ - Test bug 1593882 no pin and authenticated request exception - """ - # GIVEN: Test object and mocks - pjlink = pjlink_test - pjlink.pin = None - mock_ready_read.return_value = True - - # WHEN: call with authentication request and pin not set - pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE) - - # THEN: 'No Authentication' signal should have been sent - mock_authentication.emit.assert_called_with(pjlink.ip) - - @patch.object(pjlink_test, 'waitForReadyRead') - @patch.object(pjlink_test, 'state') - @patch.object(pjlink_test, '_send_command') - @patch.object(pjlink_test, 'timer') - @patch.object(pjlink_test, 'socket_timer') - def test_bug_1593883_pjlink_authentication(self, - mock_socket_timer, - mock_timer, - mock_send_command, - mock_state, - mock_waitForReadyRead): - """ - Test bugfix 1593883 pjlink authentication - """ - # GIVEN: Test object and data - pjlink = pjlink_test - pjlink.pin = TEST_PIN - mock_state.return_value = pjlink.ConnectedState - mock_waitForReadyRead.return_value = True - - # WHEN: Athenticated connection is called - pjlink.check_login(data=TEST_CONNECT_AUTHENTICATE) - - # THEN: send_command should have the proper authentication - self.assertEqual("{test}".format(test=mock_send_command.call_args), - "call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH)) - - @patch.object(pjlink_test, 'disconnect_from_host') - def test_socket_abort(self, mock_disconnect): - """ - Test PJLink.socket_abort calls disconnect_from_host - """ - # GIVEN: Test object - pjlink = pjlink_test - - # WHEN: Calling socket_abort - pjlink.socket_abort() - - # THEN: disconnect_from_host should be called - self.assertTrue(mock_disconnect.called, 'Should have called disconnect_from_host') - - def test_poll_loop_not_connected(self): - """ - Test PJLink.poll_loop not connected return - """ - # GIVEN: Test object and mocks - pjlink = pjlink_test - pjlink.state = MagicMock() - pjlink.timer = MagicMock() - pjlink.state.return_value = False - pjlink.ConnectedState = True - - # WHEN: PJLink.poll_loop called - pjlink.poll_loop() - - # THEN: poll_loop should exit without calling any other method - self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method') - - @patch.object(pjlink_test, 'send_command') - def test_poll_loop_start(self, mock_send_command): - """ - Test PJLink.poll_loop makes correct calls - """ - # GIVEN: test object and test data - pjlink = pjlink_test - pjlink.state = MagicMock() - pjlink.timer = MagicMock() - pjlink.timer.interval = MagicMock() - pjlink.timer.setInterval = MagicMock() - pjlink.timer.start = MagicMock() - pjlink.poll_time = 20 - pjlink.power = S_ON - pjlink.source_available = None - pjlink.other_info = None - pjlink.manufacturer = None - pjlink.model = None - pjlink.pjlink_name = None - pjlink.ConnectedState = S_CONNECTED - pjlink.timer.interval.return_value = 10 - pjlink.state.return_value = S_CONNECTED - call_list = [ - call('POWR', queue=True), - call('ERST', queue=True), - call('LAMP', queue=True), - call('AVMT', queue=True), - call('INPT', queue=True), - call('INST', queue=True), - call('INFO', queue=True), - call('INF1', queue=True), - call('INF2', queue=True), - call('NAME', queue=True), - ] - - # WHEN: PJLink.poll_loop is called - pjlink.poll_loop() - - # THEN: proper calls were made to retrieve projector data - # First, call to update the timer with the next interval - self.assertTrue(pjlink.timer.setInterval.called, 'Should have updated the timer') - # Next, should have called the timer to start - self.assertTrue(pjlink.timer.start.called, 'Should have started the timer') - # Finally, should have called send_command with a list of projetctor status checks - mock_send_command.assert_has_calls(call_list, 'Should have queued projector queries') diff --git a/tests/functional/openlp_core/lib/test_serviceitem.py b/tests/functional/openlp_core/lib/test_serviceitem.py index 78ad018fb..d069f18bf 100644 --- a/tests/functional/openlp_core/lib/test_serviceitem.py +++ b/tests/functional/openlp_core/lib/test_serviceitem.py @@ -28,7 +28,9 @@ from unittest.mock import MagicMock, patch from openlp.core.common import md5_hash from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType, FormattingTags +from tests.helpers.testmixin import TestMixin from tests.utils import assert_length, convert_file_service_item @@ -59,19 +61,31 @@ RENDERED_VERSE = 'The Lord said to <span style="-webkit-text-fill-color:red">Noa FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456'] TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'service')) +__default_settings__ = { + 'songs/enable chords': True, +} -class TestServiceItem(TestCase): + +class TestServiceItem(TestCase, TestMixin): def setUp(self): """ Set up the Registry """ + self.build_settings() + Settings().extend_default_settings(__default_settings__) Registry.create() mocked_renderer = MagicMock() mocked_renderer.format_slide.return_value = [VERSE] Registry().register('renderer', mocked_renderer) Registry().register('image_manager', MagicMock()) + def tearDown(self): + """ + Clean up + """ + self.destroy_settings() + def test_service_item_basic(self): """ Test the Service Item - basic test @@ -82,8 +96,8 @@ class TestServiceItem(TestCase): service_item = ServiceItem(None) # THEN: We should get back a valid service item - self.assertTrue(service_item.is_valid, 'The new service item should be valid') - self.assertTrue(service_item.missing_frames(), 'There should not be any frames in the service item') + assert service_item.is_valid is True, 'The new service item should be valid' + assert service_item.missing_frames() is True, 'There should not be any frames in the service item' def test_service_item_load_custom_from_service(self): """ @@ -99,7 +113,7 @@ class TestServiceItem(TestCase): service_item.set_from_service(line) # THEN: We should get back a valid service item - self.assertTrue(service_item.is_valid, 'The new service item should be valid') + assert service_item.is_valid is True, 'The new service item should be valid' assert_length(0, service_item._display_frames, 'The service item should have no display frames') assert_length(5, service_item.capabilities, 'There should be 5 default custom item capabilities') @@ -107,14 +121,14 @@ class TestServiceItem(TestCase): service_item.render(True) # THEN: The frames should also be valid - self.assertEqual('Test Custom', service_item.get_display_title(), 'The title should be "Test Custom"') - self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'], - 'The returned text matches the input, except the last line feed') - self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1), - 'The first line has been returned') - self.assertEqual('Slide 1', service_item.get_frame_title(0), '"Slide 1" has been returned as the title') - self.assertEqual('Slide 2', service_item.get_frame_title(1), '"Slide 2" has been returned as the title') - self.assertEqual('', service_item.get_frame_title(2), 'Blank has been returned as the title of slide 3') + assert 'Test Custom' == service_item.get_display_title(), 'The title should be "Test Custom"' + assert CLEANED_VERSE[:-1] == service_item.get_frames()[0]['text'], \ + 'The returned text matches the input, except the last line feed' + assert RENDERED_VERSE.split('\n', 1)[0] == service_item.get_rendered_frame(1), \ + 'The first line has been returned' + assert 'Slide 1' == service_item.get_frame_title(0), '"Slide 1" has been returned as the title' + assert 'Slide 2' == service_item.get_frame_title(1), '"Slide 2" has been returned as the title' + assert '' == service_item.get_frame_title(2), 'Blank has been returned as the title of slide 3' def test_service_item_load_image_from_service(self): """ @@ -138,26 +152,22 @@ class TestServiceItem(TestCase): service_item.set_from_service(line, TEST_PATH) # THEN: We should get back a valid service item - self.assertTrue(service_item.is_valid, 'The new service item should be valid') - self.assertEqual(os.path.normpath(test_file), os.path.normpath(service_item.get_rendered_frame(0)), - 'The first frame should match the path to the image') - self.assertEqual(frame_array, service_item.get_frames()[0], - 'The return should match frame array1') - self.assertEqual(test_file, service_item.get_frame_path(0), - 'The frame path should match the full path to the image') - self.assertEqual(image_name, service_item.get_frame_title(0), - 'The frame title should match the image name') - self.assertEqual(image_name, service_item.get_display_title(), - 'The display title should match the first image name') - self.assertTrue(service_item.is_image(), 'This service item should be of an "image" type') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanMaintain), - 'This service item should be able to be Maintained') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanPreview), - 'This service item should be able to be be Previewed') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanLoop), - 'This service item should be able to be run in a can be made to Loop') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend), - 'This service item should be able to have new items added to it') + assert service_item.is_valid is True, 'The new service item should be valid' + assert os.path.normpath(test_file) == os.path.normpath(service_item.get_rendered_frame(0)), \ + 'The first frame should match the path to the image' + assert frame_array == service_item.get_frames()[0], 'The return should match frame array1' + assert test_file == service_item.get_frame_path(0), 'The frame path should match the full path to the image' + assert image_name == service_item.get_frame_title(0), 'The frame title should match the image name' + assert image_name == service_item.get_display_title(), 'The display title should match the first image name' + assert service_item.is_image() is True, 'This service item should be of an "image" type' + assert service_item.is_capable(ItemCapabilities.CanMaintain) is True, \ + 'This service item should be able to be Maintained' + assert service_item.is_capable(ItemCapabilities.CanPreview) is True, \ + 'This service item should be able to be be Previewed' + assert service_item.is_capable(ItemCapabilities.CanLoop) is True, \ + 'This service item should be able to be run in a can be made to Loop' + assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \ + 'This service item should be able to have new items added to it' def test_service_item_load_image_from_local_service(self): """ @@ -193,35 +203,33 @@ class TestServiceItem(TestCase): # This test is copied from service_item.py, but is changed since to conform to # new layout of service item. The layout use in serviceitem_image_2.osd is actually invalid now. - self.assertTrue(service_item.is_valid, 'The first service item should be valid') - self.assertTrue(service_item2.is_valid, 'The second service item should be valid') + assert service_item.is_valid is True, 'The first service item should be valid' + assert service_item2.is_valid is True, 'The second service item should be valid' # These test will fail on windows due to the difference in folder seperators if os.name != 'nt': - self.assertEqual(test_file1, service_item.get_rendered_frame(0), - 'The first frame should match the path to the image') - self.assertEqual(test_file2, service_item2.get_rendered_frame(0), - 'The Second frame should match the path to the image') - self.assertEqual(frame_array1, service_item.get_frames()[0], 'The return should match the frame array1') - self.assertEqual(frame_array2, service_item2.get_frames()[0], 'The return should match the frame array2') - self.assertEqual(test_file1, service_item.get_frame_path(0), - 'The frame path should match the full path to the image') - self.assertEqual(test_file2, service_item2.get_frame_path(0), - 'The frame path should match the full path to the image') - self.assertEqual(image_name1, service_item.get_frame_title(0), - 'The 1st frame title should match the image name') - self.assertEqual(image_name2, service_item2.get_frame_title(0), - 'The 2nd frame title should match the image name') - self.assertEqual(service_item.name, service_item.title.lower(), - 'The plugin name should match the display title, as there are > 1 Images') - self.assertTrue(service_item.is_image(), 'This service item should be of an "image" type') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanMaintain), - 'This service item should be able to be Maintained') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanPreview), - 'This service item should be able to be be Previewed') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanLoop), - 'This service item should be able to be run in a can be made to Loop') - self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend), - 'This service item should be able to have new items added to it') + assert test_file1 == service_item.get_rendered_frame(0), \ + 'The first frame should match the path to the image' + assert test_file2 == service_item2.get_rendered_frame(0), \ + 'The Second frame should match the path to the image' + assert frame_array1 == service_item.get_frames()[0], 'The return should match the frame array1' + assert frame_array2 == service_item2.get_frames()[0], 'The return should match the frame array2' + assert test_file1 == service_item.get_frame_path(0), \ + 'The frame path should match the full path to the image' + assert test_file2 == service_item2.get_frame_path(0), \ + 'The frame path should match the full path to the image' + assert image_name1 == service_item.get_frame_title(0), 'The 1st frame title should match the image name' + assert image_name2 == service_item2.get_frame_title(0), 'The 2nd frame title should match the image name' + assert service_item.name == service_item.title.lower(), \ + 'The plugin name should match the display title, as there are > 1 Images' + assert service_item.is_image() is True, 'This service item should be of an "image" type' + assert service_item.is_capable(ItemCapabilities.CanMaintain) is True, \ + 'This service item should be able to be Maintained' + assert service_item.is_capable(ItemCapabilities.CanPreview) is True, \ + 'This service item should be able to be be Previewed' + assert service_item.is_capable(ItemCapabilities.CanLoop) is True, \ + 'This service item should be able to be run in a can be made to Loop' + assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \ + 'This service item should be able to have new items added to it' def test_add_from_command_for_a_presentation(self): """ @@ -240,8 +248,8 @@ class TestServiceItem(TestCase): service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes) # THEN: verify that it is setup as a Command and that the frame data matches - self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command') - self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match') + assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command' + assert service_item.get_frames()[0] == frame, 'Frames should match' def test_add_from_comamnd_without_display_title_and_notes(self): """ @@ -258,8 +266,8 @@ class TestServiceItem(TestCase): service_item.add_from_command(TEST_PATH, image_name, image) # THEN: verify that it is setup as a Command and that the frame data matches - self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command') - self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match') + assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command' + assert service_item.get_frames()[0] == frame, 'Frames should match' @patch(u'openlp.core.lib.serviceitem.ServiceItem.image_manager') @patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') @@ -287,9 +295,9 @@ class TestServiceItem(TestCase): service_item.add_from_command(TEST_PATH, presentation_name, thumb, display_title, notes) # THEN: verify that it is setup as a Command and that the frame data matches - self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command') - self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match') - self.assertEqual(1, mocked_image_manager.add_image.call_count, 'image_manager should be used') + assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command' + assert service_item.get_frames()[0] == frame, 'Frames should match' + assert 1 == mocked_image_manager.add_image.call_count, 'image_manager should be used' def test_service_item_load_optical_media_from_service(self): """ @@ -306,11 +314,11 @@ class TestServiceItem(TestCase): service_item.set_from_service(line) # THEN: We should get back a valid service item with optical media info - self.assertTrue(service_item.is_valid, 'The service item should be valid') - self.assertTrue(service_item.is_capable(ItemCapabilities.IsOptical), 'The item should be Optical') - self.assertEqual(service_item.start_time, 654.375, 'Start time should be 654.375') - self.assertEqual(service_item.end_time, 672.069, 'End time should be 672.069') - self.assertEqual(service_item.media_length, 17.694, 'Media length should be 17.694') + assert service_item.is_valid is True, 'The service item should be valid' + assert service_item.is_capable(ItemCapabilities.IsOptical) is True, 'The item should be Optical' + assert service_item.start_time == 654.375, 'Start time should be 654.375' + assert service_item.end_time == 672.069, 'End time should be 672.069' + assert service_item.media_length == 17.694, 'Media length should be 17.694' def test_service_item_load_song_and_audio_from_service(self): """ @@ -326,22 +334,22 @@ class TestServiceItem(TestCase): service_item.set_from_service(line, '/test/') # THEN: We should get back a valid service item - self.assertTrue(service_item.is_valid, 'The new service item should be valid') - assert_length(0, service_item._display_frames, 'The service item should have no display frames') - assert_length(7, service_item.capabilities, 'There should be 7 default custom item capabilities') + assert service_item.is_valid is True, 'The new service item should be valid' + assert 0 == len(service_item._display_frames), 'The service item should have no display frames' + assert 7 == len(service_item.capabilities), 'There should be 7 default custom item capabilities' # WHEN: We render the frames of the service item service_item.render(True) # THEN: The frames should also be valid - self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"') - self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'], - 'The returned text matches the input, except the last line feed') - self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1), - 'The first line has been returned') - self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0), - '"Amazing Grace! how sweet the s" has been returned as the title') - self.assertEqual('’Twas grace that taught my hea', service_item.get_frame_title(1), - '"’Twas grace that taught my hea" has been returned as the title') - self.assertEqual('/test/amazing_grace.mp3', service_item.background_audio[0], - '"/test/amazing_grace.mp3" should be in the background_audio list') + assert 'Amazing Grace' == service_item.get_display_title(), 'The title should be "Amazing Grace"' + assert CLEANED_VERSE[:-1] == service_item.get_frames()[0]['text'], \ + 'The returned text matches the input, except the last line feed' + assert RENDERED_VERSE.split('\n', 1)[0] == service_item.get_rendered_frame(1), \ + 'The first line has been returned' + assert 'Amazing Grace! how sweet the s' == service_item.get_frame_title(0), \ + '"Amazing Grace! how sweet the s" has been returned as the title' + assert '’Twas grace that taught my hea' == service_item.get_frame_title(1), \ + '"’Twas grace that taught my hea" has been returned as the title' + assert '/test/amazing_grace.mp3' == service_item.background_audio[0], \ + '"/test/amazing_grace.mp3" should be in the background_audio list' diff --git a/tests/functional/openlp_core/lib/test_theme.py b/tests/functional/openlp_core/lib/test_theme.py index 93bc06f24..4e6407d1c 100644 --- a/tests/functional/openlp_core/lib/test_theme.py +++ b/tests/functional/openlp_core/lib/test_theme.py @@ -90,8 +90,8 @@ class TestTheme(TestCase): # THEN: The filename of the background should be correct expected_filename = path / 'MyBeautifulTheme' / 'video.mp4' - self.assertEqual(expected_filename, theme.background_filename) - self.assertEqual('MyBeautifulTheme', theme.theme_name) + assert expected_filename == theme.background_filename + assert 'MyBeautifulTheme' == theme.theme_name def test_save_retrieve(self): """ @@ -107,9 +107,9 @@ class TestTheme(TestCase): self.check_theme(lt) def check_theme(self, theme): - self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"') - self.assertEqual('solid', theme.background_type, 'background_type should be "solid"') - self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0') - self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False') - self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"') - self.assertEqual(47, len(theme.__dict__), 'The theme should have 47 attributes') + assert '#000000' == theme.background_border_color, 'background_border_color should be "#000000"' + assert 'solid' == theme.background_type, 'background_type should be "solid"' + assert 0 == theme.display_vertical_align, 'display_vertical_align should be 0' + assert theme.font_footer_bold is False, 'font_footer_bold should be False' + assert 'Arial' == theme.font_main_name, 'font_main_name should be "Arial"' + assert 47 == len(theme.__dict__), 'The theme should have 47 attributes' diff --git a/tests/functional/openlp_core/lib/test_ui.py b/tests/functional/openlp_core/lib/test_ui.py index 8aec310ba..7c1e94f36 100644 --- a/tests/functional/openlp_core/lib/test_ui.py +++ b/tests/functional/openlp_core/lib/test_ui.py @@ -49,8 +49,8 @@ class TestUi(TestCase): add_welcome_page(wizard, ':/wizards/wizard_firsttime.bmp') # THEN: The wizard should have one page with a pixmap. - self.assertEqual(1, len(wizard.pageIds()), 'The wizard should have one page.') - self.assertIsInstance(wizard.page(0).pixmap(QtWidgets.QWizard.WatermarkPixmap), QtGui.QPixmap) + assert 1 == len(wizard.pageIds()), 'The wizard should have one page.' + assert isinstance(wizard.page(0).pixmap(QtWidgets.QWizard.WatermarkPixmap), QtGui.QPixmap) def test_create_button_box(self): """ @@ -63,22 +63,22 @@ class TestUi(TestCase): btnbox = create_button_box(dialog, 'my_btns', ['ok', 'save', 'cancel', 'close', 'defaults']) # THEN: We should get a QDialogButtonBox with five buttons - self.assertIsInstance(btnbox, QtWidgets.QDialogButtonBox) - self.assertEqual(5, len(btnbox.buttons())) + assert isinstance(btnbox, QtWidgets.QDialogButtonBox) + assert 5 == len(btnbox.buttons()) # WHEN: We create the button box with a custom button btnbox = create_button_box(dialog, 'my_btns', None, [QtWidgets.QPushButton('Custom')]) # THEN: We should get a QDialogButtonBox with one button - self.assertIsInstance(btnbox, QtWidgets.QDialogButtonBox) - self.assertEqual(1, len(btnbox.buttons())) + assert isinstance(btnbox, QtWidgets.QDialogButtonBox) + assert 1 == len(btnbox.buttons()) # WHEN: We create the button box with a custom button and a custom role btnbox = create_button_box(dialog, 'my_btns', None, [(QtWidgets.QPushButton('Help'), QtWidgets.QDialogButtonBox.HelpRole)]) # THEN: We should get a QDialogButtonBox with one button with a certain role - self.assertIsInstance(btnbox, QtWidgets.QDialogButtonBox) - self.assertEqual(1, len(btnbox.buttons())) - self.assertEqual(QtWidgets.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])) + assert isinstance(btnbox, QtWidgets.QDialogButtonBox) + assert 1 == len(btnbox.buttons()) + assert QtWidgets.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0]) def test_create_horizontal_adjusting_combo_box(self): """ @@ -91,9 +91,9 @@ class TestUi(TestCase): combo = create_horizontal_adjusting_combo_box(dialog, 'combo1') # THEN: We should get a ComboBox - self.assertIsInstance(combo, QtWidgets.QComboBox) - self.assertEqual('combo1', combo.objectName()) - self.assertEqual(QtWidgets.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy()) + assert isinstance(combo, QtWidgets.QComboBox) + assert 'combo1' == combo.objectName() + assert QtWidgets.QComboBox.AdjustToMinimumContentsLength == combo.sizeAdjustPolicy() def test_create_button(self): """ @@ -106,26 +106,26 @@ class TestUi(TestCase): btn = create_button(dialog, 'my_btn') # THEN: We should get a button with a name - self.assertIsInstance(btn, QtWidgets.QPushButton) - self.assertEqual('my_btn', btn.objectName()) - self.assertTrue(btn.isEnabled()) + assert isinstance(btn, QtWidgets.QPushButton) + assert 'my_btn' == btn.objectName() + assert btn.isEnabled() is True # WHEN: We create a button with some attributes btn = create_button(dialog, 'my_btn', text='Hello', tooltip='How are you?', enabled=False) # THEN: We should get a button with those attributes - self.assertIsInstance(btn, QtWidgets.QPushButton) - self.assertEqual('Hello', btn.text()) - self.assertEqual('How are you?', btn.toolTip()) - self.assertFalse(btn.isEnabled()) + assert isinstance(btn, QtWidgets.QPushButton) + assert 'Hello' == btn.text() + assert 'How are you?' == btn.toolTip() + assert btn.isEnabled() is False # WHEN: We create a toolbutton btn = create_button(dialog, 'my_btn', btn_class='toolbutton') # THEN: We should get a toolbutton - self.assertIsInstance(btn, QtWidgets.QToolButton) - self.assertEqual('my_btn', btn.objectName()) - self.assertTrue(btn.isEnabled()) + assert isinstance(btn, QtWidgets.QToolButton) + assert 'my_btn' == btn.objectName() + assert btn.isEnabled() is True def test_create_action(self): """ @@ -138,19 +138,19 @@ class TestUi(TestCase): action = create_action(dialog, 'my_action') # THEN: We should get a QAction - self.assertIsInstance(action, QtWidgets.QAction) - self.assertEqual('my_action', action.objectName()) + assert isinstance(action, QtWidgets.QAction) + assert 'my_action' == action.objectName() # WHEN: We create an action with some properties action = create_action(dialog, 'my_action', text='my text', icon=':/wizards/wizard_firsttime.bmp', tooltip='my tooltip', statustip='my statustip') # THEN: These properties should be set - self.assertIsInstance(action, QtWidgets.QAction) - self.assertEqual('my text', action.text()) - self.assertIsInstance(action.icon(), QtGui.QIcon) - self.assertEqual('my tooltip', action.toolTip()) - self.assertEqual('my statustip', action.statusTip()) + assert isinstance(action, QtWidgets.QAction) + assert 'my text' == action.text() + assert isinstance(action.icon(), QtGui.QIcon) + assert 'my tooltip' == action.toolTip() + assert 'my statustip' == action.statusTip() def test_create_action_on_mac_osx(self): """ @@ -186,8 +186,8 @@ class TestUi(TestCase): create_action(dialog, 'my_action') # THEN: setIconVisibleInMenu should not be called - self.assertEqual(0, mocked_action.setIconVisibleInMenu.call_count, - 'setIconVisibleInMenu should not have been called') + assert 0 == mocked_action.setIconVisibleInMenu.call_count, \ + 'setIconVisibleInMenu should not have been called' def test_create_checked_disabled_invisible_action(self): """ @@ -200,9 +200,9 @@ class TestUi(TestCase): action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) # THEN: These properties should be set - self.assertTrue(action.isChecked(), 'The action should be checked') - self.assertFalse(action.isEnabled(), 'The action should be disabled') - self.assertFalse(action.isVisible(), 'The action should be invisble') + assert action.isChecked() is True, 'The action should be checked' + assert action.isEnabled() is False, 'The action should be disabled' + assert action.isVisible() is False, 'The action should be invisble' def test_create_action_separator(self): """ @@ -215,7 +215,7 @@ class TestUi(TestCase): action = create_action(dialog, 'my_action', separator=True) # THEN: The action should be a separator - self.assertTrue(action.isSeparator(), 'The action should be a separator') + assert action.isSeparator() is True, 'The action should be a separator' def test_create_valign_selection_widgets(self): """ @@ -228,11 +228,11 @@ class TestUi(TestCase): label, combo = create_valign_selection_widgets(dialog) # THEN: We should get a label and a combobox. - self.assertEqual(translate('OpenLP.Ui', '&Vertical Align:'), label.text()) - self.assertIsInstance(combo, QtWidgets.QComboBox) - self.assertEqual(combo, label.buddy()) + assert translate('OpenLP.Ui', '&Vertical Align:') == label.text() + assert isinstance(combo, QtWidgets.QComboBox) + assert combo == label.buddy() for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]: - self.assertTrue(combo.findText(text) >= 0) + assert combo.findText(text) >= 0 def test_find_and_set_in_combo_box(self): """ @@ -247,19 +247,19 @@ class TestUi(TestCase): find_and_set_in_combo_box(combo, 'Four', set_missing=False) # THEN: The index should not have changed - self.assertEqual(1, combo.currentIndex()) + assert 1 == combo.currentIndex() # WHEN: We call the method with a non-existing value find_and_set_in_combo_box(combo, 'Four') # THEN: The index should have been reset - self.assertEqual(0, combo.currentIndex()) + assert 0 == combo.currentIndex() # WHEN: We call the method with the default behavior find_and_set_in_combo_box(combo, 'Three') # THEN: The index should have changed - self.assertEqual(2, combo.currentIndex()) + assert 2 == combo.currentIndex() def test_create_widget_action(self): """ @@ -272,8 +272,8 @@ class TestUi(TestCase): action = create_widget_action(button, 'some action') # THEN: The action should be returned - self.assertIsInstance(action, QtWidgets.QAction) - self.assertEqual(action.objectName(), 'some action') + assert isinstance(action, QtWidgets.QAction) + assert action.objectName() == 'some action' def test_set_case_insensitive_completer(self): """ @@ -288,5 +288,5 @@ class TestUi(TestCase): # THEN: The Combobox should have a completer which is case insensitive completer = line_edit.completer() - self.assertIsInstance(completer, QtWidgets.QCompleter) - self.assertEqual(completer.caseSensitivity(), QtCore.Qt.CaseInsensitive) + assert isinstance(completer, QtWidgets.QCompleter) + assert completer.caseSensitivity() == QtCore.Qt.CaseInsensitive diff --git a/tests/interfaces/openlp_core_ui/__init__.py b/tests/functional/openlp_core/projectors/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_ui/__init__.py rename to tests/functional/openlp_core/projectors/__init__.py diff --git a/tests/functional/openlp_core/projectors/test_projector_bugfixes_01.py b/tests/functional/openlp_core/projectors/test_projector_bugfixes_01.py new file mode 100644 index 000000000..da0aae47f --- /dev/null +++ b/tests/functional/openlp_core/projectors/test_projector_bugfixes_01.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2015 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.projectors.pjlink base package. +""" +from unittest import TestCase + +from openlp.core.projectors.db import Projector +from openlp.core.projectors.pjlink import PJLink + +from tests.resources.projector.data import TEST1_DATA + + +class TestPJLinkBugs(TestCase): + """ + Tests for the PJLink module bugfixes + """ + def setUp(self): + ''' + Initialization + ''' + self.pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True) + + def tearDown(self): + ''' + Cleanups + ''' + self.pjlink_test = None + + def test_bug_1550891_process_clss_nonstandard_reply_1(self): + """ + Bugfix 1550891: CLSS request returns non-standard reply with Optoma/Viewsonic projector + """ + # GIVEN: Test object + pjlink = self.pjlink_test + + # WHEN: Process non-standard reply + pjlink.process_clss('Class 1') + + # THEN: Projector class should be set with proper value + self.assertEqual(pjlink.pjlink_class, '1', + 'Non-standard class reply should have set class=1') + + def test_bug_1550891_process_clss_nonstandard_reply_2(self): + """ + Bugfix 1550891: CLSS request returns non-standard reply with BenQ projector + """ + # GIVEN: Test object + pjlink = self.pjlink_test + + # WHEN: Process non-standard reply + pjlink.process_clss('Version2') + + # THEN: Projector class should be set with proper value + # NOTE: At this time BenQ is Class 1, but we're trying a different value to verify + self.assertEqual(pjlink.pjlink_class, '2', + 'Non-standard class reply should have set class=2') + + def test_bug_1593882_no_pin_authenticated_connection(self): + """ + Test bug 1593882 no pin and authenticated request exception + """ + # Test now part of test_projector_pjlink_commands_02 + # Keeping here for bug reference + pass + + def test_bug_1593883_pjlink_authentication(self): + """ + Test bugfix 1593883 pjlink authentication and ticket 92187 + """ + # Test now part of test_projector_pjlink_commands_02 + # Keeping here for bug reference + pass + + def test_bug_1734275_process_lamp_nonstandard_reply(self): + """ + Test bugfix 17342785 non-standard LAMP response + """ + # GIVEN: Test object + pjlink = self.pjlink_test + + # WHEN: Process lamp command called with only hours and no lamp power state + pjlink.process_lamp("45") + + # THEN: Lamp should show hours as 45 and lamp power as Unavailable + self.assertEqual(len(pjlink.lamp), 1, 'There should only be 1 lamp available') + self.assertEqual(pjlink.lamp[0]['Hours'], 45, 'Lamp hours should have equalled 45') + self.assertIsNone(pjlink.lamp[0]['On'], 'Lamp power should be "None"') diff --git a/tests/functional/openlp_core/lib/test_projector_constants.py b/tests/functional/openlp_core/projectors/test_projector_constants.py similarity index 93% rename from tests/functional/openlp_core/lib/test_projector_constants.py rename to tests/functional/openlp_core/projectors/test_projector_constants.py index 90fee1e13..ed1afa677 100644 --- a/tests/functional/openlp_core/lib/test_projector_constants.py +++ b/tests/functional/openlp_core/projectors/test_projector_constants.py @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.lib.projector.constants package. +Package to test the openlp.core.projectors.constants module. """ from unittest import TestCase @@ -37,7 +37,7 @@ class TestProjectorConstants(TestCase): from tests.resources.projector.data import TEST_VIDEO_CODES # WHEN: Import projector PJLINK_DEFAULT_CODES - from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES + from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES # THEN: Verify dictionary was build correctly self.assertEqual(PJLINK_DEFAULT_CODES, TEST_VIDEO_CODES, 'PJLink video strings should match') diff --git a/tests/functional/openlp_core/lib/test_projector_db.py b/tests/functional/openlp_core/projectors/test_projector_db.py similarity index 98% rename from tests/functional/openlp_core/lib/test_projector_db.py rename to tests/functional/openlp_core/projectors/test_projector_db.py index bcbc9c547..1dfe47a54 100644 --- a/tests/functional/openlp_core/lib/test_projector_db.py +++ b/tests/functional/openlp_core/projectors/test_projector_db.py @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.ui.projectordb find, edit, delete +Package to test the openlp.core.projectors.db module. record functions. PREREQUISITE: add_record() and get_all() functions validated. @@ -32,10 +32,10 @@ from tempfile import mkdtemp from unittest import TestCase from unittest.mock import patch -from openlp.core.lib.projector import upgrade from openlp.core.lib.db import upgrade_db -from openlp.core.lib.projector.constants import PJLINK_PORT -from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source +from openlp.core.projectors import upgrade +from openlp.core.projectors.constants import PJLINK_PORT +from openlp.core.projectors.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA from tests.utils.constants import TEST_RESOURCES_PATH @@ -129,7 +129,7 @@ class TestProjectorDB(TestCase): """ Test case for ProjectorDB """ - @patch('openlp.core.lib.projector.db.init_url') + @patch('openlp.core.projectors.db.init_url') def setUp(self, mocked_init_url): """ Set up anything necessary for all tests diff --git a/tests/functional/openlp_core/projectors/test_projector_pjlink_base.py b/tests/functional/openlp_core/projectors/test_projector_pjlink_base.py new file mode 100644 index 000000000..c86904bad --- /dev/null +++ b/tests/functional/openlp_core/projectors/test_projector_pjlink_base.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2015 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.projectors.pjlink base package. +""" +from unittest import TestCase +from unittest.mock import call, patch, MagicMock + +from openlp.core.projectors.constants import E_PARAMETER, ERROR_STRING, S_ON, S_CONNECTED, S_QSOCKET_STATE +from openlp.core.projectors.db import Projector +from openlp.core.projectors.pjlink import PJLink + +from tests.resources.projector.data import TEST1_DATA + +pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True) + + +class TestPJLinkBase(TestCase): + """ + Tests for the PJLink module + """ + def setUp(self): + ''' + TestPJLinkCommands part 2 initialization + ''' + self.pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True) + + def tearDown(self): + ''' + TestPJLinkCommands part 2 cleanups + ''' + self.pjlink_test = None + + @patch.object(pjlink_test, 'change_status') + def test_status_change(self, mock_change_status): + """ + Test process_command call with ERR2 (Parameter) status + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: process_command is called with "ERR2" status from projector + pjlink.process_command('POWR', 'ERR2') + + # THEN: change_status should have called change_status with E_UNDEFINED + # as first parameter + mock_change_status.called_with(E_PARAMETER, + 'change_status should have been called with "{}"'.format( + ERROR_STRING[E_PARAMETER])) + + @patch.object(pjlink_test, 'disconnect_from_host') + def test_socket_abort(self, mock_disconnect): + """ + Test PJLink.socket_abort calls disconnect_from_host + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: Calling socket_abort + pjlink.socket_abort() + + # THEN: disconnect_from_host should be called + self.assertTrue(mock_disconnect.called, 'Should have called disconnect_from_host') + + def test_poll_loop_not_connected(self): + """ + Test PJLink.poll_loop not connected return + """ + # GIVEN: Test object and mocks + pjlink = pjlink_test + pjlink.state = MagicMock() + pjlink.timer = MagicMock() + pjlink.state.return_value = False + pjlink.ConnectedState = True + + # WHEN: PJLink.poll_loop called + pjlink.poll_loop() + + # THEN: poll_loop should exit without calling any other method + self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method') + + def test_poll_loop_start(self): + """ + Test PJLink.poll_loop makes correct calls + """ + # GIVEN: Mocks and test data + mock_state = patch.object(self.pjlink_test, 'state').start() + mock_state.return_value = S_QSOCKET_STATE['ConnectedState'] + mock_timer = patch.object(self.pjlink_test, 'timer').start() + mock_timer.interval.return_value = 10 + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + + pjlink = self.pjlink_test + pjlink.poll_time = 20 + pjlink.power = S_ON + pjlink.source_available = None + pjlink.other_info = None + pjlink.manufacturer = None + pjlink.model = None + pjlink.pjlink_name = None + pjlink.ConnectedState = S_CONNECTED + call_list = [ + call('POWR'), + call('ERST'), + call('LAMP'), + call('AVMT'), + call('INPT'), + call('INST'), + call('INFO'), + call('INF1'), + call('INF2'), + call('NAME'), + ] + + # WHEN: PJLink.poll_loop is called + pjlink.poll_loop() + + # THEN: proper calls were made to retrieve projector data + # First, call to update the timer with the next interval + self.assertTrue(mock_timer.setInterval.called) + # Next, should have called the timer to start + self.assertTrue(mock_timer.start.called, 'Should have started the timer') + # Finally, should have called send_command with a list of projetctor status checks + mock_send_command.assert_has_calls(call_list, 'Should have queued projector queries') diff --git a/tests/functional/openlp_core/lib/test_projector_pjlink_cmd_routing.py b/tests/functional/openlp_core/projectors/test_projector_pjlink_cmd_routing.py similarity index 77% rename from tests/functional/openlp_core/lib/test_projector_pjlink_cmd_routing.py rename to tests/functional/openlp_core/projectors/test_projector_pjlink_cmd_routing.py index 006abfff6..4ff133061 100644 --- a/tests/functional/openlp_core/lib/test_projector_pjlink_cmd_routing.py +++ b/tests/functional/openlp_core/projectors/test_projector_pjlink_cmd_routing.py @@ -20,20 +20,20 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.lib.projector.pjlink class command routing. +Package to test the openlp.core.projectors.pjlink command routing. """ from unittest import TestCase from unittest.mock import patch, MagicMock -import openlp.core.lib.projector.pjlink -from openlp.core.lib.projector.db import Projector -from openlp.core.lib.projector.pjlink import PJLink -from openlp.core.lib.projector.constants import PJLINK_ERRORS, \ +import openlp.core.projectors.pjlink +from openlp.core.projectors.constants import PJLINK_ERRORS, \ E_AUTHENTICATION, E_PARAMETER, E_PROJECTOR, E_UNAVAILABLE, E_UNDEFINED +from openlp.core.projectors.db import Projector +from openlp.core.projectors.pjlink import PJLink ''' -from openlp.core.lib.projector.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \ +from openlp.core.projectors.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \ PJLINK_POWR_STATUS, PJLINK_VALID_CMD, E_WARN, E_ERROR, S_OFF, S_STANDBY, S_ON ''' from tests.resources.projector.data import TEST_PIN, TEST1_DATA @@ -46,7 +46,19 @@ class TestPJLinkRouting(TestCase): """ Tests for the PJLink module command routing """ - @patch.object(openlp.core.lib.projector.pjlink, 'log') + def setUp(self): + ''' + TestPJLinkCommands part 2 initialization + ''' + self.pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True) + + def tearDown(self): + ''' + TestPJLinkCommands part 2 cleanups + ''' + self.pjlink_test = None + + @patch.object(openlp.core.projectors.pjlink, 'log') def test_process_command_call_clss(self, mock_log): """ Test process_command calls proper function @@ -66,7 +78,7 @@ class TestPJLinkRouting(TestCase): mock_process_clss.assert_called_with('1') @patch.object(pjlink_test, 'change_status') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_process_command_err1(self, mock_log, mock_change_status): """ Test ERR1 - Undefined projector function @@ -85,7 +97,7 @@ class TestPJLinkRouting(TestCase): mock_log.error.assert_called_with(log_text) @patch.object(pjlink_test, 'change_status') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_process_command_err2(self, mock_log, mock_change_status): """ Test ERR2 - Parameter Error @@ -104,7 +116,7 @@ class TestPJLinkRouting(TestCase): mock_log.error.assert_called_with(log_text) @patch.object(pjlink_test, 'change_status') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_process_command_err3(self, mock_log, mock_change_status): """ Test ERR3 - Unavailable error @@ -123,7 +135,7 @@ class TestPJLinkRouting(TestCase): mock_log.error.assert_called_with(log_text) @patch.object(pjlink_test, 'change_status') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_process_command_err4(self, mock_log, mock_change_status): """ Test ERR3 - Unavailable error @@ -144,7 +156,7 @@ class TestPJLinkRouting(TestCase): @patch.object(pjlink_test, 'projectorAuthentication') @patch.object(pjlink_test, 'change_status') @patch.object(pjlink_test, 'disconnect_from_host') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_process_command_erra(self, mock_log, mock_disconnect, mock_change_status, mock_err_authenticate): """ Test ERRA - Authentication Error @@ -163,28 +175,27 @@ class TestPJLinkRouting(TestCase): mock_change_status.assert_called_once_with(E_AUTHENTICATION) mock_log.error.assert_called_with(log_text) - @patch.object(openlp.core.lib.projector.pjlink, 'log') - def test_process_command_future(self, mock_log): + def test_process_command_future(self): """ Test command valid but no method to process yet """ - # GIVEN: Test object - pjlink = pjlink_test - log_text = "(127.0.0.1) Unable to process command='CLSS' (Future option)" - mock_log.reset_mock() - # Remove a valid command so we can test valid command but not available yet - pjlink.pjlink_functions.pop('CLSS') + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start() + mock_functions = patch.object(self.pjlink_test, 'pjlink_functions').start() + mock_functions.return_value = [] + + pjlink = self.pjlink_test + log_text = '(111.111.111.111) Unable to process command="CLSS" (Future option?)' # WHEN: process_command called with an unknown command - with patch.object(pjlink, 'pjlink_functions') as mock_functions: - pjlink.process_command(cmd='CLSS', data='DONT CARE') + pjlink.process_command(cmd='CLSS', data='DONT CARE') # THEN: Error should be logged and no command called self.assertFalse(mock_functions.called, 'Should not have gotten to the end of the method') mock_log.warning.assert_called_once_with(log_text) @patch.object(pjlink_test, 'pjlink_functions') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_process_command_invalid(self, mock_log, mock_functions): """ Test not a valid command @@ -196,29 +207,26 @@ class TestPJLinkRouting(TestCase): # WHEN: process_command called with an unknown command pjlink.process_command(cmd='Unknown', data='Dont Care') - log_text = "(127.0.0.1) Ignoring command='Unknown' (Invalid/Unknown)" + log_text = '(127.0.0.1) Ignoring command="Unknown" (Invalid/Unknown)' # THEN: Error should be logged and no command called self.assertFalse(mock_functions.called, 'Should not have gotten to the end of the method') mock_log.error.assert_called_once_with(log_text) - @patch.object(pjlink_test, 'pjlink_functions') - @patch.object(openlp.core.lib.projector.pjlink, 'log') - def test_process_command_ok(self, mock_log, mock_functions): + def test_process_command_ok(self): """ Test command returned success """ - # GIVEN: Test object - pjlink = pjlink_test - mock_functions.reset_mock() - mock_log.reset_mock() + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() - # WHEN: process_command called with an unknown command - pjlink.process_command(cmd='CLSS', data='OK') - log_text = '(127.0.0.1) Command "CLSS" returned OK' + pjlink = self.pjlink_test + log_text = '(111.111.111.111) Command "POWR" returned OK' - # THEN: Error should be logged and no command called - self.assertFalse(mock_functions.called, 'Should not have gotten to the end of the method') - self.assertEqual(mock_log.debug.call_count, 2, 'log.debug() should have been called twice') - # Although we called it twice, only the last log entry is saved + # WHEN: process_command called with a command that returns OK + pjlink.process_command(cmd='POWR', data='OK') + + # THEN: Appropriate calls should have been made mock_log.debug.assert_called_with(log_text) + mock_send_command.assert_called_once_with(cmd='POWR') diff --git a/tests/functional/openlp_core/lib/test_projector_pjlink_commands.py b/tests/functional/openlp_core/projectors/test_projector_pjlink_commands_01.py similarity index 91% rename from tests/functional/openlp_core/lib/test_projector_pjlink_commands.py rename to tests/functional/openlp_core/projectors/test_projector_pjlink_commands_01.py index 143206d0a..8340a0fd0 100644 --- a/tests/functional/openlp_core/lib/test_projector_pjlink_commands.py +++ b/tests/functional/openlp_core/projectors/test_projector_pjlink_commands_01.py @@ -20,18 +20,18 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.lib.projector.pjlink commands package. +Package to test the openlp.core.projectors.pjlink commands package. """ from unittest import TestCase from unittest.mock import patch -import openlp.core.lib.projector.pjlink -from openlp.core.lib.projector.db import Projector -from openlp.core.lib.projector.pjlink import PJLink -from openlp.core.lib.projector.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \ +import openlp.core.projectors.pjlink +from openlp.core.projectors.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \ PJLINK_POWR_STATUS, \ E_ERROR, E_NOT_CONNECTED, E_SOCKET_ADDRESS_NOT_AVAILABLE, E_UNKNOWN_SOCKET_ERROR, E_WARN, \ S_CONNECTED, S_OFF, S_ON, S_NOT_CONNECTED, S_CONNECTING, S_STANDBY +from openlp.core.projectors.db import Projector +from openlp.core.projectors.pjlink import PJLink from tests.resources.projector.data import TEST_PIN, TEST1_DATA @@ -47,10 +47,10 @@ for pos in range(0, len(PJLINK_ERST_DATA)): class TestPJLinkCommands(TestCase): """ - Tests for the PJLink module + Tests for the PJLinkCommands class part 1 """ @patch.object(pjlink_test, 'changeStatus') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_change_status_connection_error(self, mock_log, mock_change_status): """ Test change_status with connection error @@ -74,7 +74,7 @@ class TestPJLinkCommands(TestCase): self.assertEqual(mock_log.debug.call_count, 3, 'Debug log should have been called 3 times') @patch.object(pjlink_test, 'changeStatus') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_change_status_connection_status_connecting(self, mock_log, mock_change_status): """ Test change_status with connection status @@ -97,7 +97,7 @@ class TestPJLinkCommands(TestCase): self.assertEqual(mock_log.debug.call_count, 3, 'Debug log should have been called 3 times') @patch.object(pjlink_test, 'changeStatus') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_change_status_connection_status_connected(self, mock_log, mock_change_status): """ Test change_status with connection status @@ -120,7 +120,7 @@ class TestPJLinkCommands(TestCase): self.assertEqual(mock_log.debug.call_count, 3, 'Debug log should have been called 3 times') @patch.object(pjlink_test, 'changeStatus') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_change_status_connection_status_with_message(self, mock_log, mock_change_status): """ Test change_status with connection status @@ -144,7 +144,7 @@ class TestPJLinkCommands(TestCase): self.assertEqual(mock_log.debug.call_count, 3, 'Debug log should have been called 3 times') @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_av_mute_status(self, mock_log, mock_send_command): """ Test sending command to retrieve shutter/audio state @@ -164,7 +164,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_available_inputs(self, mock_log, mock_send_command): """ Test sending command to retrieve avaliable inputs @@ -184,7 +184,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_error_status(self, mock_log, mock_send_command): """ Test sending command to retrieve projector error status @@ -204,7 +204,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_input_source(self, mock_log, mock_send_command): """ Test sending command to retrieve current input @@ -224,7 +224,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_lamp_status(self, mock_log, mock_send_command): """ Test sending command to retrieve lamp(s) status @@ -244,7 +244,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_manufacturer(self, mock_log, mock_send_command): """ Test sending command to retrieve manufacturer name @@ -264,7 +264,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_model(self, mock_log, mock_send_command): """ Test sending command to get model information @@ -284,7 +284,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_name(self, mock_log, mock_send_command): """ Test sending command to get user-assigned name @@ -304,7 +304,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_other_info(self, mock_log, mock_send_command): """ Test sending command to retrieve other information @@ -324,7 +324,7 @@ class TestPJLinkCommands(TestCase): mock_send_command.assert_called_once_with(cmd=test_data) @patch.object(pjlink_test, 'send_command') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_get_power_status(self, mock_log, mock_send_command): """ Test sending command to retrieve current power state @@ -570,36 +570,7 @@ class TestPJLinkCommands(TestCase): self.assertEqual(pjlink.pjlink_class, '2', 'Projector should have set class=2') - def test_projector_process_clss_nonstandard_reply_optoma(self): - """ - Bugfix 1550891: CLSS request returns non-standard reply with Optoma projector - """ - # GIVEN: Test object - pjlink = pjlink_test - - # WHEN: Process non-standard reply - pjlink.process_clss('Class 1') - - # THEN: Projector class should be set with proper value - self.assertEqual(pjlink.pjlink_class, '1', - 'Non-standard class reply should have set class=1') - - def test_projector_process_clss_nonstandard_reply_benq(self): - """ - Bugfix 1550891: CLSS request returns non-standard reply with BenQ projector - """ - # GIVEN: Test object - pjlink = pjlink_test - - # WHEN: Process non-standard reply - pjlink.process_clss('Version2') - - # THEN: Projector class should be set with proper value - # NOTE: At this time BenQ is Class 1, but we're trying a different value to verify - self.assertEqual(pjlink.pjlink_class, '2', - 'Non-standard class reply should have set class=2') - - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_clss_invalid_nan(self, mock_log): """ Test CLSS reply has no class number @@ -609,14 +580,14 @@ class TestPJLinkCommands(TestCase): # WHEN: Process invalid reply pjlink.process_clss('Z') - log_text = "(127.0.0.1) NAN clss version reply 'Z' - defaulting to class '1'" + log_text = '(127.0.0.1) NAN CLSS version reply "Z" - defaulting to class "1"' # THEN: Projector class should be set with default value self.assertEqual(pjlink.pjlink_class, '1', 'Non-standard class reply should have set class=1') mock_log.error.assert_called_once_with(log_text) - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_clss_invalid_no_version(self, mock_log): """ Test CLSS reply has no class number @@ -626,7 +597,7 @@ class TestPJLinkCommands(TestCase): # WHEN: Process invalid reply pjlink.process_clss('Invalid') - log_text = "(127.0.0.1) No numbers found in class version reply 'Invalid' - defaulting to class '1'" + log_text = '(127.0.0.1) No numbers found in class version reply "Invalid" - defaulting to class "1"' # THEN: Projector class should be set with default value self.assertEqual(pjlink.pjlink_class, '1', @@ -648,7 +619,7 @@ class TestPJLinkCommands(TestCase): # THEN: PJLink instance errors should be None self.assertIsNone(pjlink.projector_errors, 'projector_errors should have been set to None') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_erst_data_invalid_length(self, mock_log): """ Test test_projector_process_erst_data_invalid_length @@ -656,7 +627,7 @@ class TestPJLinkCommands(TestCase): # GIVEN: Test object pjlink = pjlink_test pjlink.projector_errors = None - log_text = "127.0.0.1) Invalid error status response '11111111': length != 6" + log_text = '127.0.0.1) Invalid error status response "11111111": length != 6' # WHEN: process_erst called with invalid data (too many values pjlink.process_erst('11111111') @@ -666,7 +637,7 @@ class TestPJLinkCommands(TestCase): self.assertTrue(mock_log.warning.called, 'Warning should have been logged') mock_log.warning.assert_called_once_with(log_text) - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_erst_data_invalid_nan(self, mock_log): """ Test test_projector_process_erst_data_invalid_nan @@ -674,7 +645,7 @@ class TestPJLinkCommands(TestCase): # GIVEN: Test object pjlink = pjlink_test pjlink.projector_errors = None - log_text = "(127.0.0.1) Invalid error status response '1111Z1'" + log_text = '(127.0.0.1) Invalid error status response "1111Z1"' # WHEN: process_erst called with invalid data (too many values pjlink.process_erst('1111Z1') @@ -700,8 +671,8 @@ class TestPJLinkCommands(TestCase): # THEN: PJLink instance errors should match chk_value for chk in pjlink.projector_errors: self.assertEqual(pjlink.projector_errors[chk], chk_string, - "projector_errors['{chk}'] should have been set to {err}".format(chk=chk, - err=chk_string)) + 'projector_errors["{chk}"] should have been set to "{err}"'.format(chk=chk, + err=chk_string)) def test_projector_process_erst_all_error(self): """ @@ -719,8 +690,8 @@ class TestPJLinkCommands(TestCase): # THEN: PJLink instance errors should match chk_value for chk in pjlink.projector_errors: self.assertEqual(pjlink.projector_errors[chk], chk_string, - "projector_errors['{chk}'] should have been set to {err}".format(chk=chk, - err=chk_string)) + 'projector_errors["{chk}"] should have been set to "{err}"'.format(chk=chk, + err=chk_string)) def test_projector_process_erst_warn_cover_only(self): """ @@ -764,7 +735,7 @@ class TestPJLinkCommands(TestCase): self.assertEqual(pjlink.source, '1', 'Input source should be set to "1"') @patch.object(pjlink_test, 'projectorUpdateIcons') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_inst(self, mock_log, mock_UpdateIcons): """ Test saving video source available information @@ -773,9 +744,9 @@ class TestPJLinkCommands(TestCase): pjlink = pjlink_test pjlink.source_available = [] test_data = '21 10 30 31 11 20' - test_saved = ['10', '11', '20', '21', '30', '31'] - log_data = '(127.0.0.1) Setting projector sources_available to ' \ - '"[\'10\', \'11\', \'20\', \'21\', \'30\', \'31\']"' + test_saved = ["10", "11", "20", "21", "30", "31"] + log_data = "(127.0.0.1) Setting projector sources_available to " \ + "\"['10', '11', '20', '21', '30', '31']\"" mock_UpdateIcons.reset_mock() mock_log.reset_mock() @@ -787,7 +758,7 @@ class TestPJLinkCommands(TestCase): mock_log.debug.assert_called_once_with(log_data) self.assertTrue(mock_UpdateIcons.emit.called, 'Update Icons should have been called') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_lamp_invalid(self, mock_log): """ Test status multiple lamp on/off and hours @@ -858,7 +829,7 @@ class TestPJLinkCommands(TestCase): self.assertEqual(pjlink.lamp[0]['Hours'], 22222, 'Lamp hours should have been set to 22222') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_name(self, mock_log): """ Test saving NAME data from projector @@ -1040,7 +1011,7 @@ class TestPJLinkCommands(TestCase): self.assertNotEquals(pjlink.serial_no, test_number, 'Projector serial number should NOT have been set') - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_sver(self, mock_log): """ Test invalid software version information - too long @@ -1050,7 +1021,7 @@ class TestPJLinkCommands(TestCase): pjlink.sw_version = None pjlink.sw_version_received = None test_data = 'Test 1 Subtest 1' - test_log = "(127.0.0.1) Setting projector software version to 'Test 1 Subtest 1'" + test_log = '(127.0.0.1) Setting projector software version to "Test 1 Subtest 1"' mock_log.reset_mock() # WHEN: process_sver called with invalid data @@ -1061,7 +1032,7 @@ class TestPJLinkCommands(TestCase): self.assertIsNone(pjlink.sw_version_received, 'Received software version should not have changed') mock_log.debug.assert_called_once_with(test_log) - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_sver_changed(self, mock_log): """ Test invalid software version information - Received different than saved @@ -1086,7 +1057,7 @@ class TestPJLinkCommands(TestCase): # There was 4 calls, but only the last one is checked with this method mock_log.warning.assert_called_with(test_log) - @patch.object(openlp.core.lib.projector.pjlink, 'log') + @patch.object(openlp.core.projectors.pjlink, 'log') def test_projector_process_sver_invalid(self, mock_log): """ Test invalid software version information - too long diff --git a/tests/functional/openlp_core/projectors/test_projector_pjlink_commands_02.py b/tests/functional/openlp_core/projectors/test_projector_pjlink_commands_02.py new file mode 100644 index 000000000..62ba4ec76 --- /dev/null +++ b/tests/functional/openlp_core/projectors/test_projector_pjlink_commands_02.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2015 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.projectors.pjlink commands package. +""" +from unittest import TestCase +from unittest.mock import patch, call + +import openlp.core.projectors.pjlink +from openlp.core.projectors.constants import S_CONNECTED +from openlp.core.projectors.db import Projector +from openlp.core.projectors.pjlink import PJLink + +from tests.resources.projector.data import TEST_HASH, TEST_PIN, TEST_SALT, TEST1_DATA + + +class TestPJLinkCommands(TestCase): + """ + Tests for the PJLinkCommands class part 2 + """ + def setUp(self): + ''' + TestPJLinkCommands part 2 initialization + ''' + self.pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True) + + def tearDown(self): + ''' + TestPJLinkCommands part 2 cleanups + ''' + self.pjlink_test = None + + def test_process_pjlink_normal(self): + """ + Test initial connection prompt with no authentication + """ + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, "log").start() + mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + mock_readyRead = patch.object(self.pjlink_test, 'readyRead').start() + mock_change_status = patch.object(self.pjlink_test, 'change_status').start() + pjlink = self.pjlink_test + pjlink.pin = None + log_check = [call("({111.111.111.111}) process_pjlink(): Sending 'CLSS' initial command'"), ] + + # WHEN: process_pjlink called with no authentication required + pjlink.process_pjlink(data="0") + + # THEN: proper processing should have occured + mock_log.debug.has_calls(log_check) + mock_disconnect_from_host.assert_not_called() + self.assertEqual(mock_readyRead.connect.call_count, 1, 'Should have only been called once') + mock_change_status.assert_called_once_with(S_CONNECTED) + mock_send_command.assert_called_with(cmd='CLSS', priority=True, salt=None) + + def test_process_pjlink_authenticate(self): + """ + Test initial connection prompt with authentication + """ + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, "log").start() + mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + mock_readyRead = patch.object(self.pjlink_test, 'readyRead').start() + mock_change_status = patch.object(self.pjlink_test, 'change_status').start() + pjlink = self.pjlink_test + pjlink.pin = TEST_PIN + log_check = [call("({111.111.111.111}) process_pjlink(): Sending 'CLSS' initial command'"), ] + + # WHEN: process_pjlink called with no authentication required + pjlink.process_pjlink(data='1 {salt}'.format(salt=TEST_SALT)) + + # THEN: proper processing should have occured + mock_log.debug.has_calls(log_check) + mock_disconnect_from_host.assert_not_called() + self.assertEqual(mock_readyRead.connect.call_count, 1, 'Should have only been called once') + mock_change_status.assert_called_once_with(S_CONNECTED) + mock_send_command.assert_called_with(cmd='CLSS', priority=True, salt=TEST_HASH) + + def test_process_pjlink_normal_pin_set_error(self): + """ + Test process_pjlinnk called with no authentication but pin is set + """ + # GIVEN: Initial mocks and data + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start() + mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + pjlink = self.pjlink_test + pjlink.pin = TEST_PIN + log_check = [call('(111.111.111.111) Normal connection but PIN set - aborting'), ] + + # WHEN: process_pjlink called with invalid authentication scheme + pjlink.process_pjlink(data='0') + + # THEN: Proper calls should be made + mock_log.error.assert_has_calls(log_check) + self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once') + mock_send_command.assert_not_called() + + def test_process_pjlink_normal_with_salt_error(self): + """ + Test process_pjlinnk called with no authentication but pin is set + """ + # GIVEN: Initial mocks and data + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start() + mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + pjlink = self.pjlink_test + pjlink.pin = TEST_PIN + log_check = [call('(111.111.111.111) Normal connection with extra information - aborting'), ] + + # WHEN: process_pjlink called with invalid authentication scheme + pjlink.process_pjlink(data='0 {salt}'.format(salt=TEST_SALT)) + + # THEN: Proper calls should be made + mock_log.error.assert_has_calls(log_check) + self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once') + mock_send_command.assert_not_called() + + def test_process_pjlink_invalid_authentication_scheme_length_error(self): + """ + Test initial connection prompt with authentication scheme longer than 1 character + """ + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start() + mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + pjlink = self.pjlink_test + log_check = [call('(111.111.111.111) Invalid initial authentication scheme - aborting'), ] + + # WHEN: process_pjlink called with invalid authentication scheme + pjlink.process_pjlink(data='01') + + # THEN: socket should be closed and invalid data logged + mock_log.error.assert_has_calls(log_check) + self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once') + mock_send_command.assert_not_called() + + def test_process_pjlink_invalid_authentication_data_length_error(self): + """ + Test initial connection prompt with authentication no salt + """ + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start() + mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + log_check = [call('(111.111.111.111) Authenticated connection but not enough info - aborting'), ] + pjlink = self.pjlink_test + + # WHEN: process_pjlink called with no salt + pjlink.process_pjlink(data='1') + + # THEN: socket should be closed and invalid data logged + mock_log.error.assert_has_calls(log_check) + self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once') + mock_send_command.assert_not_called() + + def test_process_pjlink_authenticate_pin_not_set_error(self): + """ + Test process_pjlink authentication but pin not set + """ + # GIVEN: Initial mocks and data + mock_log = patch.object(openlp.core.projectors.pjlink, 'log').start() + mock_disconnect_from_host = patch.object(self.pjlink_test, 'disconnect_from_host').start() + mock_send_command = patch.object(self.pjlink_test, 'send_command').start() + log_check = [call('(111.111.111.111) Authenticate connection but no PIN - aborting'), ] + pjlink = self.pjlink_test + pjlink.pin = None + + # WHEN: process_pjlink called with no salt + pjlink.process_pjlink(data='1 {salt}'.format(salt=TEST_SALT)) + + # THEN: socket should be closed and invalid data logged + mock_log.error.assert_has_calls(log_check) + self.assertEqual(mock_disconnect_from_host.call_count, 1, 'Should have only been called once') + mock_send_command.assert_not_called() diff --git a/tests/functional/openlp_core/test_app.py b/tests/functional/openlp_core/test_app.py index 0f5633c3e..eccb81447 100644 --- a/tests/functional/openlp_core/test_app.py +++ b/tests/functional/openlp_core/test_app.py @@ -29,8 +29,6 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.app import OpenLP, parse_options from openlp.core.common.settings import Settings -from tests.helpers.testmixin import TestMixin - TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'resources')) @@ -141,23 +139,33 @@ class TestOpenLP(TestCase): """ Test the OpenLP app class """ - @patch('openlp.core.app.QtWidgets.QApplication.exec') - def test_exec(self, mocked_exec): + def setUp(self): + self.build_settings() + self.qapplication_patcher = patch('openlp.core.app.QtGui.QApplication') + self.mocked_qapplication = self.qapplication_patcher.start() + self.openlp = OpenLP([]) + + def tearDown(self): + self.qapplication_patcher.stop() + self.destroy_settings() + del self.openlp + self.openlp = None + + def test_exec(self): """ Test the exec method """ # GIVEN: An app - app = OpenLP([]) - app.shared_memory = MagicMock() - mocked_exec.return_value = False + self.openlp.shared_memory = MagicMock() + self.mocked_qapplication.exec.return_value = False # WHEN: exec() is called - result = app.exec() + result = self.openlp.exec() # THEN: The right things should be called - assert app.is_event_loop_active is True - mocked_exec.assert_called_once_with() - app.shared_memory.detach.assert_called_once_with() + assert self.openlp.is_event_loop_active is True + self.mocked_qapplication.exec.assert_called_once_with() + self.openlp.shared_memory.detach.assert_called_once_with() assert result is False @patch('openlp.core.app.QtCore.QSharedMemory') @@ -169,10 +177,9 @@ class TestOpenLP(TestCase): mocked_shared_memory = MagicMock() mocked_shared_memory.attach.return_value = False MockedSharedMemory.return_value = mocked_shared_memory - app = OpenLP([]) # WHEN: is_already_running() is called - result = app.is_already_running() + result = self.openlp.is_already_running() # THEN: The result should be false MockedSharedMemory.assert_called_once_with('OpenLP') @@ -193,10 +200,9 @@ class TestOpenLP(TestCase): MockedSharedMemory.return_value = mocked_shared_memory MockedStandardButtons.return_value = 0 mocked_critical.return_value = QtWidgets.QMessageBox.Yes - app = OpenLP([]) # WHEN: is_already_running() is called - result = app.is_already_running() + result = self.openlp.is_already_running() # THEN: The result should be false MockedSharedMemory.assert_called_once_with('OpenLP') @@ -218,10 +224,9 @@ class TestOpenLP(TestCase): MockedSharedMemory.return_value = mocked_shared_memory MockedStandardButtons.return_value = 0 mocked_critical.return_value = QtWidgets.QMessageBox.No - app = OpenLP([]) # WHEN: is_already_running() is called - result = app.is_already_running() + result = self.openlp.is_already_running() # THEN: The result should be false MockedSharedMemory.assert_called_once_with('OpenLP') @@ -235,11 +240,9 @@ class TestOpenLP(TestCase): Test that the app.process_events() method simply calls the Qt method """ # GIVEN: An app - app = OpenLP([]) - # WHEN: process_events() is called - with patch.object(app, 'processEvents') as mocked_processEvents: - app.process_events() + with patch.object(self.openlp, 'processEvents') as mocked_processEvents: + self.openlp.process_events() # THEN: processEvents was called mocked_processEvents.assert_called_once_with() @@ -249,12 +252,10 @@ class TestOpenLP(TestCase): Test that the set_busy_cursor() method sets the cursor """ # GIVEN: An app - app = OpenLP([]) - # WHEN: set_busy_cursor() is called - with patch.object(app, 'setOverrideCursor') as mocked_setOverrideCursor, \ - patch.object(app, 'processEvents') as mocked_processEvents: - app.set_busy_cursor() + with patch.object(self.openlp, 'setOverrideCursor') as mocked_setOverrideCursor, \ + patch.object(self.openlp, 'processEvents') as mocked_processEvents: + self.openlp.set_busy_cursor() # THEN: The cursor should have been set mocked_setOverrideCursor.assert_called_once_with(QtCore.Qt.BusyCursor) @@ -265,29 +266,15 @@ class TestOpenLP(TestCase): Test that the set_normal_cursor() method resets the cursor """ # GIVEN: An app - app = OpenLP([]) - # WHEN: set_normal_cursor() is called - with patch.object(app, 'restoreOverrideCursor') as mocked_restoreOverrideCursor, \ - patch.object(app, 'processEvents') as mocked_processEvents: - app.set_normal_cursor() + with patch.object(self.openlp, 'restoreOverrideCursor') as mocked_restoreOverrideCursor, \ + patch.object(self.openlp, 'processEvents') as mocked_processEvents: + self.openlp.set_normal_cursor() # THEN: The cursor should have been set mocked_restoreOverrideCursor.assert_called_once_with() mocked_processEvents.assert_called_once_with() - -class TestInit(TestCase, TestMixin): - def setUp(self): - self.build_settings() - with patch('openlp.core.app.OpenLPMixin.__init__') as constructor: - constructor.return_value = None - self.openlp = OpenLP(list()) - - def tearDown(self): - self.destroy_settings() - del self.openlp - def test_event(self): """ Test the reimplemented event method diff --git a/tests/functional/openlp_core/ui/lib/test_listwidgetwithdnd.py b/tests/functional/openlp_core/ui/lib/test_listwidgetwithdnd.py deleted file mode 100755 index 2a4e039fa..000000000 --- a/tests/functional/openlp_core/ui/lib/test_listwidgetwithdnd.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -This module contains tests for the openlp.core.lib.listwidgetwithdnd module -""" -from unittest import TestCase -from unittest.mock import MagicMock, patch -from types import GeneratorType - -from openlp.core.common.i18n import UiStrings -from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD - - -class TestListWidgetWithDnD(TestCase): - """ - Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class - """ - def test_clear(self): - """ - Test the clear method when called without any arguments. - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - - # WHEN: Calling clear with out any arguments - widget.clear() - - # THEN: The results text should be the standard 'no results' text. - self.assertEqual(widget.no_results_text, UiStrings().NoResults) - - def test_clear_search_while_typing(self): - """ - Test the clear method when called with the search_while_typing argument set to True - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - - # WHEN: Calling clear with search_while_typing set to True - widget.clear(search_while_typing=True) - - # THEN: The results text should be the 'short results' text. - self.assertEqual(widget.no_results_text, UiStrings().ShortResults) - - def test_all_items_no_list_items(self): - """ - Test allItems when there are no items in the list widget - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - with patch.object(widget, 'count', return_value=0), \ - patch.object(widget, 'item', side_effect=lambda x: [][x]): - - # WHEN: Calling allItems - result = widget.allItems() - - # THEN: An instance of a Generator object should be returned. The generator should not yeild any results - self.assertIsInstance(result, GeneratorType) - self.assertEqual(list(result), []) - - def test_all_items_list_items(self): - """ - Test allItems when the list widget contains some items. - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - with patch.object(widget, 'count', return_value=2), \ - patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]): - - # WHEN: Calling allItems - result = widget.allItems() - - # THEN: An instance of a Generator object should be returned. The generator should not yeild any results - self.assertIsInstance(result, GeneratorType) - self.assertEqual(list(result), [5, 3]) - - def test_paint_event(self): - """ - Test the paintEvent method when the list is not empty - """ - # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 1 - # (i.e the list has an item) - widget = ListWidgetWithDnD() - with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ - patch.object(widget, 'count', return_value=1), \ - patch.object(widget, 'viewport') as mocked_viewport: - mocked_event = MagicMock() - - # WHEN: Calling paintEvent - widget.paintEvent(mocked_event) - - # THEN: The overridden paintEvnet should have been called - mocked_paint_event.assert_called_once_with(mocked_event) - self.assertFalse(mocked_viewport.called) - - def test_paint_event_no_items(self): - """ - Test the paintEvent method when the list is empty - """ - # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 0 - # (i.e the list is empty) - widget = ListWidgetWithDnD() - mocked_painter_instance = MagicMock() - mocked_qrect = MagicMock() - with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ - patch.object(widget, 'count', return_value=0), \ - patch.object(widget, 'viewport'), \ - patch('openlp.core.ui.lib.listwidgetwithdnd.QtGui.QPainter', - return_value=mocked_painter_instance) as mocked_qpainter, \ - patch('openlp.core.ui.lib.listwidgetwithdnd.QtCore.QRect', return_value=mocked_qrect): - mocked_event = MagicMock() - - # WHEN: Calling paintEvent - widget.paintEvent(mocked_event) - - # THEN: The overridden paintEvnet should have been called, and some text should be drawn. - mocked_paint_event.assert_called_once_with(mocked_event) - mocked_qpainter.assert_called_once_with(widget.viewport()) - mocked_painter_instance.drawText.assert_called_once_with(mocked_qrect, 4100, 'No Search Results') diff --git a/tests/functional/openlp_core/ui/media/test_vlcplayer.py b/tests/functional/openlp_core/ui/media/test_vlcplayer.py index 6e2fe73c6..96ab629c6 100644 --- a/tests/functional/openlp_core/ui/media/test_vlcplayer.py +++ b/tests/functional/openlp_core/ui/media/test_vlcplayer.py @@ -693,9 +693,9 @@ class TestVLCPlayer(TestCase, TestMixin): vlc_player.set_state(MediaState.Paused, mocked_display) # WHEN: play() is called - with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait, \ - patch.object(vlc_player, 'volume') as mocked_volume: - mocked_media_state_wait.return_value = True + with patch.object(vlc_player, 'media_state_wait', return_value=True) as mocked_media_state_wait, \ + patch.object(vlc_player, 'volume') as mocked_volume, \ + patch.object(vlc_player, 'get_live_state', return_value=MediaState.Loaded): result = vlc_player.play(mocked_display) # THEN: A bunch of things should happen to play the media @@ -872,7 +872,7 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display = MagicMock() mocked_display.controller.media_info.media_type = MediaType.DVD mocked_display.vlc_media_player.is_seekable.return_value = True - mocked_display.controller.media_info.start_time = 3 + mocked_display.controller.media_info.start_time = 3000 vlc_player = VlcPlayer(None) # WHEN: seek() is called @@ -976,7 +976,7 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display = MagicMock() mocked_display.controller = mocked_controller mocked_display.vlc_media.get_state.return_value = 1 - mocked_display.vlc_media_player.get_time.return_value = 400000 + mocked_display.vlc_media_player.get_time.return_value = 400 mocked_display.controller.media_info.media_type = MediaType.DVD vlc_player = VlcPlayer(None) @@ -990,7 +990,7 @@ class TestVLCPlayer(TestCase, TestMixin): self.assertEqual(2, mocked_stop.call_count) mocked_display.vlc_media_player.get_time.assert_called_with() mocked_set_visible.assert_called_with(mocked_display, False) - mocked_controller.seek_slider.setSliderPosition.assert_called_with(300000) + mocked_controller.seek_slider.setSliderPosition.assert_called_with(300) expected_calls = [call(True), call(False)] self.assertEqual(expected_calls, mocked_controller.seek_slider.blockSignals.call_args_list) diff --git a/tests/functional/openlp_core/ui/test_aboutform.py b/tests/functional/openlp_core/ui/test_aboutform.py index c30ef588e..0cba7b008 100644 --- a/tests/functional/openlp_core/ui/test_aboutform.py +++ b/tests/functional/openlp_core/ui/test_aboutform.py @@ -62,7 +62,7 @@ class TestFirstTimeForm(TestCase, TestMixin): self.assertTrue('OpenLP 3.1.5 build 3000' in about_form.about_text_edit.toPlainText(), "The build number should be set correctly") - def test_about_form_date_test(self): + def test_about_form_date(self): """ Test that the copyright date is included correctly """ diff --git a/tests/functional/openlp_core/ui/test_first_time.py b/tests/functional/openlp_core/ui/test_first_time.py index eb9464375..2be5e1ad6 100644 --- a/tests/functional/openlp_core/ui/test_first_time.py +++ b/tests/functional/openlp_core/ui/test_first_time.py @@ -40,7 +40,7 @@ class TestFirstTimeWizard(TestMixin, TestCase): Test get_web_page will attempt CONNECTION_RETRIES+1 connections - bug 1409031 """ # GIVEN: Initial settings and mocks - mocked_requests.get.side_effect = IOError('Unable to connect') + mocked_requests.get.side_effect = OSError('Unable to connect') # WHEN: A webpage is requested try: diff --git a/tests/functional/openlp_core/ui/test_mainwindow.py b/tests/functional/openlp_core/ui/test_mainwindow.py index 4bb1cee37..9dee80c0d 100644 --- a/tests/functional/openlp_core/ui/test_mainwindow.py +++ b/tests/functional/openlp_core/ui/test_mainwindow.py @@ -26,10 +26,11 @@ import os from unittest import TestCase from unittest.mock import MagicMock, patch -from PyQt5 import QtWidgets +from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings from openlp.core.common.registry import Registry +from openlp.core.display.screens import ScreenList from openlp.core.ui.mainwindow import MainWindow from tests.helpers.testmixin import TestMixin @@ -37,8 +38,23 @@ from tests.utils.constants import TEST_RESOURCES_PATH class TestMainWindow(TestCase, TestMixin): + """ + Test the main window + """ + def _create_mock_action(self, parent, name, **kwargs): + """ + Create a fake action with some "real" attributes + """ + action = QtWidgets.QAction(parent) + action.setObjectName(name) + if kwargs.get('triggers'): + action.triggered.connect(kwargs.pop('triggers')) + return action def setUp(self): + """ + Set up the objects we need for all of the tests + """ Registry.create() self.registry = Registry() self.setup_application() @@ -48,30 +64,18 @@ class TestMainWindow(TestCase, TestMixin): self.app.args = [] Registry().register('application', self.app) Registry().set_flag('no_web_server', False) - # Mock classes and methods used by mainwindow. - with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \ - patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \ - patch('openlp.core.ui.mainwindow.LiveController') as mocked_live_controller, \ - patch('openlp.core.ui.mainwindow.PreviewController') as mocked_preview_controller, \ - patch('openlp.core.ui.mainwindow.OpenLPDockWidget') as mocked_dock_widget, \ - patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \ - patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \ - patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \ - patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \ - patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ - patch('openlp.core.ui.mainwindow.server.HttpServer'): - self.mocked_settings_form = mocked_settings_form - self.mocked_image_manager = mocked_image_manager - self.mocked_live_controller = mocked_live_controller - self.mocked_preview_controller = mocked_preview_controller - self.mocked_dock_widget = mocked_dock_widget - self.mocked_q_tool_box_class = mocked_q_tool_box_class - self.mocked_add_dock_method = mocked_add_dock_method - self.mocked_theme_manager = mocked_theme_manager - self.mocked_renderer = mocked_renderer + self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action') + self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start() + self.mocked_add_toolbar_action.side_effect = self._create_mock_action + with patch('openlp.core.display.screens.ScreenList.__instance__', spec=ScreenList) as mocked_screen_list: + mocked_screen_list.current = {'number': 0, 'size': QtCore.QSize(600, 800), 'primary': True} self.main_window = MainWindow() def tearDown(self): + """ + Delete all the C++ objects and stop all the patchers + """ + self.add_toolbar_action_patcher.stop() del self.main_window def test_cmd_line_file(self): @@ -81,13 +85,13 @@ class TestMainWindow(TestCase, TestMixin): # GIVEN a service as an argument to openlp service = os.path.join(TEST_RESOURCES_PATH, 'service', 'test.osz') self.main_window.arguments = [service] - with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_path: - # WHEN the argument is processed + # WHEN the argument is processed + with patch.object(self.main_window.service_manager, 'load_file') as mocked_load_file: self.main_window.open_cmd_line_files(service) - # THEN the service from the arguments is loaded - mocked_load_path.assert_called_with(service), 'load_path should have been called with the service\'s path' + # THEN the service from the arguments is loaded + mocked_load_file.assert_called_with(service) def test_cmd_line_arg(self): """ @@ -96,13 +100,13 @@ class TestMainWindow(TestCase, TestMixin): # GIVEN a non service file as an argument to openlp service = os.path.join('openlp.py') self.main_window.arguments = [service] - with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_path: + with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_file: # WHEN the argument is processed self.main_window.open_cmd_line_files("") # THEN the file should not be opened - assert not mocked_load_path.called, 'load_path should not have been called' + assert mocked_load_file.called is False, 'load_file should not have been called' def test_main_window_title(self): """ @@ -151,14 +155,14 @@ class TestMainWindow(TestCase, TestMixin): # WHEN: you check the started functions # THEN: the following registry functions should have been registered - self.assertEqual(len(self.registry.service_list), 6, 'The registry should have 6 services.') - self.assertEqual(len(self.registry.functions_list), 18, 'The registry should have 18 functions') - self.assertTrue('application' in self.registry.service_list, 'The application should have been registered.') - self.assertTrue('main_window' in self.registry.service_list, 'The main_window should have been registered.') - self.assertTrue('media_controller' in self.registry.service_list, 'The media_controller should have been ' - 'registered.') - self.assertTrue('plugin_manager' in self.registry.service_list, - 'The plugin_manager should have been registered.') + assert len(self.registry.service_list) == 13, \ + 'The registry should have 12 services, got {}'.format(self.registry.service_list.keys()) + assert len(self.registry.functions_list) == 19, \ + 'The registry should have 19 functions, got {}'.format(self.registry.functions_list.keys()) + assert 'application' in self.registry.service_list, 'The application should have been registered.' + assert 'main_window' in self.registry.service_list, 'The main_window should have been registered.' + assert 'media_controller' in self.registry.service_list, 'The media_controller should have been registered.' + assert 'plugin_manager' in self.registry.service_list, 'The plugin_manager should have been registered.' def test_projector_manager_hidden_on_startup(self): """ @@ -167,7 +171,7 @@ class TestMainWindow(TestCase, TestMixin): # GIVEN: A built main window # WHEN: OpenLP is started # THEN: The projector manager should be hidden - self.main_window.projector_manager_dock.setVisible.assert_called_once_with(False) + assert self.main_window.projector_manager_dock.isVisible() is False def test_on_search_shortcut_triggered_shows_media_manager(self): """ @@ -203,56 +207,38 @@ class TestMainWindow(TestCase, TestMixin): self.assertEqual(0, mocked_media_manager_dock.setVisible.call_count) mocked_widget.on_focus.assert_called_with() - @patch('openlp.core.ui.mainwindow.MainWindow.plugin_manager') - @patch('openlp.core.ui.mainwindow.MainWindow.first_time') - @patch('openlp.core.ui.mainwindow.MainWindow.application') @patch('openlp.core.ui.mainwindow.FirstTimeForm') @patch('openlp.core.ui.mainwindow.QtWidgets.QMessageBox.warning') @patch('openlp.core.ui.mainwindow.Settings') - def test_on_first_time_wizard_clicked_show_projectors_after(self, mocked_Settings, mocked_warning, - mocked_FirstTimeForm, mocked_application, - mocked_first_time, - mocked_plugin_manager): + def test_on_first_time_wizard_clicked_show_projectors_after(self, MockSettings, mocked_warning, MockWizard): + """Test that the projector manager is shown after the FTW is run""" # GIVEN: Main_window, patched things, patched "Yes" as confirmation to re-run wizard, settings to True. - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = True - mocked_Settings.return_value = mocked_Settings_obj + MockSettings.return_value.value.return_value = True mocked_warning.return_value = QtWidgets.QMessageBox.Yes - mocked_FirstTimeForm_obj = MagicMock() - mocked_FirstTimeForm_obj.was_cancelled = False - mocked_FirstTimeForm.return_value = mocked_FirstTimeForm_obj - mocked_plugin_manager.plugins = [] - self.main_window.projector_manager_dock = MagicMock() + MockWizard.return_value.was_cancelled = False - # WHEN: on_first_time_wizard_clicked is called - self.main_window.on_first_time_wizard_clicked() + with patch.object(self.main_window, 'projector_manager_dock') as mocked_dock, \ + patch.object(self.registry, 'execute'), patch.object(self.main_window, 'theme_manager_contents'): + # WHEN: on_first_time_wizard_clicked is called + self.main_window.on_first_time_wizard_clicked() # THEN: projector_manager_dock.setVisible should had been called once - self.main_window.projector_manager_dock.setVisible.assert_called_once_with(True) + mocked_dock.setVisible.assert_called_once_with(True) - @patch('openlp.core.ui.mainwindow.MainWindow.plugin_manager') - @patch('openlp.core.ui.mainwindow.MainWindow.first_time') - @patch('openlp.core.ui.mainwindow.MainWindow.application') @patch('openlp.core.ui.mainwindow.FirstTimeForm') @patch('openlp.core.ui.mainwindow.QtWidgets.QMessageBox.warning') @patch('openlp.core.ui.mainwindow.Settings') - def test_on_first_time_wizard_clicked_hide_projectors_after(self, mocked_Settings, mocked_warning, - mocked_FirstTimeForm, mocked_application, - mocked_first_time, - mocked_plugin_manager): + def test_on_first_time_wizard_clicked_hide_projectors_after(self, MockSettings, mocked_warning, MockWizard): + """Test that the projector manager is hidden after the FTW is run""" # GIVEN: Main_window, patched things, patched "Yes" as confirmation to re-run wizard, settings to False. - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = False - mocked_Settings.return_value = mocked_Settings_obj + MockSettings.return_value.value.return_value = False mocked_warning.return_value = QtWidgets.QMessageBox.Yes - mocked_FirstTimeForm_obj = MagicMock() - mocked_FirstTimeForm_obj.was_cancelled = False - mocked_FirstTimeForm.return_value = mocked_FirstTimeForm_obj - mocked_plugin_manager.plugins = [] - self.main_window.projector_manager_dock = MagicMock() + MockWizard.return_value.was_cancelled = False # WHEN: on_first_time_wizard_clicked is called - self.main_window.on_first_time_wizard_clicked() + with patch.object(self.main_window, 'projector_manager_dock') as mocked_dock, \ + patch.object(self.registry, 'execute'), patch.object(self.main_window, 'theme_manager_contents'): + self.main_window.on_first_time_wizard_clicked() # THEN: projector_manager_dock.setVisible should had been called once - self.main_window.projector_manager_dock.setVisible.assert_called_once_with(False) + mocked_dock.setVisible.assert_called_once_with(False) diff --git a/tests/functional/openlp_core/ui/test_servicemanager.py b/tests/functional/openlp_core/ui/test_servicemanager.py index 487f56362..3c0958506 100644 --- a/tests/functional/openlp_core/ui/test_servicemanager.py +++ b/tests/functional/openlp_core/ui/test_servicemanager.py @@ -32,7 +32,7 @@ from openlp.core.common import ThemeLevel from openlp.core.common.registry import Registry from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities from openlp.core.ui import ServiceManager -from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.widgets.toolbar import OpenLPToolbar class TestServiceManager(TestCase): @@ -637,7 +637,7 @@ class TestServiceManager(TestCase): Registry().register('main_window', mocked_main_window) Registry().register('application', MagicMock()) service_manager = ServiceManager(None) - service_manager._file_name = os.path.join('temp', 'filename.osz') + service_manager._service_path = os.path.join('temp', 'filename.osz') service_manager._save_lite = False service_manager.service_items = [] service_manager.service_theme = 'Default' @@ -666,7 +666,7 @@ class TestServiceManager(TestCase): Registry().register('main_window', mocked_main_window) Registry().register('application', MagicMock()) service_manager = ServiceManager(None) - service_manager._file_name = os.path.join('temp', 'filename.osz') + service_manager._service_path = os.path.join('temp', 'filename.osz') service_manager._save_lite = False service_manager.service_items = [] service_manager.service_theme = 'Default' diff --git a/tests/functional/openlp_core/ui/test_slidecontroller.py b/tests/functional/openlp_core/ui/test_slidecontroller.py index 145f779ac..bc13f6db8 100644 --- a/tests/functional/openlp_core/ui/test_slidecontroller.py +++ b/tests/functional/openlp_core/ui/test_slidecontroller.py @@ -208,6 +208,33 @@ class TestSlideController(TestCase): mocked_on_theme_display.assert_called_once_with(False) mocked_on_hide_display.assert_called_once_with(False) + def test_on_go_live_preview_controller(self): + """ + Test that when the on_go_preview() method is called the message is sent to the preview controller and focus is + set correctly. + """ + # GIVEN: A new SlideController instance and plugin preview then pressing go live should respond + mocked_display = MagicMock() + mocked_preview_controller = MagicMock() + mocked_preview_widget = MagicMock() + mocked_service_item = MagicMock() + mocked_service_item.from_service = False + mocked_preview_widget.current_slide_number.return_value = 1 + mocked_preview_widget.slide_count = MagicMock(return_value=2) + mocked_preview_controller.preview_widget = MagicMock() + Registry.create() + Registry().register('preview_controller', mocked_preview_controller) + slide_controller = SlideController(None) + slide_controller.service_item = mocked_service_item + slide_controller.preview_widget = mocked_preview_widget + slide_controller.display = mocked_display + + # WHEN: on_go_live() is called + slide_controller.on_go_preview() + + # THEN: the preview controller should have the service item and the focus set to live + mocked_preview_controller.preview_widget.setFocus.assert_called_once_with() + def test_on_go_live_live_controller(self): """ Test that when the on_go_live() method is called the message is sent to the live controller and focus is @@ -691,7 +718,7 @@ class TestSlideController(TestCase): ]) @patch('openlp.core.ui.slidecontroller.Settings') - def on_preview_double_click_unblank_display_test(self, MockedSettings): + def test_on_preview_double_click_unblank_display(self, MockedSettings): # GIVEN: A slide controller, actions needed, settins set to True. slide_controller = SlideController(None) mocked_settings = MagicMock() @@ -714,7 +741,7 @@ class TestSlideController(TestCase): self.assertEqual(0, slide_controller.on_preview_add_to_service.call_count, 'Should have not been called.') @patch('openlp.core.ui.slidecontroller.Settings') - def on_preview_double_click_add_to_service_test(self, MockedSettings): + def test_on_preview_double_click_add_to_service(self, MockedSettings): # GIVEN: A slide controller, actions needed, settins set to False. slide_controller = SlideController(None) mocked_settings = MagicMock() diff --git a/tests/functional/openlp_core/ui/test_thememanager.py b/tests/functional/openlp_core/ui/test_thememanager.py index c1ba80f1d..f44f558b0 100644 --- a/tests/functional/openlp_core/ui/test_thememanager.py +++ b/tests/functional/openlp_core/ui/test_thememanager.py @@ -52,25 +52,24 @@ class TestThemeManager(TestCase): """ shutil.rmtree(self.temp_folder) - def test_export_theme(self): + @patch('openlp.core.ui.thememanager.zipfile.ZipFile.__init__') + @patch('openlp.core.ui.thememanager.zipfile.ZipFile.write') + def test_export_theme(self, mocked_zipfile_write, mocked_zipfile_init): """ Test exporting a theme . """ # GIVEN: A new ThemeManager instance. theme_manager = ThemeManager() theme_manager.theme_path = Path(TEST_RESOURCES_PATH, 'themes') - with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \ - patch('zipfile.ZipFile.write') as mocked_zipfile_write: - mocked_zipfile_init.return_value = None + mocked_zipfile_init.return_value = None - # WHEN: The theme is exported - theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default') + # WHEN: The theme is exported + theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default') - # THEN: The zipfile should be created at the given path - mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') - mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', - 'Default', 'Default.xml'), - os.path.join('Default', 'Default.xml')) + # THEN: The zipfile should be created at the given path + mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') + mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', 'Default', 'Default.xml'), + os.path.join('Default', 'Default.xml')) def test_initial_theme_manager(self): """ @@ -83,53 +82,53 @@ class TestThemeManager(TestCase): # THEN: The the controller should be registered in the registry. self.assertIsNotNone(Registry().get('theme_manager'), 'The base theme manager should be registered') - def test_write_theme_same_image(self): + @patch('openlp.core.ui.thememanager.copyfile') + @patch('openlp.core.ui.thememanager.create_paths') + def test_write_theme_same_image(self, mocked_create_paths, mocked_copyfile): """ Test that we don't try to overwrite a theme background image with itself """ # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile, # theme, create_paths and thememanager-attributes. - with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \ - patch('openlp.core.ui.thememanager.create_paths'): - theme_manager = ThemeManager(None) - theme_manager.old_background_image = None - theme_manager.generate_and_save_image = MagicMock() - theme_manager.theme_path = MagicMock() - mocked_theme = MagicMock() - mocked_theme.theme_name = 'themename' - mocked_theme.extract_formatted_xml = MagicMock() - mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() + theme_manager = ThemeManager(None) + theme_manager.old_background_image = None + theme_manager.generate_and_save_image = MagicMock() + theme_manager.theme_path = MagicMock() + mocked_theme = MagicMock() + mocked_theme.theme_name = 'themename' + mocked_theme.extract_formatted_xml = MagicMock() + mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() - # WHEN: Calling _write_theme with path to the same image, but the path written slightly different - file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') - theme_manager._write_theme(mocked_theme, file_name1, file_name1) + # WHEN: Calling _write_theme with path to the same image, but the path written slightly different + file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') + theme_manager._write_theme(mocked_theme, file_name1, file_name1) - # THEN: The mocked_copyfile should not have been called - self.assertFalse(mocked_copyfile.called, 'copyfile should not be called') + # THEN: The mocked_copyfile should not have been called + assert mocked_copyfile.called is False, 'copyfile should not be called' - def test_write_theme_diff_images(self): + @patch('openlp.core.ui.thememanager.copyfile') + @patch('openlp.core.ui.thememanager.create_paths') + def test_write_theme_diff_images(self, mocked_create_paths, mocked_copyfile): """ Test that we do overwrite a theme background image when a new is submitted """ # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile, # theme, create_paths and thememanager-attributes. - with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \ - patch('openlp.core.ui.thememanager.create_paths'): - theme_manager = ThemeManager(None) - theme_manager.old_background_image = None - theme_manager.generate_and_save_image = MagicMock() - theme_manager.theme_path = MagicMock() - mocked_theme = MagicMock() - mocked_theme.theme_name = 'themename' - mocked_theme.filename = "filename" + theme_manager = ThemeManager(None) + theme_manager.old_background_image = None + theme_manager.generate_and_save_image = MagicMock() + theme_manager.theme_path = MagicMock() + mocked_theme = MagicMock() + mocked_theme.theme_name = 'themename' + mocked_theme.filename = "filename" - # WHEN: Calling _write_theme with path to different images - file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') - file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg') - theme_manager._write_theme(mocked_theme, file_name1, file_name2) + # WHEN: Calling _write_theme with path to different images + file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') + file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg') + theme_manager._write_theme(mocked_theme, file_name1, file_name2) - # THEN: The mocked_copyfile should not have been called - self.assertTrue(mocked_copyfile.called, 'copyfile should be called') + # THEN: The mocked_copyfile should not have been called + assert mocked_copyfile.called is True, 'copyfile should be called' def test_write_theme_special_char_name(self): """ @@ -151,45 +150,43 @@ class TestThemeManager(TestCase): self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme æ„› name', 'theme æ„› name.json')), 'Theme with special characters should have been created!') - def test_over_write_message_box_yes(self): + @patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', return_value=QtWidgets.QMessageBox.Yes) + @patch('openlp.core.ui.thememanager.translate') + def test_over_write_message_box_yes(self, mocked_translate, mocked_qmessagebox_question): """ Test that theme_manager.over_write_message_box returns True when the user clicks yes. """ # GIVEN: A patched QMessageBox.question and an instance of ThemeManager - with patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', - return_value=QtWidgets.QMessageBox.Yes) as mocked_qmessagebox_question,\ - patch('openlp.core.ui.thememanager.translate') as mocked_translate: - mocked_translate.side_effect = lambda context, text: text - theme_manager = ThemeManager(None) + mocked_translate.side_effect = lambda context, text: text + theme_manager = ThemeManager(None) - # WHEN: Calling over_write_message_box with 'Theme Name' - result = theme_manager.over_write_message_box('Theme Name') + # WHEN: Calling over_write_message_box with 'Theme Name' + result = theme_manager.over_write_message_box('Theme Name') - # THEN: over_write_message_box should return True and the message box should contain the theme name - self.assertTrue(result) - mocked_qmessagebox_question.assert_called_once_with( - theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', - defaultButton=ANY) + # THEN: over_write_message_box should return True and the message box should contain the theme name + assert result is True + mocked_qmessagebox_question.assert_called_once_with( + theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', + defaultButton=ANY) - def test_over_write_message_box_no(self): + @patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', return_value=QtWidgets.QMessageBox.No) + @patch('openlp.core.ui.thememanager.translate') + def test_over_write_message_box_no(self, mocked_translate, mocked_qmessagebox_question): """ Test that theme_manager.over_write_message_box returns False when the user clicks no. """ # GIVEN: A patched QMessageBox.question and an instance of ThemeManager - with patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', return_value=QtWidgets.QMessageBox.No)\ - as mocked_qmessagebox_question,\ - patch('openlp.core.ui.thememanager.translate') as mocked_translate: - mocked_translate.side_effect = lambda context, text: text - theme_manager = ThemeManager(None) + mocked_translate.side_effect = lambda context, text: text + theme_manager = ThemeManager(None) - # WHEN: Calling over_write_message_box with 'Theme Name' - result = theme_manager.over_write_message_box('Theme Name') + # WHEN: Calling over_write_message_box with 'Theme Name' + result = theme_manager.over_write_message_box('Theme Name') - # THEN: over_write_message_box should return False and the message box should contain the theme name - self.assertFalse(result) - mocked_qmessagebox_question.assert_called_once_with( - theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', - defaultButton=ANY) + # THEN: over_write_message_box should return False and the message box should contain the theme name + assert result is False + mocked_qmessagebox_question.assert_called_once_with( + theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', + defaultButton=ANY) def test_unzip_theme(self): """ @@ -202,16 +199,16 @@ class TestThemeManager(TestCase): theme_manager._create_theme_from_xml = MagicMock() theme_manager.generate_and_save_image = MagicMock() theme_manager.theme_path = None - folder = Path(mkdtemp()) + folder_path = Path(mkdtemp()) theme_file = Path(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz') # WHEN: We try to unzip it - theme_manager.unzip_theme(theme_file, folder) + theme_manager.unzip_theme(theme_file, folder_path) # THEN: Files should be unpacked - self.assertTrue((folder / 'Moss on tree' / 'Moss on tree.xml').exists()) + self.assertTrue((folder_path / 'Moss on tree' / 'Moss on tree.xml').exists()) self.assertEqual(mocked_critical_error_message_box.call_count, 0, 'No errors should have happened') - shutil.rmtree(str(folder)) + folder_path.rmtree() def test_unzip_theme_invalid_version(self): """ diff --git a/tests/interfaces/openlp_core_lib/__init__.py b/tests/functional/openlp_core/widgets/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_lib/__init__.py rename to tests/functional/openlp_core/widgets/__init__.py diff --git a/tests/functional/openlp_core/ui/lib/test_colorbutton.py b/tests/functional/openlp_core/widgets/test_buttons.py similarity index 70% rename from tests/functional/openlp_core/ui/lib/test_colorbutton.py rename to tests/functional/openlp_core/widgets/test_buttons.py index b010cbbdc..4abd38da4 100644 --- a/tests/functional/openlp_core/ui/lib/test_colorbutton.py +++ b/tests/functional/openlp_core/widgets/test_buttons.py @@ -20,12 +20,12 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -This module contains tests for the openlp.core.ui.lib.colorbutton module +This module contains tests for the openlp.core.widgets.buttons module """ from unittest import TestCase from unittest.mock import MagicMock, call, patch -from openlp.core.ui.lib import ColorButton +from openlp.core.widgets.buttons import ColorButton class TestColorDialog(TestCase): @@ -33,11 +33,11 @@ class TestColorDialog(TestCase): Test the :class:`~openlp.core.lib.colorbutton.ColorButton` class """ def setUp(self): - self.change_color_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.change_color') - self.clicked_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.clicked') - self.color_changed_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.colorChanged') - self.qt_gui_patcher = patch('openlp.core.ui.lib.colorbutton.QtWidgets') - self.translate_patcher = patch('openlp.core.ui.lib.colorbutton.translate', **{'return_value': 'Tool Tip Text'}) + self.change_color_patcher = patch('openlp.core.widgets.buttons.ColorButton.change_color') + self.clicked_patcher = patch('openlp.core.widgets.buttons.ColorButton.clicked') + self.color_changed_patcher = patch('openlp.core.widgets.buttons.ColorButton.colorChanged') + self.qt_gui_patcher = patch('openlp.core.widgets.buttons.QtWidgets') + self.translate_patcher = patch('openlp.core.widgets.buttons.translate', **{'return_value': 'Tool Tip Text'}) self.addCleanup(self.change_color_patcher.stop) self.addCleanup(self.clicked_patcher.stop) self.addCleanup(self.color_changed_patcher.stop) @@ -49,41 +49,40 @@ class TestColorDialog(TestCase): self.mocked_qt_widgets = self.qt_gui_patcher.start() self.mocked_translate = self.translate_patcher.start() - def test_constructor(self): + @patch('openlp.core.widgets.buttons.ColorButton.setToolTip') + def test_constructor(self, mocked_set_tool_tip): """ Test that constructing a ColorButton object works correctly """ # GIVEN: The ColorButton class, a mocked change_color, setToolTip methods and clicked signal - with patch('openlp.core.ui.lib.colorbutton.ColorButton.setToolTip') as mocked_set_tool_tip: + # WHEN: The ColorButton object is instantiated + widget = ColorButton() - # WHEN: The ColorButton object is instantiated - widget = ColorButton() + # THEN: The widget __init__ method should have the correct properties and methods called + self.assertEqual(widget.parent, None, + 'The parent should be the same as the one that the class was instianted with') + self.mocked_change_color.assert_called_once_with('#ffffff') + mocked_set_tool_tip.assert_called_once_with('Tool Tip Text') + self.mocked_clicked.connect.assert_called_once_with(widget.on_clicked) - # THEN: The widget __init__ method should have the correct properties and methods called - self.assertEqual(widget.parent, None, - 'The parent should be the same as the one that the class was instianted with') - self.mocked_change_color.assert_called_once_with('#ffffff') - mocked_set_tool_tip.assert_called_once_with('Tool Tip Text') - self.mocked_clicked.connect.assert_called_once_with(widget.on_clicked) - - def test_change_color(self): + @patch('openlp.core.widgets.buttons.ColorButton.setStyleSheet') + def test_change_color(self, mocked_set_style_sheet): """ Test that change_color sets the new color and the stylesheet """ self.change_color_patcher.stop() # GIVEN: An instance of the ColorButton object, and a mocked out setStyleSheet - with patch('openlp.core.ui.lib.colorbutton.ColorButton.setStyleSheet') as mocked_set_style_sheet: - widget = ColorButton() + widget = ColorButton() - # WHEN: Changing the color - widget.change_color('#000000') + # WHEN: Changing the color + widget.change_color('#000000') - # THEN: The _color attribute should be set to #000000 and setStyleSheet should have been called twice - self.assertEqual(widget._color, '#000000', '_color should have been set to #000000') - mocked_set_style_sheet.assert_has_calls( - [call('background-color: #ffffff'), call('background-color: #000000')]) + # THEN: The _color attribute should be set to #000000 and setStyleSheet should have been called twice + self.assertEqual(widget._color, '#000000', '_color should have been set to #000000') + mocked_set_style_sheet.assert_has_calls( + [call('background-color: #ffffff'), call('background-color: #000000')]) self.mocked_change_color = self.change_color_patcher.start() @@ -91,22 +90,6 @@ class TestColorDialog(TestCase): """ Test that the color property method returns the set color """ - - # GIVEN: An instance of ColorButton, with a set _color attribute - widget = ColorButton() - widget._color = '#000000' - - # WHEN: Accesing the color property - value = widget.color - - # THEN: The value set in _color should be returned - self.assertEqual(value, '#000000', 'The value returned should be equal to the one we set') - - def test_color(self): - """ - Test that the color property method returns the set color - """ - # GIVEN: An instance of ColorButton, with a set _color attribute widget = ColorButton() widget._color = '#000000' @@ -117,20 +100,19 @@ class TestColorDialog(TestCase): # THEN: The value set in _color should be returned self.assertEqual(value, '#000000', 'The value returned should be equal to the one we set') + # @patch('openlp.core.widgets.buttons.ColorButton.__init__', **{'return_value': None}) def test_color_setter(self): """ Test that the color property setter method sets the color """ - # GIVEN: An instance of ColorButton, with a mocked __init__ - with patch('openlp.core.ui.lib.colorbutton.ColorButton.__init__', **{'return_value': None}): - widget = ColorButton() + widget = ColorButton() - # WHEN: Setting the color property - widget.color = '#000000' + # WHEN: Setting the color property + widget.color = '#000000' - # THEN: Then change_color should have been called with the value we set - self.mocked_change_color.assert_called_once_with('#000000') + # THEN: Then change_color should have been called with the value we set + self.mocked_change_color.assert_called_with('#000000') def test_on_clicked_invalid_color(self): """ diff --git a/tests/functional/openlp_core/ui/lib/test_filedialog.py b/tests/functional/openlp_core/widgets/test_dialogs.py similarity index 95% rename from tests/functional/openlp_core/ui/lib/test_filedialog.py rename to tests/functional/openlp_core/widgets/test_dialogs.py index 777ff65ec..fd65de33e 100755 --- a/tests/functional/openlp_core/ui/lib/test_filedialog.py +++ b/tests/functional/openlp_core/widgets/test_dialogs.py @@ -5,12 +5,12 @@ from unittest.mock import patch from PyQt5 import QtWidgets from openlp.core.common.path import Path -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog class TestFileDialogPatches(TestCase): """ - Tests for the :mod:`openlp.core.ui.lib.filedialogpatches` module + Tests for the :mod:`openlp.core.widgets.dialogs` module """ def test_file_dialog(self): @@ -55,7 +55,7 @@ class TestFileDialogPatches(TestCase): order """ # GIVEN: FileDialog - with patch('openlp.core.ui.lib.filedialog.QtWidgets.QFileDialog.getExistingDirectory', return_value='') \ + with patch('openlp.core.widgets.dialogs.QtWidgets.QFileDialog.getExistingDirectory', return_value='') \ as mocked_get_existing_directory: # WHEN: Calling the getExistingDirectory method with all parameters set diff --git a/tests/functional/openlp_core/ui/lib/test_pathedit.py b/tests/functional/openlp_core/widgets/test_edits.py similarity index 81% rename from tests/functional/openlp_core/ui/lib/test_pathedit.py rename to tests/functional/openlp_core/widgets/test_edits.py index 227a4317a..5ce6dc9df 100755 --- a/tests/functional/openlp_core/ui/lib/test_pathedit.py +++ b/tests/functional/openlp_core/widgets/test_edits.py @@ -20,23 +20,24 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -This module contains tests for the openlp.core.ui.lib.pathedit module +This module contains tests for the openlp.core.widgets.edits module """ import os from unittest import TestCase from unittest.mock import MagicMock, PropertyMock, patch from openlp.core.common.path import Path -from openlp.core.ui.lib import PathEdit, PathType -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType +from openlp.core.widgets.dialogs import FileDialog class TestPathEdit(TestCase): """ - Test the :class:`~openlp.core.lib.pathedit.PathEdit` class + Test the :class:`~openlp.core.widgets.edits.PathEdit` class """ def setUp(self): - with patch('openlp.core.ui.lib.pathedit.PathEdit._setup'): + with patch('openlp.core.widgets.edits.PathEdit._setup'): self.widget = PathEdit() def test_path_getter(self): @@ -73,7 +74,7 @@ class TestPathEdit(TestCase): # GIVEN: An instance of PathEdit # WHEN: Reading the `path` property # THEN: The default value should be returned - self.assertEqual(self.widget.path_type, PathType.Files) + self.assertEqual(self.widget.path_type, PathEditType.Files) def test_path_type_setter(self): """ @@ -83,11 +84,11 @@ class TestPathEdit(TestCase): with patch.object(self.widget, 'update_button_tool_tips') as mocked_update_button_tool_tips: # WHEN: Writing to a different value than default to the `path_type` property - self.widget.path_type = PathType.Directories + self.widget.path_type = PathEditType.Directories # THEN: The `_path_type` instance variable should be set with the test data and not the default. The # update_button_tool_tips should have been called. - self.assertEqual(self.widget._path_type, PathType.Directories) + self.assertEqual(self.widget._path_type, PathEditType.Directories) mocked_update_button_tool_tips.assert_called_once_with() def test_update_button_tool_tips_directories(self): @@ -97,7 +98,7 @@ class TestPathEdit(TestCase): # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` self.widget.browse_button = MagicMock() self.widget.revert_button = MagicMock() - self.widget._path_type = PathType.Directories + self.widget._path_type = PathEditType.Directories # WHEN: Calling update_button_tool_tips self.widget.update_button_tool_tips() @@ -112,7 +113,7 @@ class TestPathEdit(TestCase): # GIVEN: An instance of PathEdit with the `path_type` set to `Files` self.widget.browse_button = MagicMock() self.widget.revert_button = MagicMock() - self.widget._path_type = PathType.Files + self.widget._path_type = PathEditType.Files # WHEN: Calling update_button_tool_tips self.widget.update_button_tool_tips() @@ -120,26 +121,25 @@ class TestPathEdit(TestCase): self.widget.browse_button.setToolTip.assert_called_once_with('Browse for file.') self.widget.revert_button.setToolTip.assert_called_once_with('Revert to default file.') - def test_on_browse_button_clicked_directory(self): + @patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory', return_value=None) + @patch('openlp.core.widgets.edits.FileDialog.getOpenFileName') + def test_on_browse_button_clicked_directory(self, mocked_get_open_file_name, mocked_get_existing_directory): """ Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Directories. """ # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked # QFileDialog.getExistingDirectory - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory', return_value=None) as \ - mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName') as mocked_get_open_file_name: - self.widget._path_type = PathType.Directories - self.widget._path = Path('test', 'path') + self.widget._path_type = PathEditType.Directories + self.widget._path = Path('test', 'path') - # WHEN: Calling on_browse_button_clicked - self.widget.on_browse_button_clicked() + # WHEN: Calling on_browse_button_clicked + self.widget.on_browse_button_clicked() - # THEN: The FileDialog.getExistingDirectory should have been called with the default caption - mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory', - Path('test', 'path'), - FileDialog.ShowDirsOnly) - self.assertFalse(mocked_get_open_file_name.called) + # THEN: The FileDialog.getExistingDirectory should have been called with the default caption + mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory', + Path('test', 'path'), + FileDialog.ShowDirsOnly) + self.assertFalse(mocked_get_open_file_name.called) def test_on_browse_button_clicked_directory_custom_caption(self): """ @@ -148,10 +148,10 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked # QFileDialog.getExistingDirectory with `default_caption` set. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory', return_value=None) as \ + with patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory', return_value=None) as \ mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName') as mocked_get_open_file_name: - self.widget._path_type = PathType.Directories + patch('openlp.core.widgets.edits.FileDialog.getOpenFileName') as mocked_get_open_file_name: + self.widget._path_type = PathEditType.Directories self.widget._path = Path('test', 'path') self.widget.dialog_caption = 'Directory Caption' @@ -169,10 +169,10 @@ class TestPathEdit(TestCase): Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Files. """ # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \ + with patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ + patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(None, '')) as \ mocked_get_open_file_name: - self.widget._path_type = PathType.Files + self.widget._path_type = PathEditType.Files self.widget._path = Path('test', 'pat.h') # WHEN: Calling on_browse_button_clicked @@ -190,10 +190,10 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName # with `default_caption` set. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \ + with patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ + patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(None, '')) as \ mocked_get_open_file_name: - self.widget._path_type = PathType.Files + self.widget._path_type = PathEditType.Files self.widget._path = Path('test', 'pat.h') self.widget.dialog_caption = 'File Caption' @@ -212,7 +212,7 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns an empty str for the # file path. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \ + with patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(None, '')) as \ mocked_get_open_file_name: # WHEN: Calling on_browse_button_clicked @@ -228,7 +228,7 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns a str for the file # path. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', + with patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(Path('test', 'pat.h'), '')) as mocked_get_open_file_name, \ patch.object(self.widget, 'on_new_path'): @@ -272,7 +272,7 @@ class TestPathEdit(TestCase): Test `on_new_path` when called with a path that is the same as the existing path. """ # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal - with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock): + with patch('openlp.core.widgets.edits.PathEdit.path', new_callable=PropertyMock): self.widget._path = Path('/old', 'test', 'pat.h') self.widget.pathChanged = MagicMock() @@ -287,7 +287,7 @@ class TestPathEdit(TestCase): Test `on_new_path` when called with a path that is the different to the existing path. """ # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal - with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock): + with patch('openlp.core.widgets.edits.PathEdit.path', new_callable=PropertyMock): self.widget._path = Path('/old', 'test', 'pat.h') self.widget.pathChanged = MagicMock() diff --git a/tests/functional/openlp_core/ui/lib/test_listpreviewwidget.py b/tests/functional/openlp_core/widgets/test_views.py similarity index 75% rename from tests/functional/openlp_core/ui/lib/test_listpreviewwidget.py rename to tests/functional/openlp_core/widgets/test_views.py index a64ae5e0e..0fa028f11 100644 --- a/tests/functional/openlp_core/ui/lib/test_listpreviewwidget.py +++ b/tests/functional/openlp_core/widgets/test_views.py @@ -20,15 +20,17 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.ui.lib.listpreviewwidget package. +Package to test the openlp.core.widgets.views package. """ +from types import GeneratorType from unittest import TestCase from unittest.mock import MagicMock, patch, call from PyQt5 import QtGui -from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget +from openlp.core.common.i18n import UiStrings from openlp.core.lib import ImageSource +from openlp.core.widgets.views import ListPreviewWidget, ListWidgetWithDnD, TreeWidgetWithDnD class TestListPreviewWidget(TestCase): @@ -38,13 +40,13 @@ class TestListPreviewWidget(TestCase): Mock out stuff for all the tests """ # Mock self.parent().width() - self.parent_patcher = patch('openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.parent') + self.parent_patcher = patch('openlp.core.widgets.views.ListPreviewWidget.parent') self.mocked_parent = self.parent_patcher.start() self.mocked_parent.width.return_value = 100 self.addCleanup(self.parent_patcher.stop) # Mock Settings().value() - self.Settings_patcher = patch('openlp.core.ui.lib.listpreviewwidget.Settings') + self.Settings_patcher = patch('openlp.core.widgets.views.Settings') self.mocked_Settings = self.Settings_patcher.start() self.mocked_Settings_obj = MagicMock() self.mocked_Settings_obj.value.return_value = None @@ -52,7 +54,7 @@ class TestListPreviewWidget(TestCase): self.addCleanup(self.Settings_patcher.stop) # Mock self.viewport().width() - self.viewport_patcher = patch('openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.viewport') + self.viewport_patcher = patch('openlp.core.widgets.views.ListPreviewWidget.viewport') self.mocked_viewport = self.viewport_patcher.start() self.mocked_viewport_obj = MagicMock() self.mocked_viewport_obj.width.return_value = 200 @@ -72,9 +74,9 @@ class TestListPreviewWidget(TestCase): self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None') self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.image_manager') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.image_manager') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_service_item_thumbs(self, mocked_setRowHeight, mocked_resizeRowsToContents, mocked_image_manager): """ @@ -119,8 +121,8 @@ class TestListPreviewWidget(TestCase): call('TEST3', ImageSource.CommandPlugins), call('TEST4', ImageSource.CommandPlugins)] mocked_image_manager.get_image.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_text(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, txt slides unchanged in replace_service_item & __recalc... @@ -151,8 +153,8 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_img(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, img slides unchanged in replace_service_item & __recalc... @@ -188,8 +190,8 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400), call(0, 400), call(1, 400)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, img slides resized in replace_service_item & __recalc... @@ -223,8 +225,8 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_img_auto(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." auto, img slides resized in replace_service_item & __recalc... @@ -261,9 +263,9 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 100), call(1, 100), call(0, 150), call(1, 150), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, text-based slides not affected in row_resized. @@ -295,9 +297,9 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, image-based slides not affected in row_resized. @@ -332,9 +334,9 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, image-based slides are scaled in row_resized. @@ -367,9 +369,9 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_setting_changed(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled while item live, program doesn't crash on row_resized. @@ -402,10 +404,10 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should fail self.assertRaises(Exception) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.selectRow') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.slide_count') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.selectRow') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.item') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.slide_count') def test_autoscroll_setting_invalid(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): """ Test if 'advanced/autoscrolling' setting None or invalid, that no autoscrolling occurs on change_slide(). @@ -438,10 +440,10 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_selectRow.call_count, 0, 'Should not be called') self.assertEquals(mocked_item.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.selectRow') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.slide_count') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.selectRow') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.item') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.slide_count') def test_autoscroll_dist_bounds(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): """ Test if 'advanced/autoscrolling' setting asks to scroll beyond list bounds, that it does not beyond. @@ -468,10 +470,10 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 0), call(0, 0)] mocked_item.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.selectRow') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.slide_count') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.selectRow') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.item') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.slide_count') def test_autoscroll_normal(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): """ Test if 'advanced/autoscrolling' setting valid, autoscrolling called as expected. @@ -499,3 +501,129 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_item.call_count, 3, 'Should be called') calls = [call(0, 0), call(1, 0), call(2, 0)] mocked_item.assert_has_calls(calls) + + +class TestListWidgetWithDnD(TestCase): + """ + Test the :class:`~openlp.core.widgets.views.ListWidgetWithDnD` class + """ + def test_clear(self): + """ + Test the clear method when called without any arguments. + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + + # WHEN: Calling clear with out any arguments + widget.clear() + + # THEN: The results text should be the standard 'no results' text. + self.assertEqual(widget.no_results_text, UiStrings().NoResults) + + def test_clear_search_while_typing(self): + """ + Test the clear method when called with the search_while_typing argument set to True + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + + # WHEN: Calling clear with search_while_typing set to True + widget.clear(search_while_typing=True) + + # THEN: The results text should be the 'short results' text. + self.assertEqual(widget.no_results_text, UiStrings().ShortResults) + + def test_all_items_no_list_items(self): + """ + Test allItems when there are no items in the list widget + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + with patch.object(widget, 'count', return_value=0), \ + patch.object(widget, 'item', side_effect=lambda x: [][x]): + + # WHEN: Calling allItems + result = widget.allItems() + + # THEN: An instance of a Generator object should be returned. The generator should not yeild any results + self.assertIsInstance(result, GeneratorType) + self.assertEqual(list(result), []) + + def test_all_items_list_items(self): + """ + Test allItems when the list widget contains some items. + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + with patch.object(widget, 'count', return_value=2), \ + patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]): + + # WHEN: Calling allItems + result = widget.allItems() + + # THEN: An instance of a Generator object should be returned. The generator should not yeild any results + self.assertIsInstance(result, GeneratorType) + self.assertEqual(list(result), [5, 3]) + + def test_paint_event(self): + """ + Test the paintEvent method when the list is not empty + """ + # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 1 + # (i.e the list has an item) + widget = ListWidgetWithDnD() + with patch('openlp.core.widgets.views.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ + patch.object(widget, 'count', return_value=1), \ + patch.object(widget, 'viewport') as mocked_viewport: + mocked_event = MagicMock() + + # WHEN: Calling paintEvent + widget.paintEvent(mocked_event) + + # THEN: The overridden paintEvnet should have been called + mocked_paint_event.assert_called_once_with(mocked_event) + self.assertFalse(mocked_viewport.called) + + def test_paint_event_no_items(self): + """ + Test the paintEvent method when the list is empty + """ + # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 0 + # (i.e the list is empty) + widget = ListWidgetWithDnD() + mocked_painter_instance = MagicMock() + mocked_qrect = MagicMock() + with patch('openlp.core.widgets.views.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ + patch.object(widget, 'count', return_value=0), \ + patch.object(widget, 'viewport'), \ + patch('openlp.core.widgets.views.QtGui.QPainter', + return_value=mocked_painter_instance) as mocked_qpainter, \ + patch('openlp.core.widgets.views.QtCore.QRect', return_value=mocked_qrect): + mocked_event = MagicMock() + + # WHEN: Calling paintEvent + widget.paintEvent(mocked_event) + + # THEN: The overridden paintEvnet should have been called, and some text should be drawn. + mocked_paint_event.assert_called_once_with(mocked_event) + mocked_qpainter.assert_called_once_with(widget.viewport()) + mocked_painter_instance.drawText.assert_called_once_with(mocked_qrect, 4100, 'No Search Results') + + +class TestTreeWidgetWithDnD(TestCase): + """ + Test the :class:`~openlp.core.widgets.views.TreeWidgetWithDnD` class + """ + def test_constructor(self): + """ + Test the constructor + """ + # GIVEN: A TreeWidgetWithDnD + # WHEN: An instance is created + widget = TreeWidgetWithDnD(name='Test') + + # THEN: It should be initialised correctly + assert widget.mime_data_text == 'Test' + assert widget.allow_internal_dnd is False + assert widget.indentation() == 0 + assert widget.isAnimated() is True diff --git a/tests/functional/openlp_plugins/bibles/test_bibleimport.py b/tests/functional/openlp_plugins/bibles/test_bibleimport.py index a75741d76..9b9a8fae8 100644 --- a/tests/functional/openlp_plugins/bibles/test_bibleimport.py +++ b/tests/functional/openlp_plugins/bibles/test_bibleimport.py @@ -30,6 +30,7 @@ from lxml import etree, objectify from PyQt5.QtWidgets import QDialog from openlp.core.common.i18n import Language +from openlp.core.common.path import Path from openlp.core.lib.exceptions import ValidationError from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.db import BibleDB @@ -48,7 +49,7 @@ class TestBibleImport(TestCase): b' <data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data>\n' b'</root>' ) - self.open_patcher = patch('builtins.open') + self.open_patcher = patch.object(Path, 'open') self.addCleanup(self.open_patcher.stop) self.mocked_open = self.open_patcher.start() self.critical_error_message_box_patcher = \ @@ -66,7 +67,7 @@ class TestBibleImport(TestCase): self.addCleanup(self.registry_patcher.stop) self.registry_patcher.start() - def init_kwargs_none_test(self): + def test_init_kwargs_none(self): """ Test the initialisation of the BibleImport Class when no key word arguments are supplied """ @@ -74,282 +75,284 @@ class TestBibleImport(TestCase): # WHEN: Creating an instance of BibleImport with no key word arguments instance = BibleImport(MagicMock()) - # THEN: The filename attribute should be None - self.assertIsNone(instance.filename) - self.assertIsInstance(instance, BibleDB) + # THEN: The file_path attribute should be None + assert instance.file_path is None + assert isinstance(instance, BibleDB) - def init_kwargs_set_test(self): + def test_init_kwargs_set(self): """ Test the initialisation of the BibleImport Class when supplied with select keyword arguments """ # GIVEN: A patched BibleDB._setup, BibleImport class and mocked parent # WHEN: Creating an instance of BibleImport with selected key word arguments - kwargs = {'filename': 'bible.xml'} + kwargs = {'file_path': 'bible.xml'} instance = BibleImport(MagicMock(), **kwargs) - # THEN: The filename keyword should be set to bible.xml - self.assertEqual(instance.filename, 'bible.xml') - self.assertIsInstance(instance, BibleDB) + # THEN: The file_path keyword should be set to bible.xml + assert instance.file_path == 'bible.xml' + assert isinstance(instance, BibleDB) - def get_language_canceled_test(self): + @patch.object(BibleDB, '_setup') + @patch('openlp.plugins.bibles.forms.LanguageForm') + def test_get_language_canceled(self, MockedLanguageForm, mocked_setup): """ Test the BibleImport.get_language method when the user rejects the dialog box """ # GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Rejected and an instance of BibleDB - with patch.object(BibleDB, '_setup'), patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form: + # TODO: The integer value of QtDialog.Rejected is 0. Using the enumeration causes a seg fault for some reason + MockedLanguageForm.return_value.exec.return_value = 0 + instance = BibleImport(MagicMock()) + mocked_wizard = MagicMock() + instance.wizard = mocked_wizard - # The integer value of QtDialog.Rejected is 0. Using the enumeration causes a seg fault for some reason - mocked_language_form_instance = MagicMock(**{'exec.return_value': 0}) - mocked_language_form.return_value = mocked_language_form_instance - instance = BibleImport(MagicMock()) - mocked_wizard = MagicMock() - instance.wizard = mocked_wizard + # WHEN: Calling get_language() + result = instance.get_language('ESV') - # WHEN: Calling get_language() - result = instance.get_language() + # THEN: get_language() should return False + MockedLanguageForm.assert_called_once_with(mocked_wizard) + MockedLanguageForm.return_value.exec.assert_called_once_with('ESV') + assert result is False, 'get_language() should return False if the user rejects the dialog box' - # THEN: get_language() should return False - mocked_language_form.assert_called_once_with(mocked_wizard) - mocked_language_form_instance.exec.assert_called_once_with(None) - self.assertFalse(result, 'get_language() should return False if the user rejects the dialog box') - - def get_language_accepted_test(self): + @patch.object(BibleDB, 'save_meta') + @patch.object(BibleDB, '_setup') + @patch('openlp.plugins.bibles.forms.LanguageForm') + def test_get_language_accepted(self, MockedLanguageForm, mocked_setup, mocked_save_meta): """ Test the BibleImport.get_language method when the user accepts the dialog box """ # GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Accepted an instance of BibleDB and # a combobox with the selected item data as 10 - with patch.object(BibleDB, 'save_meta'), patch.object(BibleDB, '_setup'), \ - patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form: + # The integer value of QtDialog.Accepted is 1. Using the enumeration causes a seg fault for some reason + MockedLanguageForm.return_value.exec.return_value = 1 + MockedLanguageForm.return_value.language_combo_box.itemData.return_value = 10 + instance = BibleImport(MagicMock()) + mocked_wizard = MagicMock() + instance.wizard = mocked_wizard - # The integer value of QtDialog.Accepted is 1. Using the enumeration causes a seg fault for some reason - mocked_language_form_instance = MagicMock(**{'exec.return_value': 1, - 'language_combo_box.itemData.return_value': 10}) - mocked_language_form.return_value = mocked_language_form_instance - instance = BibleImport(MagicMock()) - mocked_wizard = MagicMock() - instance.wizard = mocked_wizard + # WHEN: Calling get_language() + result = instance.get_language('Bible Name') - # WHEN: Calling get_language() - result = instance.get_language('Bible Name') + # THEN: get_language() should return the id of the selected language in the combo box + MockedLanguageForm.assert_called_once_with(mocked_wizard) + MockedLanguageForm.return_value.exec.assert_called_once_with('Bible Name') + assert result == 10, 'get_language() should return the id of the language the user has chosen when ' \ + 'they accept the dialog box' - # THEN: get_language() should return the id of the selected language in the combo box - mocked_language_form.assert_called_once_with(mocked_wizard) - mocked_language_form_instance.exec.assert_called_once_with('Bible Name') - self.assertEqual(result, 10, 'get_language() should return the id of the language the user has chosen when ' - 'they accept the dialog box') - - def get_language_id_language_found_test(self): + @patch('openlp.plugins.bibles.lib.bibleimport.get_language') + @patch.object(BibleImport, 'get_language') + def test_get_language_id_language_found(self, mocked_db_get_language, mocked_get_language): """ Test get_language_id() when called with a name found in the languages list """ # GIVEN: A mocked languages.get_language which returns language and an instance of BibleImport - with patch('openlp.core.common.languages.get_language', return_value=Language(30, 'English', 'en')) \ - as mocked_languages_get_language, \ - patch.object(BibleImport, 'get_language') as mocked_db_get_language: - instance = BibleImport(MagicMock()) - instance.save_meta = MagicMock() + mocked_get_language.return_value = Language(30, 'English', 'en') + instance = BibleImport(MagicMock()) + instance.save_meta = MagicMock() - # WHEN: Calling get_language_id() with a language name and bible name - result = instance.get_language_id('English', 'KJV') + # WHEN: Calling get_language_id() with a language name and bible name + result = instance.get_language_id('English', 'KJV') - # THEN: The id of the language returned from languages.get_language should be returned - mocked_languages_get_language.assert_called_once_with('English') - self.assertFalse(mocked_db_get_language.called) - instance.save_meta.assert_called_once_with('language_id', 30) - self.assertEqual(result, 30) + # THEN: The id of the language returned from languages.get_language should be returned + mocked_get_language.assert_called_once_with('English') + assert mocked_db_get_language.called is False + instance.save_meta.assert_called_once_with('language_id', 30) + assert result == 30, 'Result should be 30, was {}'.format(result) - def get_language_id_language_not_found_test(self): + @patch('openlp.plugins.bibles.lib.bibleimport.get_language', return_value=None) + @patch.object(BibleImport, 'get_language', return_value=20) + def test_get_language_id_language_not_found(self, mocked_db_get_language, mocked_languages_get_language): """ Test get_language_id() when called with a name not found in the languages list """ # GIVEN: A mocked languages.get_language which returns language and an instance of BibleImport - with patch('openlp.core.common.languages.get_language', return_value=None) as mocked_languages_get_language, \ - patch.object(BibleImport, 'get_language', return_value=20) as mocked_db_get_language: - instance = BibleImport(MagicMock()) - instance.save_meta = MagicMock() + instance = BibleImport(MagicMock()) + instance.save_meta = MagicMock() - # WHEN: Calling get_language_id() with a language name and bible name - result = instance.get_language_id('RUS', 'KJV') + # WHEN: Calling get_language_id() with a language name and bible name + result = instance.get_language_id('RUS', 'KJV') - # THEN: The id of the language returned from languages.get_language should be returned - mocked_languages_get_language.assert_called_once_with('RUS') - mocked_db_get_language.assert_called_once_with('KJV') - instance.save_meta.assert_called_once_with('language_id', 20) - self.assertEqual(result, 20) + # THEN: The id of the language returned from languages.get_language should be returned + mocked_languages_get_language.assert_called_once_with('RUS') + mocked_db_get_language.assert_called_once_with('KJV') + instance.save_meta.assert_called_once_with('language_id', 20) + assert result == 20 - def get_language_id_user_choice_test(self): + @patch('openlp.plugins.bibles.lib.bibleimport.get_language', return_value=None) + @patch.object(BibleImport, 'get_language', return_value=40) + @patch.object(BibleImport, 'log_error') + def test_get_language_id_user_choice(self, mocked_log_error, mocked_db_get_language, mocked_languages_get_language): """ Test get_language_id() when the language is not found and the user is asked for the language """ # GIVEN: A mocked languages.get_language which returns None a mocked BibleDB.get_language which returns a # language id. - with patch('openlp.core.common.languages.get_language', return_value=None) as mocked_languages_get_language, \ - patch.object(BibleImport, 'get_language', return_value=40) as mocked_db_get_language, \ - patch.object(BibleImport, 'log_error') as mocked_log_error: - instance = BibleImport(MagicMock()) - instance.save_meta = MagicMock() + instance = BibleImport(MagicMock()) + instance.save_meta = MagicMock() - # WHEN: Calling get_language_id() with a language name and bible name - result = instance.get_language_id('English', 'KJV') + # WHEN: Calling get_language_id() with a language name and bible name + result = instance.get_language_id('English', 'KJV') - # THEN: The id of the language returned from BibleDB.get_language should be returned - mocked_languages_get_language.assert_called_once_with('English') - mocked_db_get_language.assert_called_once_with('KJV') - self.assertFalse(mocked_log_error.error.called) - instance.save_meta.assert_called_once_with('language_id', 40) - self.assertEqual(result, 40) + # THEN: The id of the language returned from BibleDB.get_language should be returned + mocked_languages_get_language.assert_called_once_with('English') + mocked_db_get_language.assert_called_once_with('KJV') + assert mocked_log_error.error.called is False + instance.save_meta.assert_called_once_with('language_id', 40) + assert result == 40 - def get_language_id_user_choice_rejected_test(self): + @patch('openlp.plugins.bibles.lib.bibleimport.get_language', return_value=None) + @patch.object(BibleImport, 'get_language', return_value=None) + @patch.object(BibleImport, 'log_error') + def test_get_language_id_user_choice_rejected(self, mocked_log_error, mocked_db_get_language, + mocked_languages_get_language): """ Test get_language_id() when the language is not found and the user rejects the dilaog box """ # GIVEN: A mocked languages.get_language which returns None a mocked BibleDB.get_language which returns a # language id. - with patch('openlp.core.common.languages.get_language', return_value=None) as mocked_languages_get_language, \ - patch.object(BibleImport, 'get_language', return_value=None) as mocked_db_get_language, \ - patch.object(BibleImport, 'log_error') as mocked_log_error: - instance = BibleImport(MagicMock()) - instance.save_meta = MagicMock() + instance = BibleImport(MagicMock()) + instance.save_meta = MagicMock() - # WHEN: Calling get_language_id() with a language name and bible name - result = instance.get_language_id('Qwerty', 'KJV') + # WHEN: Calling get_language_id() with a language name and bible name + result = instance.get_language_id('Qwerty', 'KJV') - # THEN: None should be returned and an error should be logged - mocked_languages_get_language.assert_called_once_with('Qwerty') - mocked_db_get_language.assert_called_once_with('KJV') - mocked_log_error.assert_called_once_with( - 'Language detection failed when importing from "KJV". User aborted language selection.') - self.assertFalse(instance.save_meta.called) - self.assertIsNone(result) + # THEN: None should be returned and an error should be logged + mocked_languages_get_language.assert_called_once_with('Qwerty') + mocked_db_get_language.assert_called_once_with('KJV') + mocked_log_error.assert_called_once_with( + 'Language detection failed when importing from "KJV". User aborted language selection.') + assert instance.save_meta.called is False + assert result is None - def get_book_ref_id_by_name_get_book_test(self): + @patch.object(BibleImport, 'log_debug') + @patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', **{'get_book.return_value': {'id': 20}}) + def test_get_book_ref_id_by_name_get_book(self, MockBibleResourcesDB, mocked_log_debug): """ Test get_book_ref_id_by_name when the book is found as a book in BiblesResourcesDB """ # GIVEN: An instance of BibleImport and a mocked BiblesResourcesDB which returns a book id when get_book is # called - with patch.object(BibleImport, 'log_debug'), \ - patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', - **{'get_book.return_value': {'id': 20}}): - instance = BibleImport(MagicMock()) + instance = BibleImport(MagicMock()) - # WHEN: Calling get_book_ref_id_by_name - result = instance.get_book_ref_id_by_name('Gen', 66, 4) + # WHEN: Calling get_book_ref_id_by_name + result = instance.get_book_ref_id_by_name('Gen', 66, 4) - # THEN: The bible id should be returned - self.assertEqual(result, 20) + # THEN: The bible id should be returned + assert result == 20 - def get_book_ref_id_by_name_get_alternative_book_name_test(self): + @patch.object(BibleImport, 'log_debug') + @patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', + **{'get_book.return_value': None, 'get_alternative_book_name.return_value': 30}) + def test_get_book_ref_id_by_name_get_alternative_book_name(self, MockBibleResourcesDB, mocked_log_debug): """ Test get_book_ref_id_by_name when the book is found as an alternative book in BiblesResourcesDB """ # GIVEN: An instance of BibleImport and a mocked BiblesResourcesDB which returns a book id when # get_alternative_book_name is called - with patch.object(BibleImport, 'log_debug'), \ - patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', - **{'get_book.return_value': None, 'get_alternative_book_name.return_value': 30}): - instance = BibleImport(MagicMock()) + instance = BibleImport(MagicMock()) - # WHEN: Calling get_book_ref_id_by_name - result = instance.get_book_ref_id_by_name('Gen', 66, 4) + # WHEN: Calling get_book_ref_id_by_name + result = instance.get_book_ref_id_by_name('Gen', 66, 4) - # THEN: The bible id should be returned - self.assertEqual(result, 30) + # THEN: The bible id should be returned + assert result == 30 - def get_book_ref_id_by_name_get_book_reference_id_test(self): + @patch.object(BibleImport, 'log_debug') + @patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', + **{'get_book.return_value': None, 'get_alternative_book_name.return_value': None}) + @patch('openlp.plugins.bibles.lib.bibleimport.AlternativeBookNamesDB', **{'get_book_reference_id.return_value': 40}) + def test_get_book_ref_id_by_name_get_book_reference_id(self, MockAlterativeBookNamesDB, MockBibleResourcesDB, + mocked_log_debug): """ Test get_book_ref_id_by_name when the book is found as a book in AlternativeBookNamesDB """ # GIVEN: An instance of BibleImport and a mocked AlternativeBookNamesDB which returns a book id when # get_book_reference_id is called - with patch.object(BibleImport, 'log_debug'), \ - patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', - **{'get_book.return_value': None, 'get_alternative_book_name.return_value': None}), \ - patch('openlp.plugins.bibles.lib.bibleimport.AlternativeBookNamesDB', - **{'get_book_reference_id.return_value': 40}): - instance = BibleImport(MagicMock()) + instance = BibleImport(MagicMock()) - # WHEN: Calling get_book_ref_id_by_name - result = instance.get_book_ref_id_by_name('Gen', 66, 4) + # WHEN: Calling get_book_ref_id_by_name + result = instance.get_book_ref_id_by_name('Gen', 66, 4) - # THEN: The bible id should be returned - self.assertEqual(result, 40) + # THEN: The bible id should be returned + assert result == 40 - def get_book_ref_id_by_name_book_name_form_rejected_test(self): + @patch.object(BibleImport, 'log_debug') + @patch.object(BibleImport, 'get_books') + @patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', + **{'get_book.return_value': None, 'get_alternative_book_name.return_value': None}) + @patch('openlp.plugins.bibles.lib.bibleimport.AlternativeBookNamesDB', + **{'get_book_reference_id.return_value': None}) + @patch('openlp.plugins.bibles.forms.BookNameForm', + return_value=MagicMock(**{'exec.return_value': QDialog.Rejected})) + def test_get_book_ref_id_by_name_book_name_form_rejected(self, MockBookNameForm, MockAlterativeBookNamesDB, + MockBibleResourcesDB, mocked_get_books, mocked_log_debug): """ Test get_book_ref_id_by_name when the user rejects the BookNameForm """ # GIVEN: An instance of BibleImport and a mocked BookNameForm which simulates a user rejecting the dialog - with patch.object(BibleImport, 'log_debug'), patch.object(BibleImport, 'get_books'), \ - patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', - **{'get_book.return_value': None, 'get_alternative_book_name.return_value': None}), \ - patch('openlp.plugins.bibles.lib.bibleimport.AlternativeBookNamesDB', - **{'get_book_reference_id.return_value': None}), \ - patch('openlp.plugins.bibles.forms.BookNameForm', - return_value=MagicMock(**{'exec.return_value': QDialog.Rejected})): - instance = BibleImport(MagicMock()) + instance = BibleImport(MagicMock()) - # WHEN: Calling get_book_ref_id_by_name - result = instance.get_book_ref_id_by_name('Gen', 66, 4) + # WHEN: Calling get_book_ref_id_by_name + result = instance.get_book_ref_id_by_name('Gen', 66, 4) - # THEN: None should be returned - self.assertIsNone(result) + # THEN: None should be returned + assert result is None - def get_book_ref_id_by_name_book_name_form_accepted_test(self): + @patch.object(BibleImport, 'log_debug') + @patch.object(BibleImport, 'get_books') + @patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', + **{'get_book.return_value': None, 'get_alternative_book_name.return_value': None}) + @patch('openlp.plugins.bibles.lib.bibleimport.AlternativeBookNamesDB', + **{'get_book_reference_id.return_value': None}) + @patch('openlp.plugins.bibles.forms.BookNameForm', + return_value=MagicMock(**{'exec.return_value': QDialog.Accepted, 'book_id': 50})) + def test_get_book_ref_id_by_name_book_name_form_accepted(self, MockBookNameForm, MockAlterativeBookNamesDB, + MockBibleResourcesDB, mocked_get_books, mocked_log_debug): """ Test get_book_ref_id_by_name when the user accepts the BookNameForm """ # GIVEN: An instance of BibleImport and a mocked BookNameForm which simulates a user accepting the dialog - with patch.object(BibleImport, 'log_debug'), patch.object(BibleImport, 'get_books'), \ - patch('openlp.plugins.bibles.lib.bibleimport.BiblesResourcesDB', - **{'get_book.return_value': None, 'get_alternative_book_name.return_value': None}), \ - patch('openlp.plugins.bibles.lib.bibleimport.AlternativeBookNamesDB', - **{'get_book_reference_id.return_value': None}) as mocked_alternative_book_names_db, \ - patch('openlp.plugins.bibles.forms.BookNameForm', - return_value=MagicMock(**{'exec.return_value': QDialog.Accepted, 'book_id': 50})): - instance = BibleImport(MagicMock()) + instance = BibleImport(MagicMock()) - # WHEN: Calling get_book_ref_id_by_name - result = instance.get_book_ref_id_by_name('Gen', 66, 4) + # WHEN: Calling get_book_ref_id_by_name + result = instance.get_book_ref_id_by_name('Gen', 66, 4) - # THEN: An alternative book name should be created and a bible id should be returned - mocked_alternative_book_names_db.create_alternative_book_name.assert_called_once_with('Gen', 50, 4) - self.assertEqual(result, 50) + # THEN: An alternative book name should be created and a bible id should be returned + MockAlterativeBookNamesDB.create_alternative_book_name.assert_called_once_with('Gen', 50, 4) + assert result == 50 - def is_compressed_compressed_test(self): + @patch('openlp.plugins.bibles.lib.bibleimport.is_zipfile', return_value=True) + def test_is_compressed_compressed(self, mocked_is_zipfile): """ Test is_compressed when the 'file' being tested is compressed """ # GIVEN: An instance of BibleImport and a mocked is_zipfile which returns True - with patch('openlp.plugins.bibles.lib.bibleimport.is_zipfile', return_value=True): - instance = BibleImport(MagicMock()) + instance = BibleImport(MagicMock()) - # WHEN: Calling is_compressed - result = instance.is_compressed('file.ext') + # WHEN: Calling is_compressed + result = instance.is_compressed('file.ext') - # THEN: Then critical_error_message_box should be called informing the user that the file is compressed and - # True should be returned - self.mocked_critical_error_message_box.assert_called_once_with( - message='The file "file.ext" you supplied is compressed. You must decompress it before import.') - self.assertTrue(result) + # THEN: Then critical_error_message_box should be called informing the user that the file is compressed and + # True should be returned + self.mocked_critical_error_message_box.assert_called_once_with( + message='The file "file.ext" you supplied is compressed. You must decompress it before import.') + assert result is True - def is_compressed_not_compressed_test(self): + @patch('openlp.plugins.bibles.lib.bibleimport.is_zipfile', return_value=False) + def test_is_compressed_not_compressed(self, mocked_is_zipfile): """ Test is_compressed when the 'file' being tested is not compressed """ # GIVEN: An instance of BibleImport and a mocked is_zipfile which returns False - with patch('openlp.plugins.bibles.lib.bibleimport.is_zipfile', return_value=False): - instance = BibleImport(MagicMock()) + instance = BibleImport(MagicMock()) - # WHEN: Calling is_compressed - result = instance.is_compressed('file.ext') + # WHEN: Calling is_compressed + result = instance.is_compressed('file.ext') - # THEN: False should be returned and critical_error_message_box should not have been called - self.assertFalse(result) - self.assertFalse(self.mocked_critical_error_message_box.called) + # THEN: False should be returned and critical_error_message_box should not have been called + assert result is False + assert self.mocked_critical_error_message_box.called is False - def parse_xml_etree_test(self): + def test_parse_xml_etree(self): """ Test BibleImport.parse_xml() when called with the use_objectify default value """ @@ -359,15 +362,14 @@ class TestBibleImport(TestCase): instance.wizard = MagicMock() # WHEN: Calling parse_xml - result = instance.parse_xml('file.tst') + result = instance.parse_xml(Path('file.tst')) # THEN: The result returned should contain the correct data, and should be an instance of eetree_Element - self.assertEqual(etree.tostring(result), - b'<root>\n <data><div>Test<p>data</p><a>to</a>keep</div></data>\n' - b' <data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data>\n</root>') - self.assertIsInstance(result, etree._Element) + assert etree.tostring(result) == b'<root>\n <data><div>Test<p>data</p><a>to</a>keep</div></data>\n' \ + b' <data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data>\n</root>' + assert isinstance(result, etree._Element) - def parse_xml_etree_use_objectify_test(self): + def test_parse_xml_etree_use_objectify(self): """ Test BibleImport.parse_xml() when called with use_objectify set to True """ @@ -377,15 +379,14 @@ class TestBibleImport(TestCase): instance.wizard = MagicMock() # WHEN: Calling parse_xml - result = instance.parse_xml('file.tst', use_objectify=True) + result = instance.parse_xml(Path('file.tst'), use_objectify=True) # THEN: The result returned should contain the correct data, and should be an instance of ObjectifiedElement - self.assertEqual(etree.tostring(result), - b'<root><data><div>Test<p>data</p><a>to</a>keep</div></data>' - b'<data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data></root>') - self.assertIsInstance(result, objectify.ObjectifiedElement) + assert etree.tostring(result) == b'<root><data><div>Test<p>data</p><a>to</a>keep</div></data>' \ + b'<data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data></root>' + assert isinstance(result, objectify.ObjectifiedElement) - def parse_xml_elements_test(self): + def test_parse_xml_elements(self): """ Test BibleImport.parse_xml() when given a tuple of elements to remove """ @@ -396,13 +397,13 @@ class TestBibleImport(TestCase): instance.wizard = MagicMock() # WHEN: Calling parse_xml, with a test file - result = instance.parse_xml('file.tst', elements=elements) + result = instance.parse_xml(Path('file.tst'), elements=elements) # THEN: The result returned should contain the correct data - self.assertEqual(etree.tostring(result), - b'<root>\n <data><div>Test<p>data</p><a>to</a>keep</div></data>\n <data/>\n</root>') + assert etree.tostring(result) == \ + b'<root>\n <data><div>Test<p>data</p><a>to</a>keep</div></data>\n <data/>\n</root>' - def parse_xml_tags_test(self): + def test_parse_xml_tags(self): """ Test BibleImport.parse_xml() when given a tuple of tags to remove """ @@ -413,13 +414,13 @@ class TestBibleImport(TestCase): instance.wizard = MagicMock() # WHEN: Calling parse_xml, with a test file - result = instance.parse_xml('file.tst', tags=tags) + result = instance.parse_xml(Path('file.tst'), tags=tags) # THEN: The result returned should contain the correct data - self.assertEqual(etree.tostring(result), b'<root>\n <data>Testdatatokeep</data>\n <data><unsupported>Test' - b'<x>data</x><y>to</y>discard</unsupported></data>\n</root>') + assert etree.tostring(result) == b'<root>\n <data>Testdatatokeep</data>\n <data><unsupported>Test' \ + b'<x>data</x><y>to</y>discard</unsupported></data>\n</root>' - def parse_xml_elements_tags_test(self): + def test_parse_xml_elements_tags(self): """ Test BibleImport.parse_xml() when given a tuple of elements and of tags to remove """ @@ -431,61 +432,61 @@ class TestBibleImport(TestCase): instance.wizard = MagicMock() # WHEN: Calling parse_xml, with a test file - result = instance.parse_xml('file.tst', elements=elements, tags=tags) + result = instance.parse_xml(Path('file.tst'), elements=elements, tags=tags) # THEN: The result returned should contain the correct data - self.assertEqual(etree.tostring(result), b'<root>\n <data>Testdatatokeep</data>\n <data/>\n</root>') + assert etree.tostring(result) == b'<root>\n <data>Testdatatokeep</data>\n <data/>\n</root>' - def parse_xml_file_file_not_found_exception_test(self): + @patch.object(BibleImport, 'log_exception') + def test_parse_xml_file_file_not_found_exception(self, mocked_log_exception): """ Test that parse_xml handles a FileNotFoundError exception correctly """ - with patch.object(BibleImport, 'log_exception') as mocked_log_exception: - # GIVEN: A mocked open which raises a FileNotFoundError and an instance of BibleImporter - exception = FileNotFoundError() - exception.filename = 'file.tst' - exception.strerror = 'No such file or directory' - self.mocked_open.side_effect = exception - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + # GIVEN: A mocked open which raises a FileNotFoundError and an instance of BibleImporter + exception = FileNotFoundError() + exception.filename = 'file.tst' + exception.strerror = 'No such file or directory' + self.mocked_open.side_effect = exception + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling parse_xml - result = importer.parse_xml('file.tst') + # WHEN: Calling parse_xml + result = importer.parse_xml(Path('file.tst')) - # THEN: parse_xml should have caught the error, informed the user and returned None - mocked_log_exception.assert_called_once_with('Opening file.tst failed.') - self.mocked_critical_error_message_box.assert_called_once_with( - title='An Error Occured When Opening A File', - message='The following error occurred when trying to open\nfile.tst:\n\nNo such file or directory') - self.assertIsNone(result) + # THEN: parse_xml should have caught the error, informed the user and returned None + mocked_log_exception.assert_called_once_with('Opening file.tst failed.') + self.mocked_critical_error_message_box.assert_called_once_with( + title='An Error Occured When Opening A File', + message='The following error occurred when trying to open\nfile.tst:\n\nNo such file or directory') + assert result is None - def parse_xml_file_permission_error_exception_test(self): + @patch.object(BibleImport, 'log_exception') + def test_parse_xml_file_permission_error_exception(self, mocked_log_exception): """ Test that parse_xml handles a PermissionError exception correctly """ - with patch.object(BibleImport, 'log_exception') as mocked_log_exception: - # GIVEN: A mocked open which raises a PermissionError and an instance of BibleImporter - exception = PermissionError() - exception.filename = 'file.tst' - exception.strerror = 'Permission denied' - self.mocked_open.side_effect = exception - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + # GIVEN: A mocked open which raises a PermissionError and an instance of BibleImporter + exception = PermissionError() + exception.filename = 'file.tst' + exception.strerror = 'Permission denied' + self.mocked_open.side_effect = exception + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling parse_xml - result = importer.parse_xml('file.tst') + # WHEN: Calling parse_xml + result = importer.parse_xml(Path('file.tst')) - # THEN: parse_xml should have caught the error, informed the user and returned None - mocked_log_exception.assert_called_once_with('Opening file.tst failed.') - self.mocked_critical_error_message_box.assert_called_once_with( - title='An Error Occured When Opening A File', - message='The following error occurred when trying to open\nfile.tst:\n\nPermission denied') - self.assertIsNone(result) + # THEN: parse_xml should have caught the error, informed the user and returned None + mocked_log_exception.assert_called_once_with('Opening file.tst failed.') + self.mocked_critical_error_message_box.assert_called_once_with( + title='An Error Occured When Opening A File', + message='The following error occurred when trying to open\nfile.tst:\n\nPermission denied') + assert result is None - def set_current_chapter_test(self): + def test_set_current_chapter(self): """ Test set_current_chapter """ # GIVEN: An instance of BibleImport and a mocked wizard - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) importer.wizard = MagicMock() # WHEN: Calling set_current_chapter @@ -494,121 +495,121 @@ class TestBibleImport(TestCase): # THEN: Increment_progress_bar should have been called with a text string importer.wizard.increment_progress_bar.assert_called_once_with('Importing Book_Name Chapter...') - def validate_xml_file_compressed_file_test(self): + @patch.object(BibleImport, 'is_compressed', return_value=True) + def test_validate_xml_file_compressed_file(self, mocked_is_compressed): """ Test that validate_xml_file raises a ValidationError when is_compressed returns True """ # GIVEN: A mocked parse_xml which returns None - with patch.object(BibleImport, 'is_compressed', return_value=True): - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling is_compressed - # THEN: ValidationError should be raised, with the message 'Compressed file' - with self.assertRaises(ValidationError) as context: - importer.validate_xml_file('file.name', 'xbible') - self.assertEqual(context.exception.msg, 'Compressed file') + # WHEN: Calling is_compressed + # THEN: ValidationError should be raised, with the message 'Compressed file' + with self.assertRaises(ValidationError) as context: + importer.validate_xml_file('file.name', 'xbible') + assert context.exception.msg == 'Compressed file' - def validate_xml_file_parse_xml_fails_test(self): + @patch.object(BibleImport, 'parse_xml', return_value=None) + @patch.object(BibleImport, 'is_compressed', return_value=False) + def test_validate_xml_file_parse_xml_fails(self, mocked_is_compressed, mocked_parse_xml): """ Test that validate_xml_file raises a ValidationError when parse_xml returns None """ # GIVEN: A mocked parse_xml which returns None - with patch.object(BibleImport, 'parse_xml', return_value=None), \ - patch.object(BibleImport, 'is_compressed', return_value=False): - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling validate_xml_file - # THEN: ValidationError should be raised, with the message 'Error when opening file' - # the user that an OpenSong bible was found - with self.assertRaises(ValidationError) as context: - importer.validate_xml_file('file.name', 'xbible') - self.assertEqual(context.exception.msg, 'Error when opening file') + # WHEN: Calling validate_xml_file + # THEN: ValidationError should be raised, with the message 'Error when opening file' + # the user that an OpenSong bible was found + with self.assertRaises(ValidationError) as context: + importer.validate_xml_file('file.name', 'xbible') + assert context.exception.msg == 'Error when opening file' - def validate_xml_file_success_test(self): + @patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring('<bible></bible>')) + @patch.object(BibleImport, 'is_compressed', return_value=False) + def test_validate_xml_file_success(self, mocked_is_compressed, mocked_parse_xml): """ Test that validate_xml_file returns True with valid XML """ # GIVEN: Some test data with an OpenSong Bible "bible" root tag - with patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring('<bible></bible>')), \ - patch.object(BibleImport, 'is_compressed', return_value=False): - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling validate_xml_file - result = importer.validate_xml_file('file.name', 'bible') + # WHEN: Calling validate_xml_file + result = importer.validate_xml_file('file.name', 'bible') - # THEN: True should be returned - self.assertTrue(result) + # THEN: True should be returned + assert result is True - def validate_xml_file_opensong_root_test(self): + @patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring('<bible></bible>')) + @patch.object(BibleImport, 'is_compressed', return_value=False) + def test_validate_xml_file_opensong_root(self, mocked_is_compressed, mocked_parse_xml): """ Test that validate_xml_file raises a ValidationError with an OpenSong root tag """ # GIVEN: Some test data with an Zefania root tag and an instance of BibleImport - with patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring('<bible></bible>')), \ - patch.object(BibleImport, 'is_compressed', return_value=False): - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling validate_xml_file - # THEN: ValidationError should be raised, and the critical error message box should was called informing - # the user that an OpenSong bible was found - with self.assertRaises(ValidationError) as context: - importer.validate_xml_file('file.name', 'xbible') - self.assertEqual(context.exception.msg, 'Invalid xml.') - self.mocked_critical_error_message_box.assert_called_once_with( - message='Incorrect Bible file type supplied. This looks like an OpenSong XML bible.') + # WHEN: Calling validate_xml_file + # THEN: ValidationError should be raised, and the critical error message box should was called informing + # the user that an OpenSong bible was found + with self.assertRaises(ValidationError) as context: + importer.validate_xml_file('file.name', 'xbible') + assert context.exception.msg == 'Invalid xml.' + self.mocked_critical_error_message_box.assert_called_once_with( + message='Incorrect Bible file type supplied. This looks like an OpenSong XML bible.') - def validate_xml_file_osis_root_test(self): + @patch.object(BibleImport, 'parse_xml') + @patch.object(BibleImport, 'is_compressed', return_value=False) + def test_validate_xml_file_osis_root(self, mocked_is_compressed, mocked_parse_xml): """ Test that validate_xml_file raises a ValidationError with an OSIS root tag """ # GIVEN: Some test data with an Zefania root tag and an instance of BibleImport - with patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring( - '<osis xmlns=\'http://www.bibletechnologies.net/2003/OSIS/namespace\'></osis>')), \ - patch.object(BibleImport, 'is_compressed', return_value=False): - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + mocked_parse_xml.return_value = objectify.fromstring( + '<osis xmlns=\'http://www.bibletechnologies.net/2003/OSIS/namespace\'></osis>') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling validate_xml_file - # THEN: ValidationError should be raised, and the critical error message box should was called informing - # the user that an OSIS bible was found - with self.assertRaises(ValidationError) as context: - importer.validate_xml_file('file.name', 'xbible') - self.assertEqual(context.exception.msg, 'Invalid xml.') - self.mocked_critical_error_message_box.assert_called_once_with( - message='Incorrect Bible file type supplied. This looks like an OSIS XML bible.') + # WHEN: Calling validate_xml_file + # THEN: ValidationError should be raised, and the critical error message box should was called informing + # the user that an OSIS bible was found + with self.assertRaises(ValidationError) as context: + importer.validate_xml_file('file.name', 'xbible') + assert context.exception.msg == 'Invalid xml.' + self.mocked_critical_error_message_box.assert_called_once_with( + message='Incorrect Bible file type supplied. This looks like an OSIS XML bible.') - def validate_xml_file_zefania_root_test(self): + @patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring('<xmlbible></xmlbible>')) + @patch.object(BibleImport, 'is_compressed', return_value=False) + def test_validate_xml_file_zefania_root(self, mocked_is_compressed, mocked_parse_xml): """ Test that validate_xml_file raises a ValidationError with an Zefania root tag """ # GIVEN: Some test data with an Zefania root tag and an instance of BibleImport - with patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring('<xmlbible></xmlbible>')), \ - patch.object(BibleImport, 'is_compressed', return_value=False): - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling validate_xml_file - # THEN: ValidationError should be raised, and the critical error message box should was called informing - # the user that an Zefania bible was found - with self.assertRaises(ValidationError) as context: - importer.validate_xml_file('file.name', 'xbible') - self.assertEqual(context.exception.msg, 'Invalid xml.') - self.mocked_critical_error_message_box.assert_called_once_with( - message='Incorrect Bible file type supplied. This looks like an Zefania XML bible.') + # WHEN: Calling validate_xml_file + # THEN: ValidationError should be raised, and the critical error message box should was called informing + # the user that an Zefania bible was found + with self.assertRaises(ValidationError) as context: + importer.validate_xml_file('file.name', 'xbible') + assert context.exception.msg == 'Invalid xml.' + self.mocked_critical_error_message_box.assert_called_once_with( + message='Incorrect Bible file type supplied. This looks like an Zefania XML bible.') - def validate_xml_file_unknown_root_test(self): + @patch.object(BibleImport, 'parse_xml', return_value=objectify.fromstring('<unknownbible></unknownbible>')) + @patch.object(BibleImport, 'is_compressed', return_value=False) + def test_validate_xml_file_unknown_root(self, mocked_is_compressed, mocked_parse_xml): """ Test that validate_xml_file raises a ValidationError with an unknown root tag """ # GIVEN: Some test data with an unknown root tag and an instance of BibleImport - with patch.object( - BibleImport, 'parse_xml', return_value=objectify.fromstring('<unknownbible></unknownbible>')), \ - patch.object(BibleImport, 'is_compressed', return_value=False): - importer = BibleImport(MagicMock(), path='.', name='.', filename='') + importer = BibleImport(MagicMock(), path='.', name='.', file_path=None) - # WHEN: Calling validate_xml_file - # THEN: ValidationError should be raised, and the critical error message box should was called informing - # the user that a unknown xml bible was found - with self.assertRaises(ValidationError) as context: - importer.validate_xml_file('file.name', 'xbible') - self.assertEqual(context.exception.msg, 'Invalid xml.') - self.mocked_critical_error_message_box.assert_called_once_with( - message='Incorrect Bible file type supplied. This looks like an unknown type of XML bible.') + # WHEN: Calling validate_xml_file + # THEN: ValidationError should be raised, and the critical error message box should was called informing + # the user that a unknown xml bible was found + with self.assertRaises(ValidationError) as context: + importer.validate_xml_file('file.name', 'xbible') + assert context.exception.msg == 'Invalid xml.' + self.mocked_critical_error_message_box.assert_called_once_with( + message='Incorrect Bible file type supplied. This looks like an unknown type of XML bible.') diff --git a/tests/functional/openlp_plugins/bibles/test_csvimport.py b/tests/functional/openlp_plugins/bibles/test_csvimport.py index 63d3d5282..4d948f53b 100644 --- a/tests/functional/openlp_plugins/bibles/test_csvimport.py +++ b/tests/functional/openlp_plugins/bibles/test_csvimport.py @@ -29,6 +29,7 @@ from collections import namedtuple from unittest import TestCase from unittest.mock import ANY, MagicMock, PropertyMock, call, patch +from openlp.core.common.path import Path from openlp.core.lib.exceptions import ValidationError from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.importers.csvbible import Book, CSVBible, Verse @@ -59,14 +60,15 @@ class TestCSVImport(TestCase): mocked_manager = MagicMock() # WHEN: An importer object is created - importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv') + importer = \ + CSVBible(mocked_manager, path='.', name='.', books_path=Path('books.csv'), verse_path=Path('verse.csv')) # THEN: The importer should be an instance of BibleImport self.assertIsInstance(importer, BibleImport) - self.assertEqual(importer.books_file, 'books.csv') - self.assertEqual(importer.verses_file, 'verse.csv') + self.assertEqual(importer.books_path, Path('books.csv')) + self.assertEqual(importer.verses_path, Path('verse.csv')) - def book_namedtuple_test(self): + def test_book_namedtuple(self): """ Test that the Book namedtuple is created as expected """ @@ -80,7 +82,7 @@ class TestCSVImport(TestCase): self.assertEqual(result.name, 'name') self.assertEqual(result.abbreviation, 'abbreviation') - def verse_namedtuple_test(self): + def test_verse_namedtuple(self): """ Test that the Verse namedtuple is created as expected """ @@ -94,7 +96,7 @@ class TestCSVImport(TestCase): self.assertEqual(result.number, 'number') self.assertEqual(result.text, 'text') - def get_book_name_id_test(self): + def test_get_book_name_id(self): """ Test that get_book_name() returns the correct book when called with an id """ @@ -109,7 +111,7 @@ class TestCSVImport(TestCase): # THEN: get_book_name() should return the book name associated with that id from the books dictionary self.assertEqual(actual_result, expected_result) - def get_book_name_test(self): + def test_get_book_name(self): """ Test that get_book_name() returns the name when called with a non integer value """ @@ -124,7 +126,7 @@ class TestCSVImport(TestCase): # THEN: get_book_name() should return the input self.assertEqual(actual_result, expected_result) - def parse_csv_file_test(self): + def test_parse_csv_file(self): """ Test the parse_csv_file() with sample data """ @@ -134,58 +136,59 @@ class TestCSVImport(TestCase): with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding', return_value={'encoding': 'utf-8', 'confidence': 0.99}),\ - patch('openlp.plugins.bibles.lib.importers.csvbible.open', create=True) as mocked_open,\ + patch('openlp.plugins.bibles.lib.importers.csvbible.Path.open', create=True) as mocked_open,\ patch('openlp.plugins.bibles.lib.importers.csvbible.csv.reader', return_value=iter(test_data)) as mocked_reader: # WHEN: Calling the CSVBible parse_csv_file method with a file name and TestTuple - result = CSVBible.parse_csv_file('file.csv', TestTuple) + result = CSVBible.parse_csv_file(Path('file.csv'), TestTuple) # THEN: A list of TestTuple instances with the parsed data should be returned self.assertEqual(result, [TestTuple('1', 'Line 1', 'Data 1'), TestTuple('2', 'Line 2', 'Data 2'), TestTuple('3', 'Line 3', 'Data 3')]) - mocked_open.assert_called_once_with('file.csv', 'r', encoding='utf-8', newline='') + mocked_open.assert_called_once_with('r', encoding='utf-8', newline='') mocked_reader.assert_called_once_with(ANY, delimiter=',', quotechar='"') - def parse_csv_file_oserror_test(self): + def test_parse_csv_file_oserror(self): """ Test the parse_csv_file() handles an OSError correctly """ # GIVEN: Mocked a mocked open object which raises an OSError with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding', return_value={'encoding': 'utf-8', 'confidence': 0.99}),\ - patch('openlp.plugins.bibles.lib.importers.csvbible.open', side_effect=OSError, create=True): + patch('openlp.plugins.bibles.lib.importers.csvbible.Path.open', side_effect=OSError, create=True): # WHEN: Calling CSVBible.parse_csv_file # THEN: A ValidationError should be raised with self.assertRaises(ValidationError) as context: - CSVBible.parse_csv_file('file.csv', None) + CSVBible.parse_csv_file(Path('file.csv'), None) self.assertEqual(context.exception.msg, 'Parsing "file.csv" failed') - def parse_csv_file_csverror_test(self): + def test_parse_csv_file_csverror(self): """ Test the parse_csv_file() handles an csv.Error correctly """ # GIVEN: Mocked a csv.reader which raises an csv.Error with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding', return_value={'encoding': 'utf-8', 'confidence': 0.99}),\ - patch('openlp.plugins.bibles.lib.importers.csvbible.open', create=True),\ + patch('openlp.plugins.bibles.lib.importers.csvbible.Path.open', create=True),\ patch('openlp.plugins.bibles.lib.importers.csvbible.csv.reader', side_effect=csv.Error): # WHEN: Calling CSVBible.parse_csv_file # THEN: A ValidationError should be raised with self.assertRaises(ValidationError) as context: - CSVBible.parse_csv_file('file.csv', None) + CSVBible.parse_csv_file(Path('file.csv'), None) self.assertEqual(context.exception.msg, 'Parsing "file.csv" failed') - def process_books_stopped_import_test(self): + def test_process_books_stopped_import(self): """ Test process books when the import is stopped """ # GIVEN: An instance of CSVBible with the stop_import_flag set to True mocked_manager = MagicMock() with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'): - importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv') + importer = CSVBible(mocked_manager, path='.', name='.', books_path=Path('books.csv'), + verse_path=Path('verse.csv')) type(importer).application = PropertyMock() importer.stop_import_flag = True importer.wizard = MagicMock() @@ -197,7 +200,7 @@ class TestCSVImport(TestCase): self.assertFalse(importer.wizard.increment_progress_bar.called) self.assertEqual(result, {}) - def process_books_test(self): + def test_process_books(self): """ Test process books when it completes successfully """ @@ -205,7 +208,8 @@ class TestCSVImport(TestCase): mocked_manager = MagicMock() with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\ patch('openlp.plugins.bibles.lib.importers.csvbible.translate'): - importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv') + importer = CSVBible(mocked_manager, path='.', name='.', books_path=Path('books.csv'), + verse_path=Path('verse.csv')) importer.find_and_create_book = MagicMock() importer.language_id = 10 importer.stop_import_flag = False @@ -222,14 +226,15 @@ class TestCSVImport(TestCase): [call('1. Mosebog', 2, 10), call('2. Mosebog', 2, 10)]) self.assertDictEqual(result, {1: '1. Mosebog', 2: '2. Mosebog'}) - def process_verses_stopped_import_test(self): + def test_process_verses_stopped_import(self): """ Test process_verses when the import is stopped """ # GIVEN: An instance of CSVBible with the stop_import_flag set to True mocked_manager = MagicMock() with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'): - importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv') + importer = CSVBible(mocked_manager, path='.', name='.', books_path=Path('books.csv'), + verse_path=Path('verse.csv')) importer.get_book_name = MagicMock() importer.session = MagicMock() importer.stop_import_flag = True @@ -242,7 +247,7 @@ class TestCSVImport(TestCase): self.assertFalse(importer.get_book_name.called) self.assertIsNone(result) - def process_verses_successful_test(self): + def test_process_verses_successful(self): """ Test process_verses when the import is successful """ @@ -250,7 +255,8 @@ class TestCSVImport(TestCase): mocked_manager = MagicMock() with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\ patch('openlp.plugins.bibles.lib.importers.csvbible.translate'): - importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv') + importer = CSVBible(mocked_manager, path='.', name='.', books_path=Path('books.csv'), + verse_path=Path('verse.csv')) importer.create_verse = MagicMock() importer.get_book = MagicMock(return_value=Book('1', '1', '1. Mosebog', '1Mos')) importer.get_book_name = MagicMock(return_value='1. Mosebog') @@ -274,14 +280,15 @@ class TestCSVImport(TestCase): call('1', 1, 2, 'Og Jorden var øde og tom, og der var Mørke over Verdensdybet. ' 'Men Guds Ã…nd svævede over Vandene.')]) - def do_import_invalid_language_id_test(self): + def test_do_import_invalid_language_id(self): """ Test do_import when the user cancels the language selection dialog box """ # GIVEN: An instance of CSVBible and a mocked get_language which simulates the user cancelling the language box mocked_manager = MagicMock() with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'): - importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv') + importer = CSVBible(mocked_manager, path='.', name='.', books_path=Path('books.csv'), + verse_path=Path('verse.csv')) importer.get_language = MagicMock(return_value=None) # WHEN: Calling do_import @@ -291,14 +298,15 @@ class TestCSVImport(TestCase): importer.get_language.assert_called_once_with('Bible Name') self.assertFalse(result) - def do_import_success_test(self): + def test_do_import_success(self): """ Test do_import when the import succeeds """ # GIVEN: An instance of CSVBible mocked_manager = MagicMock() with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'): - importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verses.csv') + importer = CSVBible(mocked_manager, path='.', name='.', books_path=Path('books.csv'), + verse_path=Path('verses.csv')) importer.get_language = MagicMock(return_value=10) importer.parse_csv_file = MagicMock(side_effect=[['Book 1'], ['Verse 1']]) importer.process_books = MagicMock(return_value=['Book 1']) @@ -312,12 +320,13 @@ class TestCSVImport(TestCase): # THEN: parse_csv_file should be called twice, # and True should be returned. - self.assertEqual(importer.parse_csv_file.mock_calls, [call('books.csv', Book), call('verses.csv', Verse)]) + self.assertEqual(importer.parse_csv_file.mock_calls, + [call(Path('books.csv'), Book), call(Path('verses.csv'), Verse)]) importer.process_books.assert_called_once_with(['Book 1']) importer.process_verses.assert_called_once_with(['Verse 1'], ['Book 1']) self.assertTrue(result) - def file_import_test(self): + def test_file_import(self): """ Test the actual import of CSV Bible file """ @@ -325,12 +334,12 @@ class TestCSVImport(TestCase): # get_book_ref_id_by_name, create_verse, create_book, session and get_language. result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb') test_data = json.loads(result_file.read().decode()) - books_file = os.path.join(TEST_PATH, 'dk1933-books.csv') - verses_file = os.path.join(TEST_PATH, 'dk1933-verses.csv') + books_file = Path(TEST_PATH, 'dk1933-books.csv') + verses_file = Path(TEST_PATH, 'dk1933-verses.csv') with patch('openlp.plugins.bibles.lib.importers.csvbible.CSVBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = CSVBible(mocked_manager, path='.', name='.', booksfile=books_file, versefile=verses_file) + importer = CSVBible(mocked_manager, path='.', name='.', books_path=books_file, verse_path=verses_file) importer.wizard = mocked_import_wizard importer.get_book_ref_id_by_name = MagicMock() importer.create_verse = MagicMock() diff --git a/tests/functional/openlp_plugins/bibles/test_opensongimport.py b/tests/functional/openlp_plugins/bibles/test_opensongimport.py index 5afd2d7d9..eab4d33a9 100644 --- a/tests/functional/openlp_plugins/bibles/test_opensongimport.py +++ b/tests/functional/openlp_plugins/bibles/test_opensongimport.py @@ -29,6 +29,7 @@ from unittest.mock import MagicMock, patch, call from lxml import objectify +from openlp.core.common.path import Path from openlp.core.common.registry import Registry from openlp.plugins.bibles.lib.importers.opensong import OpenSongBible, get_text, parse_chapter_number from openlp.plugins.bibles.lib.bibleimport import BibleImport @@ -64,12 +65,12 @@ class TestOpenSongImport(TestCase, TestMixin): mocked_manager = MagicMock() # WHEN: An importer object is created - importer = OpenSongBible(mocked_manager, path='.', name='.', filename='') + importer = OpenSongBible(mocked_manager, path='.', name='.', file_path=None) # THEN: The importer should be an instance of BibleDB self.assertIsInstance(importer, BibleImport) - def get_text_no_text_test(self): + def test_get_text_no_text(self): """ Test that get_text handles elements containing text in a combination of text and tail attributes """ @@ -82,7 +83,7 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: A blank string should be returned self.assertEqual(result, '') - def get_text_text_test(self): + def test_get_text_text(self): """ Test that get_text handles elements containing text in a combination of text and tail attributes """ @@ -99,7 +100,7 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: The text returned should be as expected self.assertEqual(result, 'Element text sub_text_tail text sub_text_tail tail sub_text text sub_tail tail') - def parse_chapter_number_test(self): + def test_parse_chapter_number(self): """ Test parse_chapter_number when supplied with chapter number and an instance of OpenSongBible """ @@ -110,7 +111,7 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: The 10 should be returned as an Int self.assertEqual(result, 10) - def parse_chapter_number_empty_attribute_test(self): + def test_parse_chapter_number_empty_attribute(self): """ Testparse_chapter_number when the chapter number is an empty string. (Bug #1074727) """ @@ -121,12 +122,12 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: parse_chapter_number should increment the previous verse number self.assertEqual(result, 13) - def parse_verse_number_valid_verse_no_test(self): + def test_parse_verse_number_valid_verse_no(self): """ Test parse_verse_number when supplied with a valid verse number """ # GIVEN: An instance of OpenSongBible, the number 15 represented as a string and an instance of OpenSongBible - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling parse_verse_number result = importer.parse_verse_number('15', 0) @@ -134,12 +135,12 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: parse_verse_number should return the verse number self.assertEqual(result, 15) - def parse_verse_number_verse_range_test(self): + def test_parse_verse_number_verse_range(self): """ Test parse_verse_number when supplied with a verse range """ # GIVEN: An instance of OpenSongBible, and the range 24-26 represented as a string - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling parse_verse_number result = importer.parse_verse_number('24-26', 0) @@ -147,12 +148,12 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: parse_verse_number should return the first verse number in the range self.assertEqual(result, 24) - def parse_verse_number_invalid_verse_no_test(self): + def test_parse_verse_number_invalid_verse_no(self): """ Test parse_verse_number when supplied with a invalid verse number """ # GIVEN: An instance of OpenSongBible, a non numeric string represented as a string - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling parse_verse_number result = importer.parse_verse_number('invalid', 41) @@ -160,25 +161,25 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: parse_verse_number should increment the previous verse number self.assertEqual(result, 42) - def parse_verse_number_empty_attribute_test(self): + def test_parse_verse_number_empty_attribute(self): """ Test parse_verse_number when the verse number is an empty string. (Bug #1074727) """ # GIVEN: An instance of OpenSongBible, an empty string, and the previous verse number set as 14 - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling parse_verse_number result = importer.parse_verse_number('', 14) # THEN: parse_verse_number should increment the previous verse number self.assertEqual(result, 15) - def parse_verse_number_invalid_type_test(self): + def test_parse_verse_number_invalid_type(self): """ Test parse_verse_number when the verse number is an invalid type) """ with patch.object(OpenSongBible, 'log_warning')as mocked_log_warning: # GIVEN: An instanceofOpenSongBible, a Tuple, and the previous verse number set as 12 - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling parse_verse_number result = importer.parse_verse_number((1, 2, 3), 12) @@ -188,12 +189,12 @@ class TestOpenSongImport(TestCase, TestMixin): mocked_log_warning.assert_called_once_with('Illegal verse number: (1, 2, 3)') self.assertEqual(result, 13) - def process_books_stop_import_test(self): + def test_process_books_stop_import(self): """ Test process_books when stop_import is set to True """ # GIVEN: An instance of OpenSongBible - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: stop_import_flag is set to True importer.stop_import_flag = True @@ -202,14 +203,14 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: find_and_create_book should not have been called self.assertFalse(self.mocked_find_and_create_book.called) - def process_books_completes_test(self): + def test_process_books_completes(self): """ Test process_books when it processes all books """ # GIVEN: An instance of OpenSongBible Importer and two mocked books self.mocked_find_and_create_book.side_effect = ['db_book1', 'db_book2'] with patch.object(OpenSongBible, 'process_chapters') as mocked_process_chapters: - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) book1 = MagicMock() book1.attrib = {'n': 'Name1'} @@ -231,12 +232,12 @@ class TestOpenSongImport(TestCase, TestMixin): [call('db_book1', 'Chapter1'), call('db_book2', 'Chapter2')]) self.assertEqual(importer.session.commit.call_count, 2) - def process_chapters_stop_import_test(self): + def test_process_chapters_stop_import(self): """ Test process_chapters when stop_import is set to True """ # GIVEN: An isntance of OpenSongBible - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) importer.parse_chapter_number = MagicMock() # WHEN: stop_import_flag is set to True @@ -247,12 +248,12 @@ class TestOpenSongImport(TestCase, TestMixin): self.assertFalse(importer.parse_chapter_number.called) @patch('openlp.plugins.bibles.lib.importers.opensong.parse_chapter_number', **{'side_effect': [1, 2]}) - def process_chapters_completes_test(self, mocked_parse_chapter_number): + def test_process_chapters_completes(self, mocked_parse_chapter_number): """ Test process_chapters when it completes """ # GIVEN: An instance of OpenSongBible - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) importer.wizard = MagicMock() # WHEN: called with some valid data @@ -279,12 +280,12 @@ class TestOpenSongImport(TestCase, TestMixin): self.assertEqual(importer.wizard.increment_progress_bar.call_args_list, [call('Importing Book 1...'), call('Importing Book 2...')]) - def process_verses_stop_import_test(self): + def test_process_verses_stop_import(self): """ Test process_verses when stop_import is set to True """ # GIVEN: An isntance of OpenSongBible - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) importer.parse_verse_number = MagicMock() # WHEN: stop_import_flag is set to True @@ -294,7 +295,7 @@ class TestOpenSongImport(TestCase, TestMixin): # THEN: importer.parse_verse_number not have been called self.assertFalse(importer.parse_verse_number.called) - def process_verses_completes_test(self): + def test_process_verses_completes(self): """ Test process_verses when it completes """ @@ -303,7 +304,7 @@ class TestOpenSongImport(TestCase, TestMixin): patch.object(OpenSongBible, 'parse_verse_number', **{'side_effect': [1, 2]}) as mocked_parse_verse_number: # GIVEN: An instance of OpenSongBible - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) importer.wizard = MagicMock() # WHEN: called with some valid data @@ -329,7 +330,7 @@ class TestOpenSongImport(TestCase, TestMixin): importer.create_verse.call_args_list, [call(1, 1, 1, 'Verse1 Text'), call(1, 1, 2, 'Verse2 Text')]) - def do_import_parse_xml_fails_test(self): + def test_do_import_parse_xml_fails(self): """ Test do_import when parse_xml fails (returns None) """ @@ -338,7 +339,7 @@ class TestOpenSongImport(TestCase, TestMixin): patch.object(OpenSongBible, 'validate_xml_file'), \ patch.object(OpenSongBible, 'parse_xml', return_value=None), \ patch.object(OpenSongBible, 'get_language_id') as mocked_language_id: - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling do_import result = importer.do_import() @@ -347,7 +348,7 @@ class TestOpenSongImport(TestCase, TestMixin): self.assertFalse(result) self.assertFalse(mocked_language_id.called) - def do_import_no_language_test(self): + def test_do_import_no_language(self): """ Test do_import when the user cancels the language selection dialog """ @@ -357,7 +358,7 @@ class TestOpenSongImport(TestCase, TestMixin): patch.object(OpenSongBible, 'parse_xml'), \ patch.object(OpenSongBible, 'get_language_id', return_value=False), \ patch.object(OpenSongBible, 'process_books') as mocked_process_books: - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling do_import result = importer.do_import() @@ -366,7 +367,7 @@ class TestOpenSongImport(TestCase, TestMixin): self.assertFalse(result) self.assertFalse(mocked_process_books.called) - def do_import_completes_test(self): + def test_do_import_completes(self): """ Test do_import when it completes successfully """ @@ -376,7 +377,7 @@ class TestOpenSongImport(TestCase, TestMixin): patch.object(OpenSongBible, 'parse_xml'), \ patch.object(OpenSongBible, 'get_language_id', return_value=10), \ patch.object(OpenSongBible, 'process_books'): - importer = OpenSongBible(MagicMock(), path='.', name='.', filename='') + importer = OpenSongBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling do_import result = importer.do_import() @@ -406,7 +407,7 @@ class TestOpenSongImportFileImports(TestCase, TestMixin): with patch('openlp.plugins.bibles.lib.importers.opensong.OpenSongBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = OpenSongBible(mocked_manager, path='.', name='.', filename='') + importer = OpenSongBible(mocked_manager, path='.', name='.', file_path=None) importer.wizard = mocked_import_wizard importer.get_book_ref_id_by_name = MagicMock() importer.create_verse = MagicMock() @@ -416,7 +417,7 @@ class TestOpenSongImportFileImports(TestCase, TestMixin): importer.get_language.return_value = 'Danish' # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) + importer.file_path = Path(TEST_PATH, bible_file) importer.do_import() # THEN: The create_verse() method should have been called with each verse in the file. diff --git a/tests/functional/openlp_plugins/bibles/test_osisimport.py b/tests/functional/openlp_plugins/bibles/test_osisimport.py index 5be6c459c..02c6c3654 100644 --- a/tests/functional/openlp_plugins/bibles/test_osisimport.py +++ b/tests/functional/openlp_plugins/bibles/test_osisimport.py @@ -27,6 +27,7 @@ import json from unittest import TestCase from unittest.mock import MagicMock, call, patch +from openlp.core.common.path import Path from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.db import BibleDB from openlp.plugins.bibles.lib.importers.osis import OSISBible @@ -63,17 +64,17 @@ class TestOsisImport(TestCase): mocked_manager = MagicMock() # WHEN: An importer object is created - importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer = OSISBible(mocked_manager, path='.', name='.', file_path=None) # THEN: The importer should be an instance of BibleDB self.assertIsInstance(importer, BibleDB) - def process_books_stop_import_test(self): + def test_process_books_stop_import(self): """ Test process_books when stop_import is set to True """ # GIVEN: An instance of OSISBible adn some mocked data - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) mocked_data = MagicMock(**{'xpath.return_value': ['Book']}) # WHEN: stop_import_flag is set to True and process_books is called @@ -83,14 +84,14 @@ class TestOsisImport(TestCase): # THEN: find_and_create_book should not have been called self.assertFalse(self.mocked_find_and_create_book.called) - def process_books_completes_test(self): + def test_process_books_completes(self): """ Test process_books when it processes all books """ # GIVEN: An instance of OSISBible Importer and two mocked books self.mocked_find_and_create_book.side_effect = ['db_book1', 'db_book2'] with patch.object(OSISBible, 'process_chapters') as mocked_process_chapters: - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) book1 = MagicMock() book1.get.return_value = 'Name1' @@ -111,7 +112,7 @@ class TestOsisImport(TestCase): [call('db_book1', book1), call('db_book2', book2)]) self.assertEqual(importer.session.commit.call_count, 2) - def process_chapters_verse_in_chapter_verse_text_test(self): + def test_process_chapters_verse_in_chapter_verse_text(self): """ Test process_chapters when supplied with an etree element with a verse element nested in it """ @@ -128,7 +129,7 @@ class TestOsisImport(TestCase): test_chapter = MagicMock() test_chapter.__iter__.return_value = [test_verse] test_chapter.get.side_effect = lambda x: {'osisID': '1.2.4', 'sID': '999'}.get(x) - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_chapters importer.process_chapters(test_book, [test_chapter]) @@ -137,7 +138,7 @@ class TestOsisImport(TestCase): mocked_set_current_chapter.assert_called_once_with(test_book.name, 2) mocked_process_verse.assert_called_once_with(test_book, 2, test_verse) - def process_chapters_verse_in_chapter_verse_milestone_test(self): + def test_process_chapters_verse_in_chapter_verse_milestone(self): """ Test process_chapters when supplied with an etree element with a verse element nested, when the verse system is based on milestones @@ -155,7 +156,7 @@ class TestOsisImport(TestCase): test_chapter = MagicMock() test_chapter.__iter__.return_value = [test_verse] test_chapter.get.side_effect = lambda x: {'osisID': '1.2.4', 'sID': '999'}.get(x) - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_chapters importer.process_chapters(test_book, [test_chapter]) @@ -164,7 +165,7 @@ class TestOsisImport(TestCase): mocked_set_current_chapter.assert_called_once_with(test_book.name, 2) mocked_process_verse.assert_called_once_with(test_book, 2, test_verse, use_milestones=True) - def process_chapters_milestones_chapter_no_sid_test(self): + def test_process_chapters_milestones_chapter_no_sid(self): """ Test process_chapters when supplied with an etree element with a chapter and verse element in the milestone configuration, where the chapter is the "closing" milestone. (Missing the sID attribute) @@ -180,14 +181,14 @@ class TestOsisImport(TestCase): test_chapter.get.side_effect = lambda x: {'osisID': '1.2.4'}.get(x) # WHEN: Calling process_chapters - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) importer.process_chapters(test_book, [test_chapter]) # THEN: neither set_current_chapter or process_verse should have been called self.assertFalse(mocked_set_current_chapter.called) self.assertFalse(mocked_process_verse.called) - def process_chapters_milestones_chapter_sid_test(self): + def test_process_chapters_milestones_chapter_sid(self): """ Test process_chapters when supplied with an etree element with a chapter and verse element in the milestone configuration, where the chapter is the "opening" milestone. (Has the sID attribute) @@ -201,7 +202,7 @@ class TestOsisImport(TestCase): test_chapter = MagicMock() test_chapter.tag = '{http://www.bibletechnologies.net/2003/OSIS/namespace}chapter' test_chapter.get.side_effect = lambda x: {'osisID': '1.2.4', 'sID': '999'}.get(x) - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_chapters importer.process_chapters(test_book, [test_chapter]) @@ -210,7 +211,7 @@ class TestOsisImport(TestCase): mocked_set_current_chapter.assert_called_once_with(test_book.name, 2) self.assertFalse(mocked_process_verse.called) - def process_chapters_milestones_verse_tag_test(self): + def test_process_chapters_milestones_verse_tag(self): """ Test process_chapters when supplied with an etree element with a chapter and verse element in the milestone configuration, where the verse is the "opening" milestone. (Has the sID attribute) @@ -228,14 +229,14 @@ class TestOsisImport(TestCase): test_verse.text = 'Verse Text' # WHEN: Calling process_chapters - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) importer.process_chapters(test_book, [test_verse]) # THEN: process_verse should have been called with the test data self.assertFalse(mocked_set_current_chapter.called) mocked_process_verse.assert_called_once_with(test_book, 0, test_verse, use_milestones=True) - def process_verse_no_osis_id_test(self): + def test_process_verse_no_osis_id(self): """ Test process_verse when the element supplied does not have and osisID attribute """ @@ -245,7 +246,7 @@ class TestOsisImport(TestCase): test_verse.get.side_effect = lambda x: {}.get(x) test_verse.tail = 'Verse Text' test_verse.text = None - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_verse with the test data importer.process_verse(test_book, 2, test_verse) @@ -253,7 +254,7 @@ class TestOsisImport(TestCase): # THEN: create_verse should not have been called self.assertFalse(self.mocked_create_verse.called) - def process_verse_use_milestones_no_s_id_test(self): + def test_process_verse_use_milestones_no_s_id(self): """ Test process_verse when called with use_milestones set to True, but the element supplied does not have and sID attribute @@ -264,7 +265,7 @@ class TestOsisImport(TestCase): test_verse.get.side_effect = lambda x: {}.get(x) test_verse.tail = 'Verse Text' test_verse.text = None - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_verse with the test data importer.process_verse(test_book, 2, test_verse) @@ -272,7 +273,7 @@ class TestOsisImport(TestCase): # THEN: create_verse should not have been called self.assertFalse(self.mocked_create_verse.called) - def process_verse_use_milestones_no_tail_test(self): + def test_process_verse_use_milestones_no_tail(self): """ Test process_verse when called with use_milestones set to True, but the element supplied does not have a 'tail' """ @@ -282,7 +283,7 @@ class TestOsisImport(TestCase): test_verse.tail = None test_verse.text = None test_verse.get.side_effect = lambda x: {'osisID': '1.2.4', 'sID': '999'}.get(x) - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_verse with the test data importer.process_verse(test_book, 2, test_verse, use_milestones=True) @@ -290,7 +291,7 @@ class TestOsisImport(TestCase): # THEN: create_verse should not have been called self.assertFalse(self.mocked_create_verse.called) - def process_verse_use_milestones_success_test(self): + def test_process_verse_use_milestones_success(self): """ Test process_verse when called with use_milestones set to True, and the verse element successfully imports """ @@ -301,7 +302,7 @@ class TestOsisImport(TestCase): test_verse.tail = 'Verse Text' test_verse.text = None test_verse.get.side_effect = lambda x: {'osisID': '1.2.4', 'sID': '999'}.get(x) - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_verse with the test data importer.process_verse(test_book, 2, test_verse, use_milestones=True) @@ -309,7 +310,7 @@ class TestOsisImport(TestCase): # THEN: create_verse should have been called with the test data self.mocked_create_verse.assert_called_once_with(1, 2, 4, 'Verse Text') - def process_verse_no_text_test(self): + def test_process_verse_no_text(self): """ Test process_verse when called with an empty verse element """ @@ -320,7 +321,7 @@ class TestOsisImport(TestCase): test_verse.tail = '\n ' # Whitespace test_verse.text = None test_verse.get.side_effect = lambda x: {'osisID': '1.2.4', 'sID': '999'}.get(x) - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_verse with the test data importer.process_verse(test_book, 2, test_verse) @@ -328,7 +329,7 @@ class TestOsisImport(TestCase): # THEN: create_verse should not have been called self.assertFalse(self.mocked_create_verse.called) - def process_verse_success_test(self): + def test_process_verse_success(self): """ Test process_verse when called with an element with text set """ @@ -339,7 +340,7 @@ class TestOsisImport(TestCase): test_verse.tail = '\n ' # Whitespace test_verse.text = 'Verse Text' test_verse.get.side_effect = lambda x: {'osisID': '1.2.4', 'sID': '999'}.get(x) - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling process_verse with the test data importer.process_verse(test_book, 2, test_verse) @@ -347,7 +348,7 @@ class TestOsisImport(TestCase): # THEN: create_verse should have been called with the test data self.mocked_create_verse.assert_called_once_with(1, 2, 4, 'Verse Text') - def do_import_parse_xml_fails_test(self): + def test_do_import_parse_xml_fails(self): """ Test do_import when parse_xml fails (returns None) """ @@ -356,7 +357,7 @@ class TestOsisImport(TestCase): patch.object(OSISBible, 'validate_xml_file'), \ patch.object(OSISBible, 'parse_xml', return_value=None), \ patch.object(OSISBible, 'get_language_id') as mocked_language_id: - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling do_import result = importer.do_import() @@ -365,7 +366,7 @@ class TestOsisImport(TestCase): self.assertFalse(result) self.assertFalse(mocked_language_id.called) - def do_import_no_language_test(self): + def test_do_import_no_language(self): """ Test do_import when the user cancels the language selection dialog """ @@ -375,7 +376,7 @@ class TestOsisImport(TestCase): patch.object(OSISBible, 'parse_xml'), \ patch.object(OSISBible, 'get_language_id', **{'return_value': False}), \ patch.object(OSISBible, 'process_books') as mocked_process_books: - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling do_import result = importer.do_import() @@ -384,7 +385,7 @@ class TestOsisImport(TestCase): self.assertFalse(result) self.assertFalse(mocked_process_books.called) - def do_import_completes_test(self): + def test_do_import_completes(self): """ Test do_import when it completes successfully """ @@ -394,7 +395,7 @@ class TestOsisImport(TestCase): patch.object(OSISBible, 'parse_xml'), \ patch.object(OSISBible, 'get_language_id', **{'return_value': 10}), \ patch.object(OSISBible, 'process_books'): - importer = OSISBible(MagicMock(), path='.', name='.', filename='') + importer = OSISBible(MagicMock(), path='.', name='.', file_path=None) # WHEN: Calling do_import result = importer.do_import() @@ -427,7 +428,7 @@ class TestOsisImportFileImports(TestCase): with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer = OSISBible(mocked_manager, path='.', name='.', file_path=None) importer.wizard = mocked_import_wizard importer.get_book_ref_id_by_name = MagicMock() importer.create_verse = MagicMock() @@ -437,7 +438,7 @@ class TestOsisImportFileImports(TestCase): importer.get_language.return_value = 'Danish' # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) + importer.file_path = Path(TEST_PATH, bible_file) importer.do_import() # THEN: The create_verse() method should have been called with each verse in the file. @@ -457,7 +458,7 @@ class TestOsisImportFileImports(TestCase): with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer = OSISBible(mocked_manager, path='.', name='.', file_path=None) importer.wizard = mocked_import_wizard importer.get_book_ref_id_by_name = MagicMock() importer.create_verse = MagicMock() @@ -467,7 +468,7 @@ class TestOsisImportFileImports(TestCase): importer.get_language.return_value = 'English' # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) + importer.file_path = Path(TEST_PATH, bible_file) importer.do_import() # THEN: The create_verse() method should have been called with each verse in the file. @@ -487,7 +488,7 @@ class TestOsisImportFileImports(TestCase): with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer = OSISBible(mocked_manager, path='.', name='.', file_path=None) importer.wizard = mocked_import_wizard importer.get_book_ref_id_by_name = MagicMock() importer.create_verse = MagicMock() @@ -497,7 +498,7 @@ class TestOsisImportFileImports(TestCase): importer.get_language.return_value = 'English' # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) + importer.file_path = Path(TEST_PATH, bible_file) importer.do_import() # THEN: The create_verse() method should have been called with each verse in the file. @@ -517,7 +518,7 @@ class TestOsisImportFileImports(TestCase): with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer = OSISBible(mocked_manager, path='.', name='.', file_path=None) importer.wizard = mocked_import_wizard importer.get_book_ref_id_by_name = MagicMock() importer.create_verse = MagicMock() @@ -527,7 +528,7 @@ class TestOsisImportFileImports(TestCase): importer.get_language.return_value = 'Danish' # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) + importer.file_path = Path(TEST_PATH, bible_file) importer.do_import() # THEN: The create_verse() method should have been called with each verse in the file. diff --git a/tests/functional/openlp_plugins/bibles/test_swordimport.py b/tests/functional/openlp_plugins/bibles/test_swordimport.py index 235beea58..34e011498 100644 --- a/tests/functional/openlp_plugins/bibles/test_swordimport.py +++ b/tests/functional/openlp_plugins/bibles/test_swordimport.py @@ -64,7 +64,7 @@ class TestSwordImport(TestCase): mocked_manager = MagicMock() # WHEN: An importer object is created - importer = SwordBible(mocked_manager, path='.', name='.', filename='', sword_key='', sword_path='') + importer = SwordBible(mocked_manager, path='.', name='.', file_path=None, sword_key='', sword_path='') # THEN: The importer should be an instance of BibleDB self.assertIsInstance(importer, BibleDB) @@ -80,7 +80,7 @@ class TestSwordImport(TestCase): # Also mocked pysword structures mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = SwordBible(mocked_manager, path='.', name='.', filename='', sword_key='', sword_path='') + importer = SwordBible(mocked_manager, path='.', name='.', file_path=None, sword_key='', sword_path='') result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb') test_data = json.loads(result_file.read().decode()) importer.wizard = mocked_import_wizard diff --git a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py index 6e62dae9e..fbf5b0412 100644 --- a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py +++ b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py @@ -26,6 +26,7 @@ import os from unittest import TestCase from unittest.mock import MagicMock, patch, call +from openlp.core.common.path import Path from openlp.plugins.bibles.lib.importers.wordproject import WordProjectBible @@ -48,19 +49,17 @@ class TestWordProjectImport(TestCase): self.addCleanup(self.manager_patcher.stop) self.manager_patcher.start() - @patch('openlp.plugins.bibles.lib.importers.wordproject.os') - @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') - def test_process_books(self, mocked_open, mocked_os): + @patch.object(Path, 'read_text') + def test_process_books(self, mocked_read_text): """ Test the process_books() method """ # GIVEN: A WordProject importer and a bunch of mocked things - importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') - importer.base_dir = '' + importer = WordProjectBible(MagicMock(), path='.', name='.', file_path=Path('kj.zip')) + importer.base_path = Path() importer.stop_import_flag = False importer.language_id = 'en' - mocked_open.return_value.__enter__.return_value.read.return_value = INDEX_PAGE - mocked_os.path.join.side_effect = lambda *x: ''.join(x) + mocked_read_text.return_value = INDEX_PAGE # WHEN: process_books() is called with patch.object(importer, 'find_and_create_book') as mocked_find_and_create_book, \ @@ -69,26 +68,22 @@ class TestWordProjectImport(TestCase): importer.process_books() # THEN: The right methods should have been called - mocked_os.path.join.assert_called_once_with('', 'index.htm') - mocked_open.assert_called_once_with('index.htm', encoding='utf-8', errors='ignore') + mocked_read_text.assert_called_once_with(encoding='utf-8', errors='ignore') assert mocked_find_and_create_book.call_count == 66, 'There should be 66 books' assert mocked_process_chapters.call_count == 66, 'There should be 66 books' assert mocked_session.commit.call_count == 66, 'There should be 66 books' - @patch('openlp.plugins.bibles.lib.importers.wordproject.os') - @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') - def test_process_chapters(self, mocked_open, mocked_os): + @patch.object(Path, 'read_text') + def test_process_chapters(self, mocked_read_text): """ Test the process_chapters() method """ # GIVEN: A WordProject importer and a bunch of mocked things - importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') - importer.base_dir = '' + importer = WordProjectBible(MagicMock(), path='.', name='.', file_path=Path('kj.zip')) + importer.base_path = Path() importer.stop_import_flag = False importer.language_id = 'en' - mocked_open.return_value.__enter__.return_value.read.return_value = CHAPTER_PAGE - mocked_os.path.join.side_effect = lambda *x: ''.join(x) - mocked_os.path.normpath.side_effect = lambda x: x + mocked_read_text.return_value = CHAPTER_PAGE mocked_db_book = MagicMock() mocked_db_book.name = 'Genesis' book_id = 1 @@ -102,24 +97,21 @@ class TestWordProjectImport(TestCase): # THEN: The right methods should have been called expected_set_current_chapter_calls = [call('Genesis', ch) for ch in range(1, 51)] expected_process_verses_calls = [call(mocked_db_book, 1, ch) for ch in range(1, 51)] - mocked_os.path.join.assert_called_once_with('', '01/1.htm') - mocked_open.assert_called_once_with('01/1.htm', encoding='utf-8', errors='ignore') + mocked_read_text.assert_called_once_with(encoding='utf-8', errors='ignore') assert mocked_set_current_chapter.call_args_list == expected_set_current_chapter_calls assert mocked_process_verses.call_args_list == expected_process_verses_calls - @patch('openlp.plugins.bibles.lib.importers.wordproject.os') - @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') - def test_process_verses(self, mocked_open, mocked_os): + @patch.object(Path, 'read_text') + def test_process_verses(self, mocked_read_text): """ Test the process_verses() method """ # GIVEN: A WordProject importer and a bunch of mocked things - importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') - importer.base_dir = '' + importer = WordProjectBible(MagicMock(), path='.', name='.', file_path=Path('kj.zip')) + importer.base_path = Path() importer.stop_import_flag = False importer.language_id = 'en' - mocked_open.return_value.__enter__.return_value.read.return_value = CHAPTER_PAGE - mocked_os.path.join.side_effect = lambda *x: '/'.join(x) + mocked_read_text.return_value = CHAPTER_PAGE mocked_db_book = MagicMock() mocked_db_book.name = 'Genesis' book_number = 1 @@ -130,8 +122,7 @@ class TestWordProjectImport(TestCase): importer.process_verses(mocked_db_book, book_number, chapter_number) # THEN: All the right methods should have been called - mocked_os.path.join.assert_called_once_with('', '01', '1.htm') - mocked_open.assert_called_once_with('/01/1.htm', encoding='utf-8', errors='ignore') + mocked_read_text.assert_called_once_with(encoding='utf-8', errors='ignore') assert mocked_process_verse.call_count == 31 def test_process_verse(self): @@ -139,7 +130,7 @@ class TestWordProjectImport(TestCase): Test the process_verse() method """ # GIVEN: An importer and a mocked method - importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer = WordProjectBible(MagicMock(), path='.', name='.', file_path=Path('kj.zip')) mocked_db_book = MagicMock() mocked_db_book.id = 1 chapter_number = 1 @@ -158,7 +149,7 @@ class TestWordProjectImport(TestCase): Test the process_verse() method when there's no text """ # GIVEN: An importer and a mocked method - importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer = WordProjectBible(MagicMock(), path='.', name='.', file_path=Path('kj.zip')) mocked_db_book = MagicMock() mocked_db_book.id = 1 chapter_number = 1 @@ -177,7 +168,7 @@ class TestWordProjectImport(TestCase): Test the do_import() method """ # GIVEN: An importer and mocked methods - importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer = WordProjectBible(MagicMock(), path='.', name='.', file_path='kj.zip') # WHEN: do_import() is called with patch.object(importer, '_unzip_file') as mocked_unzip_file, \ @@ -199,7 +190,7 @@ class TestWordProjectImport(TestCase): Test the do_import() method when the language is not available """ # GIVEN: An importer and mocked methods - importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer = WordProjectBible(MagicMock(), path='.', name='.', file_path='kj.zip') # WHEN: do_import() is called with patch.object(importer, '_unzip_file') as mocked_unzip_file, \ diff --git a/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py b/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py index 8e43d55b5..d423a2153 100644 --- a/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py +++ b/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py @@ -27,6 +27,7 @@ import json from unittest import TestCase from unittest.mock import MagicMock, patch +from openlp.core.common.path import Path from openlp.plugins.bibles.lib.importers.zefania import ZefaniaBible from openlp.plugins.bibles.lib.db import BibleDB @@ -55,7 +56,7 @@ class TestZefaniaImport(TestCase): mocked_manager = MagicMock() # WHEN: An importer object is created - importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='') + importer = ZefaniaBible(mocked_manager, path='.', name='.', file_path=None) # THEN: The importer should be an instance of BibleDB self.assertIsInstance(importer, BibleDB) @@ -72,7 +73,7 @@ class TestZefaniaImport(TestCase): with patch('openlp.plugins.bibles.lib.importers.zefania.ZefaniaBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='') + importer = ZefaniaBible(mocked_manager, path='.', name='.', file_path=None) importer.wizard = mocked_import_wizard importer.create_verse = MagicMock() importer.create_book = MagicMock() @@ -81,7 +82,7 @@ class TestZefaniaImport(TestCase): importer.get_language.return_value = 'Danish' # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) + importer.file_path = Path(TEST_PATH, bible_file) importer.do_import() # THEN: The create_verse() method should have been called with each verse in the file. @@ -102,7 +103,7 @@ class TestZefaniaImport(TestCase): with patch('openlp.plugins.bibles.lib.importers.zefania.ZefaniaBible.application'): mocked_manager = MagicMock() mocked_import_wizard = MagicMock() - importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='') + importer = ZefaniaBible(mocked_manager, path='.', name='.', file_path=None) importer.wizard = mocked_import_wizard importer.create_verse = MagicMock() importer.create_book = MagicMock() @@ -111,7 +112,7 @@ class TestZefaniaImport(TestCase): importer.get_language.return_value = 'Russian' # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) + importer.file_path = Path(TEST_PATH, bible_file) importer.do_import() # THEN: The create_verse() method should have been called with each verse in the file. diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index 4dfef57da..877ad722d 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -173,12 +173,14 @@ class TestImageMediaItem(TestCase): """ # GIVEN: A mocked version of reset_action self.media_item.reset_action = MagicMock() + self.media_item.reset_action_context = MagicMock() # WHEN: on_reset_click is called self.media_item.on_reset_click() # THEN: the reset_action should be set visible, and the image should be reset self.media_item.reset_action.setVisible.assert_called_with(False) + self.media_item.reset_action_context.setVisible.assert_called_with(False) self.media_item.live_controller.display.reset_image.assert_called_with() @patch('openlp.plugins.images.lib.mediaitem.delete_file') diff --git a/tests/functional/openlp_plugins/presentations/test_mediaitem.py b/tests/functional/openlp_plugins/presentations/test_mediaitem.py index 1116ce4cb..fd28b9b03 100644 --- a/tests/functional/openlp_plugins/presentations/test_mediaitem.py +++ b/tests/functional/openlp_plugins/presentations/test_mediaitem.py @@ -133,3 +133,27 @@ class TestMediaItem(TestCase, TestMixin): # THEN: doc.presentation_deleted should have been called since the presentation file did not exists. mocked_doc.assert_has_calls([call.get_thumbnail_path(1, True), call.presentation_deleted()], True) + + @patch('openlp.plugins.presentations.lib.mediaitem.MediaManagerItem._setup') + @patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item') + @patch('openlp.plugins.presentations.lib.mediaitem.Settings') + def test_search(self, mocked_settings, *unreferenced_mocks): + """ + Test that the search method finds the correct results + """ + # GIVEN: A mocked Settings class which returns a list of Path objects, + # and an instance of the PresentationMediaItem + path_1 = Path('some_dir', 'Impress_file_1') + path_2 = Path('some_other_dir', 'impress_file_2') + path_3 = Path('another_dir', 'ppt_file') + mocked_returned_settings = MagicMock() + mocked_returned_settings.value.return_value = [path_1, path_2, path_3] + mocked_settings.return_value = mocked_returned_settings + media_item = PresentationMediaItem(None, MagicMock(), None) + media_item.settings_section = '' + + # WHEN: Calling search + results = media_item.search('IMPRE', False) + + # THEN: The first two results should have been returned + assert results == [[str(path_1), 'Impress_file_1'], [str(path_2), 'impress_file_2']] diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py index a7281e062..9bd492983 100644 --- a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -67,10 +67,10 @@ class TestPdfController(TestCase, TestMixin): self.desktop.screenGeometry.return_value = SCREEN['size'] self.screens = ScreenList.create(self.desktop) Settings().extend_default_settings(__default_settings__) - self.temp_folder = Path(mkdtemp()) - self.thumbnail_folder = Path(mkdtemp()) + self.temp_folder_path = Path(mkdtemp()) + self.thumbnail_folder_path = Path(mkdtemp()) self.mock_plugin = MagicMock() - self.mock_plugin.settings_section = self.temp_folder + self.mock_plugin.settings_section = self.temp_folder_path def tearDown(self): """ @@ -78,8 +78,8 @@ class TestPdfController(TestCase, TestMixin): """ del self.screens self.destroy_settings() - shutil.rmtree(str(self.thumbnail_folder)) - shutil.rmtree(str(self.temp_folder)) + self.thumbnail_folder_path.rmtree() + self.temp_folder_path.rmtree() def test_constructor(self): """ @@ -105,8 +105,8 @@ class TestPdfController(TestCase, TestMixin): controller = PdfController(plugin=self.mock_plugin) if not controller.check_available(): raise SkipTest('Could not detect mudraw or ghostscript, so skipping PDF test') - controller.temp_folder = self.temp_folder - controller.thumbnail_folder = self.thumbnail_folder + controller.temp_folder = self.temp_folder_path + controller.thumbnail_folder = self.thumbnail_folder_path document = PdfDocument(controller, test_file) loaded = document.load_presentation() @@ -125,14 +125,14 @@ class TestPdfController(TestCase, TestMixin): controller = PdfController(plugin=self.mock_plugin) if not controller.check_available(): raise SkipTest('Could not detect mudraw or ghostscript, so skipping PDF test') - controller.temp_folder = self.temp_folder - controller.thumbnail_folder = self.thumbnail_folder + controller.temp_folder = self.temp_folder_path + controller.thumbnail_folder = self.thumbnail_folder_path document = PdfDocument(controller, test_file) loaded = document.load_presentation() # THEN: The load should succeed and pictures should be created and have been scales to fit the screen self.assertTrue(loaded, 'The loading of the PDF should succeed.') - image = QtGui.QImage(os.path.join(str(self.temp_folder), 'pdf_test1.pdf', 'mainslide001.png')) + image = QtGui.QImage(os.path.join(str(self.temp_folder_path), 'pdf_test1.pdf', 'mainslide001.png')) # Based on the converter used the resolution will differ a bit if controller.gsbin: self.assertEqual(760, image.height(), 'The height should be 760') diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index 30ab11561..a921ef81e 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -144,7 +144,7 @@ class TestPresentationController(TestCase): # GIVEN: A mocked open, get_thumbnail_folder and exists with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: - mocked_read_text.side_effect = IOError() + mocked_read_text.side_effect = OSError() mocked_get_thumbnail_folder.return_value = Path('test') # WHEN: calling get_titles_and_notes diff --git a/tests/functional/openlp_plugins/songs/test_ewimport.py b/tests/functional/openlp_plugins/songs/test_ewimport.py index ae9f873c5..e384319f0 100644 --- a/tests/functional/openlp_plugins/songs/test_ewimport.py +++ b/tests/functional/openlp_plugins/songs/test_ewimport.py @@ -239,160 +239,160 @@ class TestEasyWorshipSongImport(TestCase): self.assertIsNone(return_value, 'db_set_record_struct should return None') mocked_struct.Struct.assert_called_with('>50sHIB250s250s10sQ') - def test_get_field(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + def test_get_field(self, MockSongImport): """ Test the :mod:`db_get_field` module """ # GIVEN: A mocked out SongImport class, a mocked out "manager", an encoding and some test data and known results - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'): - mocked_manager = MagicMock() - importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) - importer.encoding = TEST_DATA_ENCODING - importer.fields = TEST_FIELDS - importer.field_descriptions = TEST_FIELD_DESCS - field_results = [(0, b'A Heart Like Thine'), (1, 100), (2, 102), (3, True), (6, None), (7, None)] + mocked_manager = MagicMock() + importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) + importer.encoding = TEST_DATA_ENCODING + importer.fields = TEST_FIELDS + importer.field_descriptions = TEST_FIELD_DESCS + field_results = [(0, b'A Heart Like Thine'), (1, 100), (2, 102), (3, True), (6, None), (7, None)] - # WHEN: Called with test data - for field_index, result in field_results: - return_value = importer.db_get_field(field_index) + # WHEN: Called with test data + for field_index, result in field_results: + return_value = importer.db_get_field(field_index) - # THEN: db_get_field should return the known results - self.assertEqual(return_value, result, - 'db_get_field should return "%s" when called with "%s"' % - (result, TEST_FIELDS[field_index])) + # THEN: db_get_field should return the known results + self.assertEqual(return_value, result, + 'db_get_field should return "%s" when called with "%s"' % + (result, TEST_FIELDS[field_index])) - def test_get_memo_field(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + def test_get_memo_field(self, MockSongImport): """ Test the :mod:`db_get_field` module """ for test_results in GET_MEMO_FIELD_TEST_RESULTS: # GIVEN: A mocked out SongImport class, a mocked out "manager", a mocked out memo_file and an encoding - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'): - mocked_manager = MagicMock() - mocked_memo_file = MagicMock() - importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) - importer.memo_file = mocked_memo_file - importer.encoding = TEST_DATA_ENCODING + mocked_manager = MagicMock() + mocked_memo_file = MagicMock() + importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) + importer.memo_file = mocked_memo_file + importer.encoding = TEST_DATA_ENCODING - # WHEN: Supplied with test fields and test field descriptions - importer.fields = TEST_FIELDS - importer.field_descriptions = TEST_FIELD_DESCS - field_index = test_results[0] - mocked_memo_file.read.return_value = test_results[1] - get_field_result = test_results[2]['return'] - get_field_read_calls = test_results[2]['read'] - get_field_seek_calls = test_results[2]['seek'] + # WHEN: Supplied with test fields and test field descriptions + importer.fields = TEST_FIELDS + importer.field_descriptions = TEST_FIELD_DESCS + field_index = test_results[0] + mocked_memo_file.read.return_value = test_results[1] + get_field_result = test_results[2]['return'] + get_field_read_calls = test_results[2]['read'] + get_field_seek_calls = test_results[2]['seek'] - # THEN: db_get_field should return the appropriate value with the appropriate mocked objects being - # called - self.assertEqual(importer.db_get_field(field_index), get_field_result) - for call in get_field_read_calls: - mocked_memo_file.read.assert_any_call(call) - for call in get_field_seek_calls: - if isinstance(call, int): - mocked_memo_file.seek.assert_any_call(call) - else: - mocked_memo_file.seek.assert_any_call(call[0], call[1]) + # THEN: db_get_field should return the appropriate value with the appropriate mocked objects being + # called + self.assertEqual(importer.db_get_field(field_index), get_field_result) + for call in get_field_read_calls: + mocked_memo_file.read.assert_any_call(call) + for call in get_field_seek_calls: + if isinstance(call, int): + mocked_memo_file.seek.assert_any_call(call) + else: + mocked_memo_file.seek.assert_any_call(call[0], call[1]) - def test_do_import_source_invalid(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', side_effect=[True, False]) + def test_do_import_source_invalid(self, mocked_is_file, MockSongImport): """ Test the :mod:`do_import` module produces an error when Songs.MB not found. """ # GIVEN: A mocked out SongImport class, a mocked out "manager" - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', side_effect=[True, False]): - mocked_manager = MagicMock() - importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) - with patch.object(importer, 'log_error') as mocked_log_error: + mocked_manager = MagicMock() + importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) + with patch.object(importer, 'log_error') as mocked_log_error: - # WHEN: do_import is supplied with an import source (Songs.MB missing) - importer.import_source = 'Songs.DB' - importer.do_import() + # WHEN: do_import is supplied with an import source (Songs.MB missing) + importer.import_source = 'Songs.DB' + importer.do_import() - # THEN: do_import should have logged an error that the Songs.MB file could not be found. - mocked_log_error.assert_any_call(importer.import_source, - 'Could not find the "Songs.MB" file. It must be in the same folder as ' - 'the "Songs.DB" file.') + # THEN: do_import should have logged an error that the Songs.MB file could not be found. + mocked_log_error.assert_any_call(importer.import_source, + 'Could not find the "Songs.MB" file. It must be in the same folder as ' + 'the "Songs.DB" file.') - def test_do_import_database_validity(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True) + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat') + def test_do_import_database_validity(self, mocked_stat, mocked_is_file, MockSongImport): """ Test the :mod:`do_import` module handles invalid database files correctly """ # GIVEN: A mocked out SongImport class, os.path and a mocked out "manager" - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat') as mocked_stat: + mocked_manager = MagicMock() + importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) + importer.import_source = 'Songs.DB' - mocked_manager = MagicMock() - importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) - importer.import_source = 'Songs.DB' + # WHEN: DB file size is less than 0x800 + mocked_stat.return_value.st_size = 0x7FF - # WHEN: DB file size is less than 0x800 - mocked_stat.return_value.st_size = 0x7FF + # THEN: do_import should return None having called Path.stat() + self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800') + mocked_stat.assert_called_once_with() - # THEN: do_import should return None having called Path.stat() - self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800') - mocked_stat.assert_called_once_with() - - def test_do_import_memo_validty(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True) + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat', **{'return_value.st_size': 0x800}) + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.open') + @patch('openlp.plugins.songs.lib.importers.easyworship.struct') + def test_do_import_memo_validty(self, mocked_struct, mocked_open, mocked_stat, mocked_is_file, MockSongImport): """ Test the :mod:`do_import` module handles invalid memo files correctly """ # GIVEN: A mocked out SongImport class, a mocked out "manager" - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat', **{'return_value.st_size': 0x800}), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.open') as mocked_open, \ - patch('openlp.plugins.songs.lib.importers.easyworship.struct') as mocked_struct: - mocked_manager = MagicMock() - importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) - importer.import_source = 'Songs.DB' + mocked_manager = MagicMock() + importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) + importer.import_source = 'Songs.DB' - # WHEN: Unpacking first 35 bytes of Memo file - struct_unpack_return_values = [(0, 0x700, 2, 0, 0), (0, 0x800, 0, 0, 0), (0, 0x800, 5, 0, 0)] - mocked_struct.unpack.side_effect = struct_unpack_return_values + # WHEN: Unpacking first 35 bytes of Memo file + struct_unpack_return_values = [(0, 0x700, 2, 0, 0), (0, 0x800, 0, 0, 0), (0, 0x800, 5, 0, 0)] + mocked_struct.unpack.side_effect = struct_unpack_return_values - # THEN: do_import should return None having called closed the open files db and memo files. - for effect in struct_unpack_return_values: - self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800') - self.assertEqual(mocked_open().close.call_count, 2, - 'The open db and memo files should have been closed') - mocked_open().close.reset_mock() - self.assertIs(mocked_open().seek.called, False, 'db_file.seek should not have been called.') + # THEN: do_import should return None having called closed the open files db and memo files. + for effect in struct_unpack_return_values: + self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800') + self.assertEqual(mocked_open().close.call_count, 2, + 'The open db and memo files should have been closed') + mocked_open().close.reset_mock() + self.assertIs(mocked_open().seek.called, False, 'db_file.seek should not have been called.') - def test_code_page_to_encoding(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True) + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat', **{'return_value.st_size': 0x800}) + @patch('openlp.plugins.songs.lib.importers.easyworship.Path.open') + @patch('builtins.open') + @patch('openlp.plugins.songs.lib.importers.easyworship.struct') + @patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding') + def test_code_page_to_encoding(self, mocked_retrieve_windows_encoding, mocked_struct, mocked_open, mocked_path_open, + mocked_path_stat, mocked_path_is_file, MockSongImport): """ Test the :mod:`do_import` converts the code page to the encoding correctly """ # GIVEN: A mocked out SongImport class, a mocked out "manager" - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat', **{'return_value.st_size': 0x800}), \ - patch('openlp.plugins.songs.lib.importers.easyworship.Path.open'), \ - patch('builtins.open'), patch('openlp.plugins.songs.lib.importers.easyworship.struct') as mocked_struct, \ - patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding') as \ - mocked_retrieve_windows_encoding: - mocked_manager = MagicMock() - importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) - importer.import_source = 'Songs.DB' + mocked_manager = MagicMock() + importer = EasyWorshipSongImport(mocked_manager, file_paths=[]) + importer.import_source = 'Songs.DB' - # WHEN: Unpacking the code page - for code_page, encoding in CODE_PAGE_MAPPINGS: - struct_unpack_return_values = [(0, 0x800, 2, 0, 0), (code_page, )] - mocked_struct.unpack.side_effect = struct_unpack_return_values - mocked_retrieve_windows_encoding.return_value = False + # WHEN: Unpacking the code page + for code_page, encoding in CODE_PAGE_MAPPINGS: + struct_unpack_return_values = [(0, 0x800, 2, 0, 0), (code_page, )] + mocked_struct.unpack.side_effect = struct_unpack_return_values + mocked_retrieve_windows_encoding.return_value = False - # THEN: do_import should return None having called retrieve_windows_encoding with the correct encoding. - self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800') - mocked_retrieve_windows_encoding.assert_any_call(encoding) + # THEN: do_import should return None having called retrieve_windows_encoding with the correct encoding. + self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800') + mocked_retrieve_windows_encoding.assert_any_call(encoding) def test_db_file_import(self): - return self._test_db_file_import(os.path.join(TEST_PATH, 'Songs.DB')) + return self._run_db_file_import(os.path.join(TEST_PATH, 'Songs.DB')) def test_sqlite_db_file_import(self): - return self._test_db_file_import(os.path.join(TEST_PATH, 'ew6')) + return self._run_db_file_import(os.path.join(TEST_PATH, 'ew6')) - def _test_db_file_import(self, source_path): + def _run_db_file_import(self, source_path): """ Test the actual import of real song database files and check that the imported data is correct. """ @@ -448,62 +448,61 @@ class TestEasyWorshipSongImport(TestCase): 'verse_order_list for %s should be %s' % (title, verse_order_list)) mocked_finish.assert_called_with() - def test_ews_file_import(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + @patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding') + def test_ews_file_import(self, mocked_retrieve_windows_encoding, MockSongImport): """ Test the actual import of song from ews file and check that the imported data is correct. """ # GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard", # and mocked out "author", "add_copyright", "add_verse", "finish" methods. - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \ - patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding') \ - as mocked_retrieve_windows_encoding: - mocked_retrieve_windows_encoding.return_value = 'cp1252' - mocked_manager = MagicMock() - mocked_import_wizard = MagicMock() - mocked_add_author = MagicMock() - mocked_add_verse = MagicMock() - mocked_finish = MagicMock() - mocked_title = MagicMock() - mocked_finish.return_value = True - importer = EasyWorshipSongImportLogger(mocked_manager) - importer.import_wizard = mocked_import_wizard - importer.stop_import_flag = False - importer.add_author = mocked_add_author - importer.add_verse = mocked_add_verse - importer.title = mocked_title - importer.finish = mocked_finish - importer.topics = [] + mocked_retrieve_windows_encoding.return_value = 'cp1252' + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + mocked_add_author = MagicMock() + mocked_add_verse = MagicMock() + mocked_finish = MagicMock() + mocked_title = MagicMock() + mocked_finish.return_value = True + importer = EasyWorshipSongImportLogger(mocked_manager) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = False + importer.add_author = mocked_add_author + importer.add_verse = mocked_add_verse + importer.title = mocked_title + importer.finish = mocked_finish + importer.topics = [] - # WHEN: Importing ews file - importer.import_source = os.path.join(TEST_PATH, 'test1.ews') - import_result = importer.do_import() + # WHEN: Importing ews file + importer.import_source = os.path.join(TEST_PATH, 'test1.ews') + import_result = importer.do_import() - # THEN: do_import should return none, the song data should be as expected, and finish should have been - # called. - title = EWS_SONG_TEST_DATA['title'] - self.assertIsNone(import_result, 'do_import should return None when it has completed') - self.assertIn(title, importer._title_assignment_list, 'title for should be "%s"' % title) - mocked_add_author.assert_any_call(EWS_SONG_TEST_DATA['authors'][0]) - for verse_text, verse_tag in EWS_SONG_TEST_DATA['verses']: - mocked_add_verse.assert_any_call(verse_text, verse_tag) - mocked_finish.assert_called_with() + # THEN: do_import should return none, the song data should be as expected, and finish should have been + # called. + title = EWS_SONG_TEST_DATA['title'] + self.assertIsNone(import_result, 'do_import should return None when it has completed') + self.assertIn(title, importer._title_assignment_list, 'title for should be "%s"' % title) + mocked_add_author.assert_any_call(EWS_SONG_TEST_DATA['authors'][0]) + for verse_text, verse_tag in EWS_SONG_TEST_DATA['verses']: + mocked_add_verse.assert_any_call(verse_text, verse_tag) + mocked_finish.assert_called_with() - def test_import_rtf_unescaped_unicode(self): + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') + def test_import_rtf_unescaped_unicode(self, MockSongImport): """ Test import of rtf without the expected escaping of unicode """ # GIVEN: A mocked out SongImport class, a mocked out "manager" and mocked out "author" method. - with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'): - mocked_manager = MagicMock() - mocked_add_author = MagicMock() - importer = EasyWorshipSongImportLogger(mocked_manager) - importer.add_author = mocked_add_author - importer.encoding = 'cp1252' + mocked_manager = MagicMock() + mocked_add_author = MagicMock() + importer = EasyWorshipSongImportLogger(mocked_manager) + importer.add_author = mocked_add_author + importer.encoding = 'cp1252' - # WHEN: running set_song_import_object on a verse string without the needed escaping - importer.set_song_import_object('Test Author', b'Det som var fr\x86n begynnelsen') + # WHEN: running set_song_import_object on a verse string without the needed escaping + importer.set_song_import_object('Test Author', b'Det som var fr\x86n begynnelsen') - # THEN: The import should fail - self.assertEquals(importer.entry_error_log, 'Unexpected data formatting.', 'Import should fail') + # THEN: The import should fail + self.assertEquals(importer.entry_error_log, 'Unexpected data formatting.', 'Import should fail') diff --git a/tests/functional/openlp_plugins/songs/test_openlyricsexport.py b/tests/functional/openlp_plugins/songs/test_openlyricsexport.py index 0fd4767db..85bf0818c 100644 --- a/tests/functional/openlp_plugins/songs/test_openlyricsexport.py +++ b/tests/functional/openlp_plugins/songs/test_openlyricsexport.py @@ -22,13 +22,12 @@ """ This module contains tests for the OpenLyrics song importer. """ -import shutil from tempfile import mkdtemp from unittest import TestCase from unittest.mock import MagicMock, patch from openlp.core.common.registry import Registry -from openlp.core.common.path import Path, rmtree +from openlp.core.common.path import Path from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport from tests.helpers.testmixin import TestMixin @@ -49,7 +48,7 @@ class TestOpenLyricsExport(TestCase, TestMixin): """ Cleanup """ - rmtree(self.temp_folder) + self.temp_folder.rmtree() def test_export_same_filename(self): """ diff --git a/tests/functional/openlp_plugins/songs/test_openoffice.py b/tests/functional/openlp_plugins/songs/test_openoffice.py index 4172a553c..45ef2acfd 100644 --- a/tests/functional/openlp_plugins/songs/test_openoffice.py +++ b/tests/functional/openlp_plugins/songs/test_openoffice.py @@ -22,18 +22,20 @@ """ This module contains tests for the OpenOffice/LibreOffice importer. """ -from unittest import TestCase, SkipTest +from unittest import TestCase, skipIf from unittest.mock import MagicMock, patch from openlp.core.common.registry import Registry -try: - from openlp.plugins.songs.lib.importers.openoffice import OpenOfficeImport -except ImportError: - raise SkipTest('Could not import OpenOfficeImport probably due to unavailability of uno') from tests.helpers.testmixin import TestMixin +try: + from openlp.plugins.songs.lib.importers.openoffice import OpenOfficeImport +except ImportError: + OpenOfficeImport = None + +@skipIf(OpenOfficeImport is None, 'Could not import OpenOfficeImport probably due to unavailability of uno') class TestOpenOfficeImport(TestCase, TestMixin): """ Test the :class:`~openlp.plugins.songs.lib.importer.openoffice.OpenOfficeImport` class diff --git a/tests/functional/openlp_plugins/songs/test_songselect.py b/tests/functional/openlp_plugins/songs/test_songselect.py index 191bc4e5a..fa2b2931d 100644 --- a/tests/functional/openlp_plugins/songs/test_songselect.py +++ b/tests/functional/openlp_plugins/songs/test_songselect.py @@ -135,7 +135,7 @@ class TestSongSelectImport(TestCase, TestMixin): @patch('openlp.plugins.songs.lib.songselect.build_opener') @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') - def login_url_from_form_test(self, MockedBeautifulSoup, mocked_build_opener): + def test_login_url_from_form(self, MockedBeautifulSoup, mocked_build_opener): """ Test that the login URL is from the form """ diff --git a/tests/interfaces/openlp_core_ui_lib/__init__.py b/tests/interfaces/openlp_core/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_ui_lib/__init__.py rename to tests/interfaces/openlp_core/__init__.py diff --git a/tests/interfaces/openlp_core_ul_media_vendor/__init__.py b/tests/interfaces/openlp_core/api/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_ul_media_vendor/__init__.py rename to tests/interfaces/openlp_core/api/__init__.py diff --git a/openlp/core/ui/projector/__init__.py b/tests/interfaces/openlp_core/common/__init__.py similarity index 97% rename from openlp/core/ui/projector/__init__.py rename to tests/interfaces/openlp_core/common/__init__.py index eb20e9232..ea62548f4 100644 --- a/openlp/core/ui/projector/__init__.py +++ b/tests/interfaces/openlp_core/common/__init__.py @@ -19,6 +19,3 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -""" -The Projector driver module. -""" diff --git a/tests/interfaces/openlp_core_common/test_utils.py b/tests/interfaces/openlp_core/common/test_utils.py similarity index 100% rename from tests/interfaces/openlp_core_common/test_utils.py rename to tests/interfaces/openlp_core/common/test_utils.py diff --git a/tests/interfaces/openlp_core/lib/__init__.py b/tests/interfaces/openlp_core/lib/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/lib/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_lib/test_pluginmanager.py b/tests/interfaces/openlp_core/lib/test_pluginmanager.py similarity index 96% rename from tests/interfaces/openlp_core_lib/test_pluginmanager.py rename to tests/interfaces/openlp_core/lib/test_pluginmanager.py index 2e7d979a6..588c15520 100644 --- a/tests/interfaces/openlp_core_lib/test_pluginmanager.py +++ b/tests/interfaces/openlp_core/lib/test_pluginmanager.py @@ -23,7 +23,6 @@ Package to test the openlp.core.lib.pluginmanager package. """ import sys -import shutil import gc from tempfile import mkdtemp from unittest import TestCase @@ -50,8 +49,8 @@ class TestPluginManager(TestCase, TestMixin): """ self.setup_application() self.build_settings() - self.temp_dir = Path(mkdtemp('openlp')) - Settings().setValue('advanced/data path', self.temp_dir) + self.temp_dir_path = Path(mkdtemp('openlp')) + Settings().setValue('advanced/data path', self.temp_dir_path) Registry.create() Registry().register('service_list', MagicMock()) self.main_window = QtWidgets.QMainWindow() @@ -64,7 +63,7 @@ class TestPluginManager(TestCase, TestMixin): # On windows we need to manually garbage collect to close sqlalchemy files # to avoid errors when temporary files are deleted. gc.collect() - shutil.rmtree(str(self.temp_dir)) + self.temp_dir_path.rmtree() @patch('openlp.plugins.songusage.lib.db.init_schema') @patch('openlp.plugins.songs.lib.db.init_schema') diff --git a/tests/interfaces/openlp_core/ui/__init__.py b/tests/interfaces/openlp_core/ui/__init__.py new file mode 100644 index 000000000..7efaa18af --- /dev/null +++ b/tests/interfaces/openlp_core/ui/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Module-level functions for the functional test suite +""" diff --git a/tests/interfaces/openlp_core/ui/lib/__init__.py b/tests/interfaces/openlp_core/ui/lib/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/ui/lib/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_ui_lib/test_historycombobox.py b/tests/interfaces/openlp_core/ui/lib/test_historycombobox.py similarity index 100% rename from tests/interfaces/openlp_core_ui_lib/test_historycombobox.py rename to tests/interfaces/openlp_core/ui/lib/test_historycombobox.py diff --git a/tests/interfaces/openlp_core/ui/media/__init__.py b/tests/interfaces/openlp_core/ui/media/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/ui/media/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core/ui/media/vendor/__init__.py b/tests/interfaces/openlp_core/ui/media/vendor/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/ui/media/vendor/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py b/tests/interfaces/openlp_core/ui/media/vendor/test_mediainfoWrapper.py similarity index 97% rename from tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py rename to tests/interfaces/openlp_core/ui/media/vendor/test_mediainfoWrapper.py index d4c1891b1..6ec2431b9 100644 --- a/tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py +++ b/tests/interfaces/openlp_core/ui/media/vendor/test_mediainfoWrapper.py @@ -28,8 +28,7 @@ from unittest import TestCase from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'media')) - +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', 'resources', 'media')) TEST_MEDIA = [['avi_file.avi', 61495], ['mp3_file.mp3', 134426], ['mpg_file.mpg', 9404], ['mp4_file.mp4', 188336]] diff --git a/tests/interfaces/openlp_core_ui/test_filerenamedialog.py b/tests/interfaces/openlp_core/ui/test_filerenamedialog.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_filerenamedialog.py rename to tests/interfaces/openlp_core/ui/test_filerenamedialog.py diff --git a/tests/interfaces/openlp_core_ui/test_mainwindow.py b/tests/interfaces/openlp_core/ui/test_mainwindow.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_mainwindow.py rename to tests/interfaces/openlp_core/ui/test_mainwindow.py diff --git a/tests/interfaces/openlp_core_ui/test_projectoreditform.py b/tests/interfaces/openlp_core/ui/test_projectoreditform.py similarity index 89% rename from tests/interfaces/openlp_core_ui/test_projectoreditform.py rename to tests/interfaces/openlp_core/ui/test_projectoreditform.py index fc261d9f5..ec2539a29 100644 --- a/tests/interfaces/openlp_core_ui/test_projectoreditform.py +++ b/tests/interfaces/openlp_core/ui/test_projectoreditform.py @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Interface tests to test the openlp.core.ui.projector.editform.ProjectorEditForm() +Interface tests to test the openlp.core.projectors.editform.ProjectorEditForm() class and methods. """ import os @@ -28,8 +28,9 @@ from unittest import TestCase from unittest.mock import patch from openlp.core.common.registry import Registry -from openlp.core.lib.projector.db import Projector, ProjectorDB -from openlp.core.ui import ProjectorEditForm +from openlp.core.projectors.db import Projector, ProjectorDB +from openlp.core.projectors.editform import ProjectorEditForm +from openlp.core.projectors.manager import ProjectorManager from tests.helpers.testmixin import TestMixin from tests.resources.projector.data import TEST_DB, TEST1_DATA, TEST2_DATA @@ -48,7 +49,7 @@ class TestProjectorEditForm(TestCase, TestMixin): self.setup_application() self.build_settings() Registry.create() - with patch('openlp.core.lib.projector.db.init_url') as mocked_init_url: + with patch('openlp.core.projectors.db.init_url') as mocked_init_url: if os.path.exists(TEST_DB): os.unlink(TEST_DB) mocked_init_url.return_value = 'sqlite:///' + TEST_DB @@ -66,7 +67,7 @@ class TestProjectorEditForm(TestCase, TestMixin): del self.projector_form self.destroy_settings() - @patch('openlp.core.ui.projector.editform.QtWidgets.QDialog.exec') + @patch('openlp.core.projectors.editform.QtWidgets.QDialog.exec') def test_edit_form_add_projector(self, mocked_exec): """ Test projector edit form with no parameters creates a new entry. @@ -84,7 +85,7 @@ class TestProjectorEditForm(TestCase, TestMixin): self.assertTrue((item.ip is None and item.name is None), 'Projector edit form should have a new Projector() instance to edit') - @patch('openlp.core.ui.projector.editform.QtWidgets.QDialog.exec') + @patch('openlp.core.projectors.editform.QtWidgets.QDialog.exec') def test_edit_form_edit_projector(self, mocked_exec): """ Test projector edit form with existing projector entry diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core/ui/test_projectormanager.py similarity index 94% rename from tests/interfaces/openlp_core_ui/test_projectormanager.py rename to tests/interfaces/openlp_core/ui/test_projectormanager.py index ff95c4276..484d4d68a 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core/ui/test_projectormanager.py @@ -27,8 +27,9 @@ from unittest import TestCase from unittest.mock import patch, MagicMock from openlp.core.common.registry import Registry -from openlp.core.ui import ProjectorManager, ProjectorEditForm -from openlp.core.lib.projector.db import ProjectorDB +from openlp.core.projectors.db import ProjectorDB +from openlp.core.projectors.editform import ProjectorEditForm +from openlp.core.projectors.manager import ProjectorManager from tests.helpers.testmixin import TestMixin from tests.resources.projector.data import TEST_DB @@ -42,10 +43,10 @@ class TestProjectorManager(TestCase, TestMixin): """ Create the UI and setup necessary options """ - self.build_settings() self.setup_application() + self.build_settings() Registry.create() - with patch('openlp.core.lib.projector.db.init_url') as mocked_init_url: + with patch('openlp.core.projectors.db.init_url') as mocked_init_url: if os.path.exists(TEST_DB): os.unlink(TEST_DB) mocked_init_url.return_value = 'sqlite:///%s' % TEST_DB diff --git a/tests/interfaces/openlp_core_ui/test_projectorsourceform.py b/tests/interfaces/openlp_core/ui/test_projectorsourceform.py similarity index 95% rename from tests/interfaces/openlp_core_ui/test_projectorsourceform.py rename to tests/interfaces/openlp_core/ui/test_projectorsourceform.py index 4b9e2f402..815fe6ded 100644 --- a/tests/interfaces/openlp_core_ui/test_projectorsourceform.py +++ b/tests/interfaces/openlp_core/ui/test_projectorsourceform.py @@ -32,9 +32,9 @@ from unittest.mock import patch from PyQt5.QtWidgets import QDialog from openlp.core.common.registry import Registry -from openlp.core.lib.projector.db import ProjectorDB, Projector -from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES, PJLINK_DEFAULT_SOURCES -from openlp.core.ui.projector.sourceselectform import source_group, SourceSelectSingle +from openlp.core.projectors.db import ProjectorDB, Projector +from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES, PJLINK_DEFAULT_SOURCES +from openlp.core.projectors.sourceselectform import source_group, SourceSelectSingle from tests.helpers.testmixin import TestMixin from tests.resources.projector.data import TEST_DB, TEST1_DATA @@ -58,14 +58,14 @@ class ProjectorSourceFormTest(TestCase, TestMixin): """ Test class for the Projector Source Select form module """ - @patch('openlp.core.lib.projector.db.init_url') + @patch('openlp.core.projectors.db.init_url') def setUp(self, mocked_init_url): """ Set up anything necessary for all tests """ mocked_init_url.return_value = 'sqlite:///{}'.format(TEST_DB) - self.build_settings() self.setup_application() + self.build_settings() Registry.create() # Do not try to recreate if we've already been created from a previous test if not hasattr(self, 'projectordb'): diff --git a/tests/interfaces/openlp_core_ui/test_servicemanager.py b/tests/interfaces/openlp_core/ui/test_servicemanager.py similarity index 87% rename from tests/interfaces/openlp_core_ui/test_servicemanager.py rename to tests/interfaces/openlp_core/ui/test_servicemanager.py index 1deb1a97f..3426fffcb 100644 --- a/tests/interfaces/openlp_core_ui/test_servicemanager.py +++ b/tests/interfaces/openlp_core/ui/test_servicemanager.py @@ -25,102 +25,103 @@ from unittest import TestCase from unittest.mock import MagicMock, patch +from PyQt5 import QtCore, QtGui, QtWidgets + from openlp.core.common.registry import Registry -from openlp.core.display.screens import ScreenList from openlp.core.lib import ServiceItem, ItemCapabilities -from openlp.core.ui.mainwindow import MainWindow +from openlp.core.ui.servicemanager import ServiceManager from tests.helpers.testmixin import TestMixin -from PyQt5 import QtCore, QtGui, QtWidgets - class TestServiceManager(TestCase, TestMixin): + """ + Test the service manager + """ + + def _create_mock_action(self, name, **kwargs): + """ + Create a fake action with some "real" attributes + """ + action = QtWidgets.QAction(self.service_manager) + action.setObjectName(name) + if kwargs.get('triggers'): + action.triggered.connect(kwargs.pop('triggers')) + self.service_manager.toolbar.actions[name] = action + return action def setUp(self): """ Create the UI """ Registry.create() - Registry().set_flag('no_web_server', False) self.setup_application() - ScreenList.create(self.app.desktop()) Registry().register('application', MagicMock()) - # Mock classes and methods used by mainwindow. - with patch('openlp.core.ui.mainwindow.SettingsForm'), \ - patch('openlp.core.ui.mainwindow.ImageManager'), \ - patch('openlp.core.ui.mainwindow.LiveController'), \ - patch('openlp.core.ui.mainwindow.PreviewController'), \ - patch('openlp.core.ui.mainwindow.OpenLPDockWidget'), \ - patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox'), \ - patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget'), \ - patch('openlp.core.ui.mainwindow.ThemeManager'), \ - patch('openlp.core.ui.mainwindow.ProjectorManager'), \ - patch('openlp.core.ui.mainwindow.Renderer'), \ - patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ - patch('openlp.core.ui.mainwindow.server.HttpServer'): - self.main_window = MainWindow() - self.service_manager = Registry().get('service_manager') + Registry().register('main_window', MagicMock(service_manager_settings_section='servicemanager')) + self.service_manager = ServiceManager() + self.add_toolbar_action_patcher = patch('openlp.core.ui.servicemanager.OpenLPToolbar.add_toolbar_action') + self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start() + self.mocked_add_toolbar_action.side_effect = self._create_mock_action def tearDown(self): """ Delete all the C++ objects at the end so that we don't have a segfault """ - del self.main_window + self.add_toolbar_action_patcher.stop() + del self.service_manager def test_basic_service_manager(self): """ Test the Service Manager UI Functionality """ # GIVEN: A New Service Manager instance - # WHEN I have set up the display self.service_manager.setup_ui(self.service_manager) + # THEN the count of items should be zero self.assertEqual(self.service_manager.service_manager_list.topLevelItemCount(), 0, 'The service manager list should be empty ') - def test_default_context_menu(self): + @patch('openlp.core.ui.servicemanager.QtWidgets.QTreeWidget.itemAt') + @patch('openlp.core.ui.servicemanager.QtWidgets.QWidget.mapToGlobal') + @patch('openlp.core.ui.servicemanager.QtWidgets.QMenu.exec') + def test_default_context_menu(self, mocked_exec, mocked_mapToGlobal, mocked_item_at_method): """ Test the context_menu() method with a default service item """ # GIVEN: A service item added + mocked_item = MagicMock() + mocked_item.parent.return_value = None + mocked_item_at_method.return_value = mocked_item + mocked_item.data.return_value = 1 self.service_manager.setup_ui(self.service_manager) - with patch('PyQt5.QtWidgets.QTreeWidget.itemAt') as mocked_item_at_method, \ - patch('PyQt5.QtWidgets.QWidget.mapToGlobal'), \ - patch('PyQt5.QtWidgets.QMenu.exec'): - mocked_item = MagicMock() - mocked_item.parent.return_value = None - mocked_item_at_method.return_value = mocked_item - # We want 1 to be returned for the position - mocked_item.data.return_value = 1 - # A service item without capabilities. - service_item = ServiceItem() - self.service_manager.service_items = [{'service_item': service_item}] - q_point = None - # Mocked actions. - self.service_manager.edit_action.setVisible = MagicMock() - self.service_manager.create_custom_action.setVisible = MagicMock() - self.service_manager.maintain_action.setVisible = MagicMock() - self.service_manager.notes_action.setVisible = MagicMock() - self.service_manager.time_action.setVisible = MagicMock() - self.service_manager.auto_start_action.setVisible = MagicMock() + # A service item without capabilities. + service_item = ServiceItem() + self.service_manager.service_items = [{'service_item': service_item}] + q_point = None + # Mocked actions. + self.service_manager.edit_action.setVisible = MagicMock() + self.service_manager.create_custom_action.setVisible = MagicMock() + self.service_manager.maintain_action.setVisible = MagicMock() + self.service_manager.notes_action.setVisible = MagicMock() + self.service_manager.time_action.setVisible = MagicMock() + self.service_manager.auto_start_action.setVisible = MagicMock() - # WHEN: Show the context menu. - self.service_manager.context_menu(q_point) + # WHEN: Show the context menu. + self.service_manager.context_menu(q_point) - # THEN: The following actions should be not visible. - self.service_manager.edit_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.create_custom_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.maintain_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.notes_action.setVisible.assert_called_with(True), 'The action should be set visible.' - self.service_manager.time_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.auto_start_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' + # THEN: The following actions should be not visible. + self.service_manager.edit_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.create_custom_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.maintain_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.notes_action.setVisible.assert_called_with(True), 'The action should be set visible.' + self.service_manager.time_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.auto_start_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' def test_edit_context_menu(self): """ diff --git a/tests/interfaces/openlp_core_ui/test_servicenotedialog.py b/tests/interfaces/openlp_core/ui/test_servicenotedialog.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_servicenotedialog.py rename to tests/interfaces/openlp_core/ui/test_servicenotedialog.py diff --git a/tests/interfaces/openlp_core_ui/test_settings_form.py b/tests/interfaces/openlp_core/ui/test_settings_form.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_settings_form.py rename to tests/interfaces/openlp_core/ui/test_settings_form.py diff --git a/tests/interfaces/openlp_core_ui/test_shortcutlistform.py b/tests/interfaces/openlp_core/ui/test_shortcutlistform.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_shortcutlistform.py rename to tests/interfaces/openlp_core/ui/test_shortcutlistform.py diff --git a/tests/interfaces/openlp_core_ui/test_starttimedialog.py b/tests/interfaces/openlp_core/ui/test_starttimedialog.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_starttimedialog.py rename to tests/interfaces/openlp_core/ui/test_starttimedialog.py diff --git a/tests/interfaces/openlp_core_ui/test_thememanager.py b/tests/interfaces/openlp_core/ui/test_thememanager.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_thememanager.py rename to tests/interfaces/openlp_core/ui/test_thememanager.py index 7f3927cf5..0808b12d0 100644 --- a/tests/interfaces/openlp_core_ui/test_thememanager.py +++ b/tests/interfaces/openlp_core/ui/test_thememanager.py @@ -41,8 +41,8 @@ class TestThemeManager(TestCase, TestMixin): """ Create the UI """ - self.build_settings() self.setup_application() + self.build_settings() Registry.create() self.theme_manager = ThemeManager() diff --git a/tests/interfaces/openlp_core/widgets/__init__.py b/tests/interfaces/openlp_core/widgets/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/widgets/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_lib/test_searchedit.py b/tests/interfaces/openlp_core/widgets/test_edits.py similarity index 84% rename from tests/interfaces/openlp_core_lib/test_searchedit.py rename to tests/interfaces/openlp_core/widgets/test_edits.py index d4751ddf0..3951e5f80 100644 --- a/tests/interfaces/openlp_core_lib/test_searchedit.py +++ b/tests/interfaces/openlp_core/widgets/test_edits.py @@ -28,7 +28,7 @@ from unittest.mock import MagicMock, call, patch from PyQt5 import QtCore, QtGui, QtTest, QtWidgets from openlp.core.common.registry import Registry -from openlp.core.lib.searchedit import SearchEdit +from openlp.core.widgets.edits import SearchEdit, HistoryComboBox from tests.helpers.testmixin import TestMixin @@ -60,7 +60,7 @@ class TestSearchEdit(TestCase, TestMixin): Registry().register('main_window', self.main_window) settings_patcher = patch( - 'openlp.core.lib.searchedit.Settings', return_value=MagicMock(**{'value.return_value': SearchTypes.First})) + 'openlp.core.widgets.edits.Settings', return_value=MagicMock(**{'value.return_value': SearchTypes.First})) self.addCleanup(settings_patcher.stop) self.mocked_settings = settings_patcher.start() @@ -135,3 +135,35 @@ class TestSearchEdit(TestCase, TestMixin): # THEN: The search edit text should be cleared and the button be hidden. assert not self.search_edit.text(), "The search edit should not have any text." assert self.search_edit.clear_button.isHidden(), "The clear button should be hidden." + + +class TestHistoryComboBox(TestCase, TestMixin): + def setUp(self): + """ + Some pre-test setup required. + """ + Registry.create() + self.setup_application() + self.main_window = QtWidgets.QMainWindow() + Registry().register('main_window', self.main_window) + self.combo = HistoryComboBox(self.main_window) + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.combo + del self.main_window + + def test_get_items(self): + """ + Test the getItems() method + """ + # GIVEN: The combo. + + # WHEN: Add two items. + self.combo.addItem('test1') + self.combo.addItem('test2') + + # THEN: The list of items should contain both strings. + self.assertEqual(self.combo.getItems(), ['test1', 'test2']) diff --git a/tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py b/tests/interfaces/openlp_core/widgets/test_views.py similarity index 97% rename from tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py rename to tests/interfaces/openlp_core/widgets/test_views.py index e22f9e9a4..f4a493f4d 100644 --- a/tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py +++ b/tests/interfaces/openlp_core/widgets/test_views.py @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ - Package to test the openlp.core.ui.lib.listpreviewwidget. + Package to test the openlp.core.widgets.views. """ from unittest import TestCase from unittest.mock import MagicMock, patch @@ -29,7 +29,7 @@ from PyQt5 import QtGui, QtWidgets from openlp.core.common.registry import Registry from openlp.core.lib import ServiceItem -from openlp.core.ui.lib import ListWidgetWithDnD, ListPreviewWidget +from openlp.core.widgets.views import ListPreviewWidget from tests.utils.osdinteraction import read_service_from_file from tests.helpers.testmixin import TestMixin diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index fd5aeccfd..dd4d78354 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -36,7 +36,7 @@ def convert_file_service_item(test_path, name, row=0): try: items = json.load(open_file) first_line = items[row] - except IOError: + except OSError: first_line = '' finally: open_file.close() diff --git a/tests/utils/test_pylint.py b/tests/utils/test_pylint.py index 128c0741b..50ca64db6 100644 --- a/tests/utils/test_pylint.py +++ b/tests/utils/test_pylint.py @@ -58,17 +58,21 @@ class TestPylint(TestCase): # GIVEN: Some checks to disable and enable, and the pylint script disabled_checks = 'import-error,no-member' enabled_checks = 'missing-format-argument-key,unused-format-string-argument,bad-format-string' - if is_win() or 'arch' in platform.dist()[0].lower(): - pylint_script = 'pylint' - else: - pylint_script = 'pylint3' + pylint_kwargs = { + 'return_std': True + } + if version < '1.7.0': + if is_win() or 'arch' in platform.dist()[0].lower(): + pylint_kwargs.update({'script': 'pylint'}) + else: + pylint_kwargs.update({'script': 'pylint3'}) # WHEN: Running pylint (pylint_stdout, pylint_stderr) = \ lint.py_run('openlp --errors-only --disable={disabled} --enable={enabled} ' '--reports=no --output-format=parseable'.format(disabled=disabled_checks, enabled=enabled_checks), - return_std=True, script=pylint_script) + **pylint_kwargs) stdout = pylint_stdout.read() stderr = pylint_stderr.read() filtered_stdout = self._filter_tolerated_errors(stdout)