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

Proposed by Tim Bentley
Status: Merged
Merged at revision: 2744
Proposed branch: lp:~trb143/openlp/themecleanup
Merge into: lp:openlp
Diff against target: 797 lines (+143/-340)
9 files modified
openlp/core/lib/theme.py (+21/-266)
openlp/core/ui/lib/pathedit.py (+4/-4)
openlp/core/ui/servicemanager.py (+1/-1)
openlp/core/ui/thememanager.py (+71/-34)
openlp/plugins/presentations/presentationplugin.py (+1/-1)
openlp/plugins/songusage/forms/songusagedetaildialog.py (+1/-1)
tests/functional/openlp_core_lib/test_renderer.py (+2/-2)
tests/functional/openlp_core_lib/test_theme.py (+36/-25)
tests/functional/openlp_core_ui/test_thememanager.py (+6/-6)
To merge this branch: bzr merge lp:~trb143/openlp/themecleanup
Reviewer Review Type Date Requested Status
Tomas Groth Approve
Phill Pending
Review via email: mp+324790@code.launchpad.net

This proposal supersedes a proposal from 2017-05-24.

Description of the change

Finally got round to finishing the Theme clean up from 2,2!

Themes now save to JSON and read XML or JSON so fully compatible with 2.4.

Add this to your merge proposal:
--------------------------------
lp:~trb143/openlp/themecleanup (revision 2743)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2036/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1946/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1875/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1255/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1108/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/237/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/83/
Stopping after failure

To post a comment you must log in.
Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

I have no idea why, but this patch fixes the pylint error: https://bin.snyman.info/mmmc7cmv

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

I presume at some point we will drop support for XML themes altogether? I'd like to see a TODO, comment suggesting when we can drop it. i.e. # TODO: xml theme loading code can be removed after OpenLP 2.8 has been released.

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

> I presume at some point we will drop support for XML themes altogether? I'd
> like to see a TODO, comment suggesting when we can drop it. i.e. # TODO: xml
> theme loading code can be removed after OpenLP 2.8 has been released.

But otherwise this looks good. Not tested tho!

Revision history for this message
Tomas Groth (tomasgroth) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/lib/theme.py'
2--- openlp/core/lib/theme.py 2017-05-12 21:05:50 +0000
3+++ openlp/core/lib/theme.py 2017-05-30 14:04:15 +0000
4@@ -26,7 +26,6 @@
5 import logging
6 import json
7
8-from xml.dom.minidom import Document
9 from lxml import etree, objectify
10 from openlp.core.common import AppLocation, de_hump
11
12@@ -150,7 +149,7 @@
13 'horizontal_align', 'vertical_align', 'wrap_style']
14
15
16-class ThemeXML(object):
17+class Theme(object):
18 """
19 A class to encapsulate the Theme XML.
20 """
21@@ -195,184 +194,6 @@
22 self.background_filename = self.background_filename.strip()
23 self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
24
25- def _new_document(self, name):
26- """
27- Create a new theme XML document.
28- """
29- self.theme_xml = Document()
30- self.theme = self.theme_xml.createElement('theme')
31- self.theme_xml.appendChild(self.theme)
32- self.theme.setAttribute('version', '2.0')
33- self.name = self.theme_xml.createElement('name')
34- text_node = self.theme_xml.createTextNode(name)
35- self.name.appendChild(text_node)
36- self.theme.appendChild(self.name)
37-
38- def add_background_transparent(self):
39- """
40- Add a transparent background.
41- """
42- background = self.theme_xml.createElement('background')
43- background.setAttribute('type', 'transparent')
44- self.theme.appendChild(background)
45-
46- def add_background_solid(self, bkcolor):
47- """
48- Add a Solid background.
49-
50- :param bkcolor: The color of the background.
51- """
52- background = self.theme_xml.createElement('background')
53- background.setAttribute('type', 'solid')
54- self.theme.appendChild(background)
55- self.child_element(background, 'color', str(bkcolor))
56-
57- def add_background_gradient(self, startcolor, endcolor, direction):
58- """
59- Add a gradient background.
60-
61- :param startcolor: The gradient's starting colour.
62- :param endcolor: The gradient's ending colour.
63- :param direction: The direction of the gradient.
64- """
65- background = self.theme_xml.createElement('background')
66- background.setAttribute('type', 'gradient')
67- self.theme.appendChild(background)
68- # Create startColor element
69- self.child_element(background, 'startColor', str(startcolor))
70- # Create endColor element
71- self.child_element(background, 'endColor', str(endcolor))
72- # Create direction element
73- self.child_element(background, 'direction', str(direction))
74-
75- def add_background_image(self, filename, border_color):
76- """
77- Add a image background.
78-
79- :param filename: The file name of the image.
80- :param border_color:
81- """
82- background = self.theme_xml.createElement('background')
83- background.setAttribute('type', 'image')
84- self.theme.appendChild(background)
85- # Create Filename element
86- self.child_element(background, 'filename', filename)
87- # Create endColor element
88- self.child_element(background, 'borderColor', str(border_color))
89-
90- def add_background_video(self, filename, border_color):
91- """
92- Add a video background.
93-
94- :param filename: The file name of the video.
95- :param border_color:
96- """
97- background = self.theme_xml.createElement('background')
98- background.setAttribute('type', 'video')
99- self.theme.appendChild(background)
100- # Create Filename element
101- self.child_element(background, 'filename', filename)
102- # Create endColor element
103- self.child_element(background, 'borderColor', str(border_color))
104-
105- def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
106- line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
107- outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
108- """
109- Add a Font.
110-
111- :param name: The name of the font.
112- :param color: The colour of the font.
113- :param size: The size of the font.
114- :param override: Whether or not to override the default positioning of the theme.
115- :param fonttype: The type of font, ``main`` or ``footer``. Defaults to ``main``.
116- :param bold:
117- :param italics: The weight of then font Defaults to 50 Normal
118- :param line_adjustment: Does the font render to italics Defaults to 0 Normal
119- :param xpos: The X position of the text block.
120- :param ypos: The Y position of the text block.
121- :param width: The width of the text block.
122- :param height: The height of the text block.
123- :param outline: Whether or not to show an outline.
124- :param outline_color: The colour of the outline.
125- :param outline_pixel: How big the Shadow is
126- :param shadow: Whether or not to show a shadow.
127- :param shadow_color: The colour of the shadow.
128- :param shadow_pixel: How big the Shadow is
129- """
130- background = self.theme_xml.createElement('font')
131- background.setAttribute('type', fonttype)
132- self.theme.appendChild(background)
133- # Create Font name element
134- self.child_element(background, 'name', name)
135- # Create Font color element
136- self.child_element(background, 'color', str(color))
137- # Create Proportion name element
138- self.child_element(background, 'size', str(size))
139- # Create weight name element
140- self.child_element(background, 'bold', str(bold))
141- # Create italics name element
142- self.child_element(background, 'italics', str(italics))
143- # Create indentation name element
144- self.child_element(background, 'line_adjustment', str(line_adjustment))
145- # Create Location element
146- element = self.theme_xml.createElement('location')
147- element.setAttribute('override', str(override))
148- element.setAttribute('x', str(xpos))
149- element.setAttribute('y', str(ypos))
150- element.setAttribute('width', str(width))
151- element.setAttribute('height', str(height))
152- background.appendChild(element)
153- # Shadow
154- element = self.theme_xml.createElement('shadow')
155- element.setAttribute('shadowColor', str(shadow_color))
156- element.setAttribute('shadowSize', str(shadow_pixel))
157- value = self.theme_xml.createTextNode(str(shadow))
158- element.appendChild(value)
159- background.appendChild(element)
160- # Outline
161- element = self.theme_xml.createElement('outline')
162- element.setAttribute('outlineColor', str(outline_color))
163- element.setAttribute('outlineSize', str(outline_pixel))
164- value = self.theme_xml.createTextNode(str(outline))
165- element.appendChild(value)
166- background.appendChild(element)
167-
168- def add_display(self, horizontal, vertical, transition):
169- """
170- Add a Display options.
171-
172- :param horizontal: The horizontal alignment of the text.
173- :param vertical: The vertical alignment of the text.
174- :param transition: Whether the slide transition is active.
175- """
176- background = self.theme_xml.createElement('display')
177- self.theme.appendChild(background)
178- # Horizontal alignment
179- element = self.theme_xml.createElement('horizontalAlign')
180- value = self.theme_xml.createTextNode(str(horizontal))
181- element.appendChild(value)
182- background.appendChild(element)
183- # Vertical alignment
184- element = self.theme_xml.createElement('verticalAlign')
185- value = self.theme_xml.createTextNode(str(vertical))
186- element.appendChild(value)
187- background.appendChild(element)
188- # Slide Transition
189- element = self.theme_xml.createElement('slideTransition')
190- value = self.theme_xml.createTextNode(str(transition))
191- element.appendChild(value)
192- background.appendChild(element)
193-
194- def child_element(self, element, tag, value):
195- """
196- Generic child element creator.
197- """
198- child = self.theme_xml.createElement(tag)
199- child.appendChild(self.theme_xml.createTextNode(value))
200- element.appendChild(child)
201- return child
202-
203 def set_default_header_footer(self):
204 """
205 Set the header and footer size into the current primary screen.
206@@ -386,25 +207,24 @@
207 self.font_footer_y = current_screen['size'].height() * 9 / 10
208 self.font_footer_height = current_screen['size'].height() / 10
209
210- def dump_xml(self):
211- """
212- Dump the XML to file used for debugging
213- """
214- return self.theme_xml.toprettyxml(indent=' ')
215-
216- def extract_xml(self):
217- """
218- Print out the XML string.
219- """
220- self._build_xml_from_attrs()
221- return self.theme_xml.toxml('utf-8').decode('utf-8')
222-
223- def extract_formatted_xml(self):
224- """
225- Pull out the XML string formatted for human consumption
226- """
227- self._build_xml_from_attrs()
228- return self.theme_xml.toprettyxml(indent=' ', newl='\n', encoding='utf-8')
229+ def load_theme(self, theme):
230+ """
231+ Convert the JSON file and expand it.
232+
233+ :param theme: the theme string
234+ """
235+ jsn = json.loads(theme)
236+ self.expand_json(jsn)
237+
238+ def export_theme(self):
239+ """
240+ Loop through the fields and build a dictionary of them
241+
242+ """
243+ theme_data = {}
244+ for attr, value in self.__dict__.items():
245+ theme_data["{attr}".format(attr=attr)] = value
246+ return json.dumps(theme_data)
247
248 def parse(self, xml):
249 """
250@@ -461,7 +281,8 @@
251 if element.tag == 'name':
252 self._create_attr('theme', element.tag, element.text)
253
254- def _translate_tags(self, master, element, value):
255+ @staticmethod
256+ def _translate_tags(master, element, value):
257 """
258 Clean up XML removing and redefining tags
259 """
260@@ -514,71 +335,5 @@
261 theme_strings = []
262 for key in dir(self):
263 if key[0:1] != '_':
264- # TODO: Due to bound methods returned, I don't know how to write a proper test
265 theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
266 return '\n'.join(theme_strings)
267-
268- def _build_xml_from_attrs(self):
269- """
270- Build the XML from the varables in the object
271- """
272- self._new_document(self.theme_name)
273- if self.background_type == BackgroundType.to_string(BackgroundType.Solid):
274- self.add_background_solid(self.background_color)
275- elif self.background_type == BackgroundType.to_string(BackgroundType.Gradient):
276- self.add_background_gradient(
277- self.background_start_color,
278- self.background_end_color,
279- self.background_direction
280- )
281- elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
282- filename = os.path.split(self.background_filename)[1]
283- self.add_background_image(filename, self.background_border_color)
284- elif self.background_type == BackgroundType.to_string(BackgroundType.Video):
285- filename = os.path.split(self.background_filename)[1]
286- self.add_background_video(filename, self.background_border_color)
287- elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
288- self.add_background_transparent()
289- self.add_font(
290- self.font_main_name,
291- self.font_main_color,
292- self.font_main_size,
293- self.font_main_override, 'main',
294- self.font_main_bold,
295- self.font_main_italics,
296- self.font_main_line_adjustment,
297- self.font_main_x,
298- self.font_main_y,
299- self.font_main_width,
300- self.font_main_height,
301- self.font_main_outline,
302- self.font_main_outline_color,
303- self.font_main_outline_size,
304- self.font_main_shadow,
305- self.font_main_shadow_color,
306- self.font_main_shadow_size
307- )
308- self.add_font(
309- self.font_footer_name,
310- self.font_footer_color,
311- self.font_footer_size,
312- self.font_footer_override, 'footer',
313- self.font_footer_bold,
314- self.font_footer_italics,
315- 0, # line adjustment
316- self.font_footer_x,
317- self.font_footer_y,
318- self.font_footer_width,
319- self.font_footer_height,
320- self.font_footer_outline,
321- self.font_footer_outline_color,
322- self.font_footer_outline_size,
323- self.font_footer_shadow,
324- self.font_footer_shadow_color,
325- self.font_footer_shadow_size
326- )
327- self.add_display(
328- self.display_horizontal_align,
329- self.display_vertical_align,
330- self.display_slide_transition
331- )
332
333=== modified file 'openlp/core/ui/lib/pathedit.py'
334--- openlp/core/ui/lib/pathedit.py 2017-05-22 18:22:43 +0000
335+++ openlp/core/ui/lib/pathedit.py 2017-05-30 14:04:15 +0000
336@@ -46,16 +46,16 @@
337
338 :param parent: The parent of the widget. This is just passed to the super method.
339 :type parent: QWidget or None
340-
341+
342 :param dialog_caption: Used to customise the caption in the QFileDialog.
343 :param dialog_caption: str
344-
345+
346 :param default_path: The default path. This is set as the path when the revert button is clicked
347 :type default_path: str
348
349 :param show_revert: Used to determin if the 'revert button' should be visible.
350 :type show_revert: bool
351-
352+
353 :return: None
354 :rtype: None
355 """
356@@ -72,7 +72,7 @@
357 Set up the widget
358 :param show_revert: Show or hide the revert button
359 :type show_revert: bool
360-
361+
362 :return: None
363 :rtype: None
364 """
365
366=== modified file 'openlp/core/ui/servicemanager.py'
367--- openlp/core/ui/servicemanager.py 2016-12-31 11:01:36 +0000
368+++ openlp/core/ui/servicemanager.py 2017-05-30 14:04:15 +0000
369@@ -698,7 +698,7 @@
370 translate('OpenLP.ServiceManager',
371 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
372 else:
373- file_name, filter_uesd = QtWidgets.QFileDialog.getSaveFileName(
374+ file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
375 self.main_window, UiStrings().SaveService, path,
376 translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
377 if not file_name:
378
379=== modified file 'openlp/core/ui/thememanager.py'
380--- openlp/core/ui/thememanager.py 2016-12-31 11:01:36 +0000
381+++ openlp/core/ui/thememanager.py 2017-05-30 14:04:15 +0000
382@@ -22,6 +22,7 @@
383 """
384 The Theme Manager manages adding, deleteing and modifying of themes.
385 """
386+import json
387 import os
388 import zipfile
389 import shutil
390@@ -33,7 +34,7 @@
391 check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file
392 from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \
393 check_item_selected, create_thumb, validate_thumb
394-from openlp.core.lib.theme import ThemeXML, BackgroundType
395+from openlp.core.lib.theme import Theme, BackgroundType
396 from openlp.core.lib.ui import critical_error_message_box, create_widget_action
397 from openlp.core.ui import FileRenameForm, ThemeForm
398 from openlp.core.ui.lib import OpenLPToolbar
399@@ -245,7 +246,7 @@
400 their customisations.
401 :param field:
402 """
403- theme = ThemeXML()
404+ theme = Theme()
405 theme.set_default_header_footer()
406 self.theme_form.theme = theme
407 self.theme_form.exec()
408@@ -378,11 +379,12 @@
409 critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
410 return
411 theme = item.data(QtCore.Qt.UserRole)
412- path = QtWidgets.QFileDialog.getExistingDirectory(self,
413- translate('OpenLP.ThemeManager',
414- 'Save Theme - ({name})').format(name=theme),
415- Settings().value(self.settings_section +
416- '/last directory export'))
417+ path, filter_used = \
418+ QtWidgets.QFileDialog.getSaveFileName(self.main_window,
419+ translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
420+ format(name=theme),
421+ Settings().value(self.settings_section + '/last directory export'),
422+ translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
423 self.application.set_busy_cursor()
424 if path:
425 Settings().setValue(self.settings_section + '/last directory export', path)
426@@ -393,13 +395,12 @@
427 'Your theme has been successfully exported.'))
428 self.application.set_normal_cursor()
429
430- def _export_theme(self, path, theme):
431+ def _export_theme(self, theme_path, theme):
432 """
433 Create the zipfile with the theme contents.
434- :param path: Location where the zip file will be placed
435+ :param theme_path: Location where the zip file will be placed
436 :param theme: The name of the theme to be exported
437 """
438- theme_path = os.path.join(path, theme + '.otz')
439 theme_zip = None
440 try:
441 theme_zip = zipfile.ZipFile(theme_path, 'w')
442@@ -452,7 +453,7 @@
443 files = AppLocation.get_files(self.settings_section, '.png')
444 # No themes have been found so create one
445 if not files:
446- theme = ThemeXML()
447+ theme = Theme()
448 theme.theme_name = UiStrings().Default
449 self._write_theme(theme, None, None)
450 Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
451@@ -505,19 +506,27 @@
452
453 def get_theme_data(self, theme_name):
454 """
455- Returns a theme object from an XML file
456+ Returns a theme object from an XML or JSON file
457
458 :param theme_name: Name of the theme to load from file
459 :return: The theme object.
460 """
461 self.log_debug('get theme data for theme {name}'.format(name=theme_name))
462- xml_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
463- xml = get_text_file_string(xml_file)
464- if not xml:
465+ theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json')
466+ theme_data = get_text_file_string(theme_file)
467+ jsn = True
468+ if not theme_data:
469+ theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
470+ theme_data = get_text_file_string(theme_file)
471+ jsn = False
472+ if not theme_data:
473 self.log_debug('No theme data - using default theme')
474- return ThemeXML()
475+ return Theme()
476 else:
477- return self._create_theme_from_xml(xml, self.path)
478+ if jsn:
479+ return self._create_theme_from_json(theme_data, self.path)
480+ else:
481+ return self._create_theme_from_xml(theme_data, self.path)
482
483 def over_write_message_box(self, theme_name):
484 """
485@@ -547,18 +556,28 @@
486 out_file = None
487 file_xml = None
488 abort_import = True
489+ json_theme = False
490+ theme_name = ""
491 try:
492 theme_zip = zipfile.ZipFile(file_name)
493- xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
494- if len(xml_file) != 1:
495- self.log_error('Theme contains "{val:d}" XML files'.format(val=len(xml_file)))
496- raise ValidationError
497- xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
498- theme_version = xml_tree.get('version', default=None)
499- if not theme_version or float(theme_version) < 2.0:
500- self.log_error('Theme version is less than 2.0')
501- raise ValidationError
502- theme_name = xml_tree.find('name').text.strip()
503+ json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
504+ if len(json_file) != 1:
505+ # TODO: remove XML handling at some point but would need a auto conversion to run first.
506+ xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
507+ if len(xml_file) != 1:
508+ self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
509+ raise ValidationError
510+ xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
511+ theme_version = xml_tree.get('version', default=None)
512+ if not theme_version or float(theme_version) < 2.0:
513+ self.log_error('Theme version is less than 2.0')
514+ raise ValidationError
515+ theme_name = xml_tree.find('name').text.strip()
516+ else:
517+ new_theme = Theme()
518+ new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
519+ theme_name = new_theme.theme_name
520+ json_theme = True
521 theme_folder = os.path.join(directory, theme_name)
522 theme_exists = os.path.exists(theme_folder)
523 if theme_exists and not self.over_write_message_box(theme_name):
524@@ -574,7 +593,7 @@
525 continue
526 full_name = os.path.join(directory, out_name)
527 check_directory_exists(os.path.dirname(full_name))
528- if os.path.splitext(name)[1].lower() == '.xml':
529+ if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
530 file_xml = str(theme_zip.read(name), 'utf-8')
531 out_file = open(full_name, 'w', encoding='utf-8')
532 out_file.write(file_xml)
533@@ -597,7 +616,10 @@
534 if not abort_import:
535 # As all files are closed, we can create the Theme.
536 if file_xml:
537- theme = self._create_theme_from_xml(file_xml, self.path)
538+ if json_theme:
539+ theme = self._create_theme_from_json(file_xml, self.path)
540+ else:
541+ theme = self._create_theme_from_xml(file_xml, self.path)
542 self.generate_and_save_image(theme_name, theme)
543 # Only show the error message, when IOError was not raised (in
544 # this case the error message has already been shown).
545@@ -646,16 +668,16 @@
546 :param image_to: Where the Theme Image is to be saved to
547 """
548 name = theme.theme_name
549- theme_pretty_xml = theme.extract_formatted_xml()
550+ theme_pretty = theme.export_theme()
551 theme_dir = os.path.join(self.path, name)
552 check_directory_exists(theme_dir)
553- theme_file = os.path.join(theme_dir, name + '.xml')
554+ theme_file = os.path.join(theme_dir, name + '.json')
555 if self.old_background_image and image_to != self.old_background_image:
556 delete_file(self.old_background_image)
557 out_file = None
558 try:
559 out_file = open(theme_file, 'w', encoding='utf-8')
560- out_file.write(theme_pretty_xml.decode('utf-8'))
561+ out_file.write(theme_pretty)
562 except IOError:
563 self.log_exception('Saving theme to file failed')
564 finally:
565@@ -717,7 +739,8 @@
566 """
567 return os.path.join(self.path, theme + '.png')
568
569- def _create_theme_from_xml(self, theme_xml, image_path):
570+ @staticmethod
571+ def _create_theme_from_xml(theme_xml, image_path):
572 """
573 Return a theme object using information parsed from XML
574
575@@ -725,11 +748,25 @@
576 :param image_path: Where the theme image is stored
577 :return: Theme data.
578 """
579- theme = ThemeXML()
580+ theme = Theme()
581 theme.parse(theme_xml)
582 theme.extend_image_filename(image_path)
583 return theme
584
585+ @staticmethod
586+ def _create_theme_from_json(theme_json, image_path):
587+ """
588+ Return a theme object using information parsed from JSON
589+
590+ :param theme_json: The Theme data object.
591+ :param image_path: Where the theme image is stored
592+ :return: Theme data.
593+ """
594+ theme = Theme()
595+ theme.load_theme(theme_json)
596+ theme.extend_image_filename(image_path)
597+ return theme
598+
599 def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True):
600 """
601 Check to see if theme has been selected and the destructive action is allowed.
602
603=== modified file 'openlp/plugins/presentations/presentationplugin.py'
604--- openlp/plugins/presentations/presentationplugin.py 2017-05-22 18:27:40 +0000
605+++ openlp/plugins/presentations/presentationplugin.py 2017-05-30 14:04:15 +0000
606@@ -1,4 +1,4 @@
607- # -*- coding: utf-8 -*-
608+# -*- coding: utf-8 -*-
609 # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
610
611 ###############################################################################
612
613=== modified file 'openlp/plugins/songusage/forms/songusagedetaildialog.py'
614--- openlp/plugins/songusage/forms/songusagedetaildialog.py 2017-05-22 18:22:43 +0000
615+++ openlp/plugins/songusage/forms/songusagedetaildialog.py 2017-05-30 14:04:15 +0000
616@@ -69,7 +69,7 @@
617 self.file_horizontal_layout.setSpacing(8)
618 self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
619 self.file_horizontal_layout.setObjectName('file_horizontal_layout')
620- self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)
621+ self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False)
622 self.file_horizontal_layout.addWidget(self.report_path_edit)
623 self.vertical_layout.addWidget(self.file_group_box)
624 self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])
625
626=== modified file 'tests/functional/openlp_core_lib/test_renderer.py'
627--- tests/functional/openlp_core_lib/test_renderer.py 2017-04-24 05:17:55 +0000
628+++ tests/functional/openlp_core_lib/test_renderer.py 2017-05-30 14:04:15 +0000
629@@ -30,7 +30,7 @@
630 from openlp.core.common import Registry
631 from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
632 from openlp.core.lib.renderer import words_split, get_start_tags
633-from openlp.core.lib.theme import ThemeXML
634+from openlp.core.lib.theme import Theme
635
636
637 SCREEN = {
638@@ -189,7 +189,7 @@
639 # GIVEN: test object and data
640 mock_lyrics_css.return_value = ' FORMAT CSS; '
641 mock_outline_css.return_value = ' OUTLINE CSS; '
642- theme_data = ThemeXML()
643+ theme_data = Theme()
644 theme_data.font_main_name = 'Arial'
645 theme_data.font_main_size = 20
646 theme_data.font_main_color = '#FFFFFF'
647
648=== modified file 'tests/functional/openlp_core_lib/test_theme.py'
649--- tests/functional/openlp_core_lib/test_theme.py 2016-12-31 11:01:36 +0000
650+++ tests/functional/openlp_core_lib/test_theme.py 2017-05-30 14:04:15 +0000
651@@ -25,36 +25,30 @@
652 from unittest import TestCase
653 import os
654
655-from openlp.core.lib.theme import ThemeXML
656-
657-
658-class TestThemeXML(TestCase):
659+from openlp.core.lib.theme import Theme
660+
661+
662+class TestTheme(TestCase):
663 """
664- Test the ThemeXML class
665+ Test the Theme class
666 """
667 def test_new_theme(self):
668 """
669- Test the ThemeXML constructor
670+ Test the Theme constructor
671 """
672- # GIVEN: The ThemeXML class
673+ # GIVEN: The Theme class
674 # WHEN: A theme object is created
675- default_theme = ThemeXML()
676+ default_theme = Theme()
677
678 # THEN: The default values should be correct
679- self.assertEqual('#000000', default_theme.background_border_color,
680- 'background_border_color should be "#000000"')
681- self.assertEqual('solid', default_theme.background_type, 'background_type should be "solid"')
682- self.assertEqual(0, default_theme.display_vertical_align, 'display_vertical_align should be 0')
683- self.assertEqual('Arial', default_theme.font_footer_name, 'font_footer_name should be "Arial"')
684- self.assertFalse(default_theme.font_main_bold, 'font_main_bold should be False')
685- self.assertEqual(47, len(default_theme.__dict__), 'The theme should have 47 attributes')
686+ self.check_theme(default_theme)
687
688 def test_expand_json(self):
689 """
690 Test the expand_json method
691 """
692- # GIVEN: A ThemeXML object and some JSON to "expand"
693- theme = ThemeXML()
694+ # GIVEN: A Theme object and some JSON to "expand"
695+ theme = Theme()
696 theme_json = {
697 'background': {
698 'border_color': '#000000',
699@@ -73,31 +67,48 @@
700 }
701 }
702
703- # WHEN: ThemeXML.expand_json() is run
704+ # WHEN: Theme.expand_json() is run
705 theme.expand_json(theme_json)
706
707 # THEN: The attributes should be set on the object
708- self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
709- self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
710- self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
711- self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
712- self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
713+ self.check_theme(theme)
714
715 def test_extend_image_filename(self):
716 """
717 Test the extend_image_filename method
718 """
719 # GIVEN: A theme object
720- theme = ThemeXML()
721+ theme = Theme()
722 theme.theme_name = 'MyBeautifulTheme '
723 theme.background_filename = ' video.mp4'
724 theme.background_type = 'video'
725 path = os.path.expanduser('~')
726
727- # WHEN: ThemeXML.extend_image_filename is run
728+ # WHEN: Theme.extend_image_filename is run
729 theme.extend_image_filename(path)
730
731 # THEN: The filename of the background should be correct
732 expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')
733 self.assertEqual(expected_filename, theme.background_filename)
734 self.assertEqual('MyBeautifulTheme', theme.theme_name)
735+
736+ def test_save_retrieve(self):
737+ """
738+ Load a dummy theme, save it and reload it
739+ """
740+ # GIVEN: The default Theme class
741+ # WHEN: A theme object is created
742+ default_theme = Theme()
743+ # THEN: The default values should be correct
744+ save_theme_json = default_theme.export_theme()
745+ lt = Theme()
746+ lt.load_theme(save_theme_json)
747+ self.check_theme(lt)
748+
749+ def check_theme(self, theme):
750+ self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
751+ self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
752+ self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
753+ self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
754+ self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
755+ self.assertEqual(47, len(theme.__dict__), 'The theme should have 47 attributes')
756
757=== modified file 'tests/functional/openlp_core_ui/test_thememanager.py'
758--- tests/functional/openlp_core_ui/test_thememanager.py 2017-05-08 19:04:14 +0000
759+++ tests/functional/openlp_core_ui/test_thememanager.py 2017-05-30 14:04:15 +0000
760@@ -63,7 +63,7 @@
761 mocked_zipfile_init.return_value = None
762
763 # WHEN: The theme is exported
764- theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
765+ theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default')
766
767 # THEN: The zipfile should be created at the given path
768 mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
769@@ -126,8 +126,9 @@
770 theme_manager.path = ''
771 mocked_theme = MagicMock()
772 mocked_theme.theme_name = 'themename'
773- mocked_theme.extract_formatted_xml = MagicMock()
774- mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
775+ mocked_theme.filename = "filename"
776+ # mocked_theme.extract_formatted_xml = MagicMock()
777+ # mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
778
779 # WHEN: Calling _write_theme with path to different images
780 file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
781@@ -148,14 +149,13 @@
782 theme_manager.path = self.temp_folder
783 mocked_theme = MagicMock()
784 mocked_theme.theme_name = 'theme 愛 name'
785- mocked_theme.extract_formatted_xml = MagicMock()
786- mocked_theme.extract_formatted_xml.return_value = 'fake theme 愛 XML'.encode()
787+ mocked_theme.export_theme.return_value = "{}"
788
789 # WHEN: Calling _write_theme with a theme with a name with special characters in it
790 theme_manager._write_theme(mocked_theme, None, None)
791
792 # THEN: It should have been created
793- self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.xml')),
794+ self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')),
795 'Theme with special characters should have been created!')
796
797 def test_over_write_message_box_yes(self):