← Back to team overview

openlp-core team mailing list archive

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

 

Tim Bentley has proposed merging lp:~trb143/openlp/themecleanup into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~trb143/openlp/themecleanup/+merge/324564

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!
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/themecleanup into lp:openlp.
=== 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-24 20:20:48 +0000
@@ -26,7 +26,6 @@
 import logging
 import json
 
-from xml.dom.minidom import Document
 from lxml import etree, objectify
 from openlp.core.common import AppLocation, de_hump
 
@@ -150,7 +149,7 @@
                 'horizontal_align', 'vertical_align', 'wrap_style']
 
 
-class ThemeXML(object):
+class Theme(object):
     """
     A class to encapsulate the Theme XML.
     """
@@ -195,184 +194,6 @@
                 self.background_filename = self.background_filename.strip()
                 self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
 
-    def _new_document(self, name):
-        """
-        Create a new theme XML document.
-        """
-        self.theme_xml = Document()
-        self.theme = self.theme_xml.createElement('theme')
-        self.theme_xml.appendChild(self.theme)
-        self.theme.setAttribute('version', '2.0')
-        self.name = self.theme_xml.createElement('name')
-        text_node = self.theme_xml.createTextNode(name)
-        self.name.appendChild(text_node)
-        self.theme.appendChild(self.name)
-
-    def add_background_transparent(self):
-        """
-        Add a transparent background.
-        """
-        background = self.theme_xml.createElement('background')
-        background.setAttribute('type', 'transparent')
-        self.theme.appendChild(background)
-
-    def add_background_solid(self, bkcolor):
-        """
-        Add a Solid background.
-
-        :param bkcolor: The color of the background.
-        """
-        background = self.theme_xml.createElement('background')
-        background.setAttribute('type', 'solid')
-        self.theme.appendChild(background)
-        self.child_element(background, 'color', str(bkcolor))
-
-    def add_background_gradient(self, startcolor, endcolor, direction):
-        """
-        Add a gradient background.
-
-        :param startcolor: The gradient's starting colour.
-        :param endcolor: The gradient's ending colour.
-        :param direction: The direction of the gradient.
-        """
-        background = self.theme_xml.createElement('background')
-        background.setAttribute('type', 'gradient')
-        self.theme.appendChild(background)
-        # Create startColor element
-        self.child_element(background, 'startColor', str(startcolor))
-        # Create endColor element
-        self.child_element(background, 'endColor', str(endcolor))
-        # Create direction element
-        self.child_element(background, 'direction', str(direction))
-
-    def add_background_image(self, filename, border_color):
-        """
-        Add a image background.
-
-        :param filename: The file name of the image.
-        :param border_color:
-        """
-        background = self.theme_xml.createElement('background')
-        background.setAttribute('type', 'image')
-        self.theme.appendChild(background)
-        # Create Filename element
-        self.child_element(background, 'filename', filename)
-        # Create endColor element
-        self.child_element(background, 'borderColor', str(border_color))
-
-    def add_background_video(self, filename, border_color):
-        """
-        Add a video background.
-
-        :param filename: The file name of the video.
-        :param border_color:
-        """
-        background = self.theme_xml.createElement('background')
-        background.setAttribute('type', 'video')
-        self.theme.appendChild(background)
-        # Create Filename element
-        self.child_element(background, 'filename', filename)
-        # Create endColor element
-        self.child_element(background, 'borderColor', str(border_color))
-
-    def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
-                 line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
-                 outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
-        """
-        Add a Font.
-
-        :param name: The name of the font.
-        :param color: The colour of the font.
-        :param size: The size of the font.
-        :param override: Whether or not to override the default positioning of the theme.
-        :param fonttype: The type of font, ``main`` or ``footer``. Defaults to ``main``.
-        :param bold:
-        :param italics: The weight of then font Defaults to 50 Normal
-        :param line_adjustment: Does the font render to italics Defaults to 0 Normal
-        :param xpos: The X position of the text block.
-        :param ypos: The Y position of the text block.
-        :param width: The width of the text block.
-        :param height: The height of the text block.
-        :param outline: Whether or not to show an outline.
-        :param outline_color: The colour of the outline.
-        :param outline_pixel:  How big the Shadow is
-        :param shadow: Whether or not to show a shadow.
-        :param shadow_color: The colour of the shadow.
-        :param shadow_pixel: How big the Shadow is
-        """
-        background = self.theme_xml.createElement('font')
-        background.setAttribute('type', fonttype)
-        self.theme.appendChild(background)
-        # Create Font name element
-        self.child_element(background, 'name', name)
-        # Create Font color element
-        self.child_element(background, 'color', str(color))
-        # Create Proportion name element
-        self.child_element(background, 'size', str(size))
-        # Create weight name element
-        self.child_element(background, 'bold', str(bold))
-        # Create italics name element
-        self.child_element(background, 'italics', str(italics))
-        # Create indentation name element
-        self.child_element(background, 'line_adjustment', str(line_adjustment))
-        # Create Location element
-        element = self.theme_xml.createElement('location')
-        element.setAttribute('override', str(override))
-        element.setAttribute('x', str(xpos))
-        element.setAttribute('y', str(ypos))
-        element.setAttribute('width', str(width))
-        element.setAttribute('height', str(height))
-        background.appendChild(element)
-        # Shadow
-        element = self.theme_xml.createElement('shadow')
-        element.setAttribute('shadowColor', str(shadow_color))
-        element.setAttribute('shadowSize', str(shadow_pixel))
-        value = self.theme_xml.createTextNode(str(shadow))
-        element.appendChild(value)
-        background.appendChild(element)
-        # Outline
-        element = self.theme_xml.createElement('outline')
-        element.setAttribute('outlineColor', str(outline_color))
-        element.setAttribute('outlineSize', str(outline_pixel))
-        value = self.theme_xml.createTextNode(str(outline))
-        element.appendChild(value)
-        background.appendChild(element)
-
-    def add_display(self, horizontal, vertical, transition):
-        """
-        Add a Display options.
-
-        :param horizontal: The horizontal alignment of the text.
-        :param vertical: The vertical alignment of the text.
-        :param transition: Whether the slide transition is active.
-        """
-        background = self.theme_xml.createElement('display')
-        self.theme.appendChild(background)
-        # Horizontal alignment
-        element = self.theme_xml.createElement('horizontalAlign')
-        value = self.theme_xml.createTextNode(str(horizontal))
-        element.appendChild(value)
-        background.appendChild(element)
-        # Vertical alignment
-        element = self.theme_xml.createElement('verticalAlign')
-        value = self.theme_xml.createTextNode(str(vertical))
-        element.appendChild(value)
-        background.appendChild(element)
-        # Slide Transition
-        element = self.theme_xml.createElement('slideTransition')
-        value = self.theme_xml.createTextNode(str(transition))
-        element.appendChild(value)
-        background.appendChild(element)
-
-    def child_element(self, element, tag, value):
-        """
-        Generic child element creator.
-        """
-        child = self.theme_xml.createElement(tag)
-        child.appendChild(self.theme_xml.createTextNode(value))
-        element.appendChild(child)
-        return child
-
     def set_default_header_footer(self):
         """
         Set the header and footer size into the current primary screen.
@@ -386,25 +207,24 @@
         self.font_footer_y = current_screen['size'].height() * 9 / 10
         self.font_footer_height = current_screen['size'].height() / 10
 
-    def dump_xml(self):
-        """
-        Dump the XML to file used for debugging
-        """
-        return self.theme_xml.toprettyxml(indent='  ')
-
-    def extract_xml(self):
-        """
-        Print out the XML string.
-        """
-        self._build_xml_from_attrs()
-        return self.theme_xml.toxml('utf-8').decode('utf-8')
-
-    def extract_formatted_xml(self):
-        """
-        Pull out the XML string formatted for human consumption
-        """
-        self._build_xml_from_attrs()
-        return self.theme_xml.toprettyxml(indent='    ', newl='\n', encoding='utf-8')
+    def load_theme(self, theme):
+        """
+        Convert the JSON file and expand it.
+
+        :param theme: the theme string
+        """
+        jsn = json.loads(theme)
+        self.expand_json(jsn)
+
+    def export_theme(self):
+        """
+        Loop through the fields and build a dictionary of them
+
+        """
+        theme_data = {}
+        for attr, value in self.__dict__.items():
+            theme_data["{attr}".format(attr=attr)] = value
+        return json.dumps(theme_data)
 
     def parse(self, xml):
         """
@@ -461,7 +281,8 @@
                 if element.tag == 'name':
                     self._create_attr('theme', element.tag, element.text)
 
-    def _translate_tags(self, master, element, value):
+    @staticmethod
+    def _translate_tags(master, element, value):
         """
         Clean up XML removing and redefining tags
         """
@@ -514,71 +335,5 @@
         theme_strings = []
         for key in dir(self):
             if key[0:1] != '_':
-                # TODO: Due to bound methods returned, I don't know how to write a proper test
                 theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
         return '\n'.join(theme_strings)
-
-    def _build_xml_from_attrs(self):
-        """
-        Build the XML from the varables in the object
-        """
-        self._new_document(self.theme_name)
-        if self.background_type == BackgroundType.to_string(BackgroundType.Solid):
-            self.add_background_solid(self.background_color)
-        elif self.background_type == BackgroundType.to_string(BackgroundType.Gradient):
-            self.add_background_gradient(
-                self.background_start_color,
-                self.background_end_color,
-                self.background_direction
-            )
-        elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
-            filename = os.path.split(self.background_filename)[1]
-            self.add_background_image(filename, self.background_border_color)
-        elif self.background_type == BackgroundType.to_string(BackgroundType.Video):
-            filename = os.path.split(self.background_filename)[1]
-            self.add_background_video(filename, self.background_border_color)
-        elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
-            self.add_background_transparent()
-        self.add_font(
-            self.font_main_name,
-            self.font_main_color,
-            self.font_main_size,
-            self.font_main_override, 'main',
-            self.font_main_bold,
-            self.font_main_italics,
-            self.font_main_line_adjustment,
-            self.font_main_x,
-            self.font_main_y,
-            self.font_main_width,
-            self.font_main_height,
-            self.font_main_outline,
-            self.font_main_outline_color,
-            self.font_main_outline_size,
-            self.font_main_shadow,
-            self.font_main_shadow_color,
-            self.font_main_shadow_size
-        )
-        self.add_font(
-            self.font_footer_name,
-            self.font_footer_color,
-            self.font_footer_size,
-            self.font_footer_override, 'footer',
-            self.font_footer_bold,
-            self.font_footer_italics,
-            0,  # line adjustment
-            self.font_footer_x,
-            self.font_footer_y,
-            self.font_footer_width,
-            self.font_footer_height,
-            self.font_footer_outline,
-            self.font_footer_outline_color,
-            self.font_footer_outline_size,
-            self.font_footer_shadow,
-            self.font_footer_shadow_color,
-            self.font_footer_shadow_size
-        )
-        self.add_display(
-            self.display_horizontal_align,
-            self.display_vertical_align,
-            self.display_slide_transition
-        )

=== 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-24 20:20:48 +0000
@@ -46,16 +46,16 @@
 
         :param parent: The parent of the widget. This is just passed to the super method.
         :type parent: QWidget or None
-        
+
         :param dialog_caption: Used to customise the caption in the QFileDialog.
         :param dialog_caption: str
-        
+
         :param default_path: The default path. This is set as the path when the revert button is clicked
         :type default_path: str
 
         :param show_revert: Used to determin if the 'revert button' should be visible.
         :type show_revert: bool
-        
+
         :return: None
         :rtype: None
         """
@@ -72,7 +72,7 @@
         Set up the widget
         :param show_revert: Show or hide the revert button
         :type show_revert: bool
-        
+
         :return: None
         :rtype: None
         """

=== 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-24 20:20:48 +0000
@@ -698,7 +698,7 @@
                 translate('OpenLP.ServiceManager',
                           'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
         else:
-            file_name, filter_uesd = QtWidgets.QFileDialog.getSaveFileName(
+            file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
                 self.main_window, UiStrings().SaveService, path,
                 translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
         if not file_name:

=== 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-24 20:20:48 +0000
@@ -22,6 +22,7 @@
 """
 The Theme Manager manages adding, deleteing and modifying of themes.
 """
+import json
 import os
 import zipfile
 import shutil
@@ -33,7 +34,7 @@
     check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file
 from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \
     check_item_selected, create_thumb, validate_thumb
-from openlp.core.lib.theme import ThemeXML, BackgroundType
+from openlp.core.lib.theme import Theme, BackgroundType
 from openlp.core.lib.ui import critical_error_message_box, create_widget_action
 from openlp.core.ui import FileRenameForm, ThemeForm
 from openlp.core.ui.lib import OpenLPToolbar
@@ -245,7 +246,7 @@
         their customisations.
         :param field:
         """
-        theme = ThemeXML()
+        theme = Theme()
         theme.set_default_header_footer()
         self.theme_form.theme = theme
         self.theme_form.exec()
@@ -378,11 +379,12 @@
             critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
             return
         theme = item.data(QtCore.Qt.UserRole)
-        path = QtWidgets.QFileDialog.getExistingDirectory(self,
-                                                          translate('OpenLP.ThemeManager',
-                                                                    'Save Theme - ({name})').format(name=theme),
-                                                          Settings().value(self.settings_section +
-                                                                           '/last directory export'))
+        path, filter_used = \
+            QtWidgets.QFileDialog.getSaveFileName(self.main_window,
+                                                  translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
+                                                  format(name=theme),
+                                                  Settings().value(self.settings_section + '/last directory export'),
+                                                  translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
         self.application.set_busy_cursor()
         if path:
             Settings().setValue(self.settings_section + '/last directory export', path)
@@ -393,13 +395,12 @@
                                                             'Your theme has been successfully exported.'))
         self.application.set_normal_cursor()
 
-    def _export_theme(self, path, theme):
+    def _export_theme(self, theme_path, theme):
         """
         Create the zipfile with the theme contents.
-        :param path: Location where the zip file will be placed
+        :param theme_path: Location where the zip file will be placed
         :param theme: The name of the theme to be exported
         """
-        theme_path = os.path.join(path, theme + '.otz')
         theme_zip = None
         try:
             theme_zip = zipfile.ZipFile(theme_path, 'w')
@@ -452,7 +453,7 @@
         files = AppLocation.get_files(self.settings_section, '.png')
         # No themes have been found so create one
         if not files:
-            theme = ThemeXML()
+            theme = Theme()
             theme.theme_name = UiStrings().Default
             self._write_theme(theme, None, None)
             Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
@@ -511,13 +512,21 @@
         :return: The theme object.
         """
         self.log_debug('get theme data for theme {name}'.format(name=theme_name))
-        xml_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
-        xml = get_text_file_string(xml_file)
-        if not xml:
+        theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json')
+        theme_data = get_text_file_string(theme_file)
+        jsn = True
+        if not theme_data:
+            theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
+            theme_data = get_text_file_string(theme_file)
+            jsn = False
+        if not theme_data:
             self.log_debug('No theme data - using default theme')
-            return ThemeXML()
+            return Theme()
         else:
-            return self._create_theme_from_xml(xml, self.path)
+            if jsn:
+                return self._create_theme_from_json(theme_data, self.path)
+            else:
+                return self._create_theme_from_xml(theme_data, self.path)
 
     def over_write_message_box(self, theme_name):
         """
@@ -549,16 +558,24 @@
         abort_import = True
         try:
             theme_zip = zipfile.ZipFile(file_name)
-            xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
-            if len(xml_file) != 1:
-                self.log_error('Theme contains "{val:d}" XML files'.format(val=len(xml_file)))
-                raise ValidationError
-            xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
-            theme_version = xml_tree.get('version', default=None)
-            if not theme_version or float(theme_version) < 2.0:
-                self.log_error('Theme version is less than 2.0')
-                raise ValidationError
-            theme_name = xml_tree.find('name').text.strip()
+            json_theme = False
+            json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
+            if len(json_file) != 1:
+                xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
+                if len(xml_file) != 1:
+                    self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
+                    raise ValidationError
+                xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
+                theme_version = xml_tree.get('version', default=None)
+                if not theme_version or float(theme_version) < 2.0:
+                    self.log_error('Theme version is less than 2.0')
+                    raise ValidationError
+                theme_name = xml_tree.find('name').text.strip()
+            else:
+                theme = Theme()
+                theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
+                theme_name = theme.theme_name
+                json_theme = True
             theme_folder = os.path.join(directory, theme_name)
             theme_exists = os.path.exists(theme_folder)
             if theme_exists and not self.over_write_message_box(theme_name):
@@ -574,7 +591,7 @@
                     continue
                 full_name = os.path.join(directory, out_name)
                 check_directory_exists(os.path.dirname(full_name))
-                if os.path.splitext(name)[1].lower() == '.xml':
+                if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
                     file_xml = str(theme_zip.read(name), 'utf-8')
                     out_file = open(full_name, 'w', encoding='utf-8')
                     out_file.write(file_xml)
@@ -597,7 +614,10 @@
             if not abort_import:
                 # As all files are closed, we can create the Theme.
                 if file_xml:
-                    theme = self._create_theme_from_xml(file_xml, self.path)
+                    if json_theme:
+                        theme = self._create_theme_from_json(file_xml, self.path)
+                    else:
+                        theme = self._create_theme_from_xml(file_xml, self.path)
                     self.generate_and_save_image(theme_name, theme)
                 # Only show the error message, when IOError was not raised (in
                 # this case the error message has already been shown).
@@ -646,16 +666,16 @@
         :param image_to: Where the Theme Image is to be saved to
         """
         name = theme.theme_name
-        theme_pretty_xml = theme.extract_formatted_xml()
+        theme_pretty = theme.export_theme()
         theme_dir = os.path.join(self.path, name)
         check_directory_exists(theme_dir)
-        theme_file = os.path.join(theme_dir, name + '.xml')
+        theme_file = os.path.join(theme_dir, name + '.json')
         if self.old_background_image and image_to != self.old_background_image:
             delete_file(self.old_background_image)
         out_file = None
         try:
             out_file = open(theme_file, 'w', encoding='utf-8')
-            out_file.write(theme_pretty_xml.decode('utf-8'))
+            out_file.write(theme_pretty)
         except IOError:
             self.log_exception('Saving theme to file failed')
         finally:
@@ -717,7 +737,8 @@
         """
         return os.path.join(self.path, theme + '.png')
 
-    def _create_theme_from_xml(self, theme_xml, image_path):
+    @staticmethod
+    def _create_theme_from_xml(theme_xml, image_path):
         """
         Return a theme object using information parsed from XML
 
@@ -725,11 +746,25 @@
         :param image_path: Where the theme image is stored
         :return: Theme data.
         """
-        theme = ThemeXML()
+        theme = Theme()
         theme.parse(theme_xml)
         theme.extend_image_filename(image_path)
         return theme
 
+    @staticmethod
+    def _create_theme_from_json(theme_json, image_path):
+        """
+        Return a theme object using information parsed from JSON
+
+        :param theme_json: The Theme data object.
+        :param image_path: Where the theme image is stored
+        :return: Theme data.
+        """
+        theme = Theme()
+        theme.load_theme(theme_json)
+        theme.extend_image_filename(image_path)
+        return theme
+
     def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True):
         """
         Check to see if theme has been selected and the destructive action is allowed.

=== 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-24 20:20:48 +0000
@@ -1,4 +1,4 @@
-    # -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
 # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
 
 ###############################################################################

=== 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-24 20:20:48 +0000
@@ -69,7 +69,7 @@
         self.file_horizontal_layout.setSpacing(8)
         self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
         self.file_horizontal_layout.setObjectName('file_horizontal_layout')
-        self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)
+        self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False)
         self.file_horizontal_layout.addWidget(self.report_path_edit)
         self.vertical_layout.addWidget(self.file_group_box)
         self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])

=== 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-24 20:20:48 +0000
@@ -30,7 +30,7 @@
 from openlp.core.common import Registry
 from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
 from openlp.core.lib.renderer import words_split, get_start_tags
-from openlp.core.lib.theme import ThemeXML
+from openlp.core.lib.theme import Theme
 
 
 SCREEN = {
@@ -189,7 +189,7 @@
         # GIVEN: test object and data
         mock_lyrics_css.return_value = ' FORMAT CSS; '
         mock_outline_css.return_value = ' OUTLINE CSS; '
-        theme_data = ThemeXML()
+        theme_data = Theme()
         theme_data.font_main_name = 'Arial'
         theme_data.font_main_size = 20
         theme_data.font_main_color = '#FFFFFF'

=== 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-24 20:20:48 +0000
@@ -25,36 +25,30 @@
 from unittest import TestCase
 import os
 
-from openlp.core.lib.theme import ThemeXML
-
-
-class TestThemeXML(TestCase):
+from openlp.core.lib.theme import Theme
+
+
+class TestTheme(TestCase):
     """
-    Test the ThemeXML class
+    Test the Theme class
     """
     def test_new_theme(self):
         """
-        Test the ThemeXML constructor
+        Test the Theme constructor
         """
-        # GIVEN: The ThemeXML class
+        # GIVEN: The Theme class
         # WHEN: A theme object is created
-        default_theme = ThemeXML()
+        default_theme = Theme()
 
         # THEN: The default values should be correct
-        self.assertEqual('#000000', default_theme.background_border_color,
-                         'background_border_color should be "#000000"')
-        self.assertEqual('solid', default_theme.background_type, 'background_type should be "solid"')
-        self.assertEqual(0, default_theme.display_vertical_align, 'display_vertical_align should be 0')
-        self.assertEqual('Arial', default_theme.font_footer_name, 'font_footer_name should be "Arial"')
-        self.assertFalse(default_theme.font_main_bold, 'font_main_bold should be False')
-        self.assertEqual(47, len(default_theme.__dict__), 'The theme should have 47 attributes')
+        self.check_theme(default_theme)
 
     def test_expand_json(self):
         """
         Test the expand_json method
         """
-        # GIVEN: A ThemeXML object and some JSON to "expand"
-        theme = ThemeXML()
+        # GIVEN: A Theme object and some JSON to "expand"
+        theme = Theme()
         theme_json = {
             'background': {
                 'border_color': '#000000',
@@ -73,31 +67,48 @@
             }
         }
 
-        # WHEN: ThemeXML.expand_json() is run
+        # WHEN: Theme.expand_json() is run
         theme.expand_json(theme_json)
 
         # THEN: The attributes should be set on the object
-        self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
-        self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
-        self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
-        self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
-        self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
+        self.check_theme(theme)
 
     def test_extend_image_filename(self):
         """
         Test the extend_image_filename method
         """
         # GIVEN: A theme object
-        theme = ThemeXML()
+        theme = Theme()
         theme.theme_name = 'MyBeautifulTheme   '
         theme.background_filename = '    video.mp4'
         theme.background_type = 'video'
         path = os.path.expanduser('~')
 
-        # WHEN: ThemeXML.extend_image_filename is run
+        # WHEN: Theme.extend_image_filename is run
         theme.extend_image_filename(path)
 
         # THEN: The filename of the background should be correct
         expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')
         self.assertEqual(expected_filename, theme.background_filename)
         self.assertEqual('MyBeautifulTheme', theme.theme_name)
+
+    def test_save_retrieve(self):
+        """
+        Load a dummy theme, save it and reload it
+        """
+        # GIVEN: The default Theme class
+        # WHEN: A theme object is created
+        default_theme = Theme()
+        # THEN: The default values should be correct
+        save_theme_json = default_theme.export_theme()
+        lt = Theme()
+        lt.load_theme(save_theme_json)
+        self.check_theme(lt)
+
+    def check_theme(self, theme):
+        self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
+        self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
+        self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
+        self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
+        self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
+        self.assertEqual(47, len(theme.__dict__), 'The theme should have 47 attributes')

=== 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-24 20:20:48 +0000
@@ -63,7 +63,7 @@
             mocked_zipfile_init.return_value = None
 
             # WHEN: The theme is exported
-            theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
+            theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default')
 
             # THEN: The zipfile should be created at the given path
             mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
@@ -126,8 +126,9 @@
             theme_manager.path = ''
             mocked_theme = MagicMock()
             mocked_theme.theme_name = 'themename'
-            mocked_theme.extract_formatted_xml = MagicMock()
-            mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
+            mocked_theme.filename = "filename"
+            # mocked_theme.extract_formatted_xml = MagicMock()
+            # mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
 
             # WHEN: Calling _write_theme with path to different images
             file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
@@ -148,14 +149,13 @@
         theme_manager.path = self.temp_folder
         mocked_theme = MagicMock()
         mocked_theme.theme_name = 'theme 愛 name'
-        mocked_theme.extract_formatted_xml = MagicMock()
-        mocked_theme.extract_formatted_xml.return_value = 'fake theme 愛 XML'.encode()
+        mocked_theme.export_theme.return_value = "{}"
 
         # WHEN: Calling _write_theme with a theme with a name with special characters in it
         theme_manager._write_theme(mocked_theme, None, None)
 
         # THEN: It should have been created
-        self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.xml')),
+        self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')),
                         'Theme with special characters should have been created!')
 
     def test_over_write_message_box_yes(self):


Follow ups