Merge lp:~phill-ridout/openlp/saved_bible_verses into lp:openlp

Proposed by Phill
Status: Superseded
Proposed branch: lp:~phill-ridout/openlp/saved_bible_verses
Merge into: lp:openlp
Diff against target: 1020 lines (+318/-196)
9 files modified
openlp/core/ui/lib/listwidgetwithdnd.py (+10/-4)
openlp/plugins/bibles/lib/__init__.py (+3/-3)
openlp/plugins/bibles/lib/db.py (+13/-0)
openlp/plugins/bibles/lib/manager.py (+15/-43)
openlp/plugins/bibles/lib/mediaitem.py (+178/-63)
resources/images/openlp-2.qrc (+2/-4)
tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py (+33/-31)
tests/functional/openlp_plugins/bibles/test_mediaitem.py (+63/-47)
tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py (+1/-1)
To merge this branch: bzr merge lp:~phill-ridout/openlp/saved_bible_verses
Reviewer Review Type Date Requested Status
Tim Bentley Needs Fixing
Review via email: mp+321949@code.launchpad.net

This proposal supersedes a proposal from 2017-04-03.

This proposal has been superseded by a proposal from 2017-05-07.

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

Please can you remove the resource file as this makes it difficult to see the changes.
The resource file can be merged in the next request,

review: Needs Fixing
Revision history for this message
Tim Bentley (trb143) wrote :

Traceback (most recent call last):
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 789, in on_search_timer_timeout
    self.text_search()
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 765, in text_search
    self.display_results()
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 798, in display_results
    self.current_results = self.build_display_results(self.bible, self.second_bible, self.search_results)
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 814, in build_display_results
    version = self.plugin.manager.get_meta_data(self.bible.name, 'name').value
AttributeError: 'NoneType' object has no attribute 'name'

downloaded and started to no bibles loaded !!!!!!!

Loaded a new bible and started to type. Search as type fired and downloaded the chapter and gave me a message saying search as you type is not available!

Then a traceback
Traceback (most recent call last):
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 789, in on_search_timer_timeout
    self.text_search()
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 768, in text_search
    self.on_text_search(text)
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 732, in on_text_search
    self.display_results()
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 798, in display_results
    self.current_results = self.build_display_results(self.bible, self.second_bible, self.search_results)
  File "/home/tim/Projects/OpenLP/openlp/saved_bible_verses/openlp/plugins/bibles/lib/mediaitem.py", line 828, in build_display_results
    for count, verse in enumerate(search_results):
TypeError: 'NoneType' object is not iterable

On the select UI I only have 2 lines of search as the options is large and takes up most the the space.

review: Needs Fixing
2736. By Phill

fixes

2737. By Phill

head

2738. By Phill

merge from test_fixes

2739. By Phill

Fixed up test

2740. By Phill

PEP fixes

2741. By Phill

mor PEP fixes

2742. By Phill

HEAD

2743. By Phill

A few minor changes + annother test

2744. By Phill

Annother fix

2745. By Phill

head

2746. By Phill

PEP

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/ui/lib/listwidgetwithdnd.py' (properties changed: -x to +x)
2--- openlp/core/ui/lib/listwidgetwithdnd.py 2017-02-18 07:23:15 +0000
3+++ openlp/core/ui/lib/listwidgetwithdnd.py 2017-05-07 10:12:27 +0000
4@@ -44,7 +44,6 @@
5 self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
6 self.setAlternatingRowColors(True)
7 self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
8- self.locked = False
9
10 def activateDnD(self):
11 """
12@@ -54,15 +53,13 @@
13 self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
14 Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
15
16- def clear(self, search_while_typing=False, override_lock=False):
17+ def clear(self, search_while_typing=False):
18 """
19 Re-implement clear, so that we can customise feedback when using 'Search as you type'
20
21 :param search_while_typing: True if we want to display the customised message
22 :return: None
23 """
24- if self.locked and not override_lock:
25- return
26 if search_while_typing:
27 self.no_results_text = UiStrings().ShortResults
28 else:
29@@ -128,6 +125,15 @@
30 else:
31 event.ignore()
32
33+ def allItems(self):
34+ """
35+ An generator to list all the items in the widget
36+
37+ :return: a generator
38+ """
39+ for row in range(self.count()):
40+ yield self.item(row)
41+
42 def paintEvent(self, event):
43 """
44 Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty.
45
46=== modified file 'openlp/plugins/bibles/lib/__init__.py'
47--- openlp/plugins/bibles/lib/__init__.py 2017-02-18 07:23:15 +0000
48+++ openlp/plugins/bibles/lib/__init__.py 2017-05-07 10:12:27 +0000
49@@ -341,10 +341,10 @@
50 if not book_ref_id:
51 book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection)
52 elif not bible.get_book_by_book_ref_id(book_ref_id):
53- return False
54+ return []
55 # We have not found the book so do not continue
56 if not book_ref_id:
57- return False
58+ return []
59 ranges = match.group('ranges')
60 range_list = get_reference_match('range_separator').split(ranges)
61 ref_list = []
62@@ -403,7 +403,7 @@
63 return ref_list
64 else:
65 log.debug('Invalid reference: {text}'.format(text=reference))
66- return None
67+ return []
68
69
70 class SearchResults(object):
71
72=== modified file 'openlp/plugins/bibles/lib/db.py'
73--- openlp/plugins/bibles/lib/db.py 2017-02-18 07:23:15 +0000
74+++ openlp/plugins/bibles/lib/db.py 2017-05-07 10:12:27 +0000
75@@ -158,6 +158,7 @@
76 self.get_name()
77 if 'path' in kwargs:
78 self.path = kwargs['path']
79+ self._is_web_bible = None
80
81 def get_name(self):
82 """
83@@ -426,6 +427,18 @@
84 return 0
85 return count
86
87+ @property
88+ def is_web_bible(self):
89+ """
90+ A read only property indicating if the bible is a 'web bible'
91+
92+ :return: If the bible is a web bible.
93+ :rtype: bool
94+ """
95+ if self._is_web_bible is None:
96+ self._is_web_bible = bool(self.get_object(BibleMeta, 'download_source'))
97+ return self._is_web_bible
98+
99 def dump_bible(self):
100 """
101 Utility debugging method to dump the contents of a bible.
102
103=== modified file 'openlp/plugins/bibles/lib/manager.py'
104--- openlp/plugins/bibles/lib/manager.py 2017-02-18 07:23:15 +0000
105+++ openlp/plugins/bibles/lib/manager.py 2017-05-07 10:12:27 +0000
106@@ -142,8 +142,8 @@
107 log.debug('Bible Name: "{name}"'.format(name=name))
108 self.db_cache[name] = bible
109 # Look to see if lazy load bible exists and get create getter.
110- source = self.db_cache[name].get_object(BibleMeta, 'download_source')
111- if source:
112+ if self.db_cache[name].is_web_bible:
113+ source = self.db_cache[name].get_object(BibleMeta, 'download_source')
114 download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value
115 meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server')
116 web_bible = HTTPBible(self.parent, path=self.path, file=filename, download_source=source.value,
117@@ -278,7 +278,7 @@
118 :param show_error:
119 """
120 if not bible or not ref_list:
121- return None
122+ return []
123 return self.db_cache[bible].get_verses(ref_list, show_error)
124
125 def get_language_selection(self, bible):
126@@ -305,11 +305,17 @@
127 """
128 Does a verse search for the given bible and text.
129
130- :param bible: The bible to search in (unicode).
131- :param second_bible: The second bible (unicode). We do not search in this bible.
132- :param text: The text to search for (unicode).
133+ :param bible: The bible to search
134+ :type bible: str
135+ :param text: The text to search for
136+ :type text: str
137+
138+ :return: The search results if valid, or None if the search is invalid.
139+ :rtype: None, list
140 """
141 log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text))
142+ if not text:
143+ return None
144 # If no bibles are installed, message is given.
145 if not bible:
146 self.main_window.information_message(
147@@ -317,8 +323,7 @@
148 UiStrings().BibleNoBibles)
149 return None
150 # Check if the bible or second_bible is a web bible.
151- web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source')
152- if web_bible:
153+ if self.db_cache[bible].is_web_bible:
154 # If either Bible is Web, cursor is reset to normal and message is given.
155 self.application.set_normal_cursor()
156 self.main_window.information_message(
157@@ -328,41 +333,8 @@
158 'This means that the currently selected Bible is a Web Bible.')
159 )
160 return None
161- # Shorter than 3 char searches break OpenLP with very long search times, thus they are blocked.
162- if len(text) - text.count(' ') < 3:
163- return None
164- # Fetch the results from db. If no results are found, return None, no message is given for this.
165- elif text:
166- return self.db_cache[bible].verse_search(text)
167- else:
168- return None
169-
170- def verse_search_while_typing(self, bible, second_bible, text):
171- """
172- Does a verse search for the given bible and text.
173- This is used during "Search while typing"
174- It's the same thing as the normal text search, but it does not show the web Bible error.
175- (It would result in the error popping every time a char is entered or removed)
176- It also does not have a minimum text len, this is set in mediaitem.py
177-
178- :param bible: The bible to search in (unicode).
179- :param second_bible: The second bible (unicode). We do not search in this bible.
180- :param text: The text to search for (unicode).
181- """
182- # If no bibles are installed, message is given.
183- if not bible:
184- return None
185- # Check if the bible or second_bible is a web bible.
186- web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source')
187- second_web_bible = ''
188- if second_bible:
189- second_web_bible = self.db_cache[second_bible].get_object(BibleMeta, 'download_source')
190- if web_bible or second_web_bible:
191- # If either Bible is Web, cursor is reset to normal and search ends w/o any message.
192- self.application.set_normal_cursor()
193- return None
194- # Fetch the results from db. If no results are found, return None, no message is given for this.
195- elif text:
196+ # Fetch the results from db. If no results are found, return None, no message is given for this.
197+ if text:
198 return self.db_cache[bible].verse_search(text)
199 else:
200 return None
201
202=== modified file 'openlp/plugins/bibles/lib/mediaitem.py' (properties changed: -x to +x)
203--- openlp/plugins/bibles/lib/mediaitem.py 2017-02-18 20:22:47 +0000
204+++ openlp/plugins/bibles/lib/mediaitem.py 2017-05-07 10:12:27 +0000
205@@ -22,6 +22,7 @@
206
207 import logging
208 import re
209+from enum import Enum, unique
210
211 from PyQt5 import QtCore, QtWidgets
212
213@@ -48,15 +49,45 @@
214 'list': get_reference_separator('sep_l_display')}
215
216
217-class BibleSearch(object):
218+@unique
219+class BibleSearch(Enum):
220 """
221- Enumeration class for the different search methods for the "Search" tab.
222+ Enumeration class for the different search types for the "Search" tab.
223 """
224 Reference = 1
225 Text = 2
226 Combined = 3
227
228
229+@unique
230+class ResultsTab(Enum):
231+ """
232+ Enumeration class for the different tabs for the results list.
233+ """
234+ Saved = 0
235+ Search = 1
236+
237+
238+@unique
239+class SearchStatus(Enum):
240+ """
241+ Enumeration class for the different search methods.
242+ """
243+ SearchButton = 0
244+ SearchAsYouType = 1
245+ NotEnoughText = 2
246+
247+
248+@unique
249+class SearchTabs(Enum):
250+ """
251+ Enumeration class for the tabs on the media item.
252+ """
253+ Search = 0
254+ Select = 1
255+ Options = 2
256+
257+
258 class BibleMediaItem(MediaManagerItem):
259 """
260 This is the custom media manager item for Bibles.
261@@ -73,11 +104,13 @@
262 :param kwargs: Keyword arguments to pass to the super method. (dict)
263 """
264 self.clear_icon = build_icon(':/bibles/bibles_search_clear.png')
265- self.lock_icon = build_icon(':/bibles/bibles_search_lock.png')
266- self.unlock_icon = build_icon(':/bibles/bibles_search_unlock.png')
267+ self.save_results_icon = build_icon(':/bibles/bibles_save_results.png')
268 self.sort_icon = build_icon(':/bibles/bibles_book_sort.png')
269 self.bible = None
270 self.second_bible = None
271+ self.saved_results = []
272+ self.current_results = []
273+ self.search_status = SearchStatus().SearchButton
274 # TODO: Make more central and clean up after!
275 self.search_timer = QtCore.QTimer()
276 self.search_timer.setInterval(200)
277@@ -162,8 +195,10 @@
278 self.select_tab.setVisible(False)
279 self.page_layout.addWidget(self.select_tab)
280 # General Search Opions
281- self.options_widget = QtWidgets.QGroupBox(translate('BiblesPlugin.MediaItem', 'Options'), self)
282- self.general_bible_layout = QtWidgets.QFormLayout(self.options_widget)
283+ self.options_tab = QtWidgets.QWidget()
284+ self.options_tab.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
285+ self.search_tab_bar.addTab(translate('BiblesPlugin.MediaItem', 'Options'))
286+ self.general_bible_layout = QtWidgets.QFormLayout(self.options_tab)
287 self.version_combo_box = create_horizontal_adjusting_combo_box(self, 'version_combo_box')
288 self.general_bible_layout.addRow('{version}:'.format(version=UiStrings().Version), self.version_combo_box)
289 self.second_combo_box = create_horizontal_adjusting_combo_box(self, 'second_combo_box')
290@@ -171,20 +206,28 @@
291 self.style_combo_box = create_horizontal_adjusting_combo_box(self, 'style_combo_box')
292 self.style_combo_box.addItems(['', '', ''])
293 self.general_bible_layout.addRow(UiStrings().LayoutStyle, self.style_combo_box)
294- self.search_button_layout = QtWidgets.QHBoxLayout()
295+ self.options_tab.setVisible(False)
296+ self.page_layout.addWidget(self.options_tab)
297+ # This widget is the easier way to reset the spacing of search_button_layout. (Because page_layout has had its
298+ # spacing set to 0)
299+ self.search_button_widget = QtWidgets.QWidget()
300+ self.search_button_layout = QtWidgets.QHBoxLayout(self.search_button_widget)
301 self.search_button_layout.addStretch()
302 # Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon.
303- self.clear_button = QtWidgets.QToolButton(self)
304+ self.clear_button = QtWidgets.QPushButton()
305 self.clear_button.setIcon(self.clear_icon)
306- self.lock_button = QtWidgets.QToolButton(self)
307- self.lock_button.setIcon(self.unlock_icon)
308- self.lock_button.setCheckable(True)
309+ self.save_results_button = QtWidgets.QPushButton()
310+ self.save_results_button.setIcon(self.save_results_icon)
311 self.search_button_layout.addWidget(self.clear_button)
312- self.search_button_layout.addWidget(self.lock_button)
313+ self.search_button_layout.addWidget(self.save_results_button)
314 self.search_button = QtWidgets.QPushButton(self)
315 self.search_button_layout.addWidget(self.search_button)
316- self.general_bible_layout.addRow(self.search_button_layout)
317- self.page_layout.addWidget(self.options_widget)
318+ self.page_layout.addWidget(self.search_button_widget)
319+ self.results_view_tab = QtWidgets.QTabBar(self)
320+ self.results_view_tab.addTab('')
321+ self.results_view_tab.addTab('')
322+ self.results_view_tab.setCurrentIndex(ResultsTab.Search)
323+ self.page_layout.addWidget(self.results_view_tab)
324
325 def setupUi(self):
326 super().setupUi()
327@@ -211,12 +254,15 @@
328 # Buttons
329 self.book_order_button.toggled.connect(self.on_book_order_button_toggled)
330 self.clear_button.clicked.connect(self.on_clear_button_clicked)
331- self.lock_button.toggled.connect(self.on_lock_button_toggled)
332+ self.save_results_button.clicked.connect(self.on_save_results_button_clicked)
333 self.search_button.clicked.connect(self.on_search_button_clicked)
334 # Other stuff
335 self.search_edit.returnPressed.connect(self.on_search_button_clicked)
336 self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed)
337+ self.results_view_tab.currentChanged.connect(self.on_results_view_tab_current_changed)
338 self.search_edit.textChanged.connect(self.on_search_edit_text_changed)
339+ self.on_results_view_tab_total_update(ResultsTab.Saved)
340+ self.on_results_view_tab_total_update(ResultsTab.Search)
341
342 def retranslateUi(self):
343 log.debug('retranslateUi')
344@@ -225,9 +271,9 @@
345 self.style_combo_box.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide)
346 self.style_combo_box.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine)
347 self.style_combo_box.setItemText(LayoutStyle.Continuous, UiStrings().Continuous)
348- self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.'))
349- self.lock_button.setToolTip(
350- translate('BiblesPlugin.MediaItem', 'Toggle to keep or clear the previous results.'))
351+ self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the results on the current tab.'))
352+ self.save_results_button.setToolTip(
353+ translate('BiblesPlugin.MediaItem', 'Add the search results to the saved list.'))
354 self.search_button.setText(UiStrings().Search)
355
356 def on_focus(self):
357@@ -241,8 +287,10 @@
358 if self.search_tab.isVisible():
359 self.search_edit.setFocus()
360 self.search_edit.selectAll()
361- else:
362+ if self.select_tab.isVisible():
363 self.select_book_combo_box.setFocus()
364+ if self.options_tab.isVisible():
365+ self.version_combo_box.setFocus()
366
367 def config_update(self):
368 """
369@@ -415,14 +463,48 @@
370 """
371 Show the selected tab and set focus to it
372
373- :param index: The tab selected (int)
374+ :param index: The tab selected
375+ :type index: int
376 :return: None
377 """
378- search_tab = index == 0
379- self.search_tab.setVisible(search_tab)
380- self.select_tab.setVisible(not search_tab)
381+ if index == SearchTabs.Search or index == SearchTabs.Select:
382+ self.search_button.setEnabled(True)
383+ else:
384+ self.search_button.setEnabled(False)
385+ self.search_tab.setVisible(index == SearchTabs.Search)
386+ self.select_tab.setVisible(index == SearchTabs.Select)
387+ self.options_tab.setVisible(index == SearchTabs.Options)
388 self.on_focus()
389
390+ def on_results_view_tab_current_changed(self, index):
391+ """
392+ Update list_widget with the contents of the selected list
393+
394+ :param index: The index of the tab that has been changed to. (int)
395+ :return: None
396+ """
397+ if index == ResultsTab.Saved:
398+ self.add_built_results_to_list_widget(self.saved_results)
399+ elif index == ResultsTab.Search:
400+ self.add_built_results_to_list_widget(self.current_results)
401+
402+ def on_results_view_tab_total_update(self, index):
403+ """
404+ Update the result total count on the tab with the given index.
405+
406+ :param index: Index of the tab to update (int)
407+ :return: None
408+ """
409+ string = ''
410+ count = 0
411+ if index == ResultsTab.Saved:
412+ string = translate('BiblesPlugin.MediaItem', 'Saved ({result_count})')
413+ count = len(self.saved_results)
414+ elif index == ResultsTab.Search:
415+ string = translate('BiblesPlugin.MediaItem', 'Results ({result_count})')
416+ count = len(self.current_results)
417+ self.results_view_tab.setTabText(index, string.format(result_count=count))
418+
419 def on_book_order_button_toggled(self, checked):
420 """
421 Change the sort order of the book names
422@@ -442,22 +524,25 @@
423
424 :return: None
425 """
426- self.list_view.clear()
427- self.search_edit.clear()
428- self.on_focus()
429+ current_index = self.results_view_tab.currentIndex()
430+ for item in self.list_view.selectedItems():
431+ self.list_view.takeItem(self.list_view.row(item))
432+ results = [item.data(QtCore.Qt.UserRole) for item in self.list_view.allItems()]
433+ if current_index == ResultsTab.Saved:
434+ self.saved_results = results
435+ elif current_index == ResultsTab.Search:
436+ self.current_results = results
437+ self.on_results_view_tab_total_update(current_index)
438
439- def on_lock_button_toggled(self, checked):
440+ def on_save_results_button_clicked(self):
441 """
442- Toggle the lock button, if Search tab is used, set focus to search field.
443+ Add the selected verses to the saved_results list.
444
445- :param checked: The state of the toggle button. (bool)
446 :return: None
447 """
448- self.list_view.locked = checked
449- if checked:
450- self.sender().setIcon(self.lock_icon)
451- else:
452- self.sender().setIcon(self.unlock_icon)
453+ for verse in self.list_view.selectedItems():
454+ self.saved_results.append(verse.data(QtCore.Qt.UserRole))
455+ self.on_results_view_tab_total_update(ResultsTab.Saved)
456
457 def on_style_combo_box_index_changed(self, index):
458 """
459@@ -490,16 +575,17 @@
460 :return: None
461 """
462 new_selection = self.second_combo_box.currentData()
463- if self.list_view.count():
464+ if self.saved_results:
465 # Exclusive or (^) the new and previous selections to detect if the user has switched between single and
466 # dual bible mode
467 if (new_selection is None) ^ (self.second_bible is None):
468 if critical_error_message_box(
469 message=translate('BiblesPlugin.MediaItem',
470 'OpenLP cannot combine single and dual Bible verse search results. '
471- 'Do you want to clear your search results and start a new search?'),
472+ 'Do you want to clear your saved results?'),
473 parent=self, question=True) == QtWidgets.QMessageBox.Yes:
474- self.list_view.clear(override_lock=True)
475+ self.saved_results = []
476+ self.on_results_view_tab_total_update(ResultsTab.Saved)
477 else:
478 self.second_combo_box.setCurrentIndex(self.second_combo_box.findData(self.second_bible))
479 return
480@@ -525,7 +611,8 @@
481 log.warning('Not enough chapters in %s', book_ref_id)
482 critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.'))
483 else:
484- self.search_button.setEnabled(True)
485+ if self.select_tab.isVisible():
486+ self.search_button.setEnabled(True)
487 self.adjust_combo_box(1, self.chapter_count, self.from_chapter)
488 self.adjust_combo_box(1, self.chapter_count, self.to_chapter)
489 self.adjust_combo_box(1, verse_count, self.from_verse)
490@@ -602,6 +689,8 @@
491
492 :return: None
493 """
494+ self.search_timer.stop()
495+ self.search_status = SearchStatus().SearchButton
496 if not self.bible:
497 self.main_window.information_message(UiStrings().BibleNoBiblesTitle, UiStrings().BibleNoBibles)
498 return
499@@ -613,6 +702,7 @@
500 elif self.select_tab.isVisible():
501 self.select_search()
502 self.search_button.setEnabled(True)
503+ self.results_view_tab.setCurrentIndex(ResultsTab.Search)
504 self.application.set_normal_cursor()
505
506 def select_search(self):
507@@ -636,18 +726,21 @@
508
509 :return: None
510 """
511+ self.search_results = []
512 verse_refs = self.plugin.manager.parse_ref(self.bible.name, search_text)
513 self.search_results = self.plugin.manager.get_verses(self.bible.name, verse_refs, True)
514 if self.second_bible and self.search_results:
515 self.search_results = self.plugin.manager.get_verses(self.second_bible.name, verse_refs, True)
516 self.display_results()
517
518- def on_text_search(self, text, search_while_type=False):
519+ def on_text_search(self, text):
520 """
521 We are doing a 'Text Search'.
522 This search is called on def text_search by 'Search' Text and Combined Searches.
523 """
524 self.search_results = self.plugin.manager.verse_search(self.bible.name, text)
525+ if self.search_results is None:
526+ return
527 if self.second_bible and self.search_results:
528 filtered_search_results = []
529 not_found_count = 0
530@@ -663,7 +756,7 @@
531 verse=verse.verse, bible_name=self.second_bible.name))
532 not_found_count += 1
533 self.search_results = filtered_search_results
534- if not_found_count != 0 and not search_while_type:
535+ if not_found_count != 0 and self.search_status == SearchStatus.SearchButton:
536 self.main_window.information_message(
537 translate('BiblesPlugin.MediaItem', 'Verses not found'),
538 translate('BiblesPlugin.MediaItem',
539@@ -673,22 +766,23 @@
540 ).format(second_name=self.second_bible.name, name=self.bible.name, count=not_found_count))
541 self.display_results()
542
543- def text_search(self, search_while_type=False):
544+ def text_search(self):
545 """
546 This triggers the proper 'Search' search based on which search type is used.
547 "Eg. "Reference Search", "Text Search" or "Combined search".
548 """
549+ self.search_results = []
550 log.debug('text_search called')
551 text = self.search_edit.text()
552 if text == '':
553- self.list_view.clear()
554+ self.display_results()
555 return
556- self.list_view.clear(search_while_typing=search_while_type)
557+ self.on_results_view_tab_total_update(ResultsTab.Search)
558 if self.search_edit.current_search_type() == BibleSearch.Reference:
559 if get_reference_match('full').match(text):
560 # Valid reference found. Do reference search.
561 self.text_reference_search(text)
562- elif not search_while_type:
563+ elif self.search_status == SearchStatus.SearchButton:
564 self.main_window.information_message(
565 translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'),
566 translate('BiblesPlugin.BibleManager',
567@@ -700,10 +794,12 @@
568 self.text_reference_search(text)
569 else:
570 # It can only be a 'Combined' search without a valid reference, or a 'Text' search
571- if search_while_type:
572- if len(text) > 8 and VALID_TEXT_SEARCH.search(text):
573- self.on_text_search(text, True)
574- elif VALID_TEXT_SEARCH.search(text):
575+ if self.search_status == SearchStatus().SearchAsYouType:
576+ if len(text) <= 8:
577+ self.search_status = SearchStatus.NotEnoughText
578+ self.display_results()
579+ return
580+ if VALID_TEXT_SEARCH.search(text):
581 self.on_text_search(text)
582
583 def on_search_edit_text_changed(self):
584@@ -713,9 +809,12 @@
585
586 :return: None
587 """
588- if Settings().value('bibles/is search while typing enabled'):
589- if not self.search_timer.isActive():
590- self.search_timer.start()
591+ if not Settings().value('bibles/is search while typing enabled') or \
592+ not self.bible or self.bible.is_web_bible or \
593+ (self.second_bible and self.bible.is_web_bible):
594+ return
595+ if not self.search_timer.isActive():
596+ self.search_timer.start()
597
598 def on_search_timer_timeout(self):
599 """
600@@ -724,7 +823,9 @@
601
602 :return: None
603 """
604- self.text_search(True)
605+ self.search_status = SearchStatus().SearchAsYouType
606+ self.text_search()
607+ self.results_view_tab.setCurrentIndex(ResultsTab.Search)
608
609 def display_results(self):
610 """
611@@ -732,14 +833,16 @@
612
613 :return: None
614 """
615- self.list_view.clear()
616- if self.search_results:
617- items = self.build_display_results(self.bible, self.second_bible, self.search_results)
618- for item in items:
619- self.list_view.addItem(item)
620- self.list_view.selectAll()
621+ self.current_results = self.build_display_results(self.bible, self.second_bible, self.search_results)
622 self.search_results = []
623- self.second_search_results = []
624+ self.add_built_results_to_list_widget(self.current_results)
625+
626+ def add_built_results_to_list_widget(self, results):
627+ self.list_view.clear(self.search_status == SearchStatus.NotEnoughText)
628+ for item in self.build_list_widget_items(results):
629+ self.list_view.addItem(item)
630+ self.list_view.selectAll()
631+ self.on_results_view_tab_total_update(ResultsTab.Search)
632
633 def build_display_results(self, bible, second_bible, search_results):
634 """
635@@ -789,10 +892,17 @@
636 bible_text = '{book} {chapter:d}{sep}{verse:d} ({version}, {second_version})'
637 else:
638 bible_text = '{book} {chapter:d}{sep}{verse:d} ({version})'
639- bible_verse = QtWidgets.QListWidgetItem(bible_text.format(sep=verse_separator, **data))
640+ data['item_title'] = bible_text.format(sep=verse_separator, **data)
641+ items.append(data)
642+ return items
643+
644+ def build_list_widget_items(self, items):
645+ list_widget_items = []
646+ for data in items:
647+ bible_verse = QtWidgets.QListWidgetItem(data['item_title'])
648 bible_verse.setData(QtCore.Qt.UserRole, data)
649- items.append(bible_verse)
650- return items
651+ list_widget_items.append(bible_verse)
652+ return list_widget_items
653
654 def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
655 context=ServiceItemContext.Service):
656@@ -897,6 +1007,8 @@
657 """
658 Search for some Bible verses (by reference).
659 """
660+ if self.bible is None:
661+ return []
662 reference = self.plugin.manager.parse_ref(self.bible.name, string)
663 search_results = self.plugin.manager.get_verses(self.bible.name, reference, showError)
664 if search_results:
665@@ -908,6 +1020,9 @@
666 """
667 Create a media item from an item id.
668 """
669+ if self.bible is None:
670+ return []
671 reference = self.plugin.manager.parse_ref(self.bible.name, item_id)
672 search_results = self.plugin.manager.get_verses(self.bible.name, reference, False)
673- return self.build_display_results(self.bible, None, search_results)
674+ items = self.build_display_results(self.bible, None, search_results)
675+ return self.build_list_widget_items(items)
676
677=== added file 'resources/images/bibles_save_results.png'
678Binary files resources/images/bibles_save_results.png 1970-01-01 00:00:00 +0000 and resources/images/bibles_save_results.png 2017-05-07 10:12:27 +0000 differ
679=== removed file 'resources/images/bibles_search_lock.png'
680Binary files resources/images/bibles_search_lock.png 2011-05-11 23:59:37 +0000 and resources/images/bibles_search_lock.png 1970-01-01 00:00:00 +0000 differ
681=== removed file 'resources/images/bibles_search_unlock.png'
682Binary files resources/images/bibles_search_unlock.png 2011-05-11 23:59:37 +0000 and resources/images/bibles_search_unlock.png 1970-01-01 00:00:00 +0000 differ
683=== removed file 'resources/images/network_ssl.png'
684Binary files resources/images/network_ssl.png 2014-04-14 18:09:47 +0000 and resources/images/network_ssl.png 1970-01-01 00:00:00 +0000 differ
685=== modified file 'resources/images/openlp-2.qrc'
686--- resources/images/openlp-2.qrc 2016-12-18 14:11:31 +0000
687+++ resources/images/openlp-2.qrc 2017-05-07 10:12:27 +0000
688@@ -34,8 +34,7 @@
689 <file>bibles_search_text.png</file>
690 <file>bibles_search_reference.png</file>
691 <file>bibles_search_clear.png</file>
692- <file>bibles_search_unlock.png</file>
693- <file>bibles_search_lock.png</file>
694+ <file>bibles_save_results.png</file>
695 </qresource>
696 <qresource prefix="plugins">
697 <file>plugin_alerts.png</file>
698@@ -144,7 +143,6 @@
699 </qresource>
700 <qresource prefix="remote">
701 <file>network_server.png</file>
702- <file>network_ssl.png</file>
703 <file>network_auth.png</file>
704 </qresource>
705 <qresource prefix="songusage">
706@@ -188,4 +186,4 @@
707 <file>android_app_qr.png</file>
708 <file>ios_app_qr.png</file>
709 </qresource>
710-</RCC>
711\ No newline at end of file
712+</RCC>
713
714=== modified file 'tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py' (properties changed: -x to +x)
715--- tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py 2017-02-18 07:23:15 +0000
716+++ tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py 2017-05-07 10:12:27 +0000
717@@ -23,6 +23,7 @@
718 This module contains tests for the openlp.core.lib.listwidgetwithdnd module
719 """
720 from unittest import TestCase
721+from types import GeneratorType
722
723 from openlp.core.common.uistrings import UiStrings
724 from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
725@@ -33,37 +34,6 @@
726 """
727 Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class
728 """
729- def test_clear_locked(self):
730- """
731- Test the clear method the list is 'locked'
732- """
733- with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
734- # GIVEN: An instance of ListWidgetWithDnD
735- widget = ListWidgetWithDnD()
736-
737- # WHEN: The list is 'locked' and clear has been called
738- widget.locked = True
739- widget.clear()
740-
741- # THEN: The super method should not have been called (i.e. The list not cleared)
742- self.assertFalse(mocked_clear_super_method.called)
743-
744- def test_clear_overide_locked(self):
745- """
746- Test the clear method the list is 'locked', but clear is called with 'override_lock' set to True
747- """
748- with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
749- # GIVEN: An instance of ListWidgetWithDnD
750- widget = ListWidgetWithDnD()
751-
752- # WHEN: The list is 'locked' and clear has been called with override_lock se to True
753- widget.locked = True
754- widget.clear(override_lock=True)
755-
756- # THEN: The super method should have been called (i.e. The list is cleared regardless whether it is locked
757- # or not)
758- mocked_clear_super_method.assert_called_once_with()
759-
760 def test_clear(self):
761 """
762 Test the clear method when called without any arguments.
763@@ -90,6 +60,38 @@
764 # THEN: The results text should be the 'short results' text.
765 self.assertEqual(widget.no_results_text, UiStrings().ShortResults)
766
767+ def test_all_items_no_list_items(self):
768+ """
769+ Test allItems when there are no items in the list widget
770+ """
771+ # GIVEN: An instance of ListWidgetWithDnD
772+ widget = ListWidgetWithDnD()
773+ with patch.object(widget, 'count', return_value=0), \
774+ patch.object(widget, 'item', side_effect=lambda x: [][x]):
775+
776+ # WHEN: Calling allItems
777+ result = widget.allItems()
778+
779+ # THEN: An instance of a Generator object should be returned. The generator should not yeild any results
780+ self.assertIsInstance(result, GeneratorType)
781+ self.assertEqual(list(result), [])
782+
783+ def test_all_items_list_items(self):
784+ """
785+ Test allItems when the list widget contains some items.
786+ """
787+ # GIVEN: An instance of ListWidgetWithDnD
788+ widget = ListWidgetWithDnD()
789+ with patch.object(widget, 'count', return_value=2), \
790+ patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]):
791+
792+ # WHEN: Calling allItems
793+ result = widget.allItems()
794+
795+ # THEN: An instance of a Generator object should be returned. The generator should not yeild any results
796+ self.assertIsInstance(result, GeneratorType)
797+ self.assertEqual(list(result), [5, 3])
798+
799 def test_paint_event(self):
800 """
801 Test the paintEvent method when the list is not empty
802
803=== modified file 'tests/functional/openlp_plugins/bibles/test_mediaitem.py' (properties changed: -x to +x)
804--- tests/functional/openlp_plugins/bibles/test_mediaitem.py 2017-02-18 07:23:15 +0000
805+++ tests/functional/openlp_plugins/bibles/test_mediaitem.py 2017-05-07 10:12:27 +0000
806@@ -31,7 +31,8 @@
807
808 from openlp.core.common import Registry
809 from openlp.core.lib import MediaManagerItem
810-from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, get_reference_separators, VALID_TEXT_SEARCH
811+from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, ResultsTab, SearchStatus, \
812+ get_reference_separators, VALID_TEXT_SEARCH
813
814
815 class TestBibleMediaItemModulefunctions(TestCase):
816@@ -143,6 +144,7 @@
817 self.media_item = BibleMediaItem(None, self.mocked_plugin)
818
819 self.media_item.settings_section = 'bibles'
820+ self.media_item.results_view_tab = MagicMock()
821
822 self.mocked_book_1 = MagicMock(**{'get_name.return_value': 'Book 1', 'book_reference_id': 1})
823 self.mocked_book_2 = MagicMock(**{'get_name.return_value': 'Book 2', 'book_reference_id': 2})
824@@ -658,56 +660,65 @@
825 # THEN: The select_book_combo_box model sort should have been reset
826 self.media_item.select_book_combo_box.model().sort.assert_called_once_with(-1)
827
828- def test_on_clear_button_clicked(self):
829- """
830- Test on_clear_button_clicked
831+ def test_on_clear_button_clicked_saved_tab(self):
832+ """
833+ Test on_clear_button_clicked when the saved tab is selected
834+ """
835+ # GIVEN: An instance of :class:`MediaManagerItem` and mocked out saved_tab and select_tab and a mocked out
836+ # list_view and search_edit
837+ self.media_item.list_view = MagicMock()
838+ self.media_item.search_edit = MagicMock()
839+ self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Saved})
840+ self.media_item.saved_results = ['Some', 'Results']
841+ with patch.object(self.media_item, 'on_focus'):
842+
843+ # WHEN: Calling on_clear_button_clicked
844+ self.media_item.on_clear_button_clicked()
845+
846+ # THEN: The list_view should be cleared
847+ self.assertEqual(self.media_item.saved_results, [])
848+ self.media_item.list_view.clear.assert_called_once_with()
849+
850+ def test_on_clear_button_clicked_search_tab(self):
851+ """
852+ Test on_clear_button_clicked when the search tab is selected
853 """
854 # GIVEN: An instance of :class:`MediaManagerItem` and mocked out search_tab and select_tab and a mocked out
855 # list_view and search_edit
856 self.media_item.list_view = MagicMock()
857 self.media_item.search_edit = MagicMock()
858+ self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Search})
859+ self.media_item.current_results = ['Some', 'Results']
860 with patch.object(self.media_item, 'on_focus'):
861
862 # WHEN: Calling on_clear_button_clicked
863 self.media_item.on_clear_button_clicked()
864
865 # THEN: The list_view and the search_edit should be cleared
866+ self.assertEqual(self.media_item.current_results, [])
867 self.media_item.list_view.clear.assert_called_once_with()
868 self.media_item.search_edit.clear.assert_called_once_with()
869
870- def test_on_lock_button_toggled_search_tab_lock_icon(self):
871- """
872- Test that "on_lock_button_toggled" toggles the lock properly.
873- """
874- # GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view
875- self.media_item.list_view = MagicMock()
876- self.media_item.lock_icon = 'lock icon'
877- mocked_sender_instance = MagicMock()
878- with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance):
879-
880- # WHEN: When the lock_button is checked
881- self.media_item.on_lock_button_toggled(True)
882-
883- # THEN: list_view should be 'locked' and the lock icon set
884- self.assertTrue(self.media_item.list_view.locked)
885- mocked_sender_instance.setIcon.assert_called_once_with('lock icon')
886-
887- def test_on_lock_button_toggled_unlock_icon(self):
888- """
889- Test that "on_lock_button_toggled" toggles the lock properly.
890- """
891- # GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view
892- self.media_item.list_view = MagicMock()
893- self.media_item.unlock_icon = 'unlock icon'
894- mocked_sender_instance = MagicMock()
895- with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance):
896-
897- # WHEN: When the lock_button is unchecked
898- self.media_item.on_lock_button_toggled(False)
899-
900- # THEN: list_view should be 'unlocked' and the unlock icon set
901- self.assertFalse(self.media_item.list_view.locked)
902- mocked_sender_instance.setIcon.assert_called_once_with('unlock icon')
903+ def test_on_save_results_button_clicked(self):
904+ """
905+ Test that "on_save_results_button_clicked" saves the results.
906+ """
907+ # GIVEN: An instance of :class:`MediaManagerItem` and a mocked list_view
908+ result_1 = MagicMock(**{'data.return_value': 'R1'})
909+ result_2 = MagicMock(**{'data.return_value': 'R2'})
910+ result_3 = MagicMock(**{'data.return_value': 'R3'})
911+ self.media_item.list_view = MagicMock(**{'selectedItems.return_value': [result_1, result_2, result_3]})
912+
913+ with patch.object(self.media_item, 'on_results_view_tab_total_update') as \
914+ mocked_on_results_view_tab_total_update:
915+
916+ # WHEN: When the save_results_button is clicked
917+ self.media_item.on_save_results_button_clicked()
918+
919+ # THEN: The selected results in the list_view should be added to the 'saved_results' list. And the saved_tab
920+ # total should be updated.
921+ self.assertEqual(self.media_item.saved_results, ['R1', 'R2', 'R3'])
922+ mocked_on_results_view_tab_total_update.assert_called_once_with(ResultsTab.Saved)
923
924 def test_on_style_combo_box_changed(self):
925 """
926@@ -815,7 +826,9 @@
927 self.media_item.list_view = MagicMock(**{'count.return_value': 5})
928 self.media_item.style_combo_box = MagicMock()
929 self.media_item.select_book_combo_box = MagicMock()
930+ self.media_item.search_results = ['list', 'of', 'results']
931 with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
932+ patch.object(self.media_item, 'display_results'), \
933 patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
934 return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
935
936@@ -825,9 +838,8 @@
937 self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': self.mocked_bible_1})
938 self.media_item.on_second_combo_box_index_changed(5)
939
940- # THEN: The list_view should be cleared and the selected bible should be set as the current bible
941+ # THEN: The selected bible should be set as the current bible
942 self.assertTrue(mocked_critical_error_message_box.called)
943- self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
944 self.media_item.style_combo_box.setEnabled.assert_called_once_with(False)
945 self.assertTrue(mocked_initialise_advanced_bible.called)
946 self.assertEqual(self.media_item.second_bible, self.mocked_bible_1)
947@@ -841,7 +853,9 @@
948 self.media_item.list_view = MagicMock(**{'count.return_value': 5})
949 self.media_item.style_combo_box = MagicMock()
950 self.media_item.select_book_combo_box = MagicMock()
951+ self.media_item.search_results = ['list', 'of', 'results']
952 with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
953+ patch.object(self.media_item, 'display_results'), \
954 patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
955 return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
956 # WHEN: The previously is a bible new selection is None and the user selects yes
957@@ -850,9 +864,8 @@
958 self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': None})
959 self.media_item.on_second_combo_box_index_changed(0)
960
961- # THEN: The list_view should be cleared and the selected bible should be set as the current bible
962+ # THEN: The selected bible should be set as the current bible
963 self.assertTrue(mocked_critical_error_message_box.called)
964- self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
965 self.media_item.style_combo_box.setEnabled.assert_called_once_with(True)
966 self.assertFalse(mocked_initialise_advanced_bible.called)
967 self.assertEqual(self.media_item.second_bible, None)
968@@ -1388,8 +1401,9 @@
969 # WHEN: Calling on_search_timer_timeout
970 self.media_item.on_search_timer_timeout()
971
972- # THEN: The text_search method should have been called with True
973- mocked_text_search.assert_called_once_with(True)
974+ # THEN: The search_status should be set to SearchAsYouType and text_search should have been called
975+ self.assertEqual(self.media_item.search_status, SearchStatus().SearchAsYouType)
976+ mocked_text_search.assert_called_once_with()
977
978 def test_display_results_no_results(self):
979 """
980@@ -1407,7 +1421,6 @@
981 self.media_item.display_results()
982
983 # THEN: No items should be added to the list
984- self.media_item.list_view.clear.assert_called_once_with()
985 self.assertFalse(self.media_item.list_view.addItem.called)
986
987 def test_display_results_results(self):
988@@ -1415,7 +1428,10 @@
989 Test the display_results method when there are items to display
990 """
991 # GIVEN: An instance of BibleMediaItem and a mocked build_display_results which returns a list of results
992- with patch.object(self.media_item, 'build_display_results', return_value=['list', 'items']):
993+ with patch.object(self.media_item, 'build_display_results', return_value=[
994+ {'item_title': 'Title 1'}, {'item_title': 'Title 2'}]), \
995+ patch.object(self.media_item, 'add_built_results_to_list_widget') as \
996+ mocked_add_built_results_to_list_widget:
997 self.media_item.search_results = ['results']
998 self.media_item.list_view = MagicMock()
999
1000@@ -1423,5 +1439,5 @@
1001 self.media_item.display_results()
1002
1003 # THEN: addItem should have been with the display items
1004- self.media_item.list_view.clear.assert_called_once_with()
1005- self.assertEqual(self.media_item.list_view.addItem.call_args_list, [call('list'), call('items')])
1006+ mocked_add_built_results_to_list_widget.assert_called_once_with(
1007+ [{'item_title': 'Title 1'}, {'item_title': 'Title 2'}])
1008
1009=== modified file 'tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py'
1010--- tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py 2016-12-31 11:01:36 +0000
1011+++ tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py 2017-05-07 10:12:27 +0000
1012@@ -108,7 +108,7 @@
1013 # WHEN asking to parse the bible reference
1014 results = parse_reference('Raoul 1', self.manager.db_cache['tests'], MagicMock())
1015 # THEN a verse array should be returned
1016- self.assertEqual(False, results, "The bible Search should return False")
1017+ self.assertEqual([], results, "The bible Search should return an empty list")
1018
1019 def test_parse_reference_five(self):
1020 """