Merge lp:~trb143/openlp/reporting into lp:openlp

Proposed by Tim Bentley
Status: Superseded
Proposed branch: lp:~trb143/openlp/reporting
Merge into: lp:openlp
Diff against target: 411 lines (+215/-29)
7 files modified
openlp/core/common/settings.py (+1/-0)
openlp/plugins/custom/lib/mediaitem.py (+1/-1)
openlp/plugins/songs/forms/editsongform.py (+1/-1)
openlp/plugins/songs/lib/songcompare.py (+3/-3)
openlp/plugins/songs/reporting.py (+102/-0)
openlp/plugins/songs/songsplugin.py (+27/-8)
tests/functional/openlp_core_ui/test_servicemanager.py (+80/-16)
To merge this branch: bzr merge lp:~trb143/openlp/reporting
Reviewer Review Type Date Requested Status
Raoul Snyman Pending
Review via email: mp+306259@code.launchpad.net

This proposal supersedes a proposal from 2016-09-08.

This proposal has been superseded by a proposal from 2016-09-21.

Description of the change

My dad needed a report of all the songs on their database, they had 1800.
Made this into a reporting option and cleaned up the menu.
Fixed some errors spotted as well

Fixed issues and comments
1800 songs takes about 3 secs to run on my i7

lp:~trb143/openlp/reporting (revision 2699)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1779/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1690/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1628/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1384/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/974/
[SUCCESS] https://ci.openlp.io/job/Branch-05a-Code_Analysis/1042/
[SUCCESS] https://ci.openlp.io/job/Branch-05b-Test_Coverage/910/
[SUCCESS] https://ci.openlp.io/job/Branch-05c-Code_Analysis2/73/

To post a comment you must log in.
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

1. There's a "csv" module, use it.
2. I should be able to specify the file name, not just where to save it to.
3. Have you tested this with > 1000 songs? How long does it take? Some sort of progress window necessary?
4. You call it a report internally, but you're not very specific for the user. Rather call it a "Song List Report".

More comments inline.

review: Needs Fixing
Revision history for this message
Phill (phill-ridout) wrote : Posted in a previous version of this proposal

See my inline comments about your test doc stings and comments.

And also see my inline comments with regard to the Unicode literals. They're not required, and seem only to be implemented to ease porting from py2! https://www.python.org/dev/peps/pep-0414/#proposal

Are there any tests for the new module/method you've addded:
reporting.py
on_tools_report_song_list_triggered

Revision history for this message
Phill (phill-ridout) wrote :

Still missing tests for your new module. Guess its up to superfly if he's happy with that.

Also, just a few inline comments

lp:~trb143/openlp/reporting updated
2700. By Tim Bentley

minor updates

2701. By Tim Bentley

head

2702. By Tim Bentley

Fix song bug with formatting

2703. By Tim Bentley

Fix titles

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/common/settings.py'
2--- openlp/core/common/settings.py 2016-07-31 11:58:54 +0000
3+++ openlp/core/common/settings.py 2016-09-21 19:04:20 +0000
4@@ -379,6 +379,7 @@
5 'shortcuts/themeScreen': [QtGui.QKeySequence(QtCore.Qt.Key_T)],
6 'shortcuts/toolsReindexItem': [],
7 'shortcuts/toolsFindDuplicates': [],
8+ 'shortcuts/toolsSongListReport': [],
9 'shortcuts/toolsAlertItem': [QtGui.QKeySequence(QtCore.Qt.Key_F7)],
10 'shortcuts/toolsFirstTimeWizard': [],
11 'shortcuts/toolsOpenDataFolder': [],
12
13=== modified file 'openlp/plugins/custom/lib/mediaitem.py'
14--- openlp/plugins/custom/lib/mediaitem.py 2016-05-21 18:19:18 +0000
15+++ openlp/plugins/custom/lib/mediaitem.py 2016-09-21 19:04:20 +0000
16@@ -350,7 +350,7 @@
17 :param string: The search string
18 :param show_error: The error string to be show.
19 """
20- search = '%{search}%'.forma(search=string.lower())
21+ search = '%{search}%'.format(search=string.lower())
22 search_results = self.plugin.db_manager.get_all_objects(CustomSlide,
23 or_(func.lower(CustomSlide.title).like(search),
24 func.lower(CustomSlide.text).like(search)),
25
26=== modified file 'openlp/plugins/songs/forms/editsongform.py'
27--- openlp/plugins/songs/forms/editsongform.py 2016-05-27 08:13:14 +0000
28+++ openlp/plugins/songs/forms/editsongform.py 2016-09-21 19:04:20 +0000
29@@ -317,7 +317,7 @@
30 self.song.verse_order = re.sub('([' + verse.upper() + verse.lower() + '])(\W|$)',
31 r'\g<1>1\2', self.song.verse_order)
32 except:
33- log.exception('Problem processing song Lyrics \n{xml}'.forma(xml=sxml.dump_xml()))
34+ log.exception('Problem processing song Lyrics \n{xml}'.format(xml=sxml.dump_xml()))
35 raise
36
37 def keyPressEvent(self, event):
38
39=== modified file 'openlp/plugins/songs/lib/songcompare.py'
40--- openlp/plugins/songs/lib/songcompare.py 2015-12-31 22:46:06 +0000
41+++ openlp/plugins/songs/lib/songcompare.py 2016-09-21 19:04:20 +0000
42@@ -46,13 +46,13 @@
43 MAX_TYPO_SIZE = 3
44
45
46-def songs_probably_equal(song_tupel):
47+def songs_probably_equal(song_tuple):
48 """
49 Calculate and return whether two songs are probably equal.
50
51- :param song_tupel: A tuple of two songs to compare.
52+ :param song_tuple: A tuple of two songs to compare.
53 """
54- song1, song2 = song_tupel
55+ song1, song2 = song_tuple
56 pos1, lyrics1 = song1
57 pos2, lyrics2 = song2
58 if len(lyrics1) < len(lyrics2):
59
60=== added file 'openlp/plugins/songs/reporting.py'
61--- openlp/plugins/songs/reporting.py 1970-01-01 00:00:00 +0000
62+++ openlp/plugins/songs/reporting.py 2016-09-21 19:04:20 +0000
63@@ -0,0 +1,102 @@
64+# -*- coding: utf-8 -*-
65+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
66+
67+###############################################################################
68+# OpenLP - Open Source Lyrics Projection #
69+# --------------------------------------------------------------------------- #
70+# Copyright (c) 2008-2016 OpenLP Developers #
71+# --------------------------------------------------------------------------- #
72+# This program is free software; you can redistribute it and/or modify it #
73+# under the terms of the GNU General Public License as published by the Free #
74+# Software Foundation; version 2 of the License. #
75+# #
76+# This program is distributed in the hope that it will be useful, but WITHOUT #
77+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
78+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
79+# more details. #
80+# #
81+# You should have received a copy of the GNU General Public License along #
82+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
83+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
84+###############################################################################
85+"""
86+The :mod:`db` module provides the ability to provide a csv file of all songs
87+"""
88+import csv
89+import logging
90+
91+from PyQt5 import QtWidgets
92+
93+from openlp.core.common import Registry, translate
94+from openlp.core.lib.ui import critical_error_message_box
95+from openlp.plugins.songs.lib.db import Song
96+
97+
98+log = logging.getLogger(__name__)
99+
100+
101+def report_song_list():
102+ """
103+ Export the song list as a CSV file.
104+ :return: Nothing
105+ """
106+ main_window = Registry().get('main_window')
107+ plugin = Registry().get('songs').plugin
108+ report_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
109+ main_window, translate('SongPlugin.ReportSongList', 'Output File Location'))
110+ if not report_file_name:
111+ main_window.error_message(
112+ translate('SongPlugin.ReportSongList', 'Output Path Not Selected'),
113+ translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your '
114+ 'report. \nPlease select an existing path '
115+ 'on your computer.')
116+ )
117+ return
118+ if not report_file_name.endswith('csv'):
119+ report_file_name += '.csv'
120+ file_handle = None
121+ Registry().get('application').set_busy_cursor()
122+ try:
123+ file_handle = open(report_file_name, 'wt')
124+ fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
125+ writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
126+ headers = dict((n, n) for n in fieldnames)
127+ writer.writerow(headers)
128+ song_list = plugin.manager.get_all_objects(Song)
129+ for song in song_list:
130+ author_list = []
131+ for author_song in song.authors_songs:
132+ author_list.append(author_song.author.display_name)
133+ author_string = ' | '.join(author_list)
134+ book_list = []
135+ for book_song in song.songbook_entries:
136+ if hasattr(book_song, 'entry') and book_song.entry:
137+ book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry))
138+ book_string = ' | '.join(book_list)
139+ topic_list = []
140+ for topic_song in song.topics:
141+ if hasattr(topic_song, 'name'):
142+ topic_list.append(topic_song.name)
143+ topic_string = ' | '.join(topic_list)
144+ writer.writerow({'Title': song.title,
145+ 'Alternative Title': song.alternate_title,
146+ 'Copyright': song.copyright,
147+ 'Author(s)': author_string,
148+ 'Song Book': book_string,
149+ 'Topic': topic_string})
150+ Registry().get('application').set_normal_cursor()
151+ main_window.information_message(
152+ translate('SongPlugin.ReportSongList', 'Report Creation'),
153+ translate('SongPlugin.ReportSongList',
154+ 'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
155+ )
156+ except OSError as ose:
157+ Registry().get('application').set_normal_cursor()
158+ log.exception('Failed to write out song usage records')
159+ critical_error_message_box(translate('SongPlugin.ReportSongList', 'Song Extraction Failed'),
160+ translate('SongPlugin.ReportSongList',
161+ 'An error occurred while extracting: {error}'
162+ ).format(error=ose.strerror))
163+ finally:
164+ if file_handle:
165+ file_handle.close()
166
167=== modified file 'openlp/plugins/songs/songsplugin.py'
168--- openlp/plugins/songs/songsplugin.py 2016-03-31 16:34:22 +0000
169+++ openlp/plugins/songs/songsplugin.py 2016-09-21 19:04:20 +0000
170@@ -36,6 +36,7 @@
171 from openlp.core.lib import Plugin, StringContent, build_icon
172 from openlp.core.lib.db import Manager
173 from openlp.core.lib.ui import create_action
174+from openlp.plugins.songs import reporting
175 from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
176 from openlp.plugins.songs.forms.songselectform import SongSelectForm
177 from openlp.plugins.songs.lib import clean_song, upgrade
178@@ -102,13 +103,13 @@
179 self.songselect_form.initialise()
180 self.song_import_item.setVisible(True)
181 self.song_export_item.setVisible(True)
182- self.tools_reindex_item.setVisible(True)
183- self.tools_find_duplicates.setVisible(True)
184+ self.song_tools_menu.menuAction().setVisible(True)
185 action_list = ActionList.get_instance()
186 action_list.add_action(self.song_import_item, UiStrings().Import)
187 action_list.add_action(self.song_export_item, UiStrings().Export)
188 action_list.add_action(self.tools_reindex_item, UiStrings().Tools)
189 action_list.add_action(self.tools_find_duplicates, UiStrings().Tools)
190+ action_list.add_action(self.tools_report_song_list, UiStrings().Tools)
191
192 def add_import_menu_item(self, import_menu):
193 """
194@@ -151,19 +152,37 @@
195 :param tools_menu: The actual **Tools** menu item, so that your actions can use it as their parent.
196 """
197 log.info('add tools menu')
198+ self.tools_menu = tools_menu
199+ self.song_tools_menu = QtWidgets.QMenu(tools_menu)
200+ self.song_tools_menu.setObjectName('song_tools_menu')
201+ self.song_tools_menu.setTitle(translate('SongsPlugin', 'Songs'))
202 self.tools_reindex_item = create_action(
203 tools_menu, 'toolsReindexItem',
204 text=translate('SongsPlugin', '&Re-index Songs'),
205 icon=':/plugins/plugin_songs.png',
206 statustip=translate('SongsPlugin', 'Re-index the songs database to improve searching and ordering.'),
207- visible=False, triggers=self.on_tools_reindex_item_triggered)
208- tools_menu.addAction(self.tools_reindex_item)
209+ triggers=self.on_tools_reindex_item_triggered)
210 self.tools_find_duplicates = create_action(
211 tools_menu, 'toolsFindDuplicates',
212 text=translate('SongsPlugin', 'Find &Duplicate Songs'),
213 statustip=translate('SongsPlugin', 'Find and remove duplicate songs in the song database.'),
214- visible=False, triggers=self.on_tools_find_duplicates_triggered, can_shortcuts=True)
215- tools_menu.addAction(self.tools_find_duplicates)
216+ triggers=self.on_tools_find_duplicates_triggered, can_shortcuts=True)
217+ self.tools_report_song_list = create_action(
218+ tools_menu, 'toolsSongListReport',
219+ text=translate('SongsPlugin', 'Song List Report'),
220+ statustip=translate('SongsPlugin', 'Produce a CSV file of all the songs in the database.'),
221+ triggers=self.on_tools_report_song_list_triggered)
222+
223+ self.tools_menu.addAction(self.song_tools_menu.menuAction())
224+ self.song_tools_menu.addAction(self.tools_reindex_item)
225+ self.song_tools_menu.addAction(self.tools_find_duplicates)
226+ self.song_tools_menu.addAction(self.tools_report_song_list)
227+
228+ self.song_tools_menu.menuAction().setVisible(False)
229+
230+ @staticmethod
231+ def on_tools_report_song_list_triggered():
232+ reporting.report_song_list()
233
234 def on_tools_reindex_item_triggered(self):
235 """
236@@ -326,13 +345,13 @@
237 self.manager.finalise()
238 self.song_import_item.setVisible(False)
239 self.song_export_item.setVisible(False)
240- self.tools_reindex_item.setVisible(False)
241- self.tools_find_duplicates.setVisible(False)
242 action_list = ActionList.get_instance()
243 action_list.remove_action(self.song_import_item, UiStrings().Import)
244 action_list.remove_action(self.song_export_item, UiStrings().Export)
245 action_list.remove_action(self.tools_reindex_item, UiStrings().Tools)
246 action_list.remove_action(self.tools_find_duplicates, UiStrings().Tools)
247+ action_list.add_action(self.tools_report_song_list, UiStrings().Tools)
248+ self.song_tools_menu.menuAction().setVisible(False)
249 super(SongsPlugin, self).finalise()
250
251 def new_service_created(self):
252
253=== modified file 'tests/functional/openlp_core_ui/test_servicemanager.py'
254--- tests/functional/openlp_core_ui/test_servicemanager.py 2016-07-17 19:46:06 +0000
255+++ tests/functional/openlp_core_ui/test_servicemanager.py 2016-09-21 19:04:20 +0000
256@@ -28,6 +28,7 @@
257 import PyQt5
258
259 from openlp.core.common import Registry, ThemeLevel
260+from openlp.core.ui.lib.toolbar import OpenLPToolbar
261 from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities
262 from openlp.core.ui import ServiceManager
263
264@@ -544,8 +545,8 @@
265 self.assertEqual(service_manager.theme_menu.menuAction().setVisible.call_count, 1,
266 'Should have be called once')
267
268- @patch(u'openlp.core.ui.servicemanager.Settings')
269- @patch(u'PyQt5.QtCore.QTimer.singleShot')
270+ @patch('openlp.core.ui.servicemanager.Settings')
271+ @patch('PyQt5.QtCore.QTimer.singleShot')
272 def test_single_click_preview_true(self, mocked_singleShot, MockedSettings):
273 """
274 Test that when "Preview items when clicked in Service Manager" enabled the preview timer starts
275@@ -561,8 +562,8 @@
276 mocked_singleShot.assert_called_with(PyQt5.QtWidgets.QApplication.instance().doubleClickInterval(),
277 service_manager.on_single_click_preview_timeout)
278
279- @patch(u'openlp.core.ui.servicemanager.Settings')
280- @patch(u'PyQt5.QtCore.QTimer.singleShot')
281+ @patch('openlp.core.ui.servicemanager.Settings')
282+ @patch('PyQt5.QtCore.QTimer.singleShot')
283 def test_single_click_preview_false(self, mocked_singleShot, MockedSettings):
284 """
285 Test that when "Preview items when clicked in Service Manager" disabled the preview timer doesn't start
286@@ -577,9 +578,9 @@
287 # THEN: timer should not be started
288 self.assertEqual(mocked_singleShot.call_count, 0, 'Should not be called')
289
290- @patch(u'openlp.core.ui.servicemanager.Settings')
291- @patch(u'PyQt5.QtCore.QTimer.singleShot')
292- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
293+ @patch('openlp.core.ui.servicemanager.Settings')
294+ @patch('PyQt5.QtCore.QTimer.singleShot')
295+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_live')
296 def test_single_click_preview_double(self, mocked_make_live, mocked_singleShot, MockedSettings):
297 """
298 Test that when a double click has registered the preview timer doesn't start
299@@ -596,7 +597,7 @@
300 mocked_make_live.assert_called_with()
301 self.assertEqual(mocked_singleShot.call_count, 0, 'Should not be called')
302
303- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
304+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_preview')
305 def test_single_click_timeout_single(self, mocked_make_preview):
306 """
307 Test that when a single click has been registered, the item is sent to preview
308@@ -609,8 +610,8 @@
309 self.assertEqual(mocked_make_preview.call_count, 1,
310 'ServiceManager.make_preview() should have been called once')
311
312- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
313- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
314+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_preview')
315+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_live')
316 def test_single_click_timeout_double(self, mocked_make_live, mocked_make_preview):
317 """
318 Test that when a double click has been registered, the item does not goes to preview
319@@ -623,9 +624,9 @@
320 # THEN: make_preview() should not have been called
321 self.assertEqual(mocked_make_preview.call_count, 0, 'ServiceManager.make_preview() should not be called')
322
323- @patch(u'openlp.core.ui.servicemanager.shutil.copy')
324- @patch(u'openlp.core.ui.servicemanager.zipfile')
325- @patch(u'openlp.core.ui.servicemanager.ServiceManager.save_file_as')
326+ @patch('openlp.core.ui.servicemanager.shutil.copy')
327+ @patch('openlp.core.ui.servicemanager.zipfile')
328+ @patch('openlp.core.ui.servicemanager.ServiceManager.save_file_as')
329 def test_save_file_raises_permission_error(self, mocked_save_file_as, mocked_zipfile, mocked_shutil_copy):
330 """
331 Test that when a PermissionError is raised when trying to save a file, it is handled correctly
332@@ -652,9 +653,9 @@
333 self.assertTrue(result)
334 mocked_save_file_as.assert_called_with()
335
336- @patch(u'openlp.core.ui.servicemanager.shutil.copy')
337- @patch(u'openlp.core.ui.servicemanager.zipfile')
338- @patch(u'openlp.core.ui.servicemanager.ServiceManager.save_file_as')
339+ @patch('openlp.core.ui.servicemanager.shutil.copy')
340+ @patch('openlp.core.ui.servicemanager.zipfile')
341+ @patch('openlp.core.ui.servicemanager.ServiceManager.save_file_as')
342 def test_save_local_file_raises_permission_error(self, mocked_save_file_as, mocked_zipfile, mocked_shutil_copy):
343 """
344 Test that when a PermissionError is raised when trying to save a local file, it is handled correctly
345@@ -679,3 +680,66 @@
346 # THEN: The "save_as" method is called to save the service
347 self.assertTrue(result)
348 mocked_save_file_as.assert_called_with()
349+
350+ @patch('openlp.core.ui.servicemanager.ServiceManager.regenerate_service_items')
351+ def test_theme_change_global(self, mocked_regenerate_service_items):
352+ """
353+ Test that when a Toolbar theme combobox displays correctly when the theme is set to Global
354+ """
355+ # GIVEN: A service manager, a service to display with a theme level in the renderer
356+ mocked_renderer = MagicMock()
357+ service_manager = ServiceManager(None)
358+ Registry().register('renderer', mocked_renderer)
359+ service_manager.toolbar = OpenLPToolbar(None)
360+ service_manager.toolbar.add_toolbar_action('theme_combo_box', triggers=MagicMock())
361+ service_manager.toolbar.add_toolbar_action('theme_label', triggers=MagicMock())
362+
363+ # WHEN: The service manager has a Global theme
364+ mocked_renderer.theme_level = ThemeLevel.Global
365+ result = service_manager.theme_change()
366+
367+ # THEN: The the theme toolbar should not be visible
368+ self.assertFalse(service_manager.toolbar.actions['theme_combo_box'].isVisible(),
369+ 'The visibility should be False')
370+
371+ @patch('openlp.core.ui.servicemanager.ServiceManager.regenerate_service_items')
372+ def test_theme_change_service(self, mocked_regenerate_service_items):
373+ """
374+ Test that when a Toolbar theme combobox displays correctly when the theme is set to Theme
375+ """
376+ # GIVEN: A service manager, a service to display with a theme level in the renderer
377+ mocked_renderer = MagicMock()
378+ service_manager = ServiceManager(None)
379+ Registry().register('renderer', mocked_renderer)
380+ service_manager.toolbar = OpenLPToolbar(None)
381+ service_manager.toolbar.add_toolbar_action('theme_combo_box', triggers=MagicMock())
382+ service_manager.toolbar.add_toolbar_action('theme_label', triggers=MagicMock())
383+
384+ # WHEN: The service manager has a Service theme
385+ mocked_renderer.theme_level = ThemeLevel.Service
386+ result = service_manager.theme_change()
387+
388+ # THEN: The the theme toolbar should be visible
389+ self.assertTrue(service_manager.toolbar.actions['theme_combo_box'].isVisible(),
390+ 'The visibility should be True')
391+
392+ @patch('openlp.core.ui.servicemanager.ServiceManager.regenerate_service_items')
393+ def test_theme_change_song(self, mocked_regenerate_service_items):
394+ """
395+ Test that when a Toolbar theme combobox displays correctly when the theme is set to Song
396+ """
397+ # GIVEN: A service manager, a service to display with a theme level in the renderer
398+ mocked_renderer = MagicMock()
399+ service_manager = ServiceManager(None)
400+ Registry().register('renderer', mocked_renderer)
401+ service_manager.toolbar = OpenLPToolbar(None)
402+ service_manager.toolbar.add_toolbar_action('theme_combo_box', triggers=MagicMock())
403+ service_manager.toolbar.add_toolbar_action('theme_label', triggers=MagicMock())
404+
405+ # WHEN: The service manager has a Song theme
406+ mocked_renderer.theme_level = ThemeLevel.Song
407+ result = service_manager.theme_change()
408+
409+ # THEN: The the theme toolbar should be visible
410+ self.assertTrue(service_manager.toolbar.actions['theme_combo_box'].isVisible(),
411+ 'The visibility should be True')