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

Proposed by Tim Bentley
Status: Superseded
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
Phill Needs Information
Tomas Groth Needs Fixing
Review via email: mp+324564@code.launchpad.net

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

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.

 lp:~trb143/openlp/themecleanup (revision 2742)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2028/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1938/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1869/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1249/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1104/
[FAILURE] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/233/
Stopping after failure

openlp/core/ui/thememanager.py:576: [E0601(used-before-assignment), ThemeManager.unzip_theme] Using variable 'theme' before assignment

 theme = Theme()
 theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))

who has been feeding Jenkins bad ideas!

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

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 :

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 :

> 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!

lp:~trb143/openlp/themecleanup updated
2743. By Tim Bentley

Minor tweeks

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py 2017-05-12 21:05:50 +0000
+++ openlp/core/lib/theme.py 2017-05-30 13:56:07 +0000
@@ -26,7 +26,6 @@
26import logging26import logging
27import json27import json
2828
29from xml.dom.minidom import Document
30from lxml import etree, objectify29from lxml import etree, objectify
31from openlp.core.common import AppLocation, de_hump30from openlp.core.common import AppLocation, de_hump
3231
@@ -150,7 +149,7 @@
150 'horizontal_align', 'vertical_align', 'wrap_style']149 'horizontal_align', 'vertical_align', 'wrap_style']
151150
152151
153class ThemeXML(object):152class Theme(object):
154 """153 """
155 A class to encapsulate the Theme XML.154 A class to encapsulate the Theme XML.
156 """155 """
@@ -195,184 +194,6 @@
195 self.background_filename = self.background_filename.strip()194 self.background_filename = self.background_filename.strip()
196 self.background_filename = os.path.join(path, self.theme_name, self.background_filename)195 self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
197196
198 def _new_document(self, name):
199 """
200 Create a new theme XML document.
201 """
202 self.theme_xml = Document()
203 self.theme = self.theme_xml.createElement('theme')
204 self.theme_xml.appendChild(self.theme)
205 self.theme.setAttribute('version', '2.0')
206 self.name = self.theme_xml.createElement('name')
207 text_node = self.theme_xml.createTextNode(name)
208 self.name.appendChild(text_node)
209 self.theme.appendChild(self.name)
210
211 def add_background_transparent(self):
212 """
213 Add a transparent background.
214 """
215 background = self.theme_xml.createElement('background')
216 background.setAttribute('type', 'transparent')
217 self.theme.appendChild(background)
218
219 def add_background_solid(self, bkcolor):
220 """
221 Add a Solid background.
222
223 :param bkcolor: The color of the background.
224 """
225 background = self.theme_xml.createElement('background')
226 background.setAttribute('type', 'solid')
227 self.theme.appendChild(background)
228 self.child_element(background, 'color', str(bkcolor))
229
230 def add_background_gradient(self, startcolor, endcolor, direction):
231 """
232 Add a gradient background.
233
234 :param startcolor: The gradient's starting colour.
235 :param endcolor: The gradient's ending colour.
236 :param direction: The direction of the gradient.
237 """
238 background = self.theme_xml.createElement('background')
239 background.setAttribute('type', 'gradient')
240 self.theme.appendChild(background)
241 # Create startColor element
242 self.child_element(background, 'startColor', str(startcolor))
243 # Create endColor element
244 self.child_element(background, 'endColor', str(endcolor))
245 # Create direction element
246 self.child_element(background, 'direction', str(direction))
247
248 def add_background_image(self, filename, border_color):
249 """
250 Add a image background.
251
252 :param filename: The file name of the image.
253 :param border_color:
254 """
255 background = self.theme_xml.createElement('background')
256 background.setAttribute('type', 'image')
257 self.theme.appendChild(background)
258 # Create Filename element
259 self.child_element(background, 'filename', filename)
260 # Create endColor element
261 self.child_element(background, 'borderColor', str(border_color))
262
263 def add_background_video(self, filename, border_color):
264 """
265 Add a video background.
266
267 :param filename: The file name of the video.
268 :param border_color:
269 """
270 background = self.theme_xml.createElement('background')
271 background.setAttribute('type', 'video')
272 self.theme.appendChild(background)
273 # Create Filename element
274 self.child_element(background, 'filename', filename)
275 # Create endColor element
276 self.child_element(background, 'borderColor', str(border_color))
277
278 def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
279 line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
280 outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
281 """
282 Add a Font.
283
284 :param name: The name of the font.
285 :param color: The colour of the font.
286 :param size: The size of the font.
287 :param override: Whether or not to override the default positioning of the theme.
288 :param fonttype: The type of font, ``main`` or ``footer``. Defaults to ``main``.
289 :param bold:
290 :param italics: The weight of then font Defaults to 50 Normal
291 :param line_adjustment: Does the font render to italics Defaults to 0 Normal
292 :param xpos: The X position of the text block.
293 :param ypos: The Y position of the text block.
294 :param width: The width of the text block.
295 :param height: The height of the text block.
296 :param outline: Whether or not to show an outline.
297 :param outline_color: The colour of the outline.
298 :param outline_pixel: How big the Shadow is
299 :param shadow: Whether or not to show a shadow.
300 :param shadow_color: The colour of the shadow.
301 :param shadow_pixel: How big the Shadow is
302 """
303 background = self.theme_xml.createElement('font')
304 background.setAttribute('type', fonttype)
305 self.theme.appendChild(background)
306 # Create Font name element
307 self.child_element(background, 'name', name)
308 # Create Font color element
309 self.child_element(background, 'color', str(color))
310 # Create Proportion name element
311 self.child_element(background, 'size', str(size))
312 # Create weight name element
313 self.child_element(background, 'bold', str(bold))
314 # Create italics name element
315 self.child_element(background, 'italics', str(italics))
316 # Create indentation name element
317 self.child_element(background, 'line_adjustment', str(line_adjustment))
318 # Create Location element
319 element = self.theme_xml.createElement('location')
320 element.setAttribute('override', str(override))
321 element.setAttribute('x', str(xpos))
322 element.setAttribute('y', str(ypos))
323 element.setAttribute('width', str(width))
324 element.setAttribute('height', str(height))
325 background.appendChild(element)
326 # Shadow
327 element = self.theme_xml.createElement('shadow')
328 element.setAttribute('shadowColor', str(shadow_color))
329 element.setAttribute('shadowSize', str(shadow_pixel))
330 value = self.theme_xml.createTextNode(str(shadow))
331 element.appendChild(value)
332 background.appendChild(element)
333 # Outline
334 element = self.theme_xml.createElement('outline')
335 element.setAttribute('outlineColor', str(outline_color))
336 element.setAttribute('outlineSize', str(outline_pixel))
337 value = self.theme_xml.createTextNode(str(outline))
338 element.appendChild(value)
339 background.appendChild(element)
340
341 def add_display(self, horizontal, vertical, transition):
342 """
343 Add a Display options.
344
345 :param horizontal: The horizontal alignment of the text.
346 :param vertical: The vertical alignment of the text.
347 :param transition: Whether the slide transition is active.
348 """
349 background = self.theme_xml.createElement('display')
350 self.theme.appendChild(background)
351 # Horizontal alignment
352 element = self.theme_xml.createElement('horizontalAlign')
353 value = self.theme_xml.createTextNode(str(horizontal))
354 element.appendChild(value)
355 background.appendChild(element)
356 # Vertical alignment
357 element = self.theme_xml.createElement('verticalAlign')
358 value = self.theme_xml.createTextNode(str(vertical))
359 element.appendChild(value)
360 background.appendChild(element)
361 # Slide Transition
362 element = self.theme_xml.createElement('slideTransition')
363 value = self.theme_xml.createTextNode(str(transition))
364 element.appendChild(value)
365 background.appendChild(element)
366
367 def child_element(self, element, tag, value):
368 """
369 Generic child element creator.
370 """
371 child = self.theme_xml.createElement(tag)
372 child.appendChild(self.theme_xml.createTextNode(value))
373 element.appendChild(child)
374 return child
375
376 def set_default_header_footer(self):197 def set_default_header_footer(self):
377 """198 """
378 Set the header and footer size into the current primary screen.199 Set the header and footer size into the current primary screen.
@@ -386,25 +207,24 @@
386 self.font_footer_y = current_screen['size'].height() * 9 / 10207 self.font_footer_y = current_screen['size'].height() * 9 / 10
387 self.font_footer_height = current_screen['size'].height() / 10208 self.font_footer_height = current_screen['size'].height() / 10
388209
389 def dump_xml(self):210 def load_theme(self, theme):
390 """211 """
391 Dump the XML to file used for debugging212 Convert the JSON file and expand it.
392 """213
393 return self.theme_xml.toprettyxml(indent=' ')214 :param theme: the theme string
394215 """
395 def extract_xml(self):216 jsn = json.loads(theme)
396 """217 self.expand_json(jsn)
397 Print out the XML string.218
398 """219 def export_theme(self):
399 self._build_xml_from_attrs()220 """
400 return self.theme_xml.toxml('utf-8').decode('utf-8')221 Loop through the fields and build a dictionary of them
401222
402 def extract_formatted_xml(self):223 """
403 """224 theme_data = {}
404 Pull out the XML string formatted for human consumption225 for attr, value in self.__dict__.items():
405 """226 theme_data["{attr}".format(attr=attr)] = value
406 self._build_xml_from_attrs()227 return json.dumps(theme_data)
407 return self.theme_xml.toprettyxml(indent=' ', newl='\n', encoding='utf-8')
408228
409 def parse(self, xml):229 def parse(self, xml):
410 """230 """
@@ -461,7 +281,8 @@
461 if element.tag == 'name':281 if element.tag == 'name':
462 self._create_attr('theme', element.tag, element.text)282 self._create_attr('theme', element.tag, element.text)
463283
464 def _translate_tags(self, master, element, value):284 @staticmethod
285 def _translate_tags(master, element, value):
465 """286 """
466 Clean up XML removing and redefining tags287 Clean up XML removing and redefining tags
467 """288 """
@@ -514,71 +335,5 @@
514 theme_strings = []335 theme_strings = []
515 for key in dir(self):336 for key in dir(self):
516 if key[0:1] != '_':337 if key[0:1] != '_':
517 # TODO: Due to bound methods returned, I don't know how to write a proper test
518 theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))338 theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
519 return '\n'.join(theme_strings)339 return '\n'.join(theme_strings)
520
521 def _build_xml_from_attrs(self):
522 """
523 Build the XML from the varables in the object
524 """
525 self._new_document(self.theme_name)
526 if self.background_type == BackgroundType.to_string(BackgroundType.Solid):
527 self.add_background_solid(self.background_color)
528 elif self.background_type == BackgroundType.to_string(BackgroundType.Gradient):
529 self.add_background_gradient(
530 self.background_start_color,
531 self.background_end_color,
532 self.background_direction
533 )
534 elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
535 filename = os.path.split(self.background_filename)[1]
536 self.add_background_image(filename, self.background_border_color)
537 elif self.background_type == BackgroundType.to_string(BackgroundType.Video):
538 filename = os.path.split(self.background_filename)[1]
539 self.add_background_video(filename, self.background_border_color)
540 elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
541 self.add_background_transparent()
542 self.add_font(
543 self.font_main_name,
544 self.font_main_color,
545 self.font_main_size,
546 self.font_main_override, 'main',
547 self.font_main_bold,
548 self.font_main_italics,
549 self.font_main_line_adjustment,
550 self.font_main_x,
551 self.font_main_y,
552 self.font_main_width,
553 self.font_main_height,
554 self.font_main_outline,
555 self.font_main_outline_color,
556 self.font_main_outline_size,
557 self.font_main_shadow,
558 self.font_main_shadow_color,
559 self.font_main_shadow_size
560 )
561 self.add_font(
562 self.font_footer_name,
563 self.font_footer_color,
564 self.font_footer_size,
565 self.font_footer_override, 'footer',
566 self.font_footer_bold,
567 self.font_footer_italics,
568 0, # line adjustment
569 self.font_footer_x,
570 self.font_footer_y,
571 self.font_footer_width,
572 self.font_footer_height,
573 self.font_footer_outline,
574 self.font_footer_outline_color,
575 self.font_footer_outline_size,
576 self.font_footer_shadow,
577 self.font_footer_shadow_color,
578 self.font_footer_shadow_size
579 )
580 self.add_display(
581 self.display_horizontal_align,
582 self.display_vertical_align,
583 self.display_slide_transition
584 )
585340
=== modified file 'openlp/core/ui/lib/pathedit.py'
--- openlp/core/ui/lib/pathedit.py 2017-05-22 18:22:43 +0000
+++ openlp/core/ui/lib/pathedit.py 2017-05-30 13:56:07 +0000
@@ -46,16 +46,16 @@
4646
47 :param parent: The parent of the widget. This is just passed to the super method.47 :param parent: The parent of the widget. This is just passed to the super method.
48 :type parent: QWidget or None48 :type parent: QWidget or None
49 49
50 :param dialog_caption: Used to customise the caption in the QFileDialog.50 :param dialog_caption: Used to customise the caption in the QFileDialog.
51 :param dialog_caption: str51 :param dialog_caption: str
52 52
53 :param default_path: The default path. This is set as the path when the revert button is clicked53 :param default_path: The default path. This is set as the path when the revert button is clicked
54 :type default_path: str54 :type default_path: str
5555
56 :param show_revert: Used to determin if the 'revert button' should be visible.56 :param show_revert: Used to determin if the 'revert button' should be visible.
57 :type show_revert: bool57 :type show_revert: bool
58 58
59 :return: None59 :return: None
60 :rtype: None60 :rtype: None
61 """61 """
@@ -72,7 +72,7 @@
72 Set up the widget72 Set up the widget
73 :param show_revert: Show or hide the revert button73 :param show_revert: Show or hide the revert button
74 :type show_revert: bool74 :type show_revert: bool
75 75
76 :return: None76 :return: None
77 :rtype: None77 :rtype: None
78 """78 """
7979
=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py 2016-12-31 11:01:36 +0000
+++ openlp/core/ui/servicemanager.py 2017-05-30 13:56:07 +0000
@@ -698,7 +698,7 @@
698 translate('OpenLP.ServiceManager',698 translate('OpenLP.ServiceManager',
699 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))699 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
700 else:700 else:
701 file_name, filter_uesd = QtWidgets.QFileDialog.getSaveFileName(701 file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
702 self.main_window, UiStrings().SaveService, path,702 self.main_window, UiStrings().SaveService, path,
703 translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))703 translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
704 if not file_name:704 if not file_name:
705705
=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py 2016-12-31 11:01:36 +0000
+++ openlp/core/ui/thememanager.py 2017-05-30 13:56:07 +0000
@@ -22,6 +22,7 @@
22"""22"""
23The Theme Manager manages adding, deleteing and modifying of themes.23The Theme Manager manages adding, deleteing and modifying of themes.
24"""24"""
25import json
25import os26import os
26import zipfile27import zipfile
27import shutil28import shutil
@@ -33,7 +34,7 @@
33 check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file34 check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file
34from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \35from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \
35 check_item_selected, create_thumb, validate_thumb36 check_item_selected, create_thumb, validate_thumb
36from openlp.core.lib.theme import ThemeXML, BackgroundType37from openlp.core.lib.theme import Theme, BackgroundType
37from openlp.core.lib.ui import critical_error_message_box, create_widget_action38from openlp.core.lib.ui import critical_error_message_box, create_widget_action
38from openlp.core.ui import FileRenameForm, ThemeForm39from openlp.core.ui import FileRenameForm, ThemeForm
39from openlp.core.ui.lib import OpenLPToolbar40from openlp.core.ui.lib import OpenLPToolbar
@@ -245,7 +246,7 @@
245 their customisations.246 their customisations.
246 :param field:247 :param field:
247 """248 """
248 theme = ThemeXML()249 theme = Theme()
249 theme.set_default_header_footer()250 theme.set_default_header_footer()
250 self.theme_form.theme = theme251 self.theme_form.theme = theme
251 self.theme_form.exec()252 self.theme_form.exec()
@@ -378,11 +379,12 @@
378 critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))379 critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
379 return380 return
380 theme = item.data(QtCore.Qt.UserRole)381 theme = item.data(QtCore.Qt.UserRole)
381 path = QtWidgets.QFileDialog.getExistingDirectory(self,382 path, filter_used = \
382 translate('OpenLP.ThemeManager',383 QtWidgets.QFileDialog.getSaveFileName(self.main_window,
383 'Save Theme - ({name})').format(name=theme),384 translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
384 Settings().value(self.settings_section +385 format(name=theme),
385 '/last directory export'))386 Settings().value(self.settings_section + '/last directory export'),
387 translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
386 self.application.set_busy_cursor()388 self.application.set_busy_cursor()
387 if path:389 if path:
388 Settings().setValue(self.settings_section + '/last directory export', path)390 Settings().setValue(self.settings_section + '/last directory export', path)
@@ -393,13 +395,12 @@
393 'Your theme has been successfully exported.'))395 'Your theme has been successfully exported.'))
394 self.application.set_normal_cursor()396 self.application.set_normal_cursor()
395397
396 def _export_theme(self, path, theme):398 def _export_theme(self, theme_path, theme):
397 """399 """
398 Create the zipfile with the theme contents.400 Create the zipfile with the theme contents.
399 :param path: Location where the zip file will be placed401 :param theme_path: Location where the zip file will be placed
400 :param theme: The name of the theme to be exported402 :param theme: The name of the theme to be exported
401 """403 """
402 theme_path = os.path.join(path, theme + '.otz')
403 theme_zip = None404 theme_zip = None
404 try:405 try:
405 theme_zip = zipfile.ZipFile(theme_path, 'w')406 theme_zip = zipfile.ZipFile(theme_path, 'w')
@@ -452,7 +453,7 @@
452 files = AppLocation.get_files(self.settings_section, '.png')453 files = AppLocation.get_files(self.settings_section, '.png')
453 # No themes have been found so create one454 # No themes have been found so create one
454 if not files:455 if not files:
455 theme = ThemeXML()456 theme = Theme()
456 theme.theme_name = UiStrings().Default457 theme.theme_name = UiStrings().Default
457 self._write_theme(theme, None, None)458 self._write_theme(theme, None, None)
458 Settings().setValue(self.settings_section + '/global theme', theme.theme_name)459 Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
@@ -505,19 +506,27 @@
505506
506 def get_theme_data(self, theme_name):507 def get_theme_data(self, theme_name):
507 """508 """
508 Returns a theme object from an XML file509 Returns a theme object from an XML or JSON file
509510
510 :param theme_name: Name of the theme to load from file511 :param theme_name: Name of the theme to load from file
511 :return: The theme object.512 :return: The theme object.
512 """513 """
513 self.log_debug('get theme data for theme {name}'.format(name=theme_name))514 self.log_debug('get theme data for theme {name}'.format(name=theme_name))
514 xml_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')515 theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json')
515 xml = get_text_file_string(xml_file)516 theme_data = get_text_file_string(theme_file)
516 if not xml:517 jsn = True
518 if not theme_data:
519 theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
520 theme_data = get_text_file_string(theme_file)
521 jsn = False
522 if not theme_data:
517 self.log_debug('No theme data - using default theme')523 self.log_debug('No theme data - using default theme')
518 return ThemeXML()524 return Theme()
519 else:525 else:
520 return self._create_theme_from_xml(xml, self.path)526 if jsn:
527 return self._create_theme_from_json(theme_data, self.path)
528 else:
529 return self._create_theme_from_xml(theme_data, self.path)
521530
522 def over_write_message_box(self, theme_name):531 def over_write_message_box(self, theme_name):
523 """532 """
@@ -547,18 +556,28 @@
547 out_file = None556 out_file = None
548 file_xml = None557 file_xml = None
549 abort_import = True558 abort_import = True
559 json_theme = False
560 theme_name = ""
550 try:561 try:
551 theme_zip = zipfile.ZipFile(file_name)562 theme_zip = zipfile.ZipFile(file_name)
552 xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']563 json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
553 if len(xml_file) != 1:564 if len(json_file) != 1:
554 self.log_error('Theme contains "{val:d}" XML files'.format(val=len(xml_file)))565 # TODO: remove XML handling at some point but would need a auto conversion to run first.
555 raise ValidationError566 xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
556 xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()567 if len(xml_file) != 1:
557 theme_version = xml_tree.get('version', default=None)568 self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
558 if not theme_version or float(theme_version) < 2.0:569 raise ValidationError
559 self.log_error('Theme version is less than 2.0')570 xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
560 raise ValidationError571 theme_version = xml_tree.get('version', default=None)
561 theme_name = xml_tree.find('name').text.strip()572 if not theme_version or float(theme_version) < 2.0:
573 self.log_error('Theme version is less than 2.0')
574 raise ValidationError
575 theme_name = xml_tree.find('name').text.strip()
576 else:
577 new_theme = Theme()
578 new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
579 theme_name = new_theme.theme_name
580 json_theme = True
562 theme_folder = os.path.join(directory, theme_name)581 theme_folder = os.path.join(directory, theme_name)
563 theme_exists = os.path.exists(theme_folder)582 theme_exists = os.path.exists(theme_folder)
564 if theme_exists and not self.over_write_message_box(theme_name):583 if theme_exists and not self.over_write_message_box(theme_name):
@@ -574,7 +593,7 @@
574 continue593 continue
575 full_name = os.path.join(directory, out_name)594 full_name = os.path.join(directory, out_name)
576 check_directory_exists(os.path.dirname(full_name))595 check_directory_exists(os.path.dirname(full_name))
577 if os.path.splitext(name)[1].lower() == '.xml':596 if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
578 file_xml = str(theme_zip.read(name), 'utf-8')597 file_xml = str(theme_zip.read(name), 'utf-8')
579 out_file = open(full_name, 'w', encoding='utf-8')598 out_file = open(full_name, 'w', encoding='utf-8')
580 out_file.write(file_xml)599 out_file.write(file_xml)
@@ -597,7 +616,10 @@
597 if not abort_import:616 if not abort_import:
598 # As all files are closed, we can create the Theme.617 # As all files are closed, we can create the Theme.
599 if file_xml:618 if file_xml:
600 theme = self._create_theme_from_xml(file_xml, self.path)619 if json_theme:
620 theme = self._create_theme_from_json(file_xml, self.path)
621 else:
622 theme = self._create_theme_from_xml(file_xml, self.path)
601 self.generate_and_save_image(theme_name, theme)623 self.generate_and_save_image(theme_name, theme)
602 # Only show the error message, when IOError was not raised (in624 # Only show the error message, when IOError was not raised (in
603 # this case the error message has already been shown).625 # this case the error message has already been shown).
@@ -646,16 +668,16 @@
646 :param image_to: Where the Theme Image is to be saved to668 :param image_to: Where the Theme Image is to be saved to
647 """669 """
648 name = theme.theme_name670 name = theme.theme_name
649 theme_pretty_xml = theme.extract_formatted_xml()671 theme_pretty = theme.export_theme()
650 theme_dir = os.path.join(self.path, name)672 theme_dir = os.path.join(self.path, name)
651 check_directory_exists(theme_dir)673 check_directory_exists(theme_dir)
652 theme_file = os.path.join(theme_dir, name + '.xml')674 theme_file = os.path.join(theme_dir, name + '.json')
653 if self.old_background_image and image_to != self.old_background_image:675 if self.old_background_image and image_to != self.old_background_image:
654 delete_file(self.old_background_image)676 delete_file(self.old_background_image)
655 out_file = None677 out_file = None
656 try:678 try:
657 out_file = open(theme_file, 'w', encoding='utf-8')679 out_file = open(theme_file, 'w', encoding='utf-8')
658 out_file.write(theme_pretty_xml.decode('utf-8'))680 out_file.write(theme_pretty)
659 except IOError:681 except IOError:
660 self.log_exception('Saving theme to file failed')682 self.log_exception('Saving theme to file failed')
661 finally:683 finally:
@@ -717,7 +739,8 @@
717 """739 """
718 return os.path.join(self.path, theme + '.png')740 return os.path.join(self.path, theme + '.png')
719741
720 def _create_theme_from_xml(self, theme_xml, image_path):742 @staticmethod
743 def _create_theme_from_xml(theme_xml, image_path):
721 """744 """
722 Return a theme object using information parsed from XML745 Return a theme object using information parsed from XML
723746
@@ -725,11 +748,25 @@
725 :param image_path: Where the theme image is stored748 :param image_path: Where the theme image is stored
726 :return: Theme data.749 :return: Theme data.
727 """750 """
728 theme = ThemeXML()751 theme = Theme()
729 theme.parse(theme_xml)752 theme.parse(theme_xml)
730 theme.extend_image_filename(image_path)753 theme.extend_image_filename(image_path)
731 return theme754 return theme
732755
756 @staticmethod
757 def _create_theme_from_json(theme_json, image_path):
758 """
759 Return a theme object using information parsed from JSON
760
761 :param theme_json: The Theme data object.
762 :param image_path: Where the theme image is stored
763 :return: Theme data.
764 """
765 theme = Theme()
766 theme.load_theme(theme_json)
767 theme.extend_image_filename(image_path)
768 return theme
769
733 def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True):770 def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True):
734 """771 """
735 Check to see if theme has been selected and the destructive action is allowed.772 Check to see if theme has been selected and the destructive action is allowed.
736773
=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py 2017-05-22 18:27:40 +0000
+++ openlp/plugins/presentations/presentationplugin.py 2017-05-30 13:56:07 +0000
@@ -1,4 +1,4 @@
1 # -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=42# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
33
4###############################################################################4###############################################################################
55
=== modified file 'openlp/plugins/songusage/forms/songusagedetaildialog.py'
--- openlp/plugins/songusage/forms/songusagedetaildialog.py 2017-05-22 18:22:43 +0000
+++ openlp/plugins/songusage/forms/songusagedetaildialog.py 2017-05-30 13:56:07 +0000
@@ -69,7 +69,7 @@
69 self.file_horizontal_layout.setSpacing(8)69 self.file_horizontal_layout.setSpacing(8)
70 self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)70 self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
71 self.file_horizontal_layout.setObjectName('file_horizontal_layout')71 self.file_horizontal_layout.setObjectName('file_horizontal_layout')
72 self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)72 self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False)
73 self.file_horizontal_layout.addWidget(self.report_path_edit)73 self.file_horizontal_layout.addWidget(self.report_path_edit)
74 self.vertical_layout.addWidget(self.file_group_box)74 self.vertical_layout.addWidget(self.file_group_box)
75 self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])75 self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])
7676
=== modified file 'tests/functional/openlp_core_lib/test_renderer.py'
--- tests/functional/openlp_core_lib/test_renderer.py 2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_lib/test_renderer.py 2017-05-30 13:56:07 +0000
@@ -30,7 +30,7 @@
30from openlp.core.common import Registry30from openlp.core.common import Registry
31from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags31from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
32from openlp.core.lib.renderer import words_split, get_start_tags32from openlp.core.lib.renderer import words_split, get_start_tags
33from openlp.core.lib.theme import ThemeXML33from openlp.core.lib.theme import Theme
3434
3535
36SCREEN = {36SCREEN = {
@@ -189,7 +189,7 @@
189 # GIVEN: test object and data189 # GIVEN: test object and data
190 mock_lyrics_css.return_value = ' FORMAT CSS; '190 mock_lyrics_css.return_value = ' FORMAT CSS; '
191 mock_outline_css.return_value = ' OUTLINE CSS; '191 mock_outline_css.return_value = ' OUTLINE CSS; '
192 theme_data = ThemeXML()192 theme_data = Theme()
193 theme_data.font_main_name = 'Arial'193 theme_data.font_main_name = 'Arial'
194 theme_data.font_main_size = 20194 theme_data.font_main_size = 20
195 theme_data.font_main_color = '#FFFFFF'195 theme_data.font_main_color = '#FFFFFF'
196196
=== modified file 'tests/functional/openlp_core_lib/test_theme.py'
--- tests/functional/openlp_core_lib/test_theme.py 2016-12-31 11:01:36 +0000
+++ tests/functional/openlp_core_lib/test_theme.py 2017-05-30 13:56:07 +0000
@@ -25,36 +25,30 @@
25from unittest import TestCase25from unittest import TestCase
26import os26import os
2727
28from openlp.core.lib.theme import ThemeXML28from openlp.core.lib.theme import Theme
2929
3030
31class TestThemeXML(TestCase):31class TestTheme(TestCase):
32 """32 """
33 Test the ThemeXML class33 Test the Theme class
34 """34 """
35 def test_new_theme(self):35 def test_new_theme(self):
36 """36 """
37 Test the ThemeXML constructor37 Test the Theme constructor
38 """38 """
39 # GIVEN: The ThemeXML class39 # GIVEN: The Theme class
40 # WHEN: A theme object is created40 # WHEN: A theme object is created
41 default_theme = ThemeXML()41 default_theme = Theme()
4242
43 # THEN: The default values should be correct43 # THEN: The default values should be correct
44 self.assertEqual('#000000', default_theme.background_border_color,44 self.check_theme(default_theme)
45 'background_border_color should be "#000000"')
46 self.assertEqual('solid', default_theme.background_type, 'background_type should be "solid"')
47 self.assertEqual(0, default_theme.display_vertical_align, 'display_vertical_align should be 0')
48 self.assertEqual('Arial', default_theme.font_footer_name, 'font_footer_name should be "Arial"')
49 self.assertFalse(default_theme.font_main_bold, 'font_main_bold should be False')
50 self.assertEqual(47, len(default_theme.__dict__), 'The theme should have 47 attributes')
5145
52 def test_expand_json(self):46 def test_expand_json(self):
53 """47 """
54 Test the expand_json method48 Test the expand_json method
55 """49 """
56 # GIVEN: A ThemeXML object and some JSON to "expand"50 # GIVEN: A Theme object and some JSON to "expand"
57 theme = ThemeXML()51 theme = Theme()
58 theme_json = {52 theme_json = {
59 'background': {53 'background': {
60 'border_color': '#000000',54 'border_color': '#000000',
@@ -73,31 +67,48 @@
73 }67 }
74 }68 }
7569
76 # WHEN: ThemeXML.expand_json() is run70 # WHEN: Theme.expand_json() is run
77 theme.expand_json(theme_json)71 theme.expand_json(theme_json)
7872
79 # THEN: The attributes should be set on the object73 # THEN: The attributes should be set on the object
80 self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')74 self.check_theme(theme)
81 self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
82 self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
83 self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
84 self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
8575
86 def test_extend_image_filename(self):76 def test_extend_image_filename(self):
87 """77 """
88 Test the extend_image_filename method78 Test the extend_image_filename method
89 """79 """
90 # GIVEN: A theme object80 # GIVEN: A theme object
91 theme = ThemeXML()81 theme = Theme()
92 theme.theme_name = 'MyBeautifulTheme '82 theme.theme_name = 'MyBeautifulTheme '
93 theme.background_filename = ' video.mp4'83 theme.background_filename = ' video.mp4'
94 theme.background_type = 'video'84 theme.background_type = 'video'
95 path = os.path.expanduser('~')85 path = os.path.expanduser('~')
9686
97 # WHEN: ThemeXML.extend_image_filename is run87 # WHEN: Theme.extend_image_filename is run
98 theme.extend_image_filename(path)88 theme.extend_image_filename(path)
9989
100 # THEN: The filename of the background should be correct90 # THEN: The filename of the background should be correct
101 expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')91 expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')
102 self.assertEqual(expected_filename, theme.background_filename)92 self.assertEqual(expected_filename, theme.background_filename)
103 self.assertEqual('MyBeautifulTheme', theme.theme_name)93 self.assertEqual('MyBeautifulTheme', theme.theme_name)
94
95 def test_save_retrieve(self):
96 """
97 Load a dummy theme, save it and reload it
98 """
99 # GIVEN: The default Theme class
100 # WHEN: A theme object is created
101 default_theme = Theme()
102 # THEN: The default values should be correct
103 save_theme_json = default_theme.export_theme()
104 lt = Theme()
105 lt.load_theme(save_theme_json)
106 self.check_theme(lt)
107
108 def check_theme(self, theme):
109 self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
110 self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
111 self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
112 self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
113 self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
114 self.assertEqual(47, len(theme.__dict__), 'The theme should have 47 attributes')
104115
=== modified file 'tests/functional/openlp_core_ui/test_thememanager.py'
--- tests/functional/openlp_core_ui/test_thememanager.py 2017-05-08 19:04:14 +0000
+++ tests/functional/openlp_core_ui/test_thememanager.py 2017-05-30 13:56:07 +0000
@@ -63,7 +63,7 @@
63 mocked_zipfile_init.return_value = None63 mocked_zipfile_init.return_value = None
6464
65 # WHEN: The theme is exported65 # WHEN: The theme is exported
66 theme_manager._export_theme(os.path.join('some', 'path'), 'Default')66 theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default')
6767
68 # THEN: The zipfile should be created at the given path68 # THEN: The zipfile should be created at the given path
69 mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')69 mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
@@ -126,8 +126,9 @@
126 theme_manager.path = ''126 theme_manager.path = ''
127 mocked_theme = MagicMock()127 mocked_theme = MagicMock()
128 mocked_theme.theme_name = 'themename'128 mocked_theme.theme_name = 'themename'
129 mocked_theme.extract_formatted_xml = MagicMock()129 mocked_theme.filename = "filename"
130 mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()130 # mocked_theme.extract_formatted_xml = MagicMock()
131 # mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
131132
132 # WHEN: Calling _write_theme with path to different images133 # WHEN: Calling _write_theme with path to different images
133 file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')134 file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
@@ -148,14 +149,13 @@
148 theme_manager.path = self.temp_folder149 theme_manager.path = self.temp_folder
149 mocked_theme = MagicMock()150 mocked_theme = MagicMock()
150 mocked_theme.theme_name = 'theme 愛 name'151 mocked_theme.theme_name = 'theme 愛 name'
151 mocked_theme.extract_formatted_xml = MagicMock()152 mocked_theme.export_theme.return_value = "{}"
152 mocked_theme.extract_formatted_xml.return_value = 'fake theme 愛 XML'.encode()
153153
154 # WHEN: Calling _write_theme with a theme with a name with special characters in it154 # WHEN: Calling _write_theme with a theme with a name with special characters in it
155 theme_manager._write_theme(mocked_theme, None, None)155 theme_manager._write_theme(mocked_theme, None, None)
156156
157 # THEN: It should have been created157 # THEN: It should have been created
158 self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.xml')),158 self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')),
159 'Theme with special characters should have been created!')159 'Theme with special characters should have been created!')
160160
161 def test_over_write_message_box_yes(self):161 def test_over_write_message_box_yes(self):