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: 398 lines (+214/-28)
6 files modified
openlp/core/common/settings.py (+1/-0)
openlp/plugins/songs/lib/openlyricsxml.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+309093@code.launchpad.net

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

This proposal has been superseded by a proposal from 2016-10-25.

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 2701)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1801/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1712/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1650/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1406/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/996/
[SUCCESS] https://ci.openlp.io/job/Branch-05a-Code_Analysis/1064/
[SUCCESS] https://ci.openlp.io/job/Branch-05b-Test_Coverage/932/
[SUCCESS] https://ci.openlp.io/job/Branch-05c-Code_Analysis2/93/

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 : Posted in a previous version of this proposal

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