Merge lp:~trb143/openlp/themecleanup into lp:openlp
- themecleanup
- Merge into trunk
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 |
Related bugs: |
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.
Commit message
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:/
[SUCCESS] https:/
[SUCCESS] https:/
[SUCCESS] https:/
[SUCCESS] https:/
[SUCCESS] https:/
[FAILURE] https:/
Stopping after failure
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal | # |
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.
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!
Tomas Groth (tomasgroth) : | # |
Preview Diff
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): |
I have no idea why, but this patch fixes the pylint error: https:/ /bin.snyman. info/mmmc7cmv