diff --git a/openlp/core/common/languagemanager.py b/openlp/core/common/languagemanager.py index 4f5128425..c3e736d65 100644 --- a/openlp/core/common/languagemanager.py +++ b/openlp/core/common/languagemanager.py @@ -22,6 +22,7 @@ """ The :mod:`languagemanager` module provides all the translation settings and language file loading for OpenLP. """ +import locale import logging import re @@ -32,6 +33,8 @@ from openlp.core.common import AppLocation, Settings, translate, is_win, is_maco log = logging.getLogger(__name__) +DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE) + class LanguageManager(object): """ @@ -143,3 +146,60 @@ class LanguageManager(object): if not LanguageManager.__qm_list__: LanguageManager.init_qm_list() return LanguageManager.__qm_list__ + + +def format_time(text, local_time): + """ + Workaround for Python built-in time formatting function time.strftime(). + + time.strftime() accepts only ascii characters. This function accepts + unicode string and passes individual % placeholders to time.strftime(). + This ensures only ascii characters are passed to time.strftime(). + + :param text: The text to be processed. + :param local_time: The time to be used to add to the string. This is a time object + """ + + def match_formatting(match): + """ + Format the match + """ + return local_time.strftime(match.group()) + + return re.sub('\%[a-zA-Z]', match_formatting, text) + + +def get_locale_key(string): + """ + Creates a key for case insensitive, locale aware string sorting. + + :param string: The corresponding string. + """ + string = string.lower() + # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases. + global ICU_COLLATOR + try: + if ICU_COLLATOR is None: + import icu + language = LanguageManager.get_language() + icu_locale = icu.Locale(language) + ICU_COLLATOR = icu.Collator.createInstance(icu_locale) + return ICU_COLLATOR.getSortKey(string) + except: + return locale.strxfrm(string).encode() + + +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 and string[0].isdigit(): + return [b''] + key + return key diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index e2aadf280..ce26ae808 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -31,7 +31,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate from openlp.core.lib import ColorButton, SettingsTab, build_icon -from openlp.core.utils import format_time, get_images_filter +from openlp.core.common.languagemanager import format_time +from openlp.core.utils import get_images_filter log = logging.getLogger(__name__) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 6db34022b..129d24ca1 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -38,7 +38,8 @@ 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.utils import delete_file, split_filename, format_time +from openlp.core.utils import delete_file, split_filename +from openlp.core.common.languagemanager import format_time class ServiceManagerList(QtWidgets.QTreeWidget): diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index fdf87e528..fc632d61d 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -36,7 +36,8 @@ from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, ValidationEr from openlp.core.lib.theme import ThemeXML, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm -from openlp.core.utils import delete_file, get_locale_key, get_filesystem_encoding +from openlp.core.utils import delete_file, get_filesystem_encoding +from openlp.core.common.languagemanager import get_locale_key class Ui_ThemeManager(object): diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 800eec557..3cd1b0295 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -22,7 +22,6 @@ """ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ -import locale import logging import os import platform @@ -463,62 +462,6 @@ def get_uno_instance(resolver, connection_type='pipe'): else: return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') - -def format_time(text, local_time): - """ - Workaround for Python built-in time formatting function time.strftime(). - - time.strftime() accepts only ascii characters. This function accepts - unicode string and passes individual % placeholders to time.strftime(). - This ensures only ascii characters are passed to time.strftime(). - - :param text: The text to be processed. - :param local_time: The time to be used to add to the string. This is a time object - """ - def match_formatting(match): - """ - Format the match - """ - return local_time.strftime(match.group()) - return re.sub('\%[a-zA-Z]', match_formatting, text) - - -def get_locale_key(string): - """ - Creates a key for case insensitive, locale aware string sorting. - - :param string: The corresponding string. - """ - string = string.lower() - # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases. - global ICU_COLLATOR - try: - if ICU_COLLATOR is None: - import icu - from openlp.core.common.languagemanager import LanguageManager - language = LanguageManager.get_language() - icu_locale = icu.Locale(language) - ICU_COLLATOR = icu.Collator.createInstance(icu_locale) - return ICU_COLLATOR.getSortKey(string) - except: - return locale.strxfrm(string).encode() - - -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 and string[0].isdigit(): - return [b''] + key - return key - __all__ = ['get_application_version', 'check_latest_version', 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance', - 'delete_file', 'clean_filename', 'format_time', 'get_locale_key', 'get_natural_key'] + 'delete_file', 'clean_filename'] diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 52417f2ef..676801eb8 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -32,7 +32,7 @@ from openlp.core.common import AppLocation, Settings, UiStrings, translate from openlp.core.lib.db import delete_database from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.bibles.lib.manager import BibleFormat from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename from openlp.plugins.bibles.lib.http import CWExtract, BGExtract, BSExtract diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 94937e61b..cd728a68b 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -29,7 +29,7 @@ from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemConte from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \ critical_error_message_box, find_and_set_in_combo_box, build_icon -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm from openlp.plugins.bibles.forms.editbibleform import EditBibleForm from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \ diff --git a/openlp/plugins/custom/lib/db.py b/openlp/plugins/custom/lib/db.py index 743822072..62ec1f408 100644 --- a/openlp/plugins/custom/lib/db.py +++ b/openlp/plugins/custom/lib/db.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Table, types from sqlalchemy.orm import mapper from openlp.core.lib.db import BaseModel, init_db -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key class CustomSlide(BaseModel): diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 248da94ea..c57442156 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -29,7 +29,8 @@ from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, TreeWidgetWithDnD,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.utils import delete_file, get_locale_key, get_images_filter +from openlp.core.utils import delete_file, get_images_filter +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 09b32b308..dfe6f1fa4 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -32,7 +32,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.core.ui.media.vlcplayer import get_vlc if get_vlc() is not None: diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index b2ece3e67..b64c552b8 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -29,7 +29,7 @@ from openlp.core.common import Registry, Settings, UiStrings, translate from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.presentations.lib import MessageListener from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 8fc6e1a4a..5ea35d6b6 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -29,7 +29,7 @@ from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.sql.expression import func, text from openlp.core.lib.db import BaseModel, init_db -from openlp.core.utils import get_natural_key +from openlp.core.common.languagemanager import get_natural_key from openlp.core.lib import translate diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index b84e557c2..d724bfaf2 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -32,7 +32,7 @@ from openlp.core.common import Registry, AppLocation, Settings, check_directory_ from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \ check_item_selected, create_separated_list from openlp.core.lib.ui import create_widget_action -from openlp.core.utils import get_natural_key +from openlp.core.common.languagemanager import get_natural_key from openlp.plugins.songs.forms.editsongform import EditSongForm from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 70fe6e943..1df0bcedc 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -22,11 +22,10 @@ """ Functional tests to test the AppLocation class and related methods. """ -import os from unittest import TestCase from openlp.core.common import add_actions -from tests.functional import MagicMock, patch +from tests.functional import MagicMock class TestInit(TestCase): diff --git a/tests/functional/openlp_core_common/test_languagemanager.py b/tests/functional/openlp_core_common/test_languagemanager.py new file mode 100644 index 000000000..8fe7d543c --- /dev/null +++ b/tests/functional/openlp_core_common/test_languagemanager.py @@ -0,0 +1,66 @@ +# -*- 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 # +############################################################################### +""" +Functional tests to test the AppLocation class and related methods. +""" +from unittest import TestCase + +from tests.functional import patch +from openlp.core.common.languagemanager import get_locale_key, get_natural_key + + +class TestLanguageManager(TestCase): + """ + A test suite to test out various methods around the common __init__ class. + """ + + def get_locale_key_test(self): + """ + Test the get_locale_key(string) function + """ + 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' + unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung'] + + # WHEN: We sort the list and use get_locale_key() to generate the sorting keys + sorted_list = sorted(unsorted_list, key=get_locale_key) + + # THEN: We get a properly sorted list + self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list, + 'Strings should be sorted properly') + + def get_natural_key_test(self): + """ + Test the get_natural_key(string) function + """ + 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'] + + # WHEN: We sort the list and use get_natural_key() to generate the sorting keys + sorted_list = sorted(unsorted_list, key=get_natural_key) + + # THEN: We get a properly sorted list + self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally') diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index fd033cd88..0c4234a3d 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -25,8 +25,9 @@ Functional tests to test the AppLocation class and related methods. import os from unittest import TestCase -from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, get_locale_key, \ - get_natural_key, split_filename, _get_user_agent, get_web_page, get_uno_instance +from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, \ + split_filename, _get_user_agent, get_web_page, get_uno_instance +from openlp.core.common.languagemanager import get_locale_key, get_natural_key from tests.functional import MagicMock, patch @@ -179,38 +180,6 @@ class TestUtils(TestCase): self.assertEqual(mocked_log.exception.call_count, 1) self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError') - def get_locale_key_test(self): - """ - Test the get_locale_key(string) function - """ - 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' - unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung'] - - # WHEN: We sort the list and use get_locale_key() to generate the sorting keys - sorted_list = sorted(unsorted_list, key=get_locale_key) - - # THEN: We get a properly sorted list - self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list, - 'Strings should be sorted properly') - - def get_natural_key_test(self): - """ - Test the get_natural_key(string) function - """ - 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'] - - # WHEN: We sort the list and use get_natural_key() to generate the sorting keys - sorted_list = sorted(unsorted_list, key=get_natural_key) - - # THEN: We get a properly sorted list - self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally') - def get_uno_instance_pipe_test(self): """ Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI