diff --git a/openlp/core/common/languages.py b/openlp/core/common/languages.py new file mode 100644 index 000000000..18235ac10 --- /dev/null +++ b/openlp/core/common/languages.py @@ -0,0 +1,201 @@ +# -*- 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:`languages` module provides a list of language names with utility functions. +""" +from collections import namedtuple + +from openlp.core.common import translate + + +Language = namedtuple('Language', ['id', 'name', 'code']) +languages = sorted([ + Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'), + Language(2, translate('common.languages', 'Abkhazian', 'Language code: ab'), 'ab'), + Language(3, translate('common.languages', 'Afar', 'Language code: aa'), 'aa'), + Language(4, translate('common.languages', 'Afrikaans', 'Language code: af'), 'af'), + Language(5, translate('common.languages', 'Albanian', 'Language code: sq'), 'sq'), + Language(6, translate('common.languages', 'Amharic', 'Language code: am'), 'am'), + Language(140, translate('common.languages', 'Amuzgo', 'Language code: amu'), 'amu'), + Language(152, translate('common.languages', 'Ancient Greek', 'Language code: grc'), 'grc'), + Language(7, translate('common.languages', 'Arabic', 'Language code: ar'), 'ar'), + Language(8, translate('common.languages', 'Armenian', 'Language code: hy'), 'hy'), + Language(9, translate('common.languages', 'Assamese', 'Language code: as'), 'as'), + Language(10, translate('common.languages', 'Aymara', 'Language code: ay'), 'ay'), + Language(11, translate('common.languages', 'Azerbaijani', 'Language code: az'), 'az'), + Language(12, translate('common.languages', 'Bashkir', 'Language code: ba'), 'ba'), + Language(13, translate('common.languages', 'Basque', 'Language code: eu'), 'eu'), + Language(14, translate('common.languages', 'Bengali', 'Language code: bn'), 'bn'), + Language(15, translate('common.languages', 'Bhutani', 'Language code: dz'), 'dz'), + Language(16, translate('common.languages', 'Bihari', 'Language code: bh'), 'bh'), + Language(17, translate('common.languages', 'Bislama', 'Language code: bi'), 'bi'), + Language(18, translate('common.languages', 'Breton', 'Language code: br'), 'br'), + Language(19, translate('common.languages', 'Bulgarian', 'Language code: bg'), 'bg'), + Language(20, translate('common.languages', 'Burmese', 'Language code: my'), 'my'), + Language(21, translate('common.languages', 'Byelorussian', 'Language code: be'), 'be'), + Language(142, translate('common.languages', 'Cakchiquel', 'Language code: cak'), 'cak'), + Language(22, translate('common.languages', 'Cambodian', 'Language code: km'), 'km'), + Language(23, translate('common.languages', 'Catalan', 'Language code: ca'), 'ca'), + Language(24, translate('common.languages', 'Chinese', 'Language code: zh'), 'zh'), + Language(141, translate('common.languages', 'Comaltepec Chinantec', 'Language code: cco'), 'cco'), + Language(25, translate('common.languages', 'Corsican', 'Language code: co'), 'co'), + Language(26, translate('common.languages', 'Croatian', 'Language code: hr'), 'hr'), + Language(27, translate('common.languages', 'Czech', 'Language code: cs'), 'cs'), + Language(28, translate('common.languages', 'Danish', 'Language code: da'), 'da'), + Language(29, translate('common.languages', 'Dutch', 'Language code: nl'), 'nl'), + Language(30, translate('common.languages', 'English', 'Language code: en'), 'en'), + Language(31, translate('common.languages', 'Esperanto', 'Language code: eo'), 'eo'), + Language(32, translate('common.languages', 'Estonian', 'Language code: et'), 'et'), + Language(33, translate('common.languages', 'Faeroese', 'Language code: fo'), 'fo'), + Language(34, translate('common.languages', 'Fiji', 'Language code: fj'), 'fj'), + Language(35, translate('common.languages', 'Finnish', 'Language code: fi'), 'fi'), + Language(36, translate('common.languages', 'French', 'Language code: fr'), 'fr'), + Language(37, translate('common.languages', 'Frisian', 'Language code: fy'), 'fy'), + Language(38, translate('common.languages', 'Galician', 'Language code: gl'), 'gl'), + Language(39, translate('common.languages', 'Georgian', 'Language code: ka'), 'ka'), + Language(40, translate('common.languages', 'German', 'Language code: de'), 'de'), + Language(41, translate('common.languages', 'Greek', 'Language code: el'), 'el'), + Language(42, translate('common.languages', 'Greenlandic', 'Language code: kl'), 'kl'), + Language(43, translate('common.languages', 'Guarani', 'Language code: gn'), 'gn'), + Language(44, translate('common.languages', 'Gujarati', 'Language code: gu'), 'gu'), + Language(143, translate('common.languages', 'Haitian Creole', 'Language code: ht'), 'ht'), + Language(45, translate('common.languages', 'Hausa', 'Language code: ha'), 'ha'), + Language(46, translate('common.languages', 'Hebrew (former iw)', 'Language code: he'), 'he'), + Language(144, translate('common.languages', 'Hiligaynon', 'Language code: hil'), 'hil'), + Language(47, translate('common.languages', 'Hindi', 'Language code: hi'), 'hi'), + Language(48, translate('common.languages', 'Hungarian', 'Language code: hu'), 'hu'), + Language(49, translate('common.languages', 'Icelandic', 'Language code: is'), 'is'), + Language(50, translate('common.languages', 'Indonesian (former in)', 'Language code: id'), 'id'), + Language(51, translate('common.languages', 'Interlingua', 'Language code: ia'), 'ia'), + Language(52, translate('common.languages', 'Interlingue', 'Language code: ie'), 'ie'), + Language(54, translate('common.languages', 'Inuktitut (Eskimo)', 'Language code: iu'), 'iu'), + Language(53, translate('common.languages', 'Inupiak', 'Language code: ik'), 'ik'), + Language(55, translate('common.languages', 'Irish', 'Language code: ga'), 'ga'), + Language(56, translate('common.languages', 'Italian', 'Language code: it'), 'it'), + Language(145, translate('common.languages', 'Jakalteko', 'Language code: jac'), 'jac'), + Language(57, translate('common.languages', 'Japanese', 'Language code: ja'), 'ja'), + Language(58, translate('common.languages', 'Javanese', 'Language code: jw'), 'jw'), + Language(150, translate('common.languages', 'K\'iche\'', 'Language code: quc'), 'quc'), + Language(59, translate('common.languages', 'Kannada', 'Language code: kn'), 'kn'), + Language(60, translate('common.languages', 'Kashmiri', 'Language code: ks'), 'ks'), + Language(61, translate('common.languages', 'Kazakh', 'Language code: kk'), 'kk'), + Language(146, translate('common.languages', 'Kekchí ', 'Language code: kek'), 'kek'), + Language(62, translate('common.languages', 'Kinyarwanda', 'Language code: rw'), 'rw'), + Language(63, translate('common.languages', 'Kirghiz', 'Language code: ky'), 'ky'), + Language(64, translate('common.languages', 'Kirundi', 'Language code: rn'), 'rn'), + Language(65, translate('common.languages', 'Korean', 'Language code: ko'), 'ko'), + Language(66, translate('common.languages', 'Kurdish', 'Language code: ku'), 'ku'), + Language(67, translate('common.languages', 'Laothian', 'Language code: lo'), 'lo'), + Language(68, translate('common.languages', 'Latin', 'Language code: la'), 'la'), + Language(69, translate('common.languages', 'Latvian, Lettish', 'Language code: lv'), 'lv'), + Language(70, translate('common.languages', 'Lingala', 'Language code: ln'), 'ln'), + Language(71, translate('common.languages', 'Lithuanian', 'Language code: lt'), 'lt'), + Language(72, translate('common.languages', 'Macedonian', 'Language code: mk'), 'mk'), + Language(73, translate('common.languages', 'Malagasy', 'Language code: mg'), 'mg'), + Language(74, translate('common.languages', 'Malay', 'Language code: ms'), 'ms'), + Language(75, translate('common.languages', 'Malayalam', 'Language code: ml'), 'ml'), + Language(76, translate('common.languages', 'Maltese', 'Language code: mt'), 'mt'), + Language(148, translate('common.languages', 'Mam', 'Language code: mam'), 'mam'), + Language(77, translate('common.languages', 'Maori', 'Language code: mi'), 'mi'), + Language(147, translate('common.languages', 'Maori', 'Language code: mri'), 'mri'), + Language(78, translate('common.languages', 'Marathi', 'Language code: mr'), 'mr'), + Language(79, translate('common.languages', 'Moldavian', 'Language code: mo'), 'mo'), + Language(80, translate('common.languages', 'Mongolian', 'Language code: mn'), 'mn'), + Language(149, translate('common.languages', 'Nahuatl', 'Language code: nah'), 'nah'), + Language(81, translate('common.languages', 'Nauru', 'Language code: na'), 'na'), + Language(82, translate('common.languages', 'Nepali', 'Language code: ne'), 'ne'), + Language(83, translate('common.languages', 'Norwegian', 'Language code: no'), 'no'), + Language(84, translate('common.languages', 'Occitan', 'Language code: oc'), 'oc'), + Language(85, translate('common.languages', 'Oriya', 'Language code: or'), 'or'), + Language(86, translate('common.languages', 'Pashto, Pushto', 'Language code: ps'), 'ps'), + Language(87, translate('common.languages', 'Persian', 'Language code: fa'), 'fa'), + Language(151, translate('common.languages', 'Plautdietsch', 'Language code: pdt'), 'pdt'), + Language(88, translate('common.languages', 'Polish', 'Language code: pl'), 'pl'), + Language(89, translate('common.languages', 'Portuguese', 'Language code: pt'), 'pt'), + Language(90, translate('common.languages', 'Punjabi', 'Language code: pa'), 'pa'), + Language(91, translate('common.languages', 'Quechua', 'Language code: qu'), 'qu'), + Language(92, translate('common.languages', 'Rhaeto-Romance', 'Language code: rm'), 'rm'), + Language(93, translate('common.languages', 'Romanian', 'Language code: ro'), 'ro'), + Language(94, translate('common.languages', 'Russian', 'Language code: ru'), 'ru'), + Language(95, translate('common.languages', 'Samoan', 'Language code: sm'), 'sm'), + Language(96, translate('common.languages', 'Sangro', 'Language code: sg'), 'sg'), + Language(97, translate('common.languages', 'Sanskrit', 'Language code: sa'), 'sa'), + Language(98, translate('common.languages', 'Scots Gaelic', 'Language code: gd'), 'gd'), + Language(99, translate('common.languages', 'Serbian', 'Language code: sr'), 'sr'), + Language(100, translate('common.languages', 'Serbo-Croatian', 'Language code: sh'), 'sh'), + Language(101, translate('common.languages', 'Sesotho', 'Language code: st'), 'st'), + Language(102, translate('common.languages', 'Setswana', 'Language code: tn'), 'tn'), + Language(103, translate('common.languages', 'Shona', 'Language code: sn'), 'sn'), + Language(104, translate('common.languages', 'Sindhi', 'Language code: sd'), 'sd'), + Language(105, translate('common.languages', 'Singhalese', 'Language code: si'), 'si'), + Language(106, translate('common.languages', 'Siswati', 'Language code: ss'), 'ss'), + Language(107, translate('common.languages', 'Slovak', 'Language code: sk'), 'sk'), + Language(108, translate('common.languages', 'Slovenian', 'Language code: sl'), 'sl'), + Language(109, translate('common.languages', 'Somali', 'Language code: so'), 'so'), + Language(110, translate('common.languages', 'Spanish', 'Language code: es'), 'es'), + Language(111, translate('common.languages', 'Sudanese', 'Language code: su'), 'su'), + Language(112, translate('common.languages', 'Swahili', 'Language code: sw'), 'sw'), + Language(113, translate('common.languages', 'Swedish', 'Language code: sv'), 'sv'), + Language(114, translate('common.languages', 'Tagalog', 'Language code: tl'), 'tl'), + Language(115, translate('common.languages', 'Tajik', 'Language code: tg'), 'tg'), + Language(116, translate('common.languages', 'Tamil', 'Language code: ta'), 'ta'), + Language(117, translate('common.languages', 'Tatar', 'Language code: tt'), 'tt'), + Language(118, translate('common.languages', 'Tegulu', 'Language code: te'), 'te'), + Language(119, translate('common.languages', 'Thai', 'Language code: th'), 'th'), + Language(120, translate('common.languages', 'Tibetan', 'Language code: bo'), 'bo'), + Language(121, translate('common.languages', 'Tigrinya', 'Language code: ti'), 'ti'), + Language(122, translate('common.languages', 'Tonga', 'Language code: to'), 'to'), + Language(123, translate('common.languages', 'Tsonga', 'Language code: ts'), 'ts'), + Language(124, translate('common.languages', 'Turkish', 'Language code: tr'), 'tr'), + Language(125, translate('common.languages', 'Turkmen', 'Language code: tk'), 'tk'), + Language(126, translate('common.languages', 'Twi', 'Language code: tw'), 'tw'), + Language(127, translate('common.languages', 'Uigur', 'Language code: ug'), 'ug'), + Language(128, translate('common.languages', 'Ukrainian', 'Language code: uk'), 'uk'), + Language(129, translate('common.languages', 'Urdu', 'Language code: ur'), 'ur'), + Language(153, translate('common.languages', 'Uspanteco', 'Language code: usp'), 'usp'), + Language(130, translate('common.languages', 'Uzbek', 'Language code: uz'), 'uz'), + Language(131, translate('common.languages', 'Vietnamese', 'Language code: vi'), 'vi'), + Language(132, translate('common.languages', 'Volapuk', 'Language code: vo'), 'vo'), + Language(133, translate('common.languages', 'Welch', 'Language code: cy'), 'cy'), + Language(134, translate('common.languages', 'Wolof', 'Language code: wo'), 'wo'), + Language(135, translate('common.languages', 'Xhosa', 'Language code: xh'), 'xh'), + Language(136, translate('common.languages', 'Yiddish (former ji)', 'Language code: yi'), 'yi'), + Language(137, translate('common.languages', 'Yoruba', 'Language code: yo'), 'yo'), + Language(138, translate('common.languages', 'Zhuang', 'Language code: za'), 'za'), + Language(139, translate('common.languages', 'Zulu', 'Language code: zu'), 'zu') +], key=lambda language: language.name) + + +def get_language(name): + """ + Find the language by its name or code. + + :param name: The name or abbreviation of the language. + :return: The first match as a Language namedtuple or None + """ + if name: + name_lower = name.lower() + name_title = name_lower[:1].upper() + name_lower[1:] + for language in languages: + if language.name == name_title or language.code == name_lower: + return language + return None diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index f19a35f6f..71799eb8a 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -23,6 +23,7 @@ The :mod:`uistrings` module provides standard strings for OpenLP. """ import logging +import itertools from openlp.core.common import translate @@ -153,3 +154,30 @@ class UiStrings(object): self.View = translate('OpenLP.Ui', 'View') self.ViewMode = translate('OpenLP.Ui', 'View Mode') self.Video = translate('OpenLP.Ui', 'Video') + self.BibleShortSearchTitle = translate('OpenLP.Ui', 'Search is Empty or too Short') + self.BibleShortSearch = translate('OpenLP.Ui', 'The search you have entered is empty or shorter ' + 'than 3 characters long.

Please try again with ' + 'a longer search.') + self.BibleNoBiblesTitle = translate('OpenLP.Ui', 'No Bibles Available') + self.BibleNoBibles = translate('OpenLP.Ui', 'There are no Bibles currently installed.

' + 'Please use the Import Wizard to install one or more Bibles.') + book_chapter = translate('OpenLP.Ui', 'Book Chapter') + chapter = translate('OpenLP.Ui', 'Chapter') + verse = translate('OpenLP.Ui', 'Verse') + gap = ' | ' + psalm = translate('OpenLP.Ui', 'Psalm') + may_shorten = translate('OpenLP.Ui', 'Book names may be shortened from full names, for an example Ps 23 = ' + 'Psalm 23') + bible_scripture_items = \ + [book_chapter, gap, psalm, ' 23
', + book_chapter, '%(range)s', chapter, gap, psalm, ' 23%(range)s24
', + book_chapter, '%(verse)s', verse, '%(range)s', verse, gap, psalm, ' 23%(verse)s1%(range)s2
', + book_chapter, '%(verse)s', verse, '%(range)s', verse, '%(list)s', verse, '%(range)s', verse, gap, psalm, + ' 23%(verse)s1%(range)s2%(list)s5%(range)s6
', + book_chapter, '%(verse)s', verse, '%(range)s', verse, '%(list)s', chapter, '%(verse)s', verse, '%(range)s', + verse, gap, psalm, ' 23%(verse)s1%(range)s2%(list)s24%(verse)s1%(range)s3
', + book_chapter, '%(verse)s', verse, '%(range)s', chapter, '%(verse)s', verse, gap, psalm, + ' 23%(verse)s1%(range)s24%(verse)s1

', may_shorten] + itertools.chain.from_iterable(itertools.repeat(strings, 1) if isinstance(strings, str) + else strings for strings in bible_scripture_items) + self.BibleScriptureError = ''.join(str(joined) for joined in bible_scripture_items) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 358c82543..c7c84f912 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -651,6 +651,20 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): item.setFont(font) self.list_view.addItem(item) + def check_search_result_search_while_typing_short(self): + """ + This is used in Bible "Search while typing" if the search is shorter than the min required len. + """ + if self.list_view.count(): + return + message = translate('OpenLP.MediaManagerItem', 'Search is too short to be used in: "Search while typing"') + item = QtWidgets.QListWidgetItem(message) + item.setFlags(QtCore.Qt.NoItemFlags) + font = QtGui.QFont() + font.setItalic(True) + item.setFont(font) + self.list_view.addItem(item) + def _get_id_of_item_to_generate(self, item, remote_item): """ Utility method to check items being submitted for slide generation. diff --git a/openlp/core/lib/theme.py b/openlp/core/lib/theme.py index fc20037e7..c27b08cd0 100644 --- a/openlp/core/lib/theme.py +++ b/openlp/core/lib/theme.py @@ -474,6 +474,7 @@ class ThemeXML(object): if element.startswith('shadow') or element.startswith('outline'): master = 'font_main' # fix bold font + ret_value = None if element == 'weight': element = 'bold' if value == 'Normal': @@ -482,7 +483,7 @@ class ThemeXML(object): ret_value = True if element == 'proportion': element = 'size' - return False, master, element, ret_value + return False, master, element, ret_value if ret_value is not None else value def _create_attr(self, master, element, value): """ diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index ccc61ba56..3d27effe4 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -41,7 +41,8 @@ __default_settings__ = { 'bibles/db password': '', 'bibles/db hostname': '', 'bibles/db database': '', - 'bibles/last search type': BibleSearch.Reference, + 'bibles/last search type': BibleSearch.Combined, + 'bibles/reset to combined quick search': True, 'bibles/verse layout style': LayoutStyle.VersePerSlide, 'bibles/book name language': LanguageSelection.Bible, 'bibles/display brackets': DisplayStyle.NoBrackets, @@ -59,7 +60,9 @@ __default_settings__ = { 'bibles/range separator': '', 'bibles/list separator': '', 'bibles/end separator': '', - 'bibles/last directory import': '' + 'bibles/last directory import': '', + 'bibles/hide combined quick error': False, + 'bibles/is search while typing enabled': True } diff --git a/openlp/plugins/bibles/forms/languageform.py b/openlp/plugins/bibles/forms/languageform.py index 461f02c74..1ba71ad6e 100644 --- a/openlp/plugins/bibles/forms/languageform.py +++ b/openlp/plugins/bibles/forms/languageform.py @@ -29,9 +29,9 @@ from PyQt5.QtWidgets import QDialog from PyQt5 import QtCore from openlp.core.common import translate +from openlp.core.common.languages import languages from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.forms.languagedialog import Ui_LanguageDialog -from openlp.plugins.bibles.lib.db import BiblesResourcesDB log = logging.getLogger(__name__) @@ -51,11 +51,11 @@ class LanguageForm(QDialog, Ui_LanguageDialog): self.setupUi(self) def exec(self, bible_name): - self.language_combo_box.addItem('') if bible_name: - self.bible_label.setText(str(bible_name)) - items = BiblesResourcesDB.get_languages() - self.language_combo_box.addItems([item['name'] for item in items]) + self.bible_label.setText(bible_name) + self.language_combo_box.addItem('') + for language in languages: + self.language_combo_box.addItem(language.name, language.id) return QDialog.exec(self) def accept(self): diff --git a/openlp/plugins/bibles/lib/biblestab.py b/openlp/plugins/bibles/lib/biblestab.py index 5438d07f2..c087335ae 100644 --- a/openlp/plugins/bibles/lib/biblestab.py +++ b/openlp/plugins/bibles/lib/biblestab.py @@ -128,6 +128,20 @@ class BiblesTab(SettingsTab): self.language_selection_layout.addWidget(self.language_selection_label) self.language_selection_layout.addWidget(self.language_selection_combo_box) self.right_layout.addWidget(self.language_selection_group_box) + self.bible_quick_settings_group_box = QtWidgets.QGroupBox(self.right_column) + self.bible_quick_settings_group_box.setObjectName('bible_quick_settings_group_box') + self.right_layout.addWidget(self.bible_quick_settings_group_box) + self.search_settings_layout = QtWidgets.QFormLayout(self.bible_quick_settings_group_box) + self.search_settings_layout.setObjectName('search_settings_layout') + self.reset_to_combined_quick_search_check_box = QtWidgets.QCheckBox(self.bible_quick_settings_group_box) + self.reset_to_combined_quick_search_check_box.setObjectName('reset_to_combined_quick_search_check_box') + self.search_settings_layout.addRow(self.reset_to_combined_quick_search_check_box) + self.hide_combined_quick_error_check_box = QtWidgets.QCheckBox(self.bible_quick_settings_group_box) + self.hide_combined_quick_error_check_box.setObjectName('hide_combined_quick_error_check_box') + self.search_settings_layout.addRow(self.hide_combined_quick_error_check_box) + self.bible_search_while_typing_check_box = QtWidgets.QCheckBox(self.bible_quick_settings_group_box) + self.bible_search_while_typing_check_box.setObjectName('bible_search_while_typing_check_box') + self.search_settings_layout.addRow(self.bible_search_while_typing_check_box) self.left_layout.addStretch() self.right_layout.addStretch() # Signals and slots @@ -151,6 +165,12 @@ class BiblesTab(SettingsTab): self.end_separator_line_edit.editingFinished.connect(self.on_end_separator_line_edit_finished) Registry().register_function('theme_update_list', self.update_theme_list) self.language_selection_combo_box.activated.connect(self.on_language_selection_combo_box_changed) + self.reset_to_combined_quick_search_check_box.stateChanged.connect( + self.on_reset_to_combined_quick_search_check_box_changed) + self.hide_combined_quick_error_check_box.stateChanged.connect( + self.on_hide_combined_quick_error_check_box_changed) + self.bible_search_while_typing_check_box.stateChanged.connect( + self.on_bible_search_while_typing_check_box_changed) def retranslateUi(self): self.verse_display_group_box.setTitle(translate('BiblesPlugin.BiblesTab', 'Verse Display')) @@ -194,6 +214,17 @@ class BiblesTab(SettingsTab): LanguageSelection.Application, translate('BiblesPlugin.BiblesTab', 'Application Language')) self.language_selection_combo_box.setItemText( LanguageSelection.English, translate('BiblesPlugin.BiblesTab', 'English')) + self.bible_quick_settings_group_box.setTitle(translate('BiblesPlugin.BiblesTab', 'Quick Search Settings')) + self.reset_to_combined_quick_search_check_box.setText(translate('BiblesPlugin.BiblesTab', + 'Reset search type to "Text or Scripture' + ' Reference" on startup')) + self.hide_combined_quick_error_check_box.setText(translate('BiblesPlugin.BiblesTab', + 'Don\'t show error if nothing is found in "Text or ' + 'Scripture Reference"')) + self.bible_search_while_typing_check_box.setText(translate('BiblesPlugin.BiblesTab', + 'Search automatically while typing (Text search must' + ' contain a\nminimum of {count} characters and a ' + 'space for performance reasons)').format(count='8')) def on_bible_theme_combo_box_changed(self): self.bible_theme = self.bible_theme_combo_box.currentText() @@ -302,6 +333,24 @@ class BiblesTab(SettingsTab): self.end_separator_line_edit.setText(get_reference_separator('sep_e_default')) self.end_separator_line_edit.setPalette(self.get_grey_text_palette(True)) + def on_reset_to_combined_quick_search_check_box_changed(self, check_state): + """ + Event handler for the 'hide_combined_quick_error' check box + """ + self.reset_to_combined_quick_search = (check_state == QtCore.Qt.Checked) + + def on_hide_combined_quick_error_check_box_changed(self, check_state): + """ + Event handler for the 'hide_combined_quick_error' check box + """ + self.hide_combined_quick_error = (check_state == QtCore.Qt.Checked) + + def on_bible_search_while_typing_check_box_changed(self, check_state): + """ + Event handler for the 'hide_combined_quick_error' check box + """ + self.bible_search_while_typing = (check_state == QtCore.Qt.Checked) + def load(self): settings = Settings() settings.beginGroup(self.settings_section) @@ -355,6 +404,12 @@ class BiblesTab(SettingsTab): self.end_separator_check_box.setChecked(True) self.language_selection = settings.value('book name language') self.language_selection_combo_box.setCurrentIndex(self.language_selection) + self.reset_to_combined_quick_search = settings.value('reset to combined quick search') + self.reset_to_combined_quick_search_check_box.setChecked(self.reset_to_combined_quick_search) + self.hide_combined_quick_error = settings.value('hide combined quick error') + self.hide_combined_quick_error_check_box.setChecked(self.hide_combined_quick_error) + self.bible_search_while_typing = settings.value('is search while typing enabled') + self.bible_search_while_typing_check_box.setChecked(self.bible_search_while_typing) settings.endGroup() def save(self): @@ -386,6 +441,9 @@ class BiblesTab(SettingsTab): if self.language_selection != settings.value('book name language'): settings.setValue('book name language', self.language_selection) self.settings_form.register_post_process('bibles_load_list') + settings.setValue('reset to combined quick search', self.reset_to_combined_quick_search) + settings.setValue('hide combined quick error', self.hide_combined_quick_error) + settings.setValue('is search while typing enabled', self.bible_search_while_typing) settings.endGroup() if self.tab_visited: self.settings_form.register_post_process('bibles_config_updated') diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index c23ade4a7..ff305dec6 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -128,6 +128,12 @@ class BibleDB(Manager, RegistryProperties): The name of the database. This is also used as the file name for SQLite databases. """ log.info('BibleDB loaded') + self._setup(parent, **kwargs) + + def _setup(self, parent, **kwargs): + """ + Run some initial setup. This method is separate from __init__ in order to mock it out in tests. + """ self.bible_plugin = parent self.session = None if 'path' not in kwargs: @@ -465,14 +471,13 @@ class BibleDB(Manager, RegistryProperties): """ log.debug('BibleDB.get_language()') from openlp.plugins.bibles.forms import LanguageForm - language = None + language_id = None language_form = LanguageForm(self.wizard) if language_form.exec(bible_name): - language = str(language_form.language_combo_box.currentText()) - if not language: + combo_box = language_form.language_combo_box + language_id = combo_box.itemData(combo_box.currentIndex()) + if not language_id: return False - language = BiblesResourcesDB.get_language(language) - language_id = language['id'] self.save_meta('language_id', language_id) return language_id @@ -767,43 +772,6 @@ class BiblesResourcesDB(QtCore.QObject, Manager): return book[0] return None - @staticmethod - def get_language(name): - """ - Return a dict containing the language id, name and code by name or abbreviation. - - :param name: The name or abbreviation of the language. - """ - log.debug('BiblesResourcesDB.get_language("{name}")'.format(name=name)) - if not isinstance(name, str): - name = str(name) - language = BiblesResourcesDB.run_sql( - 'SELECT id, name, code FROM language WHERE name = ? OR code = ?', (name, name.lower())) - if language: - return { - 'id': language[0][0], - 'name': str(language[0][1]), - 'code': str(language[0][2]) - } - else: - return None - - @staticmethod - def get_languages(): - """ - Return a dict containing all languages with id, name and code. - """ - log.debug('BiblesResourcesDB.get_languages()') - languages = BiblesResourcesDB.run_sql('SELECT id, name, code FROM language ORDER by name') - if languages: - return [{ - 'id': language[0], - 'name': str(language[1]), - 'code': str(language[2]) - } for language in languages] - else: - return None - @staticmethod def get_testament_reference(): """ @@ -1014,7 +982,7 @@ class OldBibleDB(QtCore.QObject, Manager): def get_verses(self, book_id): """ - Returns the verses of the Bible. + Returns the verses of the Bible. """ verses = self.run_sql( 'SELECT book_id, chapter, verse, text FROM verse WHERE book_id = ? ORDER BY id', (book_id, )) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index c50745c2f..392cce05a 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -252,7 +252,7 @@ class BGExtract(RegistryProperties): chapter=chapter, version=version) soup = get_soup_for_bible_ref( - 'http://legacy.biblegateway.com/passage/?{url}'.format(url=url_params), + 'http://biblegateway.com/passage/?{url}'.format(url=url_params), pre_parse_regex=r'', pre_parse_substitute='') if not soup: return None @@ -281,7 +281,7 @@ class BGExtract(RegistryProperties): """ log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version)) url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)}) - reference_url = 'http://legacy.biblegateway.com/versions/?{url}#books'.format(url=url_params) + reference_url = 'http://biblegateway.com/versions/?{url}#books'.format(url=url_params) page = get_web_page(reference_url) if not page: send_error_message('download') @@ -312,7 +312,7 @@ class BGExtract(RegistryProperties): for book in content: book = book.find('td') if book: - books.append(book.contents[0]) + books.append(book.contents[1]) return books def get_bibles_from_http(self): @@ -322,11 +322,11 @@ class BGExtract(RegistryProperties): returns a list in the form [(biblename, biblekey, language_code)] """ log.debug('BGExtract.get_bibles_from_http') - bible_url = 'https://legacy.biblegateway.com/versions/' + bible_url = 'https://biblegateway.com/versions/' soup = get_soup_for_bible_ref(bible_url) if not soup: return None - bible_select = soup.find('select', {'class': 'translation-dropdown'}) + bible_select = soup.find('select', {'class': 'search-translation-select'}) if not bible_select: log.debug('No select tags found - did site change?') return None @@ -532,28 +532,26 @@ class CWExtract(RegistryProperties): returns a list in the form [(biblename, biblekey, language_code)] """ log.debug('CWExtract.get_bibles_from_http') - bible_url = 'http://www.biblestudytools.com/' + bible_url = 'http://www.biblestudytools.com/bible-versions/' soup = get_soup_for_bible_ref(bible_url) if not soup: return None - bible_select = soup.find('select') - if not bible_select: - log.debug('No select tags found - did site change?') - return None - option_tags = bible_select.find_all('option', {'class': 'log-translation'}) - if not option_tags: - log.debug('No option tags found - did site change?') + h4_tags = soup.find_all('h4', {'class': 'small-header'}) + if not h4_tags: + log.debug('No h4 tags found - did site change?') return None bibles = [] - for ot in option_tags: - tag_text = ot.get_text().strip() - try: - tag_value = ot['value'] - except KeyError: - log.exception('No value attribute found - did site change?') + for h4t in h4_tags: + short_name = None + if h4t.span: + short_name = h4t.span.get_text().strip().lower() + else: + log.error('No span tag found - did site change?') return None - if not tag_value: + if not short_name: continue + h4t.span.extract() + tag_text = h4t.get_text().strip() # The names of non-english bibles has their language in parentheses at the end if tag_text.endswith(')'): language = tag_text[tag_text.rfind('(') + 1:-1] @@ -561,12 +559,20 @@ class CWExtract(RegistryProperties): language_code = CROSSWALK_LANGUAGES[language] else: language_code = '' - # ... except for the latin vulgate + # ... except for those that don't... elif 'latin' in tag_text.lower(): language_code = 'la' + elif 'la biblia' in tag_text.lower() or 'nueva' in tag_text.lower(): + language_code = 'es' + elif 'chinese' in tag_text.lower(): + language_code = 'zh' + elif 'greek' in tag_text.lower(): + language_code = 'el' + elif 'nova' in tag_text.lower(): + language_code = 'pt' else: language_code = 'en' - bibles.append((tag_text, tag_value, language_code)) + bibles.append((tag_text, short_name, language_code)) return bibles diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 1c55222f2..9ea4c6612 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -23,8 +23,8 @@ import logging import os -from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, delete_file -from openlp.plugins.bibles.lib import parse_reference, get_reference_separator, LanguageSelection +from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, delete_file, UiStrings +from openlp.plugins.bibles.lib import parse_reference, LanguageSelection from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta from .csvbible import CSVBible from .http import HTTPBible @@ -267,42 +267,21 @@ class BibleManager(RegistryProperties): For second bible this is necessary. :param show_error: """ + # If no bibles are installed, message is given. log.debug('BibleManager.get_verses("{bible}", "{verse}")'.format(bible=bible, verse=verse_text)) if not bible: if show_error: self.main_window.information_message( - translate('BiblesPlugin.BibleManager', 'No Bibles Available'), - translate('BiblesPlugin.BibleManager', 'There are no Bibles currently installed. Please use the ' - 'Import Wizard to install one or more Bibles.') - ) + UiStrings().BibleNoBiblesTitle, + UiStrings().BibleNoBibles) return None + # Get the language for books. language_selection = self.get_language_selection(bible) ref_list = parse_reference(verse_text, self.db_cache[bible], language_selection, book_ref_id) if ref_list: return self.db_cache[bible].get_verses(ref_list, show_error) + # If nothing is found. Message is given if this is not combined search. (defined in mediaitem.py) else: - if show_error: - reference_separators = { - 'verse': get_reference_separator('sep_v_display'), - 'range': get_reference_separator('sep_r_display'), - 'list': get_reference_separator('sep_l_display')} - self.main_window.information_message( - translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'), - translate('BiblesPlugin.BibleManager', 'Your scripture reference is either not supported by ' - 'OpenLP or is invalid. Please make sure your reference ' - 'conforms to one of the following patterns or consult the manual:\n\n' - 'Book Chapter\n' - 'Book Chapter%(range)sChapter\n' - 'Book Chapter%(verse)sVerse%(range)sVerse\n' - 'Book Chapter%(verse)sVerse%(range)sVerse%(list)sVerse' - '%(range)sVerse\n' - 'Book Chapter%(verse)sVerse%(range)sVerse%(list)sChapter' - '%(verse)sVerse%(range)sVerse\n' - 'Book Chapter%(verse)sVerse%(range)sChapter%(verse)sVerse', - 'Please pay attention to the appended "s" of the wildcards ' - 'and refrain from translating the words inside the names in the brackets.') - % reference_separators - ) return None def get_language_selection(self, bible): @@ -334,13 +313,11 @@ class BibleManager(RegistryProperties): :param text: The text to search for (unicode). """ log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text)) + # If no bibles are installed, message is given. if not bible: self.main_window.information_message( - translate('BiblesPlugin.BibleManager', 'No Bibles Available'), - translate('BiblesPlugin.BibleManager', - 'There are no Bibles currently installed. Please use the Import Wizard to install one or ' - 'more Bibles.') - ) + UiStrings().BibleNoBiblesTitle, + UiStrings().BibleNoBibles) return None # Check if the bible or second_bible is a web bible. web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source') @@ -348,20 +325,56 @@ class BibleManager(RegistryProperties): if second_bible: second_web_bible = self.db_cache[second_bible].get_object(BibleMeta, 'download_source') if web_bible or second_web_bible: + # If either Bible is Web, cursor is reset to normal and message is given. + self.application.set_normal_cursor() self.main_window.information_message( - translate('BiblesPlugin.BibleManager', 'Web Bible cannot be used'), - translate('BiblesPlugin.BibleManager', 'Text Search is not available with Web Bibles.') + translate('BiblesPlugin.BibleManager', 'Web Bible cannot be used in Text Search'), + translate('BiblesPlugin.BibleManager', 'Text Search is not available with Web Bibles.\n' + 'Please use the Scripture Reference Search instead.\n\n' + 'This means that the currently used Bible\nor Second Bible ' + 'is installed as Web Bible.\n\n' + 'If you were trying to perform a Reference search\nin Combined ' + 'Search, your reference is invalid.') ) return None - if text: + # Shorter than 3 char searches break OpenLP with very long search times, thus they are blocked. + if len(text) - text.count(' ') < 3: + return None + # Fetch the results from db. If no results are found, return None, no message is given for this. + elif text: + return self.db_cache[bible].verse_search(text) + else: + return None + + def verse_search_while_typing(self, bible, second_bible, text): + """ + Does a verse search for the given bible and text. + This is used during "Search while typing" + It's the same thing as the normal text search, but it does not show the web Bible error. + (It would result in the error popping every time a char is entered or removed) + It also does not have a minimum text len, this is set in mediaitem.py + + :param bible: The bible to search in (unicode). + :param second_bible: The second bible (unicode). We do not search in this bible. + :param text: The text to search for (unicode). + """ + # If no bibles are installed, message is given. + if not bible: + return None + # Check if the bible or second_bible is a web bible. + web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source') + second_web_bible = '' + if second_bible: + second_web_bible = self.db_cache[second_bible].get_object(BibleMeta, 'download_source') + if web_bible or second_web_bible: + # If either Bible is Web, cursor is reset to normal and search ends w/o any message. + self.check_search_result() + self.application.set_normal_cursor() + return None + # Fetch the results from db. If no results are found, return None, no message is given for this. + elif text: return self.db_cache[bible].verse_search(text) else: - self.main_window.information_message( - translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'), - translate('BiblesPlugin.BibleManager', 'You did not enter a search keyword.\nYou can separate ' - 'different keywords by a space to search for all of your keywords and you can separate ' - 'them by a comma to search for one of them.') - ) return None def save_meta_data(self, bible, version, copyright, permissions, book_name_language=None): diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index d5304e867..debf786f7 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -35,6 +35,7 @@ from openlp.plugins.bibles.forms.editbibleform import EditBibleForm from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \ LanguageSelection, BibleStrings from openlp.plugins.bibles.lib.db import BiblesResourcesDB +import re log = logging.getLogger(__name__) @@ -45,6 +46,7 @@ class BibleSearch(object): """ Reference = 1 Text = 2 + Combined = 3 class BibleMediaItem(MediaManagerItem): @@ -56,6 +58,7 @@ class BibleMediaItem(MediaManagerItem): log.info('Bible Media Item loaded') def __init__(self, parent, plugin): + self.clear_icon = build_icon(':/bibles/bibles_search_clear.png') self.lock_icon = build_icon(':/bibles/bibles_search_lock.png') self.unlock_icon = build_icon(':/bibles/bibles_search_unlock.png') MediaManagerItem.__init__(self, parent, plugin) @@ -157,10 +160,15 @@ class BibleMediaItem(MediaManagerItem): search_button_layout = QtWidgets.QHBoxLayout() search_button_layout.setObjectName(prefix + 'search_button_layout') search_button_layout.addStretch() + # Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon. + clear_button = QtWidgets.QToolButton(tab) + clear_button.setIcon(self.clear_icon) + clear_button.setObjectName(prefix + 'ClearButton') lock_button = QtWidgets.QToolButton(tab) lock_button.setIcon(self.unlock_icon) lock_button.setCheckable(True) lock_button.setObjectName(prefix + 'LockButton') + search_button_layout.addWidget(clear_button) search_button_layout.addWidget(lock_button) search_button = QtWidgets.QPushButton(tab) search_button.setObjectName(prefix + 'SearchButton') @@ -176,6 +184,7 @@ class BibleMediaItem(MediaManagerItem): setattr(self, prefix + 'SecondComboBox', second_combo_box) setattr(self, prefix + 'StyleLabel', style_label) setattr(self, prefix + 'StyleComboBox', style_combo_box) + setattr(self, prefix + 'ClearButton', clear_button) setattr(self, prefix + 'LockButton', lock_button) setattr(self, prefix + 'SearchButtonLayout', search_button_layout) setattr(self, prefix + 'SearchButton', search_button) @@ -245,11 +254,14 @@ class BibleMediaItem(MediaManagerItem): self.quickStyleComboBox.activated.connect(self.on_quick_style_combo_box_changed) self.advancedStyleComboBox.activated.connect(self.on_advanced_style_combo_box_changed) # Buttons + self.advancedClearButton.clicked.connect(self.on_clear_button) + self.quickClearButton.clicked.connect(self.on_clear_button) self.advancedSearchButton.clicked.connect(self.on_advanced_search_button) self.quickSearchButton.clicked.connect(self.on_quick_search_button) # Other stuff self.quick_search_edit.returnPressed.connect(self.on_quick_search_button) self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed) + self.quick_search_edit.textChanged.connect(self.on_search_text_edit_changed) def on_focus(self): if self.quickTab.isVisible(): @@ -286,6 +298,7 @@ class BibleMediaItem(MediaManagerItem): self.quickStyleComboBox.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide) self.quickStyleComboBox.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine) self.quickStyleComboBox.setItemText(LayoutStyle.Continuous, UiStrings().Continuous) + self.quickClearButton.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.')) self.quickLockButton.setToolTip(translate('BiblesPlugin.MediaItem', 'Toggle to keep or clear the previous results.')) self.quickSearchButton.setText(UiStrings().Search) @@ -300,6 +313,7 @@ class BibleMediaItem(MediaManagerItem): self.advancedStyleComboBox.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide) self.advancedStyleComboBox.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine) self.advancedStyleComboBox.setItemText(LayoutStyle.Continuous, UiStrings().Continuous) + self.advancedClearButton.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.')) self.advancedLockButton.setToolTip(translate('BiblesPlugin.MediaItem', 'Toggle to keep or clear the previous results.')) self.advancedSearchButton.setText(UiStrings().Search) @@ -309,6 +323,9 @@ class BibleMediaItem(MediaManagerItem): self.plugin.manager.media = self self.load_bibles() self.quick_search_edit.set_search_types([ + (BibleSearch.Combined, ':/bibles/bibles_search_combined.png', + translate('BiblesPlugin.MediaItem', 'Text or Reference'), + translate('BiblesPlugin.MediaItem', 'Text or Reference...')), (BibleSearch.Reference, ':/bibles/bibles_search_reference.png', translate('BiblesPlugin.MediaItem', 'Scripture Reference'), translate('BiblesPlugin.MediaItem', 'Search Scripture Reference...')), @@ -424,18 +441,24 @@ class BibleMediaItem(MediaManagerItem): def update_auto_completer(self): """ This updates the bible book completion list for the search field. The completion depends on the bible. It is - only updated when we are doing a reference search, otherwise the auto completion list is removed. + only updated when we are doing reference or combined search, in text search the completion list is removed. """ log.debug('update_auto_completer') - # Save the current search type to the configuration. - Settings().setValue('{section}/last search type'.format(section=self.settings_section), - self.quick_search_edit.current_search_type()) + # Save the current search type to the configuration. If setting for automatically resetting the search type to + # Combined is enabled, use that otherwise use the currently selected search type. + # Note: This setting requires a restart to take effect. + if Settings().value(self.settings_section + '/reset to combined quick search'): + Settings().setValue('{section}/last search type'.format(section=self.settings_section), + BibleSearch.Combined) + else: + Settings().setValue('{section}/last search type'.format(section=self.settings_section), + self.quick_search_edit.current_search_type()) # Save the current bible to the configuration. Settings().setValue('{section}/quick bible'.format(section=self.settings_section), self.quickVersionComboBox.currentText()) books = [] - # We have to do a 'Reference Search'. - if self.quick_search_edit.current_search_type() == BibleSearch.Reference: + # We have to do a 'Reference Search' (Or as part of Combined Search). + if self.quick_search_edit.current_search_type() is not BibleSearch.Text: bibles = self.plugin.manager.get_bibles() bible = self.quickVersionComboBox.currentText() if bible: @@ -525,7 +548,15 @@ class BibleMediaItem(MediaManagerItem): self.advancedTab.setVisible(True) self.advanced_book_combo_box.setFocus() + def on_clear_button(self): + # Clear the list, then set the "No search Results" message, then clear the text field and give it focus. + self.list_view.clear() + self.check_search_result() + self.quick_search_edit.clear() + self.quick_search_edit.setFocus() + def on_lock_button_toggled(self, checked): + self.quick_search_edit.setFocus() if checked: self.sender().setIcon(self.lock_icon) else: @@ -652,10 +683,120 @@ class BibleMediaItem(MediaManagerItem): self.check_search_result() self.application.set_normal_cursor() + def on_quick_reference_search(self): + """ + We are doing a 'Reference Search'. + This search is called on def on_quick_search_button by Quick Reference and Combined Searches. + """ + # Set Bibles to use the text input from Quick search field. + bible = self.quickVersionComboBox.currentText() + second_bible = self.quickSecondComboBox.currentText() + """ + Get input from field and replace 'A-Z + . ' with '' + This will check if field has any '.' after A-Z and removes them. Eg. Gen. 1 = Ge 1 = Genesis 1 + If Book name has '.' after number. eg. 1. Genesis, the search fails without the dot, and vice versa. + A better solution would be to make '.' optional in the search results. Current solution was easier to code. + """ + text = self.quick_search_edit.text() + text = re.sub('\D[.]\s', ' ', text) + # This is triggered on reference search, use the search from manager.py + if self.quick_search_edit.current_search_type() != BibleSearch.Text: + self.search_results = self.plugin.manager.get_verses(bible, text) + if second_bible and self.search_results: + self.second_search_results = \ + self.plugin.manager.get_verses(second_bible, text, self.search_results[0].book.book_reference_id) + + def on_quick_text_search(self): + """ + We are doing a 'Text Search'. + This search is called on def on_quick_search_button by Quick Text and Combined Searches. + """ + # Set Bibles to use the text input from Quick search field. + bible = self.quickVersionComboBox.currentText() + second_bible = self.quickSecondComboBox.currentText() + text = self.quick_search_edit.text() + # If Text search ends with "," OpenLP will crash, prevent this from happening by removing all ","s. + text = re.sub('[,]', '', text) + self.application.set_busy_cursor() + # Get Bibles list + bibles = self.plugin.manager.get_bibles() + # Add results to "search_results" + self.search_results = self.plugin.manager.verse_search(bible, second_bible, text) + if second_bible and self.search_results: + # new_search_results is needed to make sure 2nd bible contains all verses. (And counting them on error) + text = [] + new_search_results = [] + count = 0 + passage_not_found = False + # Search second bible for results of search_results to make sure everythigns there. + # Count all the unfound passages. + for verse in self.search_results: + db_book = bibles[second_bible].get_book_by_book_ref_id(verse.book.book_reference_id) + if not db_book: + log.debug('Passage "{name} {chapter:d}:{verse:d}" not found in ' + 'Second Bible'.format(name=verse.book.name, chapter=verse.chapter, verse=verse.verse)) + passage_not_found = True + count += 1 + continue + new_search_results.append(verse) + text.append((verse.book.book_reference_id, verse.chapter, verse.verse, verse.verse)) + if passage_not_found: + # This is for the 2nd Bible. + self.main_window.information_message( + translate('BiblesPlugin.MediaItem', 'Information'), + translate('BiblesPlugin.MediaItem', 'The second Bible does not contain all the verses ' + 'that are in the main Bible.\nOnly verses found in both Bibles' + ' will be shown.\n\n{count:d} verses have not been included ' + 'in the results.').format(count=count)) + # Join the searches so only verses that are found on both Bibles are shown. + self.search_results = new_search_results + self.second_search_results = bibles[second_bible].get_verses(text) + + def on_quick_text_search_while_typing(self): + """ + We are doing a 'Text Search' while typing + Call the verse_search_while_typing from manager.py + It does not show web bible errors while typing. + (It would result in the error popping every time a char is entered or removed) + """ + # Set Bibles to use the text input from Quick search field. + bible = self.quickVersionComboBox.currentText() + second_bible = self.quickSecondComboBox.currentText() + text = self.quick_search_edit.text() + # If Text search ends with "," OpenLP will crash, prevent this from happening by removing all ","s. + text = re.sub('[,]', '', text) + self.application.set_busy_cursor() + # Get Bibles list + bibles = self.plugin.manager.get_bibles() + # Add results to "search_results" + self.search_results = self.plugin.manager.verse_search_while_typing(bible, second_bible, text) + if second_bible and self.search_results: + # new_search_results is needed to make sure 2nd bible contains all verses. (And counting them on error) + text = [] + new_search_results = [] + count = 0 + passage_not_found = False + # Search second bible for results of search_results to make sure everythigns there. + # Count all the unfound passages. Even thou no error is shown, this needs to be done or + # the code breaks later on. + for verse in self.search_results: + db_book = bibles[second_bible].get_book_by_book_ref_id(verse.book.book_reference_id) + if not db_book: + log.debug('Passage ("{versebookname}","{versechapter}","{verseverse}") not found in Second Bible' + .format(versebookname=verse.book.name, versechapter='verse.chapter', + verseverse=verse.verse)) + count += 1 + continue + new_search_results.append(verse) + text.append((verse.book.book_reference_id, verse.chapter, verse.verse, verse.verse)) + # Join the searches so only verses that are found on both Bibles are shown. + self.search_results = new_search_results + self.second_search_results = bibles[second_bible].get_verses(text) + def on_quick_search_button(self): """ - Does a quick search and saves the search results. Quick search can either be "Reference Search" or - "Text Search". + This triggers the proper Quick search based on which search type is used. + "Eg. "Reference Search", "Text Search" or "Combined search". """ log.debug('Quick Search Button clicked') self.quickSearchButton.setEnabled(False) @@ -664,41 +805,68 @@ class BibleMediaItem(MediaManagerItem): second_bible = self.quickSecondComboBox.currentText() text = self.quick_search_edit.text() if self.quick_search_edit.current_search_type() == BibleSearch.Reference: - # We are doing a 'Reference Search'. - self.search_results = self.plugin.manager.get_verses(bible, text) - if second_bible and self.search_results: - self.second_search_results = \ - self.plugin.manager.get_verses(second_bible, text, self.search_results[0].book.book_reference_id) - else: - # We are doing a 'Text Search'. - self.application.set_busy_cursor() - bibles = self.plugin.manager.get_bibles() - self.search_results = self.plugin.manager.verse_search(bible, second_bible, text) - if second_bible and self.search_results: - text = [] - new_search_results = [] - count = 0 - passage_not_found = False - for verse in self.search_results: - db_book = bibles[second_bible].get_book_by_book_ref_id(verse.book.book_reference_id) - if not db_book: - log.debug('Passage "{name} {chapter:d}:{verse:d}" not found in ' - 'Second Bible'.format(name=verse.book.name, chapter=verse.chapter, verse=verse.verse)) - passage_not_found = True - count += 1 - continue - new_search_results.append(verse) - text.append((verse.book.book_reference_id, verse.chapter, verse.verse, verse.verse)) - if passage_not_found: - QtWidgets.QMessageBox.information( - self, translate('BiblesPlugin.MediaItem', 'Information'), - translate('BiblesPlugin.MediaItem', - 'The second Bible does not contain all the verses that are in the main Bible. ' - 'Only verses found in both Bibles will be shown. {count:d} verses have not been ' - 'included in the results.').format(count=count), - QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok)) - self.search_results = new_search_results - self.second_search_results = bibles[second_bible].get_verses(text) + # We are doing a 'Reference Search'. (Get script from def on_quick_reference_search) + self.on_quick_reference_search() + # Get reference separators from settings. + if not self.search_results: + reference_separators = { + 'verse': get_reference_separator('sep_v_display'), + 'range': get_reference_separator('sep_r_display'), + 'list': get_reference_separator('sep_l_display')} + self.main_window.information_message( + translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'), + translate('BiblesPlugin.BibleManager', 'OpenLP couldn’t find anything ' + 'with your search.

' + 'Please make sure that your reference follows ' + 'one of these patterns:


%s' + % UiStrings().BibleScriptureError % reference_separators)) + elif self.quick_search_edit.current_search_type() == BibleSearch.Text: + # We are doing a 'Text Search'. (Get script from def on_quick_text_search) + self.on_quick_text_search() + if not self.search_results and len(text) - text.count(' ') < 3 and bible: + self.main_window.information_message( + UiStrings().BibleShortSearchTitle, + UiStrings().BibleShortSearch) + elif self.quick_search_edit.current_search_type() == BibleSearch.Combined: + # We are doing a 'Combined search'. Starting with reference search. + # Perform only if text contains any numbers + if (char.isdigit() for char in text): + self.on_quick_reference_search() + """ + If results are found, search will be finalized. + This check needs to be here in order to avoid duplicate errors. + If keyword is shorter than 3 (not including spaces), message is given. It's actually possible to find + verses with less than 3 chars (Eg. G1 = Genesis 1) thus this error is not shown if any results are found. + if no Bibles are installed, this message is not shown - "No bibles" message is shown instead. + """ + if not self.search_results and len(text) - text.count(' ') < 3 and bible: + self.main_window.information_message( + UiStrings().BibleShortSearchTitle, + UiStrings().BibleShortSearch) + if not self.search_results and len(text) - text.count(' ') > 2 and bible: + # Text search starts here if no reference was found and keyword is longer than 2. + # > 2 check is required in order to avoid duplicate error messages for short keywords. + self.on_quick_text_search() + if not self.search_results and not \ + Settings().value(self.settings_section + '/hide combined quick error'): + self.application.set_normal_cursor() + # Reference separators need to be defined both, in here and on reference search, + # error won't work if they are left out from one. + reference_separators = { + 'verse': get_reference_separator('sep_v_display'), + 'range': get_reference_separator('sep_r_display'), + 'list': get_reference_separator('sep_l_display')} + self.main_window.information_message(translate('BiblesPlugin.BibleManager', 'Nothing found'), + translate('BiblesPlugin.BibleManager', + 'OpenLP couldn’t find anything with your' + ' search.

If you tried to search' + ' with Scripture Reference, please make
sure' + ' that your reference follows one of these' + ' patterns:

%s' + % UiStrings().BibleScriptureError % + reference_separators)) + # Finalizing the search + # List is cleared if not locked, results are listed, button is set available, cursor is set to normal. if not self.quickLockButton.isChecked(): self.list_view.clear() if self.list_view.count() != 0 and self.search_results: @@ -709,6 +877,99 @@ class BibleMediaItem(MediaManagerItem): self.check_search_result() self.application.set_normal_cursor() + def on_quick_search_while_typing(self): + """ + This function is called when "Search as you type" is enabled for Bibles. + It is basically the same thing as "on_quick_search_search" but all the error messages are removed. + This also has increased min len for text search for performance reasons. + For commented version, please visit def on_quick_search_button. + """ + bible = self.quickVersionComboBox.currentText() + second_bible = self.quickSecondComboBox.currentText() + text = self.quick_search_edit.text() + if self.quick_search_edit.current_search_type() == BibleSearch.Combined: + # If text has no numbers, auto search limit is min 8 characters for performance reasons. + # If you change this value, also change it in biblestab.py (Count) in enabling search while typing. + if (char.isdigit() for char in text) and len(text) > 2: + self.on_quick_reference_search() + if not self.search_results and len(text) > 7: + self.on_quick_text_search_while_typing() + elif self.quick_search_edit.current_search_type() == BibleSearch.Reference: + self.on_quick_reference_search() + elif self.quick_search_edit.current_search_type() == BibleSearch.Text: + if len(text) > 7: + self.on_quick_text_search_while_typing() + if not self.quickLockButton.isChecked(): + self.list_view.clear() + if self.list_view.count() != 0 and self.search_results: + self.__check_second_bible(bible, second_bible) + elif self.search_results: + self.display_results(bible, second_bible) + self.check_search_result() + self.application.set_normal_cursor() + + def on_search_text_edit_changed(self): + """ + If search automatically while typing is enabled, perform the search and list results when conditions are met. + """ + if Settings().value('bibles/is search while typing enabled'): + text = self.quick_search_edit.text() + """ + Use Regex for finding space + number in reference search and space + 2 characters in text search. + Also search for two characters (Searches require at least two sets of two characters) + These are used to prevent bad search queries from starting. (Long/crashing queries) + """ + space_and_digit_reference = re.compile(' \d') + two_chars_text = re.compile('\S\S') + space_and_two_chars_text = re.compile(' \S\S') + # Turn this into a format that may be used in if statement. + count_space_digit_reference = space_and_digit_reference.findall(text) + count_two_chars_text = two_chars_text.findall(text) + count_spaces_two_chars_text = space_and_two_chars_text.findall(text) + """ + The Limit is required for setting the proper "No items found" message. + "Limit" is also hard coded to on_quick_search_while_typing, it must be there to avoid bad search + performance. Limit 8 = Text search, 3 = Reference search. + """ + limit = 8 + if self.quick_search_edit.current_search_type() == BibleSearch.Combined: + if len(count_space_digit_reference) != 0: + limit = 3 + elif self.quick_search_edit.current_search_type() == BibleSearch.Reference: + limit = 3 + """ + If text is empty, clear the list. + else: Start by checking if the search is suitable for "Search while typing" + """ + if len(text) == 0: + if not self.quickLockButton.isChecked(): + self.list_view.clear() + self.check_search_result() + else: + if limit == 3 and (len(text) < limit or len(count_space_digit_reference) == 0): + if not self.quickLockButton.isChecked(): + self.list_view.clear() + self.check_search_result() + elif (limit == 8 and (len(text) < limit or len(count_spaces_two_chars_text) == 0 or + len(count_two_chars_text) < 2)): + if not self.quickLockButton.isChecked(): + self.list_view.clear() + self.check_search_result_search_while_typing_short() + else: + """ + Start search if no chars are entered or deleted for 0.2 s + If no Timer is set, Text search will break the search by sending repeative search Quaries on + all chars. Use the self.on_quick_search_while_typing, this does not contain any error messages. + """ + self.search_timer = () + if self.search_timer: + self.search_timer.stop() + self.search_timer.deleteLater() + self.search_timer = QtCore.QTimer() + self.search_timer.timeout.connect(self.on_quick_search_while_typing) + self.search_timer.setSingleShot(True) + self.search_timer.start(200) + def display_results(self, bible, second_bible=''): """ Displays the search results in the media manager. All data needed for further action is saved for/in each row. diff --git a/openlp/plugins/bibles/lib/osis.py b/openlp/plugins/bibles/lib/osis.py index 85aea70b3..4b217022e 100644 --- a/openlp/plugins/bibles/lib/osis.py +++ b/openlp/plugins/bibles/lib/osis.py @@ -23,7 +23,7 @@ import logging from lxml import etree -from openlp.core.common import translate, trace_error_handler +from openlp.core.common import languages, translate, trace_error_handler from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB from openlp.core.lib.ui import critical_error_message_box @@ -62,9 +62,11 @@ class OSISBible(BibleDB): namespace = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'} # Find bible language language_id = None - language = osis_bible_tree.xpath("//ns:osisText/@xml:lang", namespaces=namespace) - if language: - language_id = BiblesResourcesDB.get_language(language[0]) + lang = osis_bible_tree.xpath("//ns:osisText/@xml:lang", namespaces=namespace) + if lang: + language = languages.get_language(lang[0]) + if hasattr(language, 'id'): + language_id = language.id # The language couldn't be detected, ask the user if not language_id: language_id = self.get_language(bible_name) diff --git a/openlp/plugins/bibles/lib/sword.py b/openlp/plugins/bibles/lib/sword.py index 77f08cb47..b37e9b583 100644 --- a/openlp/plugins/bibles/lib/sword.py +++ b/openlp/plugins/bibles/lib/sword.py @@ -23,7 +23,7 @@ import logging from pysword import modules -from openlp.core.common import translate +from openlp.core.common import languages, translate from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB @@ -57,9 +57,12 @@ class SwordBible(BibleDB): pysword_modules = modules.SwordModules(self.sword_path) pysword_module_json = pysword_modules.parse_modules()[self.sword_key] bible = pysword_modules.get_bible_from_module(self.sword_key) + language_id = None language = pysword_module_json['lang'] language = language[language.find('.') + 1:] - language_id = BiblesResourcesDB.get_language(language)['id'] + language = languages.get_language(language) + if hasattr(language, 'id'): + language_id = language.id self.save_meta('language_id', language_id) books = bible.get_structure().get_books() # Count number of books diff --git a/openlp/plugins/bibles/lib/zefania.py b/openlp/plugins/bibles/lib/zefania.py index 482d98107..96ab69072 100644 --- a/openlp/plugins/bibles/lib/zefania.py +++ b/openlp/plugins/bibles/lib/zefania.py @@ -21,9 +21,9 @@ ############################################################################### import logging -from lxml import etree, objectify +from lxml import etree -from openlp.core.common import translate +from openlp.core.common import languages, translate from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB @@ -62,7 +62,9 @@ class ZefaniaBible(BibleDB): language_id = None language = zefania_bible_tree.xpath("/XMLBIBLE/INFORMATION/language/text()") if language: - language_id = BiblesResourcesDB.get_language(language[0]) + language = languages.get_language(language[0]) + if hasattr(language, 'id'): + language_id = language.id # The language couldn't be detected, ask the user if not language_id: language_id = self.get_language(bible_name) diff --git a/openlp/plugins/bibles/resources/bibles_resources.sqlite b/openlp/plugins/bibles/resources/bibles_resources.sqlite index 8f1777124..0db28194e 100644 Binary files a/openlp/plugins/bibles/resources/bibles_resources.sqlite and b/openlp/plugins/bibles/resources/bibles_resources.sqlite differ diff --git a/openlp/plugins/songs/lib/importers/easyslides.py b/openlp/plugins/songs/lib/importers/easyslides.py index 907a6c90f..2ae489208 100644 --- a/openlp/plugins/songs/lib/importers/easyslides.py +++ b/openlp/plugins/songs/lib/importers/easyslides.py @@ -46,7 +46,7 @@ class EasySlidesImport(SongImport): def do_import(self): log.info('Importing EasySlides XML file {source}'.format(source=self.import_source)) - parser = etree.XMLParser(remove_blank_text=True) + parser = etree.XMLParser(remove_blank_text=True, recover=True) parsed_file = etree.parse(self.import_source, parser) xml = etree.tostring(parsed_file).decode() song_xml = objectify.fromstring(xml) diff --git a/openlp/plugins/songs/lib/importers/videopsalm.py b/openlp/plugins/songs/lib/importers/videopsalm.py index 25fd4d8eb..b536dd678 100644 --- a/openlp/plugins/songs/lib/importers/videopsalm.py +++ b/openlp/plugins/songs/lib/importers/videopsalm.py @@ -73,6 +73,14 @@ class VideoPsalmImport(SongImport): processed_content += c c = next(file_content_it) processed_content += '"' + c + # Remove control characters + elif (c < chr(32)): + processed_content += ' ' + # Handle escaped characters + elif c == '\\': + processed_content += c + c = next(file_content_it) + processed_content += c else: processed_content += c songbook = json.loads(processed_content.strip()) diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py index 5adffb300..e2964661d 100644 --- a/openlp/plugins/songs/lib/openlyricsxml.py +++ b/openlp/plugins/songs/lib/openlyricsxml.py @@ -458,7 +458,7 @@ class OpenLyrics(object): self._add_tag_to_formatting(tag, tags_element) # Replace end tags. for tag in end_tags: - text = text.replace('{/{tag}}}'.format(tag=tag), '') + text = text.replace('{{{tag}}}'.format(tag=tag), '') # Replace \n with
. text = text.replace('\n', '
') element = etree.XML('{text}'.format(text=text)) @@ -643,7 +643,7 @@ class OpenLyrics(object): # Append text from tail and add formatting end tag. # TODO: Verify format() with template variables if element.tag == NSMAP % 'tag' and use_endtag: - text += '{/{name}}}'.format(name=element.get('name')) + text += '{{{name}}}'.format(name=element.get('name')) # Append text from tail. if element.tail: text += element.tail diff --git a/resources/images/bibles_search_clear.png b/resources/images/bibles_search_clear.png new file mode 100644 index 000000000..551ce7c96 Binary files /dev/null and b/resources/images/bibles_search_clear.png differ diff --git a/resources/images/bibles_search_combined.png b/resources/images/bibles_search_combined.png new file mode 100644 index 000000000..161b3fcd6 Binary files /dev/null and b/resources/images/bibles_search_combined.png differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index f2619b0c7..b45cc745d 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -30,9 +30,11 @@ image_new_group.png + bibles_search_combined.png bibles_search_text.png bibles_search_reference.png bibles_upgrade_alert.png + bibles_search_clear.png bibles_search_unlock.png bibles_search_lock.png diff --git a/scripts/jenkins_script.py b/scripts/jenkins_script.py index 0a528d8a0..e119e144b 100755 --- a/scripts/jenkins_script.py +++ b/scripts/jenkins_script.py @@ -63,9 +63,10 @@ class OpenLPJobs(object): Branch_Windows_Interface = 'Branch-04b-Windows_Interface_Tests' Branch_PEP = 'Branch-05a-Code_Analysis' Branch_Coverage = 'Branch-05b-Test_Coverage' + Branch_Pylint = 'Branch-05c-Code_Analysis2' Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_Windows_Functional, Branch_Windows_Interface, - Branch_PEP, Branch_Coverage] + Branch_PEP, Branch_Coverage, Branch_Pylint] class Colour(object): diff --git a/tests/functional/openlp_core_common/test_languages.py b/tests/functional/openlp_core_common/test_languages.py new file mode 100644 index 000000000..fa6756d08 --- /dev/null +++ b/tests/functional/openlp_core_common/test_languages.py @@ -0,0 +1,109 @@ +# -*- 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.lib.languages package. +""" +from unittest import TestCase + +from openlp.core.common import languages + + +class TestLanguages(TestCase): + + def languages_type_test(self): + """ + Test the languages variable type + """ + + # GIVEN: The languages module + # WHEN: Accessing the languages variable + # THEN: It should be of type list + self.assertIsInstance(languages.languages, list, 'languages.languages should be of type list') + + def language_selection_languages_type_test(self): + """ + Test the selection of a language + """ + + # GIVEN: A list of languages from the languages module + # WHEN: Selecting the first item + language = languages.languages[0] + + # THEN: It should be an instance of the Language namedtuple + self.assertIsInstance(language, languages.Language) + self.assertEqual(language.id, 1) + self.assertEqual(language.name, '(Afan) Oromo') + self.assertEqual(language.code, 'om') + + def get_language_name_test(self): + """ + Test get_language() when supplied with a language name. + """ + + # GIVEN: A language name, in capitals + # WHEN: Calling get_language with it + language = languages.get_language('YORUBA') + + # THEN: The Language found using that name should be returned + self.assertIsInstance(language, languages.Language) + self.assertEqual(language.id, 137) + self.assertEqual(language.name, 'Yoruba') + self.assertEqual(language.code, 'yo') + + def get_language_code_test(self): + """ + Test get_language() when supplied with a language code. + """ + + # GIVEN: A language code in capitals + # WHEN: Calling get_language with it + language = languages.get_language('IA') + + # THEN: The Language found using that code should be returned + self.assertIsInstance(language, languages.Language) + self.assertEqual(language.id, 51) + self.assertEqual(language.name, 'Interlingua') + self.assertEqual(language.code, 'ia') + + def get_language_invalid_test(self): + """ + Test get_language() when supplied with a string which is not a valid language name or code. + """ + + # GIVEN: A language code + # WHEN: Calling get_language with it + language = languages.get_language('qwerty') + + # THEN: None should be returned + self.assertIsNone(language) + + def get_language_invalid__none_test(self): + """ + Test get_language() when supplied with a string which is not a valid language name or code. + """ + + # GIVEN: A language code + # WHEN: Calling get_language with it + language = languages.get_language(None) + + # THEN: None should be returned + self.assertIsNone(language) diff --git a/tests/functional/openlp_core_lib/test_theme.py b/tests/functional/openlp_core_lib/test_theme.py index c006abb78..db6b78d02 100644 --- a/tests/functional/openlp_core_lib/test_theme.py +++ b/tests/functional/openlp_core_lib/test_theme.py @@ -22,43 +22,82 @@ """ Package to test the openlp.core.lib.theme package. """ -from tests.functional import MagicMock, patch from unittest import TestCase +import os from openlp.core.lib.theme import ThemeXML -class TestTheme(TestCase): +class TestThemeXML(TestCase): """ - Test the functions in the Theme module + Test the ThemeXML class """ - def setUp(self): - """ - Create the UI - """ - pass - - def tearDown(self): - """ - Delete all the C++ objects at the end so that we don't have a segfault - """ - pass - def test_new_theme(self): """ - Test the theme creation - basic test + Test the ThemeXML constructor """ - # GIVEN: A new theme - - # WHEN: A theme is created + # GIVEN: The ThemeXML class + # WHEN: A theme object is created default_theme = ThemeXML() - # THEN: We should get some default behaviours - self.assertTrue(default_theme.background_border_color == '#000000', 'The theme should have a black border') - self.assertTrue(default_theme.background_type == 'solid', 'The theme should have a solid backgrounds') - self.assertTrue(default_theme.display_vertical_align == 0, - 'The theme should have a display_vertical_align of 0') - self.assertTrue(default_theme.font_footer_name == "Arial", - 'The theme should have a font_footer_name of Arial') - self.assertTrue(default_theme.font_main_bold is False, 'The theme should have a font_main_bold of false') - self.assertTrue(len(default_theme.__dict__) == 47, 'The theme should have 47 variables') + # THEN: The default values should be correct + self.assertEqual('#000000', default_theme.background_border_color, + 'background_border_color should be "#000000"') + self.assertEqual('solid', default_theme.background_type, 'background_type should be "solid"') + self.assertEqual(0, default_theme.display_vertical_align, 'display_vertical_align should be 0') + self.assertEqual('Arial', default_theme.font_footer_name, 'font_footer_name should be "Arial"') + self.assertFalse(default_theme.font_main_bold, 'font_main_bold should be False') + self.assertEqual(47, len(default_theme.__dict__), 'The theme should have 47 attributes') + + def test_expand_json(self): + """ + Test the expand_json method + """ + # GIVEN: A ThemeXML object and some JSON to "expand" + theme = ThemeXML() + theme_json = { + 'background': { + 'border_color': '#000000', + 'type': 'solid' + }, + 'display': { + 'vertical_align': 0 + }, + 'font': { + 'footer': { + 'bold': False + }, + 'main': { + 'name': 'Arial' + } + } + } + + # WHEN: ThemeXML.expand_json() is run + theme.expand_json(theme_json) + + # THEN: The attributes should be set on the object + self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"') + self.assertEqual('solid', theme.background_type, 'background_type should be "solid"') + self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0') + self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False') + self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"') + + def test_extend_image_filename(self): + """ + Test the extend_image_filename method + """ + # GIVEN: A theme object + theme = ThemeXML() + theme.theme_name = 'MyBeautifulTheme ' + theme.background_filename = ' video.mp4' + theme.background_type = 'video' + path = os.path.expanduser('~') + + # WHEN: ThemeXML.extend_image_filename is run + theme.extend_image_filename(path) + + # THEN: The filename of the background should be correct + expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4') + self.assertEqual(expected_filename, theme.background_filename) + self.assertEqual('MyBeautifulTheme', theme.theme_name) diff --git a/tests/functional/openlp_plugins/bibles/test_db.py b/tests/functional/openlp_plugins/bibles/test_db.py new file mode 100644 index 000000000..2807a8a3e --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_db.py @@ -0,0 +1,87 @@ +# -*- 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 # +############################################################################### +""" +This module contains tests for the db submodule of the Bibles plugin. +""" + +from unittest import TestCase + +from openlp.plugins.bibles.lib.db import BibleDB +from tests.functional import MagicMock, patch + + +class TestBibleDB(TestCase): + """ + Test the functions in the BibleDB class. + """ + + def test_get_language_canceled(self): + """ + Test the BibleDB.get_language method when the user rejects the dialog box + """ + # GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Rejected and an instance of BibleDB + with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\ + patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form: + + # The integer value of QtDialog.Rejected is 0. Using the enumeration causes a seg fault for some reason + mocked_language_form_instance = MagicMock(**{'exec.return_value': 0}) + mocked_language_form.return_value = mocked_language_form_instance + mocked_parent = MagicMock() + instance = BibleDB(mocked_parent) + mocked_wizard = MagicMock() + instance.wizard = mocked_wizard + + # WHEN: Calling get_language() + result = instance.get_language() + + # THEN: get_language() should return False + mocked_language_form.assert_called_once_with(mocked_wizard) + mocked_language_form_instance.exec.assert_called_once_with(None) + self.assertFalse(result, 'get_language() should return False if the user rejects the dialog box') + + def test_get_language_accepted(self): + """ + Test the BibleDB.get_language method when the user accepts the dialog box + """ + # GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Accepted an instance of BibleDB and + # a combobox with the selected item data as 10 + with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'), \ + patch('openlp.plugins.bibles.lib.db.BibleDB.save_meta'), \ + patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form: + + # The integer value of QtDialog.Accepted is 1. Using the enumeration causes a seg fault for some reason + mocked_language_form_instance = MagicMock(**{'exec.return_value': 1, + 'language_combo_box.itemData.return_value': 10}) + mocked_language_form.return_value = mocked_language_form_instance + mocked_parent = MagicMock() + instance = BibleDB(mocked_parent) + mocked_wizard = MagicMock() + instance.wizard = mocked_wizard + + # WHEN: Calling get_language() + result = instance.get_language('Bible Name') + + # THEN: get_language() should return the id of the selected language in the combo box + mocked_language_form.assert_called_once_with(mocked_wizard) + mocked_language_form_instance.exec.assert_called_once_with('Bible Name') + self.assertEqual(result, 10, 'get_language() should return the id of the language the user has chosen when ' + 'they accept the dialog box') diff --git a/tests/functional/openlp_plugins/bibles/test_mediaitem.py b/tests/functional/openlp_plugins/bibles/test_mediaitem.py index f820c7a64..05418f177 100644 --- a/tests/functional/openlp_plugins/bibles/test_mediaitem.py +++ b/tests/functional/openlp_plugins/bibles/test_mediaitem.py @@ -23,6 +23,7 @@ This module contains tests for the lib submodule of the Presentations plugin. """ from unittest import TestCase +from openlp.core.common import Registry from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -41,6 +42,9 @@ class TestMediaItem(TestCase, TestMixin): patch('openlp.plugins.bibles.lib.mediaitem.BibleMediaItem.setup_item'): self.media_item = BibleMediaItem(None, MagicMock()) self.setup_application() + self.mocked_main_window = MagicMock() + Registry.create() + Registry().register('main_window', self.mocked_main_window) def test_display_results_no_results(self): """ @@ -109,3 +113,40 @@ class TestMediaItem(TestCase, TestMixin): mocked_list_view.selectAll.assert_called_once_with() self.assertEqual(self.media_item.search_results, {}) self.assertEqual(self.media_item.second_search_results, {}) + + def on_quick_search_button_general_test(self): + """ + Test that general things, which should be called on all Quick searches are called. + """ + + # GIVEN: self.application as self.app, all the required functions + Registry.create() + Registry().register('application', self.app) + self.media_item.quickSearchButton = MagicMock() + self.app.process_events = MagicMock() + self.media_item.quickVersionComboBox = MagicMock() + self.media_item.quickVersionComboBox.currentText = MagicMock() + self.media_item.quickSecondComboBox = MagicMock() + self.media_item.quickSecondComboBox.currentText = MagicMock() + self.media_item.quick_search_edit = MagicMock() + self.media_item.quick_search_edit.text = MagicMock() + self.media_item.quickLockButton = MagicMock() + self.media_item.list_view = MagicMock() + self.media_item.search_results = MagicMock() + self.media_item.display_results = MagicMock() + self.media_item.check_search_result = MagicMock() + self.app.set_normal_cursor = MagicMock() + + # WHEN: on_quick_search_button is called + self.media_item.on_quick_search_button() + + # THEN: Search should had been started and finalized properly + self.assertEqual(1, self.app.process_events.call_count, 'Normal cursor should had been called once') + self.assertEqual(1, self.media_item.quickVersionComboBox.currentText.call_count, 'Should had been called once') + self.assertEqual(1, self.media_item.quickSecondComboBox.currentText.call_count, 'Should had been called once') + self.assertEqual(1, self.media_item.quick_search_edit.text.call_count, 'Text edit Should had been called once') + self.assertEqual(1, self.media_item.quickLockButton.isChecked.call_count, 'Lock Should had been called once') + self.assertEqual(1, self.media_item.display_results.call_count, 'Display results Should had been called once') + self.assertEqual(2, self.media_item.quickSearchButton.setEnabled.call_count, 'Disable and Enable the button') + self.assertEqual(1, self.media_item.check_search_result.call_count, 'Check results Should had been called once') + self.assertEqual(1, self.app.set_normal_cursor.call_count, 'Normal cursor should had been called once') diff --git a/tests/functional/openlp_plugins/bibles/test_swordimport.py b/tests/functional/openlp_plugins/bibles/test_swordimport.py index ca3c03691..25b6ab355 100644 --- a/tests/functional/openlp_plugins/bibles/test_swordimport.py +++ b/tests/functional/openlp_plugins/bibles/test_swordimport.py @@ -70,8 +70,8 @@ class TestSwordImport(TestCase): @patch('openlp.plugins.bibles.lib.sword.SwordBible.application') @patch('openlp.plugins.bibles.lib.sword.modules') - @patch('openlp.plugins.bibles.lib.db.BiblesResourcesDB') - def test_simple_import(self, mocked_bible_res_db, mocked_pysword_modules, mocked_application): + @patch('openlp.core.common.languages') + def test_simple_import(self, mocked_languages, mocked_pysword_modules, mocked_application): """ Test that a simple SWORD import works """ @@ -88,7 +88,7 @@ class TestSwordImport(TestCase): importer.create_verse = MagicMock() importer.create_book = MagicMock() importer.session = MagicMock() - mocked_bible_res_db.get_language.return_value = 'Danish' + mocked_languages.get_language.return_value = 'Danish' mocked_bible = MagicMock() mocked_genesis = MagicMock() mocked_genesis.name = 'Genesis' diff --git a/tests/functional/openlp_plugins/songs/test_videopsalm.py b/tests/functional/openlp_plugins/songs/test_videopsalm.py index 1bf13241d..ff1a81db5 100644 --- a/tests/functional/openlp_plugins/songs/test_videopsalm.py +++ b/tests/functional/openlp_plugins/songs/test_videopsalm.py @@ -43,3 +43,5 @@ class TestVideoPsalmFileImport(SongImportTestHelper): """ self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold.json'), self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold.json'))) + self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold2.json'), + self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold2.json'))) diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py index 4a7fb4af3..de4d4bba8 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py @@ -50,7 +50,8 @@ class TestBibleHTTP(TestCase): books = handler.get_books_from_http('NIV') # THEN: We should get back a valid service item - assert len(books) == 66, 'The bible should not have had any books added or removed' + self.assertEqual(len(books), 66, 'The bible should not have had any books added or removed') + self.assertEqual(books[0], 'Genesis', 'The first bible book should be Genesis') def test_bible_gateway_extract_books_support_redirect(self): """ @@ -63,7 +64,7 @@ class TestBibleHTTP(TestCase): books = handler.get_books_from_http('DN1933') # THEN: We should get back a valid service item - assert len(books) == 66, 'This bible should have 66 books' + self.assertEqual(len(books), 66, 'This bible should have 66 books') def test_bible_gateway_extract_verse(self): """ @@ -76,7 +77,8 @@ class TestBibleHTTP(TestCase): results = handler.get_bible_chapter('NIV', 'John', 3) # THEN: We should get back a valid service item - assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed' + self.assertEqual(len(results.verse_list), 36, + 'The book of John should not have had any verses added or removed') def test_bible_gateway_extract_verse_nkjv(self): """ @@ -89,7 +91,8 @@ class TestBibleHTTP(TestCase): results = handler.get_bible_chapter('NKJV', 'John', 3) # THEN: We should get back a valid service item - assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed' + self.assertEqual(len(results.verse_list), 36, + 'The book of John should not have had any verses added or removed') def test_crosswalk_extract_books(self): """ @@ -102,7 +105,7 @@ class TestBibleHTTP(TestCase): books = handler.get_books_from_http('niv') # THEN: We should get back a valid service item - assert len(books) == 66, 'The bible should not have had any books added or removed' + self.assertEqual(len(books), 66, 'The bible should not have had any books added or removed') def test_crosswalk_extract_verse(self): """ @@ -115,7 +118,8 @@ class TestBibleHTTP(TestCase): results = handler.get_bible_chapter('niv', 'john', 3) # THEN: We should get back a valid service item - assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed' + self.assertEqual(len(results.verse_list), 36, + 'The book of John should not have had any verses added or removed') def test_bibleserver_get_bibles(self): """ @@ -144,9 +148,8 @@ class TestBibleHTTP(TestCase): # THEN: The list should not be None, and some known bibles should be there self.assertIsNotNone(bibles) - self.assertIn(('Holman Christian Standard Bible', 'HCSB', 'en'), bibles) + self.assertIn(('Holman Christian Standard Bible (HCSB)', 'HCSB', 'en'), bibles) - @skip("Waiting for Crosswalk to fix their server") def test_crosswalk_get_bibles(self): """ Test getting list of bibles from Crosswalk.com diff --git a/tests/resources/videopsalmsongs/as-safe-a-stronghold2.json b/tests/resources/videopsalmsongs/as-safe-a-stronghold2.json new file mode 100644 index 000000000..f6becc92a --- /dev/null +++ b/tests/resources/videopsalmsongs/as-safe-a-stronghold2.json @@ -0,0 +1,35 @@ +{ + "authors": [ + ["Martin Luther", "words"], + ["Unknown", "music"] + ], + "ccli_number": "12345", + "comments": "This is\nthe first comment\nThis is\nthe second comment\nThis is\nthe third comment\n", + "copyright": "Public Domain", + "song_book_name": "SongBook1", + "song_number": 0, + "title": "A Safe Stronghold Our God is Still", + "topics": [ + "tema1", + "tema2" + ], + "verse_order_list": [], + "verses": [ + [ + "As safe a stronghold our God is still,\nA trusty shield and weapon;\nHe’ll help us clear from all the ill\nThat hath us now o’ertaken.\nThe ancient prince of hell\nHath risen with purpose fell;\nStrong mail of craft and power\nHe weareth in this hour;\nOn earth is not His fellow.", + "v" + ], + [ + "With \"force\" of arms we nothing can,\nFull soon were we down-ridden;\nBut for us fights \\ the proper Man,\nWhom God Himself hath bidden.\nAsk ye: Who is this same?\nChrist Jesus is His name,\nThe Lord Sabaoth’s Son;\nHe, and no other one,\nShall conquer in the battle.", + "v" + ], + [ + "And were this world all devils o’er,\nAnd watching to devour us,\nWe lay it not to heart so sore;\nNot they can overpower us.\nAnd let the prince of ill\nLook grim as e’er he will,\nHe harms us not a whit;\nFor why? his doom is writ;\nA word shall quickly slay him.", + "v" + ], + [ + "God’s word, for all their craft and force,\nOne moment will not linger,\nBut, spite of hell, shall have its course;\n’Tis written by His finger.\nAnd though they take our life,\nGoods, honour, children, wife,\nYet is their profit small:\nThese things shall vanish all;\nThe city of God remaineth.", + "v" + ] + ] +} diff --git a/tests/resources/videopsalmsongs/videopsalm-as-safe-a-stronghold2.json b/tests/resources/videopsalmsongs/videopsalm-as-safe-a-stronghold2.json new file mode 100644 index 000000000..11bc082e6 --- /dev/null +++ b/tests/resources/videopsalmsongs/videopsalm-as-safe-a-stronghold2.json @@ -0,0 +1,47 @@ +{Abbreviation:"SB1",Copyright:"Public domain",Songs:[{ID:3,Composer:"Unknown",Author:"Martin Luther",Copyright:"Public +Domain",Theme:"tema1 +tema2",CCLI:"12345",Alias:"A safe stronghold",Memo1:"This is +the first comment +",Memo2:"This is +the second comment +",Memo3:"This is +the third comment +",Reference:"reference",Guid:"jtCkrJdPIUOmECjaQylg/g",Verses:[{ +Text:"As safe a stronghold our God is still, +A trusty shield and weapon; +He’ll help us clear from all the ill +That hath us now o’ertaken. +The ancient prince of hell +Hath risen with purpose fell; +Strong mail of craft and power +He weareth in this hour; +On earth is not His fellow."},{ID:2, +Text:"With \"force\" of arms we nothing can, +Full soon were we down-ridden; +But for us fights \\ the proper Man, +Whom God Himself hath bidden. +Ask ye: Who is this same? +Christ Jesus is His name, +The Lord Sabaoth’s Son; +He, and no other one, +Shall conquer in the battle."},{ID:3, +Text:"And were this world all devils o’er, +And watching to devour us, +We lay it not to heart so sore; +Not they can overpower us. +And let the prince of ill +Look grim as e’er he will, +He harms us not a whit; +For why? his doom is writ; +A word shall quickly slay him."},{ID:4, +Text:"God’s word, for all their craft and force, +One moment will not linger, +But, spite of hell, shall have its course; +’Tis written by His finger. +And though they take our life, +Goods, honour, children, wife, +Yet is their profit small: +These things shall vanish all; +The city of God remaineth."}],AudioFile:"282.mp3",IsAudioFileEnabled:1, +Text:"A Safe Stronghold Our God is Still"}],Guid:"khiHU2blX0Kb41dGdbDLhA",VersionDate:"20121012000000", +Text:"SongBook1"} diff --git a/tests/utils/test_pylint.py b/tests/utils/test_pylint.py index dc6c83909..48c9e1393 100644 --- a/tests/utils/test_pylint.py +++ b/tests/utils/test_pylint.py @@ -23,8 +23,8 @@ Package to test for proper bzr tags. """ import os -import logging import platform +import sys from unittest import TestCase, SkipTest try: @@ -46,9 +46,18 @@ class TestPylint(TestCase): """ Test for pylint errors """ + # Test if this file is specified in the arguments, if not skip the test. + in_argv = False + for arg in sys.argv: + if arg.endswith('test_pylint.py') or arg.endswith('test_pylint'): + in_argv = True + break + if not in_argv: + raise SkipTest('test_pylint.py not specified in arguments - skipping tests using pylint.') + # GIVEN: Some checks to disable and enable, and the pylint script disabled_checks = 'import-error,no-member' - enabled_checks = 'missing-format-argument-key,unused-format-string-argument' + enabled_checks = 'missing-format-argument-key,unused-format-string-argument,bad-format-string' if is_win() or 'arch' in platform.dist()[0].lower(): pylint_script = 'pylint' else: @@ -84,6 +93,9 @@ class TestPylint(TestCase): # Filter out PyQt related errors elif ('no-name-in-module' in line or 'no-member' in line) and 'PyQt5' in line: continue + # Filter out distutils related errors + elif 'distutils' in line: + continue elif self._is_line_tolerated(line): continue else: