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: 408 lines (+212/-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 (+99/-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 Needs Fixing
Review via email: mp+305167@code.launchpad.net

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

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

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

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 :

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

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

Review Comments

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-19 18:52:09 +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-19 18:52:09 +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-19 18:52:09 +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-19 18:52:09 +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-19 18:52:09 +0000
63@@ -0,0 +1,99 @@
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+ try:
122+ file_handle = open(report_file_name, 'wt')
123+ fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
124+ writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
125+ headers = dict((n, n) for n in fieldnames)
126+ writer.writerow(headers)
127+ song_list = plugin.manager.get_all_objects(Song)
128+ for song in song_list:
129+ author_list = []
130+ for author_song in song.authors_songs:
131+ author_list.append(author_song.author.display_name)
132+ author_string = '{name}'.format(name=' | '.join(author_list))
133+ book_list = []
134+ for book_song in song.songbook_entries:
135+ if hasattr(book_song, 'entry') and book_song.entry:
136+ book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry))
137+ book_string = '{name}'.format(name=' | '.join(book_list))
138+ topic_list = []
139+ for topic_song in song.topics:
140+ if hasattr(topic_song, 'name'):
141+ topic_list.append(topic_song.name)
142+ topic_string = '{name}'.format(name=' | '.join(topic_list))
143+ writer.writerow({'Title': song.title,
144+ 'Alternative Title': song.alternate_title,
145+ 'Copyright': song.copyright,
146+ 'Author(s)': author_string,
147+ 'Song Book': book_string,
148+ 'Topic': topic_string})
149+ main_window.information_message(
150+ translate('SongPlugin.ReportSongList', 'Report Creation'),
151+ translate('SongPlugin.ReportSongList',
152+ 'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
153+ )
154+ except OSError as ose:
155+ log.exception('Failed to write out song usage records')
156+ critical_error_message_box(translate('SongPlugin.ReportSongList', 'Song Extraction Failed'),
157+ translate('SongPlugin.ReportSongList',
158+ 'An error occurred while extracting: {error}'
159+ ).format(error=ose.strerror))
160+ finally:
161+ if file_handle:
162+ file_handle.close()
163
164=== modified file 'openlp/plugins/songs/songsplugin.py'
165--- openlp/plugins/songs/songsplugin.py 2016-03-31 16:34:22 +0000
166+++ openlp/plugins/songs/songsplugin.py 2016-09-19 18:52:09 +0000
167@@ -36,6 +36,7 @@
168 from openlp.core.lib import Plugin, StringContent, build_icon
169 from openlp.core.lib.db import Manager
170 from openlp.core.lib.ui import create_action
171+from openlp.plugins.songs import reporting
172 from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
173 from openlp.plugins.songs.forms.songselectform import SongSelectForm
174 from openlp.plugins.songs.lib import clean_song, upgrade
175@@ -102,13 +103,13 @@
176 self.songselect_form.initialise()
177 self.song_import_item.setVisible(True)
178 self.song_export_item.setVisible(True)
179- self.tools_reindex_item.setVisible(True)
180- self.tools_find_duplicates.setVisible(True)
181+ self.song_tools_menu.menuAction().setVisible(True)
182 action_list = ActionList.get_instance()
183 action_list.add_action(self.song_import_item, UiStrings().Import)
184 action_list.add_action(self.song_export_item, UiStrings().Export)
185 action_list.add_action(self.tools_reindex_item, UiStrings().Tools)
186 action_list.add_action(self.tools_find_duplicates, UiStrings().Tools)
187+ action_list.add_action(self.tools_report_song_list, UiStrings().Tools)
188
189 def add_import_menu_item(self, import_menu):
190 """
191@@ -151,19 +152,37 @@
192 :param tools_menu: The actual **Tools** menu item, so that your actions can use it as their parent.
193 """
194 log.info('add tools menu')
195+ self.tools_menu = tools_menu
196+ self.song_tools_menu = QtWidgets.QMenu(tools_menu)
197+ self.song_tools_menu.setObjectName('song_tools_menu')
198+ self.song_tools_menu.setTitle(translate('SongsPlugin', 'Songs'))
199 self.tools_reindex_item = create_action(
200 tools_menu, 'toolsReindexItem',
201 text=translate('SongsPlugin', '&Re-index Songs'),
202 icon=':/plugins/plugin_songs.png',
203 statustip=translate('SongsPlugin', 'Re-index the songs database to improve searching and ordering.'),
204- visible=False, triggers=self.on_tools_reindex_item_triggered)
205- tools_menu.addAction(self.tools_reindex_item)
206+ triggers=self.on_tools_reindex_item_triggered)
207 self.tools_find_duplicates = create_action(
208 tools_menu, 'toolsFindDuplicates',
209 text=translate('SongsPlugin', 'Find &Duplicate Songs'),
210 statustip=translate('SongsPlugin', 'Find and remove duplicate songs in the song database.'),
211- visible=False, triggers=self.on_tools_find_duplicates_triggered, can_shortcuts=True)
212- tools_menu.addAction(self.tools_find_duplicates)
213+ triggers=self.on_tools_find_duplicates_triggered, can_shortcuts=True)
214+ self.tools_report_song_list = create_action(
215+ tools_menu, 'toolsSongListReport',
216+ text=translate('SongsPlugin', 'Song List Report'),
217+ statustip=translate('SongsPlugin', 'Produce a CSV file of all the songs in the database.'),
218+ triggers=self.on_tools_report_song_list_triggered)
219+
220+ self.tools_menu.addAction(self.song_tools_menu.menuAction())
221+ self.song_tools_menu.addAction(self.tools_reindex_item)
222+ self.song_tools_menu.addAction(self.tools_find_duplicates)
223+ self.song_tools_menu.addAction(self.tools_report_song_list)
224+
225+ self.song_tools_menu.menuAction().setVisible(False)
226+
227+ @staticmethod
228+ def on_tools_report_song_list_triggered():
229+ reporting.report_song_list()
230
231 def on_tools_reindex_item_triggered(self):
232 """
233@@ -326,13 +345,13 @@
234 self.manager.finalise()
235 self.song_import_item.setVisible(False)
236 self.song_export_item.setVisible(False)
237- self.tools_reindex_item.setVisible(False)
238- self.tools_find_duplicates.setVisible(False)
239 action_list = ActionList.get_instance()
240 action_list.remove_action(self.song_import_item, UiStrings().Import)
241 action_list.remove_action(self.song_export_item, UiStrings().Export)
242 action_list.remove_action(self.tools_reindex_item, UiStrings().Tools)
243 action_list.remove_action(self.tools_find_duplicates, UiStrings().Tools)
244+ action_list.add_action(self.tools_report_song_list, UiStrings().Tools)
245+ self.song_tools_menu.menuAction().setVisible(False)
246 super(SongsPlugin, self).finalise()
247
248 def new_service_created(self):
249
250=== modified file 'tests/functional/openlp_core_ui/test_servicemanager.py'
251--- tests/functional/openlp_core_ui/test_servicemanager.py 2016-07-17 19:46:06 +0000
252+++ tests/functional/openlp_core_ui/test_servicemanager.py 2016-09-19 18:52:09 +0000
253@@ -28,6 +28,7 @@
254 import PyQt5
255
256 from openlp.core.common import Registry, ThemeLevel
257+from openlp.core.ui.lib.toolbar import OpenLPToolbar
258 from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities
259 from openlp.core.ui import ServiceManager
260
261@@ -544,8 +545,8 @@
262 self.assertEqual(service_manager.theme_menu.menuAction().setVisible.call_count, 1,
263 'Should have be called once')
264
265- @patch(u'openlp.core.ui.servicemanager.Settings')
266- @patch(u'PyQt5.QtCore.QTimer.singleShot')
267+ @patch('openlp.core.ui.servicemanager.Settings')
268+ @patch('PyQt5.QtCore.QTimer.singleShot')
269 def test_single_click_preview_true(self, mocked_singleShot, MockedSettings):
270 """
271 Test that when "Preview items when clicked in Service Manager" enabled the preview timer starts
272@@ -561,8 +562,8 @@
273 mocked_singleShot.assert_called_with(PyQt5.QtWidgets.QApplication.instance().doubleClickInterval(),
274 service_manager.on_single_click_preview_timeout)
275
276- @patch(u'openlp.core.ui.servicemanager.Settings')
277- @patch(u'PyQt5.QtCore.QTimer.singleShot')
278+ @patch('openlp.core.ui.servicemanager.Settings')
279+ @patch('PyQt5.QtCore.QTimer.singleShot')
280 def test_single_click_preview_false(self, mocked_singleShot, MockedSettings):
281 """
282 Test that when "Preview items when clicked in Service Manager" disabled the preview timer doesn't start
283@@ -577,9 +578,9 @@
284 # THEN: timer should not be started
285 self.assertEqual(mocked_singleShot.call_count, 0, 'Should not be called')
286
287- @patch(u'openlp.core.ui.servicemanager.Settings')
288- @patch(u'PyQt5.QtCore.QTimer.singleShot')
289- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
290+ @patch('openlp.core.ui.servicemanager.Settings')
291+ @patch('PyQt5.QtCore.QTimer.singleShot')
292+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_live')
293 def test_single_click_preview_double(self, mocked_make_live, mocked_singleShot, MockedSettings):
294 """
295 Test that when a double click has registered the preview timer doesn't start
296@@ -596,7 +597,7 @@
297 mocked_make_live.assert_called_with()
298 self.assertEqual(mocked_singleShot.call_count, 0, 'Should not be called')
299
300- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
301+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_preview')
302 def test_single_click_timeout_single(self, mocked_make_preview):
303 """
304 Test that when a single click has been registered, the item is sent to preview
305@@ -609,8 +610,8 @@
306 self.assertEqual(mocked_make_preview.call_count, 1,
307 'ServiceManager.make_preview() should have been called once')
308
309- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
310- @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
311+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_preview')
312+ @patch('openlp.core.ui.servicemanager.ServiceManager.make_live')
313 def test_single_click_timeout_double(self, mocked_make_live, mocked_make_preview):
314 """
315 Test that when a double click has been registered, the item does not goes to preview
316@@ -623,9 +624,9 @@
317 # THEN: make_preview() should not have been called
318 self.assertEqual(mocked_make_preview.call_count, 0, 'ServiceManager.make_preview() should not be called')
319
320- @patch(u'openlp.core.ui.servicemanager.shutil.copy')
321- @patch(u'openlp.core.ui.servicemanager.zipfile')
322- @patch(u'openlp.core.ui.servicemanager.ServiceManager.save_file_as')
323+ @patch('openlp.core.ui.servicemanager.shutil.copy')
324+ @patch('openlp.core.ui.servicemanager.zipfile')
325+ @patch('openlp.core.ui.servicemanager.ServiceManager.save_file_as')
326 def test_save_file_raises_permission_error(self, mocked_save_file_as, mocked_zipfile, mocked_shutil_copy):
327 """
328 Test that when a PermissionError is raised when trying to save a file, it is handled correctly
329@@ -652,9 +653,9 @@
330 self.assertTrue(result)
331 mocked_save_file_as.assert_called_with()
332
333- @patch(u'openlp.core.ui.servicemanager.shutil.copy')
334- @patch(u'openlp.core.ui.servicemanager.zipfile')
335- @patch(u'openlp.core.ui.servicemanager.ServiceManager.save_file_as')
336+ @patch('openlp.core.ui.servicemanager.shutil.copy')
337+ @patch('openlp.core.ui.servicemanager.zipfile')
338+ @patch('openlp.core.ui.servicemanager.ServiceManager.save_file_as')
339 def test_save_local_file_raises_permission_error(self, mocked_save_file_as, mocked_zipfile, mocked_shutil_copy):
340 """
341 Test that when a PermissionError is raised when trying to save a local file, it is handled correctly
342@@ -679,3 +680,66 @@
343 # THEN: The "save_as" method is called to save the service
344 self.assertTrue(result)
345 mocked_save_file_as.assert_called_with()
346+
347+ @patch('openlp.core.ui.servicemanager.ServiceManager.regenerate_service_items')
348+ def test_theme_change_global(self, mocked_regenerate_service_items):
349+ """
350+ Test that when a Toolbar theme combobox displays correctly when the theme is set to Global
351+ """
352+ # GIVEN: A service manager, a service to display with a theme level in the renderer
353+ mocked_renderer = MagicMock()
354+ service_manager = ServiceManager(None)
355+ Registry().register('renderer', mocked_renderer)
356+ service_manager.toolbar = OpenLPToolbar(None)
357+ service_manager.toolbar.add_toolbar_action('theme_combo_box', triggers=MagicMock())
358+ service_manager.toolbar.add_toolbar_action('theme_label', triggers=MagicMock())
359+
360+ # WHEN: The service manager has a Global theme
361+ mocked_renderer.theme_level = ThemeLevel.Global
362+ result = service_manager.theme_change()
363+
364+ # THEN: The the theme toolbar should not be visible
365+ self.assertFalse(service_manager.toolbar.actions['theme_combo_box'].isVisible(),
366+ 'The visibility should be False')
367+
368+ @patch('openlp.core.ui.servicemanager.ServiceManager.regenerate_service_items')
369+ def test_theme_change_service(self, mocked_regenerate_service_items):
370+ """
371+ Test that when a Toolbar theme combobox displays correctly when the theme is set to Theme
372+ """
373+ # GIVEN: A service manager, a service to display with a theme level in the renderer
374+ mocked_renderer = MagicMock()
375+ service_manager = ServiceManager(None)
376+ Registry().register('renderer', mocked_renderer)
377+ service_manager.toolbar = OpenLPToolbar(None)
378+ service_manager.toolbar.add_toolbar_action('theme_combo_box', triggers=MagicMock())
379+ service_manager.toolbar.add_toolbar_action('theme_label', triggers=MagicMock())
380+
381+ # WHEN: The service manager has a Service theme
382+ mocked_renderer.theme_level = ThemeLevel.Service
383+ result = service_manager.theme_change()
384+
385+ # THEN: The the theme toolbar should be visible
386+ self.assertTrue(service_manager.toolbar.actions['theme_combo_box'].isVisible(),
387+ 'The visibility should be True')
388+
389+ @patch('openlp.core.ui.servicemanager.ServiceManager.regenerate_service_items')
390+ def test_theme_change_song(self, mocked_regenerate_service_items):
391+ """
392+ Test that when a Toolbar theme combobox displays correctly when the theme is set to Song
393+ """
394+ # GIVEN: A service manager, a service to display with a theme level in the renderer
395+ mocked_renderer = MagicMock()
396+ service_manager = ServiceManager(None)
397+ Registry().register('renderer', mocked_renderer)
398+ service_manager.toolbar = OpenLPToolbar(None)
399+ service_manager.toolbar.add_toolbar_action('theme_combo_box', triggers=MagicMock())
400+ service_manager.toolbar.add_toolbar_action('theme_label', triggers=MagicMock())
401+
402+ # WHEN: The service manager has a Song theme
403+ mocked_renderer.theme_level = ThemeLevel.Song
404+ result = service_manager.theme_change()
405+
406+ # THEN: The the theme toolbar should be visible
407+ self.assertTrue(service_manager.toolbar.actions['theme_combo_box'].isVisible(),
408+ 'The visibility should be True')