← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~trb143/openlp/refactor26 into lp:openlp

 

Tim Bentley has proposed merging lp:~trb143/openlp/refactor26 into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~trb143/openlp/refactor26/+merge/290638

Started to move things around and make more logical in 2.2 and planned to do more in 2.4.
Never got a chance in 2.4 so here goes.

Move classes from utils to common 

lp:~trb143/openlp/refactor26 (revision 2637)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1358/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1277/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1216/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1049/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/640/
[SUCCESS] https://ci.openlp.io/job/Branch-05a-Code_Analysis/707/
[SUCCESS] https://ci.openlp.io/job/Branch-05b-Test_Coverage/575/


next set is to sort out utils __init__ but that is bigger!
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/refactor26 into lp:openlp.
=== modified file 'openlp/core/__init__.py'
--- openlp/core/__init__.py	2016-01-16 20:13:41 +0000
+++ openlp/core/__init__.py	2016-03-31 17:04:24 +0000
@@ -36,8 +36,8 @@
 import time
 from PyQt5 import QtCore, QtGui, QtWidgets
 
-from openlp.core.common import Registry, OpenLPMixin, AppLocation, Settings, UiStrings, check_directory_exists, \
-    is_macosx, is_win, translate
+from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
+    check_directory_exists, is_macosx, is_win, translate
 from openlp.core.lib import ScreenList
 from openlp.core.resources import qInitResources
 from openlp.core.ui.mainwindow import MainWindow
@@ -45,7 +45,7 @@
 from openlp.core.ui.firsttimeform import FirstTimeForm
 from openlp.core.ui.exceptionform import ExceptionForm
 from openlp.core.ui import SplashScreen
-from openlp.core.utils import LanguageManager, VersionThread, get_application_version
+from openlp.core.utils import VersionThread, get_application_version
 
 
 __all__ = ['OpenLP', 'main']

=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2015-12-31 22:46:06 +0000
+++ openlp/core/common/__init__.py	2016-03-31 17:04:24 +0000
@@ -242,3 +242,5 @@
 from .settings import Settings
 from .applocation import AppLocation
 from .historycombobox import HistoryComboBox
+from .actions import ActionList
+from .languagemanager import LanguageManager

=== added file 'openlp/core/common/actions.py'
--- openlp/core/common/actions.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/actions.py	2016-03-31 17:04:24 +0000
@@ -0,0 +1,388 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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.utils.actions` module provides action list classes used
+by the shortcuts system.
+"""
+import logging
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Settings
+
+
+log = logging.getLogger(__name__)
+
+
+class ActionCategory(object):
+    """
+    The :class:`~openlp.core.utils.ActionCategory` class encapsulates a category for the
+    :class:`~openlp.core.utils.CategoryList` class.
+    """
+    def __init__(self, name, weight=0):
+        """
+        Constructor
+        """
+        self.name = name
+        self.weight = weight
+        self.actions = CategoryActionList()
+
+
+class CategoryActionList(object):
+    """
+    The :class:`~openlp.core.utils.CategoryActionList` class provides a sorted list of actions within a category.
+    """
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.index = 0
+        self.actions = []
+
+    def __contains__(self, key):
+        """
+        Implement the __contains__() method to make this class a dictionary type
+        """
+        for weight, action in self.actions:
+            if action == key:
+                return True
+        return False
+
+    def __len__(self):
+        """
+        Implement the __len__() method to make this class a dictionary type
+        """
+        return len(self.actions)
+
+    def __iter__(self):
+        """
+        Implement the __getitem__() method to make this class iterable
+        """
+        return self
+
+    def __next__(self):
+        """
+        Python 3 "next" method.
+        """
+        if self.index >= len(self.actions):
+            self.index = 0
+            raise StopIteration
+        else:
+            self.index += 1
+            return self.actions[self.index - 1][1]
+
+    def append(self, action):
+        """
+        Append an action
+        """
+        weight = 0
+        if self.actions:
+            weight = self.actions[-1][0] + 1
+        self.add(action, weight)
+
+    def add(self, action, weight=0):
+        """
+        Add an action.
+        """
+        self.actions.append((weight, action))
+        self.actions.sort(key=lambda act: act[0])
+
+    def remove(self, action):
+        """
+        Remove an action
+        """
+        for item in self.actions:
+            if item[1] == action:
+                self.actions.remove(item)
+                return
+        raise ValueError('Action "%s" does not exist.' % action)
+
+
+class CategoryList(object):
+    """
+    The :class:`~openlp.core.utils.CategoryList` class encapsulates a category list for the
+    :class:`~openlp.core.utils.ActionList` class and provides an iterator interface for walking through the list of
+    actions in this category.
+    """
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.index = 0
+        self.categories = []
+
+    def __getitem__(self, key):
+        """
+        Implement the __getitem__() method to make this class like a dictionary
+        """
+        for category in self.categories:
+            if category.name == key:
+                return category
+        raise KeyError('Category "%s" does not exist.' % key)
+
+    def __len__(self):
+        """
+        Implement the __len__() method to make this class like a dictionary
+        """
+        return len(self.categories)
+
+    def __iter__(self):
+        """
+        Implement the __iter__() method to make this class like a dictionary
+        """
+        return self
+
+    def __next__(self):
+        """
+        Python 3 "next" method for iterator.
+        """
+        if self.index >= len(self.categories):
+            self.index = 0
+            raise StopIteration
+        else:
+            self.index += 1
+            return self.categories[self.index - 1]
+
+    def __contains__(self, key):
+        """
+        Implement the __contains__() method to make this class like a dictionary
+        """
+        for category in self.categories:
+            if category.name == key:
+                return True
+        return False
+
+    def append(self, name, actions=None):
+        """
+        Append a category
+        """
+        weight = 0
+        if self.categories:
+            weight = self.categories[-1].weight + 1
+        self.add(name, weight, actions)
+
+    def add(self, name, weight=0, actions=None):
+        """
+        Add a category
+        """
+        category = ActionCategory(name, weight)
+        if actions:
+            for action in actions:
+                if isinstance(action, tuple):
+                    category.actions.add(action[0], action[1])
+                else:
+                    category.actions.append(action)
+        self.categories.append(category)
+        self.categories.sort(key=lambda cat: cat.weight)
+
+    def remove(self, name):
+        """
+        Remove a category
+        """
+        for category in self.categories:
+            if category.name == name:
+                self.categories.remove(category)
+                return
+        raise ValueError('Category "%s" does not exist.' % name)
+
+
+class ActionList(object):
+    """
+    The :class:`~openlp.core.utils.ActionList` class contains a list of menu actions and categories associated with
+    those actions. Each category also has a weight by which it is sorted when iterating through the list of actions or
+    categories.
+    """
+    instance = None
+    shortcut_map = {}
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.categories = CategoryList()
+
+    @staticmethod
+    def get_instance():
+        """
+        Get the instance of this class.
+        """
+        if ActionList.instance is None:
+            ActionList.instance = ActionList()
+        return ActionList.instance
+
+    def add_action(self, action, category=None, weight=None):
+        """
+        Add an action to the list of actions.
+
+        **Note**: The action's objectName must be set when you want to add it!
+
+        :param action: The action to add (QAction). **Note**, the action must not have an empty ``objectName``.
+        :param category: The category this action belongs to. The category has to be a python string. . **Note**,
+            if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
+            if they are added, it is possible to avoid assigning shortcuts twice, which is important.
+        :param weight: The weight specifies how important a category is. However, this only has an impact on the order
+            the categories are displayed.
+        """
+        if category not in self.categories:
+            self.categories.append(category)
+        settings = Settings()
+        settings.beginGroup('shortcuts')
+        # Get the default shortcut from the config.
+        action.default_shortcuts = settings.get_default_value(action.objectName())
+        if weight is None:
+            self.categories[category].actions.append(action)
+        else:
+            self.categories[category].actions.add(action, weight)
+        # Load the shortcut from the config.
+        shortcuts = settings.value(action.objectName())
+        settings.endGroup()
+        if not shortcuts:
+            action.setShortcuts([])
+            return
+        # We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
+        # which is only done when we convert the strings in this way (QKeySequencet -> uncode).
+        shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
+        # Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
+        #  after removing the (initial) primary shortcut due to conflicts.
+        if len(shortcuts) == 2:
+            existing_actions = ActionList.shortcut_map.get(shortcuts[1], [])
+            # Check for conflicts with other actions considering the shortcut context.
+            if self._is_shortcut_available(existing_actions, action):
+                actions = ActionList.shortcut_map.get(shortcuts[1], [])
+                actions.append(action)
+                ActionList.shortcut_map[shortcuts[1]] = actions
+            else:
+                log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
+                            (shortcuts[1], action.objectName()))
+                shortcuts.remove(shortcuts[1])
+        # Check the primary shortcut.
+        existing_actions = ActionList.shortcut_map.get(shortcuts[0], [])
+        # Check for conflicts with other actions considering the shortcut context.
+        if self._is_shortcut_available(existing_actions, action):
+            actions = ActionList.shortcut_map.get(shortcuts[0], [])
+            actions.append(action)
+            ActionList.shortcut_map[shortcuts[0]] = actions
+        else:
+            log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
+                        (shortcuts[0], action.objectName()))
+            shortcuts.remove(shortcuts[0])
+        action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts])
+
+    def remove_action(self, action, category=None):
+        """
+        This removes an action from its category. Empty categories are automatically removed.
+
+        :param action:  The ``QAction`` object to be removed.
+        :param category: The name (unicode string) of the category, which contains the action. Defaults to None.
+        """
+        if category not in self.categories:
+            return
+        self.categories[category].actions.remove(action)
+        # Remove empty categories.
+        if not self.categories[category].actions:
+            self.categories.remove(category)
+        shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
+        for shortcut in shortcuts:
+            # Remove action from the list of actions which are using this shortcut.
+            ActionList.shortcut_map[shortcut].remove(action)
+            # Remove empty entries.
+            if not ActionList.shortcut_map[shortcut]:
+                del ActionList.shortcut_map[shortcut]
+
+    def add_category(self, name, weight):
+        """
+        Add an empty category to the list of categories. This is only convenient for categories with a given weight.
+
+        :param name: The category's name.
+        :param weight: The category's weight (int).
+        """
+        if name in self.categories:
+            # Only change the weight and resort the categories again.
+            for category in self.categories:
+                if category.name == name:
+                    category.weight = weight
+            self.categories.categories.sort(key=lambda cat: cat.weight)
+            return
+        self.categories.add(name, weight)
+
+    def update_shortcut_map(self, action, old_shortcuts):
+        """
+        Remove the action for the given ``old_shortcuts`` from the ``shortcut_map`` to ensure its up-to-dateness.
+        **Note**: The new action's shortcuts **must** be assigned to the given ``action`` **before** calling this
+        method.
+
+        :param action: The action whose shortcuts are supposed to be updated in the ``shortcut_map``.
+        :param old_shortcuts: A list of unicode key sequences.
+        """
+        for old_shortcut in old_shortcuts:
+            # Remove action from the list of actions which are using this shortcut.
+            ActionList.shortcut_map[old_shortcut].remove(action)
+            # Remove empty entries.
+            if not ActionList.shortcut_map[old_shortcut]:
+                del ActionList.shortcut_map[old_shortcut]
+        new_shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
+        # Add the new shortcuts to the map.
+        for new_shortcut in new_shortcuts:
+            existing_actions = ActionList.shortcut_map.get(new_shortcut, [])
+            existing_actions.append(action)
+            ActionList.shortcut_map[new_shortcut] = existing_actions
+
+    def _is_shortcut_available(self, existing_actions, action):
+        """
+        Checks if the given ``action`` may use its assigned shortcut(s) or not. Returns ``True`` or ``False.
+
+        :param existing_actions: A list of actions which already use a particular shortcut.
+        :param action: The action which wants to use a particular shortcut.
+        """
+        global_context = action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]
+        affected_actions = []
+        if global_context:
+            affected_actions = [a for a in self.get_all_child_objects(action.parent()) if isinstance(a,
+                                                                                                     QtWidgets.QAction)]
+        for existing_action in existing_actions:
+            if action is existing_action:
+                continue
+            if existing_action in affected_actions:
+                return False
+            if existing_action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]:
+                return False
+            elif action in self.get_all_child_objects(existing_action.parent()):
+                return False
+        return True
+
+    def get_all_child_objects(self, qobject):
+        """
+        Goes recursively through the children of ``qobject`` and returns a list of all child objects.
+        """
+        children = qobject.children()
+        # Append the children's children.
+        children.extend(list(map(self.get_all_child_objects, children)))
+        return children
+
+
+class CategoryOrder(object):
+    """
+    An enumeration class for category weights.
+    """
+    standard_menu = -20
+    standard_toolbar = -10

=== added file 'openlp/core/common/db.py'
--- openlp/core/common/db.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/db.py	2016-03-31 17:04:24 +0000
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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:`db` module provides helper functions for database related methods.
+"""
+import sqlalchemy
+import logging
+
+from copy import deepcopy
+
+log = logging.getLogger(__name__)
+
+
+def drop_column(op, tablename, columnname):
+    drop_columns(op, tablename, [columnname])
+
+
+def drop_columns(op, tablename, columns):
+    """
+    Column dropping functionality for SQLite, as there is no DROP COLUMN support in SQLite
+
+    From https://github.com/klugjohannes/alembic-sqlite
+    """
+
+    # get the db engine and reflect database tables
+    engine = op.get_bind()
+    meta = sqlalchemy.MetaData(bind=engine)
+    meta.reflect()
+
+    # create a select statement from the old table
+    old_table = meta.tables[tablename]
+    select = sqlalchemy.sql.select([c for c in old_table.c if c.name not in columns])
+
+    # get remaining columns without table attribute attached
+    remaining_columns = [deepcopy(c) for c in old_table.columns if c.name not in columns]
+    for column in remaining_columns:
+        column.table = None
+
+    # create a temporary new table
+    new_tablename = '{0}_new'.format(tablename)
+    op.create_table(new_tablename, *remaining_columns)
+    meta.reflect()
+    new_table = meta.tables[new_tablename]
+
+    # copy data from old table
+    insert = sqlalchemy.sql.insert(new_table).from_select([c.name for c in remaining_columns], select)
+    engine.execute(insert)
+
+    # drop the old table and rename the new table to take the old tables
+    # position
+    op.drop_table(tablename)
+    op.rename_table(new_tablename, tablename)

=== added file 'openlp/core/common/languagemanager.py'
--- openlp/core/common/languagemanager.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/languagemanager.py	2016-03-31 17:04:24 +0000
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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:`languagemanager` module provides all the translation settings and language file loading for OpenLP.
+"""
+import logging
+import re
+import sys
+
+from PyQt5 import QtCore, QtWidgets
+
+
+from openlp.core.common import AppLocation, Settings, translate, is_win, is_macosx
+
+log = logging.getLogger(__name__)
+
+
+class LanguageManager(object):
+    """
+    Helper for Language selection
+    """
+    __qm_list__ = {}
+    auto_language = False
+
+    @staticmethod
+    def get_translator(language):
+        """
+        Set up a translator to use in this instance of OpenLP
+
+        :param language: The language to load into the translator
+        """
+        if LanguageManager.auto_language:
+            language = QtCore.QLocale.system().name()
+        lang_path = AppLocation.get_directory(AppLocation.LanguageDir)
+        app_translator = QtCore.QTranslator()
+        app_translator.load(language, lang_path)
+        # A translator for buttons and other default strings provided by Qt.
+        if not is_win() and not is_macosx():
+            lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)
+        default_translator = QtCore.QTranslator()
+        default_translator.load('qt_%s' % language, lang_path)
+        return app_translator, default_translator
+
+    @staticmethod
+    def find_qm_files():
+        """
+        Find all available language files in this OpenLP install
+        """
+        log.debug('Translation files: %s', AppLocation.get_directory(AppLocation.LanguageDir))
+        trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir))
+        file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
+        # Remove qm files from the list which start with "qt_".
+        file_names = [file_ for file_ in file_names if not file_.startswith('qt_')]
+        return list(map(trans_dir.filePath, file_names))
+
+    @staticmethod
+    def language_name(qm_file):
+        """
+        Load the language name from a language file
+
+        :param qm_file: The file to obtain the name from
+        """
+        translator = QtCore.QTranslator()
+        translator.load(qm_file)
+        return translator.translate('OpenLP.MainWindow', 'English', 'Please add the name of your language here')
+
+    @staticmethod
+    def get_language():
+        """
+        Retrieve a saved language to use from settings
+        """
+        language = Settings().value('core/language')
+        language = str(language)
+        log.info('Language file: \'%s\' Loaded from conf file' % language)
+        if re.match(r'[[].*[]]', language):
+            LanguageManager.auto_language = True
+            language = re.sub(r'[\[\]]', '', language)
+        return language
+
+    @staticmethod
+    def set_language(action, message=True):
+        """
+        Set the language to translate OpenLP into
+
+        :param action:  The language menu option
+        :param message:  Display the message option
+        """
+        language = 'en'
+        if action:
+            action_name = str(action.objectName())
+            if action_name == 'autoLanguageItem':
+                LanguageManager.auto_language = True
+            else:
+                LanguageManager.auto_language = False
+                qm_list = LanguageManager.get_qm_list()
+                language = str(qm_list[action_name])
+        if LanguageManager.auto_language:
+            language = '[%s]' % language
+        Settings().setValue('core/language', language)
+        log.info('Language file: \'%s\' written to conf file' % language)
+        if message:
+            QtWidgets.QMessageBox.information(None,
+                                              translate('OpenLP.LanguageManager', 'Language'),
+                                              translate('OpenLP.LanguageManager',
+                                                        'Please restart OpenLP to use your new language setting.'))
+
+    @staticmethod
+    def init_qm_list():
+        """
+        Initialise the list of available translations
+        """
+        LanguageManager.__qm_list__ = {}
+        qm_files = LanguageManager.find_qm_files()
+        for counter, qmf in enumerate(qm_files):
+            reg_ex = QtCore.QRegExp("^.*i18n/(.*).qm")
+            if reg_ex.exactMatch(qmf):
+                name = '%s' % reg_ex.cap(1)
+                LanguageManager.__qm_list__['%#2i %s' % (counter + 1, LanguageManager.language_name(qmf))] = name
+
+    @staticmethod
+    def get_qm_list():
+        """
+        Return the list of available translations
+        """
+        if not LanguageManager.__qm_list__:
+            LanguageManager.init_qm_list()
+        return LanguageManager.__qm_list__

=== modified file 'openlp/core/lib/ui.py'
--- openlp/core/lib/ui.py	2015-12-31 22:46:06 +0000
+++ openlp/core/lib/ui.py	2016-03-31 17:04:24 +0000
@@ -27,8 +27,8 @@
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, UiStrings, translate, is_macosx
+from openlp.core.common.actions import ActionList
 from openlp.core.lib import build_icon
-from openlp.core.utils.actions import ActionList
 
 
 log = logging.getLogger(__name__)

=== modified file 'openlp/core/ui/firsttimelanguageform.py'
--- openlp/core/ui/firsttimelanguageform.py	2016-01-09 16:26:14 +0000
+++ openlp/core/ui/firsttimelanguageform.py	2016-03-31 17:04:24 +0000
@@ -25,7 +25,7 @@
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.lib.ui import create_action
-from openlp.core.utils import LanguageManager
+from openlp.core.common import LanguageManager
 from .firsttimelanguagedialog import Ui_FirstTimeLanguageDialog
 
 

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2016-01-11 21:57:20 +0000
+++ openlp/core/ui/mainwindow.py	2016-03-31 17:04:24 +0000
@@ -24,30 +24,29 @@
 """
 import logging
 import os
+import shutil
 import sys
-import shutil
+import time
+from datetime import datetime
 from distutils import dir_util
 from distutils.errors import DistutilsFileError
 from tempfile import gettempdir
-import time
-from datetime import datetime
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
-from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, translate, \
-    is_win, is_macosx
+from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, \
+    check_directory_exists, translate, is_win, is_macosx
+from openlp.core.common.actions import ActionList, CategoryOrder
 from openlp.core.lib import Renderer, OpenLPDockWidget, PluginManager, ImageManager, PluginStatus, ScreenList, \
     build_icon
 from openlp.core.lib.ui import UiStrings, create_action
 from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
     MediaDockManager, ShortcutListForm, FormattingTagForm, PreviewController
-
+from openlp.core.ui.firsttimeform import FirstTimeForm
 from openlp.core.ui.media import MediaController
-from openlp.core.utils import LanguageManager, add_actions, get_application_version
-from openlp.core.utils.actions import ActionList, CategoryOrder
-from openlp.core.ui.firsttimeform import FirstTimeForm
+from openlp.core.ui.printserviceform import PrintServiceForm
 from openlp.core.ui.projector.manager import ProjectorManager
-from openlp.core.ui.printserviceform import PrintServiceForm
+from openlp.core.utils import get_application_version, add_actions
 
 log = logging.getLogger(__name__)
 

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2016-01-19 07:02:47 +0000
+++ openlp/core/ui/servicemanager.py	2016-03-31 17:04:24 +0000
@@ -23,23 +23,22 @@
 The service manager sets up, loads, saves and manages services.
 """
 import html
+import json
 import os
 import shutil
 import zipfile
-import json
+from datetime import datetime, timedelta
 from tempfile import mkstemp
-from datetime import datetime, timedelta
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \
     RegistryMixin, check_directory_exists, UiStrings, translate
+from openlp.core.common.actions import ActionList, CategoryOrder
 from openlp.core.lib import OpenLPToolbar, 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.printserviceform import PrintServiceForm
 from openlp.core.utils import delete_file, split_filename, format_time
-from openlp.core.utils.actions import ActionList, CategoryOrder
 
 
 class ServiceManagerList(QtWidgets.QTreeWidget):

=== modified file 'openlp/core/ui/shortcutlistform.py'
--- openlp/core/ui/shortcutlistform.py	2016-02-04 21:25:06 +0000
+++ openlp/core/ui/shortcutlistform.py	2016-03-31 17:04:24 +0000
@@ -27,7 +27,7 @@
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import RegistryProperties, Settings, translate
-from openlp.core.utils.actions import ActionList
+from openlp.core.common.actions import ActionList
 from .shortcutlistdialog import Ui_ShortcutListDialog
 
 REMOVE_AMPERSAND = re.compile(r'&{1}')

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2016-02-28 20:33:19 +0000
+++ openlp/core/ui/slidecontroller.py	2016-03-31 17:04:24 +0000
@@ -23,20 +23,20 @@
 The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller
 """
 
+import copy
 import os
-import copy
 from collections import deque
 from threading import Lock
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, Settings, SlideLimits, UiStrings, translate, \
-    RegistryMixin, OpenLPMixin, is_win
+    RegistryMixin, OpenLPMixin
+from openlp.core.common.actions import ActionList, CategoryOrder
 from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, \
     ScreenList, build_icon, build_html
+from openlp.core.lib.ui import create_action
 from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
-from openlp.core.lib.ui import create_action
-from openlp.core.utils.actions import ActionList, CategoryOrder
 from openlp.core.ui.listpreviewwidget import ListPreviewWidget
 
 # Threshold which has to be trespassed to toggle.

=== modified file 'openlp/core/utils/__init__.py'
--- openlp/core/utils/__init__.py	2015-12-31 22:46:06 +0000
+++ openlp/core/utils/__init__.py	2016-03-31 17:04:24 +0000
@@ -22,29 +22,28 @@
 """
 The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
 """
-from datetime import datetime
-from distutils.version import LooseVersion
-from http.client import HTTPException
+import locale
 import logging
-import locale
 import os
 import platform
 import re
 import socket
+import sys
 import time
-from shutil import which
-from subprocess import Popen, PIPE
-import sys
-import urllib.request
 import urllib.error
 import urllib.parse
+import urllib.request
+from datetime import datetime
+from distutils.version import LooseVersion
+from http.client import HTTPException
 from random import randint
+from shutil import which
+from subprocess import Popen, PIPE
 
 from PyQt5 import QtGui, QtCore
 
 from openlp.core.common import Registry, AppLocation, Settings, is_win, is_macosx
 
-
 if not is_win() and not is_macosx():
     try:
         from xdg import BaseDirectory
@@ -511,7 +510,7 @@
     try:
         if ICU_COLLATOR is None:
             import icu
-            from .languagemanager import LanguageManager
+            from openlp.core.common.languagemanager import LanguageManager
             language = LanguageManager.get_language()
             icu_locale = icu.Locale(language)
             ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
@@ -523,21 +522,18 @@
 def get_natural_key(string):
     """
     Generate a key for locale aware natural string sorting.
+
+    :param string: string to be sorted by
     Returns a list of string compare keys and integers.
     """
     key = DIGITS_OR_NONDIGITS.findall(string)
     key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
     # Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
     # and int.
-    if string[0].isdigit():
+    if string and string[0].isdigit():
         return [b''] + key
     return key
 
-
-from .languagemanager import LanguageManager
-from .actions import ActionList
-
-
-__all__ = ['ActionList', 'LanguageManager', 'get_application_version', 'check_latest_version',
+__all__ = ['get_application_version', 'check_latest_version',
            'add_actions', 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance',
            'delete_file', 'clean_filename', 'format_time', 'get_locale_key', 'get_natural_key']

=== removed file 'openlp/core/utils/actions.py'
--- openlp/core/utils/actions.py	2015-12-31 22:46:06 +0000
+++ openlp/core/utils/actions.py	1970-01-01 00:00:00 +0000
@@ -1,388 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection                                      #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2016 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.utils.actions` module provides action list classes used
-by the shortcuts system.
-"""
-import logging
-
-from PyQt5 import QtCore, QtGui, QtWidgets
-
-from openlp.core.common import Settings
-
-
-log = logging.getLogger(__name__)
-
-
-class ActionCategory(object):
-    """
-    The :class:`~openlp.core.utils.ActionCategory` class encapsulates a category for the
-    :class:`~openlp.core.utils.CategoryList` class.
-    """
-    def __init__(self, name, weight=0):
-        """
-        Constructor
-        """
-        self.name = name
-        self.weight = weight
-        self.actions = CategoryActionList()
-
-
-class CategoryActionList(object):
-    """
-    The :class:`~openlp.core.utils.CategoryActionList` class provides a sorted list of actions within a category.
-    """
-    def __init__(self):
-        """
-        Constructor
-        """
-        self.index = 0
-        self.actions = []
-
-    def __contains__(self, key):
-        """
-        Implement the __contains__() method to make this class a dictionary type
-        """
-        for weight, action in self.actions:
-            if action == key:
-                return True
-        return False
-
-    def __len__(self):
-        """
-        Implement the __len__() method to make this class a dictionary type
-        """
-        return len(self.actions)
-
-    def __iter__(self):
-        """
-        Implement the __getitem__() method to make this class iterable
-        """
-        return self
-
-    def __next__(self):
-        """
-        Python 3 "next" method.
-        """
-        if self.index >= len(self.actions):
-            self.index = 0
-            raise StopIteration
-        else:
-            self.index += 1
-            return self.actions[self.index - 1][1]
-
-    def append(self, action):
-        """
-        Append an action
-        """
-        weight = 0
-        if self.actions:
-            weight = self.actions[-1][0] + 1
-        self.add(action, weight)
-
-    def add(self, action, weight=0):
-        """
-        Add an action.
-        """
-        self.actions.append((weight, action))
-        self.actions.sort(key=lambda act: act[0])
-
-    def remove(self, action):
-        """
-        Remove an action
-        """
-        for item in self.actions:
-            if item[1] == action:
-                self.actions.remove(item)
-                return
-        raise ValueError('Action "%s" does not exist.' % action)
-
-
-class CategoryList(object):
-    """
-    The :class:`~openlp.core.utils.CategoryList` class encapsulates a category list for the
-    :class:`~openlp.core.utils.ActionList` class and provides an iterator interface for walking through the list of
-    actions in this category.
-    """
-
-    def __init__(self):
-        """
-        Constructor
-        """
-        self.index = 0
-        self.categories = []
-
-    def __getitem__(self, key):
-        """
-        Implement the __getitem__() method to make this class like a dictionary
-        """
-        for category in self.categories:
-            if category.name == key:
-                return category
-        raise KeyError('Category "%s" does not exist.' % key)
-
-    def __len__(self):
-        """
-        Implement the __len__() method to make this class like a dictionary
-        """
-        return len(self.categories)
-
-    def __iter__(self):
-        """
-        Implement the __iter__() method to make this class like a dictionary
-        """
-        return self
-
-    def __next__(self):
-        """
-        Python 3 "next" method for iterator.
-        """
-        if self.index >= len(self.categories):
-            self.index = 0
-            raise StopIteration
-        else:
-            self.index += 1
-            return self.categories[self.index - 1]
-
-    def __contains__(self, key):
-        """
-        Implement the __contains__() method to make this class like a dictionary
-        """
-        for category in self.categories:
-            if category.name == key:
-                return True
-        return False
-
-    def append(self, name, actions=None):
-        """
-        Append a category
-        """
-        weight = 0
-        if self.categories:
-            weight = self.categories[-1].weight + 1
-        self.add(name, weight, actions)
-
-    def add(self, name, weight=0, actions=None):
-        """
-        Add a category
-        """
-        category = ActionCategory(name, weight)
-        if actions:
-            for action in actions:
-                if isinstance(action, tuple):
-                    category.actions.add(action[0], action[1])
-                else:
-                    category.actions.append(action)
-        self.categories.append(category)
-        self.categories.sort(key=lambda cat: cat.weight)
-
-    def remove(self, name):
-        """
-        Remove a category
-        """
-        for category in self.categories:
-            if category.name == name:
-                self.categories.remove(category)
-                return
-        raise ValueError('Category "%s" does not exist.' % name)
-
-
-class ActionList(object):
-    """
-    The :class:`~openlp.core.utils.ActionList` class contains a list of menu actions and categories associated with
-    those actions. Each category also has a weight by which it is sorted when iterating through the list of actions or
-    categories.
-    """
-    instance = None
-    shortcut_map = {}
-
-    def __init__(self):
-        """
-        Constructor
-        """
-        self.categories = CategoryList()
-
-    @staticmethod
-    def get_instance():
-        """
-        Get the instance of this class.
-        """
-        if ActionList.instance is None:
-            ActionList.instance = ActionList()
-        return ActionList.instance
-
-    def add_action(self, action, category=None, weight=None):
-        """
-        Add an action to the list of actions.
-
-        **Note**: The action's objectName must be set when you want to add it!
-
-        :param action: The action to add (QAction). **Note**, the action must not have an empty ``objectName``.
-        :param category: The category this action belongs to. The category has to be a python string. . **Note**,
-            if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
-            if they are added, it is possible to avoid assigning shortcuts twice, which is important.
-        :param weight: The weight specifies how important a category is. However, this only has an impact on the order
-            the categories are displayed.
-        """
-        if category not in self.categories:
-            self.categories.append(category)
-        settings = Settings()
-        settings.beginGroup('shortcuts')
-        # Get the default shortcut from the config.
-        action.default_shortcuts = settings.get_default_value(action.objectName())
-        if weight is None:
-            self.categories[category].actions.append(action)
-        else:
-            self.categories[category].actions.add(action, weight)
-        # Load the shortcut from the config.
-        shortcuts = settings.value(action.objectName())
-        settings.endGroup()
-        if not shortcuts:
-            action.setShortcuts([])
-            return
-        # We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
-        # which is only done when we convert the strings in this way (QKeySequencet -> uncode).
-        shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
-        # Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
-        #  after removing the (initial) primary shortcut due to conflicts.
-        if len(shortcuts) == 2:
-            existing_actions = ActionList.shortcut_map.get(shortcuts[1], [])
-            # Check for conflicts with other actions considering the shortcut context.
-            if self._is_shortcut_available(existing_actions, action):
-                actions = ActionList.shortcut_map.get(shortcuts[1], [])
-                actions.append(action)
-                ActionList.shortcut_map[shortcuts[1]] = actions
-            else:
-                log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
-                            (shortcuts[1], action.objectName()))
-                shortcuts.remove(shortcuts[1])
-        # Check the primary shortcut.
-        existing_actions = ActionList.shortcut_map.get(shortcuts[0], [])
-        # Check for conflicts with other actions considering the shortcut context.
-        if self._is_shortcut_available(existing_actions, action):
-            actions = ActionList.shortcut_map.get(shortcuts[0], [])
-            actions.append(action)
-            ActionList.shortcut_map[shortcuts[0]] = actions
-        else:
-            log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
-                        (shortcuts[0], action.objectName()))
-            shortcuts.remove(shortcuts[0])
-        action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts])
-
-    def remove_action(self, action, category=None):
-        """
-        This removes an action from its category. Empty categories are automatically removed.
-
-        :param action:  The ``QAction`` object to be removed.
-        :param category: The name (unicode string) of the category, which contains the action. Defaults to None.
-        """
-        if category not in self.categories:
-            return
-        self.categories[category].actions.remove(action)
-        # Remove empty categories.
-        if not self.categories[category].actions:
-            self.categories.remove(category)
-        shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
-        for shortcut in shortcuts:
-            # Remove action from the list of actions which are using this shortcut.
-            ActionList.shortcut_map[shortcut].remove(action)
-            # Remove empty entries.
-            if not ActionList.shortcut_map[shortcut]:
-                del ActionList.shortcut_map[shortcut]
-
-    def add_category(self, name, weight):
-        """
-        Add an empty category to the list of categories. This is only convenient for categories with a given weight.
-
-        :param name: The category's name.
-        :param weight: The category's weight (int).
-        """
-        if name in self.categories:
-            # Only change the weight and resort the categories again.
-            for category in self.categories:
-                if category.name == name:
-                    category.weight = weight
-            self.categories.categories.sort(key=lambda cat: cat.weight)
-            return
-        self.categories.add(name, weight)
-
-    def update_shortcut_map(self, action, old_shortcuts):
-        """
-        Remove the action for the given ``old_shortcuts`` from the ``shortcut_map`` to ensure its up-to-dateness.
-        **Note**: The new action's shortcuts **must** be assigned to the given ``action`` **before** calling this
-        method.
-
-        :param action: The action whose shortcuts are supposed to be updated in the ``shortcut_map``.
-        :param old_shortcuts: A list of unicode key sequences.
-        """
-        for old_shortcut in old_shortcuts:
-            # Remove action from the list of actions which are using this shortcut.
-            ActionList.shortcut_map[old_shortcut].remove(action)
-            # Remove empty entries.
-            if not ActionList.shortcut_map[old_shortcut]:
-                del ActionList.shortcut_map[old_shortcut]
-        new_shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
-        # Add the new shortcuts to the map.
-        for new_shortcut in new_shortcuts:
-            existing_actions = ActionList.shortcut_map.get(new_shortcut, [])
-            existing_actions.append(action)
-            ActionList.shortcut_map[new_shortcut] = existing_actions
-
-    def _is_shortcut_available(self, existing_actions, action):
-        """
-        Checks if the given ``action`` may use its assigned shortcut(s) or not. Returns ``True`` or ``False.
-
-        :param existing_actions: A list of actions which already use a particular shortcut.
-        :param action: The action which wants to use a particular shortcut.
-        """
-        global_context = action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]
-        affected_actions = []
-        if global_context:
-            affected_actions = [a for a in self.get_all_child_objects(action.parent()) if isinstance(a,
-                                                                                                     QtWidgets.QAction)]
-        for existing_action in existing_actions:
-            if action is existing_action:
-                continue
-            if existing_action in affected_actions:
-                return False
-            if existing_action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]:
-                return False
-            elif action in self.get_all_child_objects(existing_action.parent()):
-                return False
-        return True
-
-    def get_all_child_objects(self, qobject):
-        """
-        Goes recursively through the children of ``qobject`` and returns a list of all child objects.
-        """
-        children = qobject.children()
-        # Append the children's children.
-        children.extend(list(map(self.get_all_child_objects, children)))
-        return children
-
-
-class CategoryOrder(object):
-    """
-    An enumeration class for category weights.
-    """
-    standard_menu = -20
-    standard_toolbar = -10

=== removed file 'openlp/core/utils/db.py'
--- openlp/core/utils/db.py	2016-01-08 14:05:54 +0000
+++ openlp/core/utils/db.py	1970-01-01 00:00:00 +0000
@@ -1,71 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection                                      #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2016 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:`db` module provides helper functions for database related methods.
-"""
-import sqlalchemy
-import logging
-
-from copy import deepcopy
-
-log = logging.getLogger(__name__)
-
-
-def drop_column(op, tablename, columnname):
-    drop_columns(op, tablename, [columnname])
-
-
-def drop_columns(op, tablename, columns):
-    """
-    Column dropping functionality for SQLite, as there is no DROP COLUMN support in SQLite
-
-    From https://github.com/klugjohannes/alembic-sqlite
-    """
-
-    # get the db engine and reflect database tables
-    engine = op.get_bind()
-    meta = sqlalchemy.MetaData(bind=engine)
-    meta.reflect()
-
-    # create a select statement from the old table
-    old_table = meta.tables[tablename]
-    select = sqlalchemy.sql.select([c for c in old_table.c if c.name not in columns])
-
-    # get remaining columns without table attribute attached
-    remaining_columns = [deepcopy(c) for c in old_table.columns if c.name not in columns]
-    for column in remaining_columns:
-        column.table = None
-
-    # create a temporary new table
-    new_tablename = '{0}_new'.format(tablename)
-    op.create_table(new_tablename, *remaining_columns)
-    meta.reflect()
-    new_table = meta.tables[new_tablename]
-
-    # copy data from old table
-    insert = sqlalchemy.sql.insert(new_table).from_select([c.name for c in remaining_columns], select)
-    engine.execute(insert)
-
-    # drop the old table and rename the new table to take the old tables
-    # position
-    op.drop_table(tablename)
-    op.rename_table(new_tablename, tablename)

=== removed file 'openlp/core/utils/languagemanager.py'
--- openlp/core/utils/languagemanager.py	2015-12-31 22:46:06 +0000
+++ openlp/core/utils/languagemanager.py	1970-01-01 00:00:00 +0000
@@ -1,146 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection                                      #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2016 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:`languagemanager` module provides all the translation settings and language file loading for OpenLP.
-"""
-import logging
-import re
-import sys
-
-from PyQt5 import QtCore, QtWidgets
-
-
-from openlp.core.common import AppLocation, Settings, translate, is_win, is_macosx
-
-log = logging.getLogger(__name__)
-
-
-class LanguageManager(object):
-    """
-    Helper for Language selection
-    """
-    __qm_list__ = {}
-    auto_language = False
-
-    @staticmethod
-    def get_translator(language):
-        """
-        Set up a translator to use in this instance of OpenLP
-
-        :param language: The language to load into the translator
-        """
-        if LanguageManager.auto_language:
-            language = QtCore.QLocale.system().name()
-        lang_path = AppLocation.get_directory(AppLocation.LanguageDir)
-        app_translator = QtCore.QTranslator()
-        app_translator.load(language, lang_path)
-        # A translator for buttons and other default strings provided by Qt.
-        if not is_win() and not is_macosx():
-            lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)
-        default_translator = QtCore.QTranslator()
-        default_translator.load('qt_%s' % language, lang_path)
-        return app_translator, default_translator
-
-    @staticmethod
-    def find_qm_files():
-        """
-        Find all available language files in this OpenLP install
-        """
-        log.debug('Translation files: %s', AppLocation.get_directory(AppLocation.LanguageDir))
-        trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir))
-        file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
-        # Remove qm files from the list which start with "qt_".
-        file_names = [file_ for file_ in file_names if not file_.startswith('qt_')]
-        return list(map(trans_dir.filePath, file_names))
-
-    @staticmethod
-    def language_name(qm_file):
-        """
-        Load the language name from a language file
-
-        :param qm_file: The file to obtain the name from
-        """
-        translator = QtCore.QTranslator()
-        translator.load(qm_file)
-        return translator.translate('OpenLP.MainWindow', 'English', 'Please add the name of your language here')
-
-    @staticmethod
-    def get_language():
-        """
-        Retrieve a saved language to use from settings
-        """
-        language = Settings().value('core/language')
-        language = str(language)
-        log.info('Language file: \'%s\' Loaded from conf file' % language)
-        if re.match(r'[[].*[]]', language):
-            LanguageManager.auto_language = True
-            language = re.sub(r'[\[\]]', '', language)
-        return language
-
-    @staticmethod
-    def set_language(action, message=True):
-        """
-        Set the language to translate OpenLP into
-
-        :param action:  The language menu option
-        :param message:  Display the message option
-        """
-        language = 'en'
-        if action:
-            action_name = str(action.objectName())
-            if action_name == 'autoLanguageItem':
-                LanguageManager.auto_language = True
-            else:
-                LanguageManager.auto_language = False
-                qm_list = LanguageManager.get_qm_list()
-                language = str(qm_list[action_name])
-        if LanguageManager.auto_language:
-            language = '[%s]' % language
-        Settings().setValue('core/language', language)
-        log.info('Language file: \'%s\' written to conf file' % language)
-        if message:
-            QtWidgets.QMessageBox.information(None,
-                                              translate('OpenLP.LanguageManager', 'Language'),
-                                              translate('OpenLP.LanguageManager',
-                                                        'Please restart OpenLP to use your new language setting.'))
-
-    @staticmethod
-    def init_qm_list():
-        """
-        Initialise the list of available translations
-        """
-        LanguageManager.__qm_list__ = {}
-        qm_files = LanguageManager.find_qm_files()
-        for counter, qmf in enumerate(qm_files):
-            reg_ex = QtCore.QRegExp("^.*i18n/(.*).qm")
-            if reg_ex.exactMatch(qmf):
-                name = '%s' % reg_ex.cap(1)
-                LanguageManager.__qm_list__['%#2i %s' % (counter + 1, LanguageManager.language_name(qmf))] = name
-
-    @staticmethod
-    def get_qm_list():
-        """
-        Return the list of available translations
-        """
-        if not LanguageManager.__qm_list__:
-            LanguageManager.init_qm_list()
-        return LanguageManager.__qm_list__

=== modified file 'openlp/plugins/alerts/alertsplugin.py'
--- openlp/plugins/alerts/alertsplugin.py	2016-01-04 00:18:01 +0000
+++ openlp/plugins/alerts/alertsplugin.py	2016-03-31 17:04:24 +0000
@@ -24,17 +24,16 @@
 
 from PyQt5 import QtGui
 
-
 from openlp.core.common import Settings, translate
+from openlp.core.common.actions import ActionList
 from openlp.core.lib import Plugin, StringContent, build_icon
 from openlp.core.lib.db import Manager
+from openlp.core.lib.theme import VerticalType
 from openlp.core.lib.ui import create_action, UiStrings
-from openlp.core.lib.theme import VerticalType
 from openlp.core.ui import AlertLocation
-from openlp.core.utils.actions import ActionList
+from openlp.plugins.alerts.forms import AlertForm
 from openlp.plugins.alerts.lib import AlertsManager, AlertsTab
 from openlp.plugins.alerts.lib.db import init_schema
-from openlp.plugins.alerts.forms import AlertForm
 
 log = logging.getLogger(__name__)
 

=== modified file 'openlp/plugins/bibles/bibleplugin.py'
--- openlp/plugins/bibles/bibleplugin.py	2016-01-04 00:18:01 +0000
+++ openlp/plugins/bibles/bibleplugin.py	2016-03-31 17:04:24 +0000
@@ -24,13 +24,13 @@
 
 from PyQt5 import QtWidgets
 
+from openlp.core.common.actions import ActionList
 from openlp.core.lib import Plugin, StringContent, build_icon, translate
 from openlp.core.lib.ui import UiStrings, create_action
-from openlp.core.utils.actions import ActionList
+from openlp.plugins.bibles.forms import BibleUpgradeForm
 from openlp.plugins.bibles.lib import BibleManager, BiblesTab, BibleMediaItem, LayoutStyle, DisplayStyle, \
     LanguageSelection
 from openlp.plugins.bibles.lib.mediaitem import BibleSearch
-from openlp.plugins.bibles.forms import BibleUpgradeForm
 
 log = logging.getLogger(__name__)
 

=== modified file 'openlp/plugins/songs/lib/upgrade.py'
--- openlp/plugins/songs/lib/upgrade.py	2016-01-07 22:12:03 +0000
+++ openlp/plugins/songs/lib/upgrade.py	2016-03-31 17:04:24 +0000
@@ -28,8 +28,8 @@
 from sqlalchemy import Table, Column, ForeignKey, types
 from sqlalchemy.sql.expression import func, false, null, text
 
+from openlp.core.common.db import drop_columns
 from openlp.core.lib.db import get_upgrade_op
-from openlp.core.utils.db import drop_columns
 
 log = logging.getLogger(__name__)
 __version__ = 5

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2016-01-04 12:21:58 +0000
+++ openlp/plugins/songs/songsplugin.py	2016-03-31 17:04:24 +0000
@@ -26,27 +26,26 @@
 
 import logging
 import os
+import sqlite3
 from tempfile import gettempdir
-import sqlite3
 
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import UiStrings, Registry, translate
+from openlp.core.common.actions import ActionList
 from openlp.core.lib import Plugin, StringContent, build_icon
 from openlp.core.lib.db import Manager
 from openlp.core.lib.ui import create_action
-from openlp.core.utils.actions import ActionList
 from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
 from openlp.plugins.songs.forms.songselectform import SongSelectForm
 from openlp.plugins.songs.lib import clean_song, upgrade
 from openlp.plugins.songs.lib.db import init_schema, Song
-from openlp.plugins.songs.lib.mediaitem import SongSearch
 from openlp.plugins.songs.lib.importer import SongFormat
 from openlp.plugins.songs.lib.importers.openlp import OpenLPSongImport
 from openlp.plugins.songs.lib.mediaitem import SongMediaItem
+from openlp.plugins.songs.lib.mediaitem import SongSearch
 from openlp.plugins.songs.lib.songstab import SongsTab
 
-
 log = logging.getLogger(__name__)
 __default_settings__ = {
     'songs/db type': 'sqlite',

=== modified file 'openlp/plugins/songusage/songusageplugin.py'
--- openlp/plugins/songusage/songusageplugin.py	2016-02-11 21:05:41 +0000
+++ openlp/plugins/songusage/songusageplugin.py	2016-03-31 17:04:24 +0000
@@ -26,10 +26,10 @@
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry, Settings, translate
+from openlp.core.common.actions import ActionList
 from openlp.core.lib import Plugin, StringContent, build_icon
 from openlp.core.lib.db import Manager
 from openlp.core.lib.ui import create_action
-from openlp.core.utils.actions import ActionList
 from openlp.plugins.songusage.forms import SongUsageDetailForm, SongUsageDeleteForm
 from openlp.plugins.songusage.lib import upgrade
 from openlp.plugins.songusage.lib.db import init_schema, SongUsageItem

=== added file 'tests/functional/openlp_core/__init__.py'
=== modified file 'tests/functional/openlp_core/test_init.py'
--- tests/functional/openlp_core/test_init.py	2015-12-31 22:46:06 +0000
+++ tests/functional/openlp_core/test_init.py	2016-03-31 17:04:24 +0000
@@ -37,7 +37,7 @@
         # GIVEN: a a set of system arguments.
         sys.argv[1:] = []
         # WHEN: We we parse them to expand to options
-        args = parse_options()
+        args = parse_options(None)
         # THEN: the following fields will have been extracted.
         self.assertFalse(args.dev_version, 'The dev_version flag should be False')
         self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
@@ -54,7 +54,7 @@
         # GIVEN: a a set of system arguments.
         sys.argv[1:] = ['-l debug']
         # WHEN: We we parse them to expand to options
-        args = parse_options()
+        args = parse_options(None)
         # THEN: the following fields will have been extracted.
         self.assertFalse(args.dev_version, 'The dev_version flag should be False')
         self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
@@ -71,7 +71,7 @@
         # GIVEN: a a set of system arguments.
         sys.argv[1:] = ['--portable']
         # WHEN: We we parse them to expand to options
-        args = parse_options()
+        args = parse_options(None)
         # THEN: the following fields will have been extracted.
         self.assertFalse(args.dev_version, 'The dev_version flag should be False')
         self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
@@ -88,7 +88,7 @@
         # GIVEN: a a set of system arguments.
         sys.argv[1:] = ['-l debug', '-d']
         # WHEN: We we parse them to expand to options
-        args = parse_options()
+        args = parse_options(None)
         # THEN: the following fields will have been extracted.
         self.assertTrue(args.dev_version, 'The dev_version flag should be True')
         self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
@@ -105,7 +105,7 @@
         # GIVEN: a a set of system arguments.
         sys.argv[1:] = ['dummy_temp']
         # WHEN: We we parse them to expand to options
-        args = parse_options()
+        args = parse_options(None)
         # THEN: the following fields will have been extracted.
         self.assertFalse(args.dev_version, 'The dev_version flag should be False')
         self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
@@ -122,7 +122,7 @@
         # GIVEN: a a set of system arguments.
         sys.argv[1:] = ['-l debug', 'dummy_temp']
         # WHEN: We we parse them to expand to options
-        args = parse_options()
+        args = parse_options(None)
         # THEN: the following fields will have been extracted.
         self.assertFalse(args.dev_version, 'The dev_version flag should be False')
         self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
@@ -130,15 +130,3 @@
         self.assertFalse(args.portable, 'The portable flag should be set to false')
         self.assertEquals(args.style, None, 'There are no style flags to be processed')
         self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
-
-    def parse_options_two_files_test(self):
-        """
-        Test the parse options process works with a file
-
-        """
-        # GIVEN: a a set of system arguments.
-        sys.argv[1:] = ['dummy_temp', 'dummy_temp2']
-        # WHEN: We we parse them to expand to options
-        args = parse_options()
-        # THEN: the following fields will have been extracted.
-        self.assertEquals(args, None, 'The args should be None')

=== added file 'tests/functional/openlp_core_common/test_actions.py'
--- tests/functional/openlp_core_common/test_actions.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_common/test_actions.py	2016-03-31 17:04:24 +0000
@@ -0,0 +1,244 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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.common.actions package.
+"""
+from unittest import TestCase
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+
+from openlp.core.common import Settings
+from openlp.core.common.actions import CategoryActionList, ActionList
+from tests.functional import MagicMock
+from tests.helpers.testmixin import TestMixin
+
+
+class TestCategoryActionList(TestCase):
+    def setUp(self):
+        """
+        Create an instance and a few example actions.
+        """
+        self.action1 = MagicMock()
+        self.action1.text.return_value = 'first'
+        self.action2 = MagicMock()
+        self.action2.text.return_value = 'second'
+        self.list = CategoryActionList()
+
+    def tearDown(self):
+        """
+        Clean up
+        """
+        del self.list
+
+    def contains_test(self):
+        """
+        Test the __contains__() method
+        """
+        # GIVEN: The list.
+        # WHEN: Add an action
+        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)
+
+    def len_test(self):
+        """
+        Test the __len__ method
+        """
+        # GIVEN: The list.
+        # WHEN: Do nothing.
+        # THEN: Check the length.
+        self.assertEqual(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.")
+
+    def append_test(self):
+        """
+        Test the append() method
+        """
+        # GIVEN: The list.
+        # WHEN: Append an action.
+        self.list.append(self.action1)
+        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))
+
+    def add_test(self):
+        """
+        Test the add() method
+        """
+        # GIVEN: The list and weights.
+        action1_weight = 42
+        action2_weight = 41
+
+        # WHEN: Add actions and their weights.
+        self.list.add(self.action1, action1_weight)
+        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)
+        # 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))
+
+    def remove_test(self):
+        """
+        Test the remove() method
+        """
+        # GIVEN: The list
+        self.list.append(self.action1)
+
+        # WHEN: Delete an item from the list.
+        self.list.remove(self.action1)
+
+        # THEN: Now the element should not be in the list anymore.
+        self.assertFalse(self.action1 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)
+
+
+class TestActionList(TestCase, TestMixin):
+    """
+    Test the ActionList class
+    """
+
+    def setUp(self):
+        """
+        Prepare the tests
+        """
+        self.action_list = ActionList.get_instance()
+        self.build_settings()
+        self.settings = Settings()
+        self.settings.beginGroup('shortcuts')
+
+    def tearDown(self):
+        """
+        Clean up
+        """
+        self.settings.endGroup()
+        self.destroy_settings()
+
+    def test_add_action_same_parent(self):
+        """
+        ActionList test - Tests the add_action method. The actions have the same parent, the same shortcuts and both
+        have the QtCore.Qt.WindowShortcut shortcut context set.
+        """
+        # GIVEN: Two actions with the same shortcuts.
+        parent = QtCore.QObject()
+        action1 = QtWidgets.QAction(parent)
+        action1.setObjectName('action1')
+        action_with_same_shortcuts1 = QtWidgets.QAction(parent)
+        action_with_same_shortcuts1.setObjectName('action_with_same_shortcuts1')
+        # Add default shortcuts to Settings class.
+        default_shortcuts = {
+            'shortcuts/action1': [QtGui.QKeySequence(QtCore.Qt.Key_A), QtGui.QKeySequence(QtCore.Qt.Key_B)],
+            'shortcuts/action_with_same_shortcuts1': [QtGui.QKeySequence(QtCore.Qt.Key_B),
+                                                      QtGui.QKeySequence(QtCore.Qt.Key_A)]
+        }
+        Settings.extend_default_settings(default_shortcuts)
+
+        # WHEN: Add the two actions to the action list.
+        self.action_list.add_action(action1, 'example_category')
+        self.action_list.add_action(action_with_same_shortcuts1, 'example_category')
+        # Remove the actions again.
+        self.action_list.remove_action(action1, 'example_category')
+        self.action_list.remove_action(action_with_same_shortcuts1, 'example_category')
+
+        # THEN: As both actions have the same shortcuts, they should be removed from one action.
+        assert len(action1.shortcuts()) == 2, 'The action should have two shortcut assigned.'
+        assert len(action_with_same_shortcuts1.shortcuts()) == 0, 'The action should not have a shortcut assigned.'
+
+    def test_add_action_different_parent(self):
+        """
+        ActionList test - Tests the add_action method. The actions have the different parent, the same shortcuts and
+        both have the QtCore.Qt.WindowShortcut shortcut context set.
+        """
+        # GIVEN: Two actions with the same shortcuts.
+        parent = QtCore.QObject()
+        action2 = QtWidgets.QAction(parent)
+        action2.setObjectName('action2')
+        second_parent = QtCore.QObject()
+        action_with_same_shortcuts2 = QtWidgets.QAction(second_parent)
+        action_with_same_shortcuts2.setObjectName('action_with_same_shortcuts2')
+        # Add default shortcuts to Settings class.
+        default_shortcuts = {
+            'shortcuts/action2': [QtGui.QKeySequence(QtCore.Qt.Key_C), QtGui.QKeySequence(QtCore.Qt.Key_D)],
+            'shortcuts/action_with_same_shortcuts2': [QtGui.QKeySequence(QtCore.Qt.Key_D),
+                                                      QtGui.QKeySequence(QtCore.Qt.Key_C)]
+        }
+        Settings.extend_default_settings(default_shortcuts)
+
+        # WHEN: Add the two actions to the action list.
+        self.action_list.add_action(action2, 'example_category')
+        self.action_list.add_action(action_with_same_shortcuts2, 'example_category')
+        # Remove the actions again.
+        self.action_list.remove_action(action2, 'example_category')
+        self.action_list.remove_action(action_with_same_shortcuts2, 'example_category')
+
+        # THEN: As both actions have the same shortcuts, they should be removed from one action.
+        assert len(action2.shortcuts()) == 2, 'The action should have two shortcut assigned.'
+        assert len(action_with_same_shortcuts2.shortcuts()) == 0, 'The action should not have a shortcut assigned.'
+
+    def test_add_action_different_context(self):
+        """
+        ActionList test - Tests the add_action method. The actions have the different parent, the same shortcuts and
+        both have the QtCore.Qt.WidgetShortcut shortcut context set.
+        """
+        # GIVEN: Two actions with the same shortcuts.
+        parent = QtCore.QObject()
+        action3 = QtWidgets.QAction(parent)
+        action3.setObjectName('action3')
+        action3.setShortcutContext(QtCore.Qt.WidgetShortcut)
+        second_parent = QtCore.QObject()
+        action_with_same_shortcuts3 = QtWidgets.QAction(second_parent)
+        action_with_same_shortcuts3.setObjectName('action_with_same_shortcuts3')
+        action_with_same_shortcuts3.setShortcutContext(QtCore.Qt.WidgetShortcut)
+        # Add default shortcuts to Settings class.
+        default_shortcuts = {
+            'shortcuts/action3': [QtGui.QKeySequence(QtCore.Qt.Key_E), QtGui.QKeySequence(QtCore.Qt.Key_F)],
+            'shortcuts/action_with_same_shortcuts3': [QtGui.QKeySequence(QtCore.Qt.Key_E),
+                                                      QtGui.QKeySequence(QtCore.Qt.Key_F)]
+        }
+        Settings.extend_default_settings(default_shortcuts)
+
+        # WHEN: Add the two actions to the action list.
+        self.action_list.add_action(action3, 'example_category2')
+        self.action_list.add_action(action_with_same_shortcuts3, 'example_category2')
+        # Remove the actions again.
+        self.action_list.remove_action(action3, 'example_category2')
+        self.action_list.remove_action(action_with_same_shortcuts3, 'example_category2')
+
+        # THEN: Both action should keep their shortcuts.
+        assert len(action3.shortcuts()) == 2, 'The action should have two shortcut assigned.'
+        assert len(action_with_same_shortcuts3.shortcuts()) == 2, 'The action should have two shortcuts assigned.'

=== added file 'tests/functional/openlp_core_common/test_db.py'
--- tests/functional/openlp_core_common/test_db.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_common/test_db.py	2016-03-31 17:04:24 +0000
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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.common.db package.
+"""
+import gc
+import os
+import shutil
+import time
+from tempfile import mkdtemp
+from unittest import TestCase
+
+import sqlalchemy
+
+from openlp.core.common.db import drop_column, drop_columns
+from openlp.core.lib.db import init_db, get_upgrade_op
+from tests.utils.constants import TEST_RESOURCES_PATH
+
+
+class TestUtilsDBFunctions(TestCase):
+
+    def setUp(self):
+        """
+        Create temp folder for keeping db file
+        """
+        self.tmp_folder = mkdtemp()
+        db_path = os.path.join(TEST_RESOURCES_PATH, 'songs', 'songs-1.9.7.sqlite')
+        self.db_tmp_path = os.path.join(self.tmp_folder, 'songs-1.9.7.sqlite')
+        shutil.copyfile(db_path, self.db_tmp_path)
+        db_url = 'sqlite:///' + self.db_tmp_path
+        self.session, metadata = init_db(db_url)
+        self.op = get_upgrade_op(self.session)
+
+    def tearDown(self):
+        """
+        Clean up
+        """
+        self.session.close()
+        self.session = None
+        gc.collect()
+        retries = 0
+        while retries < 5:
+            try:
+                if os.path.exists(self.tmp_folder):
+                    shutil.rmtree(self.tmp_folder)
+                break
+            except:
+                time.sleep(1)
+                retries += 1
+
+    def delete_column_test(self):
+        """
+        Test deleting a single column in a table
+        """
+        # GIVEN: A temporary song db
+
+        # WHEN: Deleting a columns in a table
+        drop_column(self.op, 'songs', 'song_book_id')
+
+        # THEN: The column should have been deleted
+        meta = sqlalchemy.MetaData(bind=self.op.get_bind())
+        meta.reflect()
+        columns = meta.tables['songs'].columns
+
+        for column in columns:
+            if column.name == 'song_book_id':
+                self.fail("The column 'song_book_id' should have been deleted.")
+
+    def delete_columns_test(self):
+        """
+        Test deleting multiple columns in a table
+        """
+        # GIVEN: A temporary song db
+
+        # WHEN: Deleting a columns in a table
+        drop_columns(self.op, 'songs', ['song_book_id', 'song_number'])
+
+        # THEN: The columns should have been deleted
+        meta = sqlalchemy.MetaData(bind=self.op.get_bind())
+        meta.reflect()
+        columns = meta.tables['songs'].columns
+
+        for column in columns:
+            if column.name == 'song_book_id' or column.name == 'song_number':
+                self.fail("The column '%s' should have been deleted." % column.name)

=== removed file 'tests/functional/openlp_core_utils/test_actions.py'
--- tests/functional/openlp_core_utils/test_actions.py	2015-12-31 22:46:06 +0000
+++ tests/functional/openlp_core_utils/test_actions.py	1970-01-01 00:00:00 +0000
@@ -1,245 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection                                      #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2016 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.utils.actions package.
-"""
-from unittest import TestCase
-
-from PyQt5 import QtGui, QtCore, QtWidgets
-
-from openlp.core.common import Settings
-from openlp.core.utils import ActionList
-from openlp.core.utils.actions import CategoryActionList
-from tests.functional import MagicMock
-from tests.helpers.testmixin import TestMixin
-
-
-class TestCategoryActionList(TestCase):
-    def setUp(self):
-        """
-        Create an instance and a few example actions.
-        """
-        self.action1 = MagicMock()
-        self.action1.text.return_value = 'first'
-        self.action2 = MagicMock()
-        self.action2.text.return_value = 'second'
-        self.list = CategoryActionList()
-
-    def tearDown(self):
-        """
-        Clean up
-        """
-        del self.list
-
-    def contains_test(self):
-        """
-        Test the __contains__() method
-        """
-        # GIVEN: The list.
-        # WHEN: Add an action
-        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)
-
-    def len_test(self):
-        """
-        Test the __len__ method
-        """
-        # GIVEN: The list.
-        # WHEN: Do nothing.
-        # THEN: Check the length.
-        self.assertEqual(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.")
-
-    def append_test(self):
-        """
-        Test the append() method
-        """
-        # GIVEN: The list.
-        # WHEN: Append an action.
-        self.list.append(self.action1)
-        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))
-
-    def add_test(self):
-        """
-        Test the add() method
-        """
-        # GIVEN: The list and weights.
-        action1_weight = 42
-        action2_weight = 41
-
-        # WHEN: Add actions and their weights.
-        self.list.add(self.action1, action1_weight)
-        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)
-        # 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))
-
-    def remove_test(self):
-        """
-        Test the remove() method
-        """
-        # GIVEN: The list
-        self.list.append(self.action1)
-
-        # WHEN: Delete an item from the list.
-        self.list.remove(self.action1)
-
-        # THEN: Now the element should not be in the list anymore.
-        self.assertFalse(self.action1 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)
-
-
-class TestActionList(TestCase, TestMixin):
-    """
-    Test the ActionList class
-    """
-
-    def setUp(self):
-        """
-        Prepare the tests
-        """
-        self.action_list = ActionList.get_instance()
-        self.build_settings()
-        self.settings = Settings()
-        self.settings.beginGroup('shortcuts')
-
-    def tearDown(self):
-        """
-        Clean up
-        """
-        self.settings.endGroup()
-        self.destroy_settings()
-
-    def test_add_action_same_parent(self):
-        """
-        ActionList test - Tests the add_action method. The actions have the same parent, the same shortcuts and both
-        have the QtCore.Qt.WindowShortcut shortcut context set.
-        """
-        # GIVEN: Two actions with the same shortcuts.
-        parent = QtCore.QObject()
-        action1 = QtWidgets.QAction(parent)
-        action1.setObjectName('action1')
-        action_with_same_shortcuts1 = QtWidgets.QAction(parent)
-        action_with_same_shortcuts1.setObjectName('action_with_same_shortcuts1')
-        # Add default shortcuts to Settings class.
-        default_shortcuts = {
-            'shortcuts/action1': [QtGui.QKeySequence(QtCore.Qt.Key_A), QtGui.QKeySequence(QtCore.Qt.Key_B)],
-            'shortcuts/action_with_same_shortcuts1': [QtGui.QKeySequence(QtCore.Qt.Key_B),
-                                                      QtGui.QKeySequence(QtCore.Qt.Key_A)]
-        }
-        Settings.extend_default_settings(default_shortcuts)
-
-        # WHEN: Add the two actions to the action list.
-        self.action_list.add_action(action1, 'example_category')
-        self.action_list.add_action(action_with_same_shortcuts1, 'example_category')
-        # Remove the actions again.
-        self.action_list.remove_action(action1, 'example_category')
-        self.action_list.remove_action(action_with_same_shortcuts1, 'example_category')
-
-        # THEN: As both actions have the same shortcuts, they should be removed from one action.
-        assert len(action1.shortcuts()) == 2, 'The action should have two shortcut assigned.'
-        assert len(action_with_same_shortcuts1.shortcuts()) == 0, 'The action should not have a shortcut assigned.'
-
-    def test_add_action_different_parent(self):
-        """
-        ActionList test - Tests the add_action method. The actions have the different parent, the same shortcuts and
-        both have the QtCore.Qt.WindowShortcut shortcut context set.
-        """
-        # GIVEN: Two actions with the same shortcuts.
-        parent = QtCore.QObject()
-        action2 = QtWidgets.QAction(parent)
-        action2.setObjectName('action2')
-        second_parent = QtCore.QObject()
-        action_with_same_shortcuts2 = QtWidgets.QAction(second_parent)
-        action_with_same_shortcuts2.setObjectName('action_with_same_shortcuts2')
-        # Add default shortcuts to Settings class.
-        default_shortcuts = {
-            'shortcuts/action2': [QtGui.QKeySequence(QtCore.Qt.Key_C), QtGui.QKeySequence(QtCore.Qt.Key_D)],
-            'shortcuts/action_with_same_shortcuts2': [QtGui.QKeySequence(QtCore.Qt.Key_D),
-                                                      QtGui.QKeySequence(QtCore.Qt.Key_C)]
-        }
-        Settings.extend_default_settings(default_shortcuts)
-
-        # WHEN: Add the two actions to the action list.
-        self.action_list.add_action(action2, 'example_category')
-        self.action_list.add_action(action_with_same_shortcuts2, 'example_category')
-        # Remove the actions again.
-        self.action_list.remove_action(action2, 'example_category')
-        self.action_list.remove_action(action_with_same_shortcuts2, 'example_category')
-
-        # THEN: As both actions have the same shortcuts, they should be removed from one action.
-        assert len(action2.shortcuts()) == 2, 'The action should have two shortcut assigned.'
-        assert len(action_with_same_shortcuts2.shortcuts()) == 0, 'The action should not have a shortcut assigned.'
-
-    def test_add_action_different_context(self):
-        """
-        ActionList test - Tests the add_action method. The actions have the different parent, the same shortcuts and
-        both have the QtCore.Qt.WidgetShortcut shortcut context set.
-        """
-        # GIVEN: Two actions with the same shortcuts.
-        parent = QtCore.QObject()
-        action3 = QtWidgets.QAction(parent)
-        action3.setObjectName('action3')
-        action3.setShortcutContext(QtCore.Qt.WidgetShortcut)
-        second_parent = QtCore.QObject()
-        action_with_same_shortcuts3 = QtWidgets.QAction(second_parent)
-        action_with_same_shortcuts3.setObjectName('action_with_same_shortcuts3')
-        action_with_same_shortcuts3.setShortcutContext(QtCore.Qt.WidgetShortcut)
-        # Add default shortcuts to Settings class.
-        default_shortcuts = {
-            'shortcuts/action3': [QtGui.QKeySequence(QtCore.Qt.Key_E), QtGui.QKeySequence(QtCore.Qt.Key_F)],
-            'shortcuts/action_with_same_shortcuts3': [QtGui.QKeySequence(QtCore.Qt.Key_E),
-                                                      QtGui.QKeySequence(QtCore.Qt.Key_F)]
-        }
-        Settings.extend_default_settings(default_shortcuts)
-
-        # WHEN: Add the two actions to the action list.
-        self.action_list.add_action(action3, 'example_category2')
-        self.action_list.add_action(action_with_same_shortcuts3, 'example_category2')
-        # Remove the actions again.
-        self.action_list.remove_action(action3, 'example_category2')
-        self.action_list.remove_action(action_with_same_shortcuts3, 'example_category2')
-
-        # THEN: Both action should keep their shortcuts.
-        assert len(action3.shortcuts()) == 2, 'The action should have two shortcut assigned.'
-        assert len(action_with_same_shortcuts3.shortcuts()) == 2, 'The action should have two shortcuts assigned.'

=== removed file 'tests/functional/openlp_core_utils/test_db.py'
--- tests/functional/openlp_core_utils/test_db.py	2016-01-15 19:14:24 +0000
+++ tests/functional/openlp_core_utils/test_db.py	1970-01-01 00:00:00 +0000
@@ -1,104 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection                                      #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2016 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.utils.db package.
-"""
-from tempfile import mkdtemp
-from unittest import TestCase
-import gc
-import os
-import shutil
-import sqlalchemy
-import time
-
-from openlp.core.utils.db import drop_column, drop_columns
-from openlp.core.lib.db import init_db, get_upgrade_op
-
-from tests.utils.constants import TEST_RESOURCES_PATH
-
-
-class TestUtilsDBFunctions(TestCase):
-
-    def setUp(self):
-        """
-        Create temp folder for keeping db file
-        """
-        self.tmp_folder = mkdtemp()
-        db_path = os.path.join(TEST_RESOURCES_PATH, 'songs', 'songs-1.9.7.sqlite')
-        self.db_tmp_path = os.path.join(self.tmp_folder, 'songs-1.9.7.sqlite')
-        shutil.copyfile(db_path, self.db_tmp_path)
-        db_url = 'sqlite:///' + self.db_tmp_path
-        self.session, metadata = init_db(db_url)
-        self.op = get_upgrade_op(self.session)
-
-    def tearDown(self):
-        """
-        Clean up
-        """
-        self.session.close()
-        self.session = None
-        gc.collect()
-        retries = 0
-        while retries < 5:
-            try:
-                if os.path.exists(self.tmp_folder):
-                    shutil.rmtree(self.tmp_folder)
-                break
-            except:
-                time.sleep(1)
-                retries += 1
-
-    def delete_column_test(self):
-        """
-        Test deleting a single column in a table
-        """
-        # GIVEN: A temporary song db
-
-        # WHEN: Deleting a columns in a table
-        drop_column(self.op, 'songs', 'song_book_id')
-
-        # THEN: The column should have been deleted
-        meta = sqlalchemy.MetaData(bind=self.op.get_bind())
-        meta.reflect()
-        columns = meta.tables['songs'].columns
-
-        for column in columns:
-            if column.name == 'song_book_id':
-                self.fail("The column 'song_book_id' should have been deleted.")
-
-    def delete_columns_test(self):
-        """
-        Test deleting multiple columns in a table
-        """
-        # GIVEN: A temporary song db
-
-        # WHEN: Deleting a columns in a table
-        drop_columns(self.op, 'songs', ['song_book_id', 'song_number'])
-
-        # THEN: The columns should have been deleted
-        meta = sqlalchemy.MetaData(bind=self.op.get_bind())
-        meta.reflect()
-        columns = meta.tables['songs'].columns
-
-        for column in columns:
-            if column.name == 'song_book_id' or column.name == 'song_number':
-                self.fail("The column '%s' should have been deleted." % column.name)

=== modified file 'tests/functional/openlp_core_utils/test_utils.py'
--- tests/functional/openlp_core_utils/test_utils.py	2015-12-31 22:46:06 +0000
+++ tests/functional/openlp_core_utils/test_utils.py	2016-03-31 17:04:24 +0000
@@ -241,7 +241,7 @@
         """
         Test the get_locale_key(string) function
         """
-        with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
+        with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language:
             # GIVEN: The language is German
             # 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss".
             mocked_get_language.return_value = 'de'
@@ -258,7 +258,7 @@
         """
         Test the get_natural_key(string) function
         """
-        with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
+        with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language:
             # GIVEN: The language is English (a language, which sorts digits before letters)
             mocked_get_language.return_value = 'en'
             unsorted_list = ['item 10a', 'item 3b', '1st item']


Follow ups