Merge branch 'master' of gitlab.com:openlp/openlp

This commit is contained in:
Tim 2019-10-26 09:25:26 +01:00
commit 2d2576aa6c
No known key found for this signature in database
GPG Key ID: 3D454289AF831A6D
15 changed files with 3000 additions and 176 deletions

View File

@ -3,20 +3,25 @@ stages:
- test
- deploy
lint:
lint-python:
stage: lint
image: openlp/debian
before_script:
- sh scripts/generate_resources.sh
script:
- sh scripts/generate_resources.sh
- flake8
lint-javascript:
stage: lint
image: openlp/angular
script:
- yarn install
- yarn lint
test-debian:
stage: test
image: openlp/debian
before_script:
- sh scripts/generate_resources.sh
script:
- sh scripts/generate_resources.sh
- xvfb-run -s '-screen 0 1024x768x24' pytest-3 --color=no --disable-warnings --cov openlp --cov-report term
- mv .coverage linux.coverage
artifacts:
@ -26,35 +31,31 @@ test-debian:
test-ubuntu:
stage: test
image: openlp/ubuntu
before_script:
- sh scripts/generate_resources.sh
script:
- sh scripts/generate_resources.sh
- xvfb-run -s '-screen 0 1024x768x24' pytest-3 --color=no --disable-warnings
test-fedora:
stage: test
image: openlp/fedora
before_script:
- sh scripts/generate_resources.sh
script:
- sh scripts/generate_resources.sh
- xvfb-run -s '-screen 0 1024x768x24' pytest-3 --color=no --disable-warnings
test-arch:
stage: test
image: openlp/arch
before_script:
- sh scripts/generate_resources.sh
script:
- sh scripts/generate_resources.sh
- xvfb-run -s '-screen 0 1024x768x24' pytest --color=no --disable-warnings
test-macos:
stage: test
tags:
- macos
before_script:
script:
- export PATH=/opt/local/bin:$PATH
- sh scripts/generate_resources.sh
script:
- python3 -m pytest --color=no --disable-warnings --cov openlp
- mv .coverage macos.coverage
artifacts:
@ -76,6 +77,13 @@ test-windows:
only:
- master@openlp/openlp
test-display:
stage: test
image: openlp/angular
script:
- yarn install
- yarn test --browsers ChromiumHeadlessCI
pages:
stage: deploy
image: openlp/debian

View File

@ -23,9 +23,9 @@ module.exports = function(config) {
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
// "display.js": ["coverage"]
},
@ -34,7 +34,7 @@ module.exports = function(config) {
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["dots", "junit"],
// configure the coverateReporter
// configure the coverateReporter
/* coverageReporter: {
type : "html",
dir : "htmlcov/"
@ -64,6 +64,12 @@ module.exports = function(config) {
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["Chromium"],
customLaunchers: {
ChromiumHeadlessCI: {
base: 'ChromiumHeadless',
flags: ['--no-sandbox']
}
},
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits

View File

@ -25,10 +25,18 @@ sup {
.reveal .slides > section,
.reveal .slides > section > section {
padding: 0;
max-height: 100%;
}
.reveal > .backgrounds > .present {
visibility: hidden !important;
.reveal .slides > section.text-slides {
/* Need to override reveal styles to use our text aligment */
display: flex !important;
flex-direction: column;
word-wrap: break-word;
}
.reveal .slides > section p {
margin: 0;
}
#global-background {

View File

@ -28,19 +28,19 @@ var GradientType = {
* Horizontal alignment enumeration
*/
var HorizontalAlign = {
Left: "left",
Right: "right",
Center: "center",
Justify: "justify"
Left: 0,
Right: 1,
Center: 2,
Justify: 3
};
/**
* Vertical alignment enumeration
*/
var VerticalAlign = {
Top: "top",
Middle: "middle",
Bottom: "bottom"
Top: 0,
Middle: 1,
Bottom: 2
};
/**
@ -85,7 +85,7 @@ var AlertLocation = {
var AlertState = {
Displaying: "displaying",
NotDisplaying: "notDisplaying"
}
};
/**
* Alert delay enumeration
@ -94,7 +94,7 @@ var AlertDelay = {
FiftyMilliseconds: 50,
OneSecond: 1000,
OnePointFiveSeconds: 1500
}
};
/**
* Return an array of elements based on the selector query
@ -180,12 +180,13 @@ function _fromCamelCase(text) {
* @param {Object} rules - The rules to apply to the style
*/
function _createStyle(selector, rules) {
var style;
var id = selector.replace("#", "").replace(" .", "-").replace(".", "-").replace(" ", "_");
if ($("style#" + id).length != 0) {
var style = $("style#" + id)[0];
style = $("style#" + id)[0];
}
else {
var style = document.createElement("style");
style = document.createElement("style");
document.getElementsByTagName("head")[0].appendChild(style);
style.type = "text/css";
style.id = id;
@ -348,7 +349,7 @@ var Display = {
* Start up reveal and do any other initialisation
*/
init: function () {
Reveal.initialize(this._revealConfig);
Reveal.initialize(Display._revealConfig);
},
/**
* Reinitialise Reveal
@ -368,13 +369,17 @@ var Display = {
*/
clearSlides: function () {
$(".slides")[0].innerHTML = "";
this._slides = {};
Display._slides = {};
},
/**
* Checks if the present slide content fits within the slide
*/
doesContentFit: function () {
var currSlide = $(".slides")[0];
var currSlide = $("section.text-slides");
if (currSlide.length === 0) {
currSlide = $(".slides");
}
currSlide = currSlide[0];
console.debug("scrollHeight: " + currSlide.scrollHeight + ", clientHeight: " + currSlide.clientHeight);
return currSlide.clientHeight >= currSlide.scrollHeight;
},
@ -475,10 +480,10 @@ var Display = {
var alertText = $('#alert-text')[0];
// create styles for the alerts from the settings
_createStyle("#alert-background.settings", {
backgroundColor: settings["backgroundColor"],
fontFamily: "'" + settings["fontFace"] + "'",
fontSize: settings["fontSize"].toString() + "pt",
color: settings["fontColor"]
backgroundColor: settings.backgroundColor,
fontFamily: "'" + settings.fontFace + "'",
fontSize: settings.fontSize.toString() + "pt",
color: settings.fontColor
});
alertBackground.classList.add("settings");
alertBackground.classList.replace("hide", "show");
@ -593,33 +598,35 @@ var Display = {
* Add a slide. If the slide exists but the HTML is different, update the slide.
* @param {string} verse - The verse number, e.g. "v1"
* @param {string} text - The HTML for the verse, e.g. "line1<br>line2"
* @param {string} footer_text - The HTML for the footer"
* @param {string} footer_text - The HTML for the footer
* @param {bool} reinit - True if React should reinit when creating a new slide
*/
addTextSlide: function (verse, text, footerText) {
addTextSlide: function (verse, text, footerText, reinit=true) {
var slide;
var html = _prepareText(text);
if (this._slides.hasOwnProperty(verse)) {
var slide = $("#" + verse)[0];
if (Display._slides.hasOwnProperty(verse)) {
slide = $("#" + verse)[0];
if (slide.innerHTML != html) {
slide.innerHTML = html;
}
}
else {
var slidesDiv = $(".slides")[0];
var slide = document.createElement("section");
} else {
var parent = $("section.text-slides");
if (parent.length === 0) {
Display._createTextContainer();
parent = $("section.text-slides");
}
parent = parent[0];
slide = document.createElement("section");
slide.setAttribute("id", verse);
slide.innerHTML = html;
slidesDiv.appendChild(slide);
var slides = $(".slides > section");
this._slides[verse] = slides.length - 1;
parent.appendChild(slide);
Display._slides[verse] = parent.children.length - 1;
if (footerText) {
$(".footer")[0].innerHTML = footerText;
}
}
if ((arguments.length > 3) && (arguments[3] === true)) {
this.reinit();
}
else if (arguments.length == 3) {
this.reinit();
if (reinit) {
Display.reinit();
}
}
},
/**
@ -634,6 +641,19 @@ var Display = {
Display.reinit();
Display.goToSlide(0);
},
/**
* Create the <section> that will contain text slides (vertical slides in react)
*/
_createTextContainer: function () {
var slideContainer = document.createElement("section");
slideContainer.classList = "text-slides";
var slidesDiv = $(".slides")[0];
slidesDiv.appendChild(slideContainer);
// Apply the current theme to the new container
if (!!Display._theme) {
Display.setTheme(Display._theme);
}
},
/**
* Set image slides
* @param {Object[]} slides - A list of images to add as JS objects [{"path": "url/to/file"}]
@ -641,18 +661,20 @@ var Display = {
setImageSlides: function (slides) {
Display.clearSlides();
var slidesDiv = $(".slides")[0];
var parentSection = document.createElement("section");
slides.forEach(function (slide, index) {
var section = document.createElement("section");
section.setAttribute("id", index);
section.setAttribute("data-background", "#000");
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = slide["path"];
img.src = slide.path;
img.setAttribute("style", "max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);");
section.appendChild(img);
slidesDiv.appendChild(section);
parentSection.appendChild(section);
Display._slides[index.toString()] = index;
});
slidesDiv.appendChild(parentSection);
Display.reinit();
},
/**
@ -660,11 +682,11 @@ var Display = {
* @param {Object} video - The video to show as a JS object: {"path": "url/to/file"}
*/
setVideo: function (video) {
this.clearSlides();
Display.clearSlides();
var section = document.createElement("section");
section.setAttribute("data-background", "#000");
var videoElement = document.createElement("video");
videoElement.src = video["path"];
videoElement.src = video.path;
videoElement.preload = "auto";
videoElement.setAttribute("id", "video");
videoElement.setAttribute("style", "height: 100%; width: 100%;");
@ -690,7 +712,7 @@ var Display = {
});
section.appendChild(videoElement);
$(".slides")[0].appendChild(section);
this.reinit();
Display.reinit();
},
/**
* Play a video
@ -780,21 +802,21 @@ var Display = {
* @param slide The slide number or name, e.g. "v1", 0
*/
goToSlide: function (slide) {
if (this._slides.hasOwnProperty(slide)) {
Reveal.slide(this._slides[slide]);
if (Display._slides.hasOwnProperty(slide)) {
Reveal.slide(0, Display._slides[slide]);
}
else {
Reveal.slide(slide);
Reveal.slide(0, slide);
}
},
/**
* Go to the next slide in the list
*/
next: Reveal.next,
next: Reveal.nextFragment,
/**
* Go to the previous slide in the list
*/
prev: Reveal.prev,
prev: Reveal.prevFragment,
/**
* Blank the screen
*/
@ -833,59 +855,59 @@ var Display = {
* @param fontSize The font size in pts
*/
calculateLineCount: function (fontSize) {
var p = $(".slides > section > p");
var p = $(".slides > section > section > p");
if (p.length == 0) {
this.addSlide("v1", "Arky arky");
p = $(".slides > section > p");
Display.addSlide("v1", "Arky arky");
p = $(".slides > section > section > p");
}
p = p[0];
p.style.fontSize = "" + fontSize + "pt";
var d = $(".slides")[0];
var d = $(".slides > section")[0];
var lh = parseFloat(_getStyle(p, "line-height"));
var dh = parseFloat(_getStyle(d, "height"));
return Math.floor(dh / lh);
},
setTheme: function (theme) {
this._theme = theme;
Display._theme = theme;
// Set the background
var globalBackground = $("#global-background")[0];
var backgroundStyle = {};
var backgroundHtml = "";
switch (theme.background_type) {
case BackgroundType.Transparent:
backgroundStyle["background"] = "transparent";
backgroundStyle.background = "transparent";
break;
case BackgroundType.Solid:
backgroundStyle["background"] = theme.background_color;
backgroundStyle.background = theme.background_color;
break;
case BackgroundType.Gradient:
switch (theme.background_direction) {
case GradientType.Horizontal:
backgroundStyle["background"] = _buildLinearGradient("left top", "left bottom",
backgroundStyle.background = _buildLinearGradient("left top", "left bottom",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.Vertical:
backgroundStyle["background"] = _buildLinearGradient("left top", "right top",
backgroundStyle.background = _buildLinearGradient("left top", "right top",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.LeftTop:
backgroundStyle["background"] = _buildLinearGradient("left top", "right bottom",
backgroundStyle.background = _buildLinearGradient("left top", "right bottom",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.LeftBottom:
backgroundStyle["background"] = _buildLinearGradient("left bottom", "right top",
backgroundStyle.background = _buildLinearGradient("left bottom", "right top",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.Circular:
backgroundStyle["background"] = _buildRadialGradient(window.innerWidth / 2, theme.background_start_color,
backgroundStyle.background = _buildRadialGradient(window.innerWidth / 2, theme.background_start_color,
theme.background_end_color);
break;
default:
backgroundStyle["background"] = "#000";
backgroundStyle.background = "#000";
}
break;
case BackgroundType.Image:
@ -898,76 +920,104 @@ var Display = {
console.warn(backgroundHtml);
break;
default:
backgroundStyle["background"] = "#000";
backgroundStyle.background = "#000";
}
globalBackground.style.cssText = "";
for (var key in backgroundStyle) {
if (backgroundStyle.hasOwnProperty(key)) {
globalBackground.style.setProperty(key, backgroundStyle[key]);
for (var bgKey in backgroundStyle) {
if (backgroundStyle.hasOwnProperty(bgKey)) {
globalBackground.style.setProperty(bgKey, backgroundStyle[bgKey]);
}
}
if (!!backgroundHtml) {
globalBackground.innerHTML = backgroundHtml;
}
// set up the main area
mainStyle = {
"word-wrap": "break-word",
/*"margin": "0",
"padding": "0"*/
};
mainStyle = {};
if (!!theme.font_main_outline) {
mainStyle["-webkit-text-stroke"] = "" + theme.font_main_outline_size + "pt " +
theme.font_main_outline_color;
mainStyle["-webkit-text-fill-color"] = theme.font_main_color;
}
// These need to be fixed, in the Python they use a width passed in as a parameter
mainStyle.width = theme.font_main_width + "px";
mainStyle.height = theme.font_main_height + "px";
mainStyle.top = "" + theme.font_main_y + "px";
mainStyle.left = "" + theme.font_main_x + "px";
mainStyle.color = theme.font_main_color;
mainStyle["font-family"] = theme.font_main_name;
mainStyle["font-size"] = "" + theme.font_main_size + "pt";
mainStyle["font-style"] = !!theme.font_main_italics ? "italic" : "";
mainStyle["font-weight"] = !!theme.font_main_bold ? "bold" : "";
mainStyle["color"] = theme.font_main_color;
mainStyle["line-height"] = "" + (100 + theme.font_main_line_adjustment) + "%";
mainStyle["text-align"] = theme.display_horizontal_align;
if (theme.display_horizontal_align != HorizontalAlign.Justify) {
mainStyle["white-space"] = "pre-wrap";
// Using text-align-last because there is a <br> seperating each line
switch (theme.display_horizontal_align) {
case HorizontalAlign.Justify:
mainStyle["text-align"] = "justify";
mainStyle["text-align-last"] = "justify";
break;
case HorizontalAlign.Center:
mainStyle["text-align"] = "center";
mainStyle["text-align-last"] = "center";
break;
case HorizontalAlign.Left:
mainStyle["text-align"] = "left";
mainStyle["text-align-last"] = "left";
break;
case HorizontalAlign.Right:
mainStyle["text-align"] = "right";
mainStyle["text-align-last"] = "right";
break;
default:
mainStyle["text-align"] = "center";
mainStyle["text-align-last"] = "center";
}
switch (theme.display_vertical_align) {
case VerticalAlign.Middle:
mainStyle["justify-content"] = "center";
break;
case VerticalAlign.Top:
mainStyle["justify-content"] = "flex-start";
break;
case VerticalAlign.Bottom:
mainStyle["justify-content"] = "flex-end";
// This gets around the webkit scroll height bug
mainStyle["padding-bottom"] = "" + (theme.font_main_size / 8) + "px";
break;
default:
mainStyle["justify-content"] = "center";
}
mainStyle["vertical-align"] = theme.display_vertical_align;
if (theme.hasOwnProperty('font_main_shadow_size')) {
mainStyle["text-shadow"] = theme.font_main_shadow_color + " " + theme.font_main_shadow_size + "px " +
theme.font_main_shadow_size + "px";
mainStyle["text-shadow"] = theme.font_main_shadow_color + " " + theme.font_main_shadow_size + "pt " +
theme.font_main_shadow_size + "pt";
}
mainStyle["padding-bottom"] = theme.display_vertical_align == VerticalAlign.Bottom ? "0.5em" : "0";
mainStyle["padding-left"] = !!theme.font_main_outline ? "" + (theme.font_main_outline_size * 2) + "pt" : "0";
// These need to be fixed, in the Python they use a width passed in as a parameter
mainStyle["position"] = "absolute";
mainStyle["width"] = "" + (window.innerWidth - (theme.font_main_outline_size * 4)) + "px";
mainStyle["height"] = "" + (window.innerHeight - (theme.font_main_outline_size * 4)) + "px";
mainStyle["left"] = "" + theme.font_main_x + "px";
mainStyle["top"] = "" + theme.font_main_y + "px";
var slidesDiv = $(".slides")[0];
slidesDiv.style.cssText = "";
for (var key in mainStyle) {
if (mainStyle.hasOwnProperty(key)) {
slidesDiv.style.setProperty(key, mainStyle[key]);
var slidesDiv = $("section.text-slides");
if (slidesDiv.length > 0) {
slidesDiv = slidesDiv[0];
slidesDiv.style.cssText = "";
for (var mainKey in mainStyle) {
if (mainStyle.hasOwnProperty(mainKey)) {
slidesDiv.style.setProperty(mainKey, mainStyle[mainKey]);
}
}
}
// Set up the footer
footerStyle = {
"text-align": "left"
};
footerStyle["position"] = "absolute";
footerStyle["left"] = "" + theme.font_footer_x + "px";
footerStyle["top"] = "" + theme.font_footer_y + "px";
footerStyle["bottom"] = "" + (window.innerHeight - theme.font_footer_y - theme.font_footer_height) + "px";
footerStyle["width"] = "" + theme.font_footer_width + "px";
footerStyle.position = "absolute";
footerStyle.left = "" + theme.font_footer_x + "px";
footerStyle.top = "" + theme.font_footer_y + "px";
footerStyle.width = "" + theme.font_footer_width + "px";
footerStyle.height = "" + theme.font_footer_height + "px";
footerStyle.color = theme.font_footer_color;
footerStyle["font-family"] = theme.font_footer_name;
footerStyle["font-size"] = "" + theme.font_footer_size + "pt";
footerStyle["color"] = theme.font_footer_color;
footerStyle["white-space"] = theme.font_footer_wrap ? "normal" : "nowrap";
var footer = $(".footer")[0];
footer.style.cssText = "";
for (var key in footerStyle) {
if (footerStyle.hasOwnProperty(key)) {
footer.style.setProperty(key, footerStyle[key]);
for (var footerKey in footerStyle) {
if (footerStyle.hasOwnProperty(footerKey)) {
footer.style.setProperty(footerKey, footerStyle[footerKey]);
}
}
},

View File

@ -169,6 +169,7 @@ class Theme(object):
jsn = get_text_file_string(json_path)
self.load_theme(jsn)
self.background_filename = None
self.background_source = None
self.version = 2
def expand_json(self, var, prev=None):

View File

@ -67,7 +67,7 @@ class Ui_PrintServiceDialog(object):
self.toolbar.addWidget(self.options_button)
self.toolbar.addSeparator()
self.plain_copy = self.toolbar.addAction(UiIcons().clone,
translate('OpenLP.PrintServiceForm', 'Copy'))
translate('OpenLP.PrintServiceForm', 'Copy as Text'))
self.html_copy = self.toolbar.addAction(UiIcons().clone,
translate('OpenLP.PrintServiceForm', 'Copy as HTML'))
self.toolbar.addSeparator()
@ -139,7 +139,7 @@ class Ui_PrintServiceDialog(object):
self.zoom_in_button.setToolTip(translate('OpenLP.PrintServiceForm', 'Zoom In'))
self.options_button.setText(translate('OpenLP.PrintServiceForm', 'Options'))
self.title_label.setText(translate('OpenLP.PrintServiceForm', 'Title:'))
self.footer_label.setText(translate('OpenLP.PrintServiceForm', 'Custom Footer Text:'))
self.footer_label.setText(translate('OpenLP.PrintServiceForm', 'Service Note Text:'))
self.options_group_box.setTitle(translate('OpenLP.PrintServiceForm', 'Other Options'))
self.slide_text_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include slide text if available'))
self.page_break_after_text.setText(translate('OpenLP.PrintServiceForm', 'Add page break before each text item'))

View File

@ -197,7 +197,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
if self.footer_text_edit.toPlainText():
div = self._add_element('div', parent=html_data.body, class_id='customNotes')
self._add_element(
'span', translate('OpenLP.ServiceManager', 'Custom Service Notes: '), div, class_id='customNotesTitle')
'span', translate('OpenLP.ServiceManager', 'Service Notes: '), div, class_id='customNotesTitle')
self._add_element('span', html.escape(self.footer_text_edit.toPlainText()), div, class_id='customNotesText')
self.document.setHtml(lxml.html.tostring(html_data).decode())
self.preview_widget.updatePreview()

View File

@ -280,7 +280,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
Run the wizard.
"""
log.debug('Editing theme {name}'.format(name=self.theme.theme_name))
self.temp_background_filename = None
self.temp_background_filename = self.theme.background_source
self.update_theme_allowed = False
self.set_defaults()
self.update_theme_allowed = True
@ -325,11 +325,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.setField('background_type', 1)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
self.image_color_button.color = self.theme.background_border_color
self.image_path_edit.path = self.theme.background_filename
self.image_path_edit.path = self.theme.background_source
self.setField('background_type', 2)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
self.video_color_button.color = self.theme.background_border_color
self.video_path_edit.path = self.theme.background_filename
self.video_path_edit.path = self.theme.background_source
self.setField('background_type', 4)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Stream):
self.setField('background_type', 5)
@ -467,6 +467,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
:param pathlib.Path new_path: Path to the new image
:rtype: None
"""
self.theme.background_source = new_path
self.theme.background_filename = new_path
self.set_background_page_values()
@ -477,6 +478,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
:param pathlib.Path new_path: Path to the new video
:rtype: None
"""
self.theme.background_source = new_path
self.theme.background_filename = new_path
self.set_background_page_values()
@ -553,14 +555,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
translate('OpenLP.ThemeWizard', 'Theme Name Invalid'),
translate('OpenLP.ThemeWizard', 'Invalid theme name. Please enter one.'))
return
source_path = None
destination_path = None
if self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or \
self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
file_name = self.theme.background_filename.name
destination_path = self.path / self.theme.theme_name / file_name
source_path = self.theme.background_filename
if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
return
self.theme_manager.save_theme(self.theme, source_path, destination_path, self.preview_box.save_screenshot())
# Set the theme background to the cache location
self.theme.background_filename = destination_path
self.theme_manager.save_theme(self.theme, self.preview_box.save_screenshot())
return QtWidgets.QDialog.accept(self)

View File

@ -325,14 +325,11 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
:param str new_theme_name: The new theme name of the theme
:rtype: None
"""
destination_path = None
source_path = None
if theme_data.background_type == 'image' or theme_data.background_type == 'video':
destination_path = self.theme_path / new_theme_name / theme_data.background_filename.name
source_path = theme_data.background_filename
theme_data.background_filename = self.theme_path / new_theme_name / theme_data.background_filename.name
theme_data.theme_name = new_theme_name
theme_data.extend_image_filename(self.theme_path)
self.save_theme(theme_data, source_path, destination_path)
self.save_theme(theme_data)
self.load_themes()
def on_edit_theme(self, field=None):
@ -648,14 +645,12 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
return False
return True
def save_theme(self, theme, image_source_path=None, image_destination_path=None, image=None):
def save_theme(self, theme, image=None):
"""
Writes the theme to the disk and handles the background image if necessary
Writes the theme to the disk and including the background image and thumbnail if necessary
:param Theme theme: The theme data object.
:param Path image_source_path: Where the theme image is currently located.
:param Path image_destination_path: Where the Theme Image is to be saved to
:param image: The example image of the theme. Optionally.
:param image: The theme thumbnail. Optionally.
:rtype: None
"""
name = theme.theme_name
@ -667,12 +662,14 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
theme_path.write_text(theme_pretty)
except OSError:
self.log_exception('Saving theme to file failed')
if image_source_path and image_destination_path:
if self.old_background_image_path and image_destination_path != self.old_background_image_path:
if theme.background_source and theme.background_filename:
if self.old_background_image_path and theme.background_filename != self.old_background_image_path:
delete_file(self.old_background_image_path)
if image_source_path != image_destination_path:
if not theme.background_source.exists():
self.log_warning('Background does not exist, retaining cached background')
elif theme.background_source != theme.background_filename:
try:
shutil.copyfile(image_source_path, image_destination_path)
shutil.copyfile(theme.background_source, theme.background_filename)
except OSError:
self.log_exception('Failed to save theme image')
if image:

View File

@ -8,6 +8,7 @@
},
"dependencies": {
"jasmine-core": "^2.6.4",
"jshint": "^2.10.2",
"karma": "^3.1.4",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^1.1.2",
@ -17,7 +18,11 @@
"karma-log-reporter": "0.0.4"
},
"scripts": {
"test": "karma start --single-run"
"test": "karma start --single-run",
"lint": "jshint openlp/core/display/html/display.js"
},
"jshintConfig": {
"esversion": 6
},
"author": "OpenLP Developers",
"license": "GPL-3.0-or-later"

View File

@ -181,4 +181,4 @@ class TestTheme(TestCase):
assert 0 == theme.display_vertical_align, 'display_vertical_align should be 0'
assert theme.font_footer_bold is False, 'font_footer_bold should be False'
assert 'Arial' == theme.font_main_name, 'font_main_name should be "Arial"'
assert 48 == len(theme.__dict__), 'The theme should have 48 attributes'
assert 49 == len(theme.__dict__), 'The theme should have 49 attributes'

View File

@ -87,19 +87,22 @@ class TestThemeManager(TestCase):
Test that we don't try to overwrite a theme background image with itself
"""
# GIVEN: A new theme manager instance, with mocked builtins.open, copyfile,
# theme, create_paths and thememanager-attributes.
# theme, create_paths, thememanager-attributes and background
# .filename path is the same as the source path.
theme_manager = ThemeManager(None)
theme_manager.old_background_image = None
theme_manager.old_background_image_path = None
theme_manager.update_preview_images = MagicMock()
theme_manager.theme_path = MagicMock()
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()
# WHEN: Calling save_theme with path to the same image, but the path written slightly different
file_path_1 = RESOURCE_PATH / 'church.jpg'
theme_manager.save_theme(mocked_theme, file_path_1, file_path_1)
mocked_theme.background_filename = file_path_1
mocked_theme.background_source = file_path_1
# WHEN: Calling save_theme with both background paths to the same image
theme_manager.save_theme(mocked_theme)
# THEN: The mocked_copyfile should not have been called
assert mocked_shutil.copyfile.called is False, 'copyfile should not be called'
@ -111,30 +114,118 @@ class TestThemeManager(TestCase):
Test that we do overwrite a theme background image when a new is submitted
"""
# GIVEN: A new theme manager instance, with mocked builtins.open, copyfile,
# theme, create_paths and thememanager-attributes.
# theme, create_paths, thememanager-attributes and background
# .filename path is the same as the source path.
theme_manager = ThemeManager(None)
theme_manager.old_background_image = None
theme_manager.old_background_image_path = None
theme_manager.update_preview_images = MagicMock()
theme_manager.theme_path = MagicMock()
mocked_theme = MagicMock()
mocked_theme.theme_name = 'themename'
mocked_theme.filename = "filename"
mocked_theme.background_filename = RESOURCE_PATH / 'church.jpg'
mocked_theme.background_source = RESOURCE_PATH / 'church2.jpg'
# WHEN: Calling save_theme with path to different images
file_path_1 = RESOURCE_PATH / 'church.jpg'
file_path_2 = RESOURCE_PATH / 'church2.jpg'
theme_manager.save_theme(mocked_theme, file_path_1, file_path_2)
# WHEN: Calling save_theme with both background paths to different images
theme_manager.save_theme(mocked_theme)
# THEN: The mocked_copyfile should not have been called
# THEN: The mocked_copyfile should have been called
assert mocked_shutil.copyfile.called is True, 'copyfile should be called'
@patch('openlp.core.ui.thememanager.shutil')
@patch('openlp.core.ui.thememanager.delete_file')
@patch('openlp.core.ui.thememanager.create_paths')
def test_save_theme_delete_old_image(self, mocked_create_paths, mocked_delete_file, mocked_shutil):
"""
Test that we do delete a old theme background image when a new is submitted
"""
# GIVEN: A new theme manager instance, with mocked builtins.open,
# theme, create_paths, thememanager-attributes and background
# .filename path is the same as the source path.
theme_manager = ThemeManager(None)
theme_manager.old_background_image_path = RESOURCE_PATH / 'old_church.png'
theme_manager.update_preview_images = MagicMock()
theme_manager.theme_path = MagicMock()
mocked_theme = MagicMock()
mocked_theme.theme_name = 'themename'
mocked_theme.background_filename = RESOURCE_PATH / 'church.jpg'
mocked_theme.background_source = RESOURCE_PATH / 'church2.jpg'
# WHEN: Calling save_theme with both background paths to different images
theme_manager.save_theme(mocked_theme)
# THEN: The mocked_delete_file should have been called to delete the old cached background
assert mocked_delete_file.called is True, 'delete_file should be called'
@patch.object(ThemeManager, 'log_exception')
@patch('openlp.core.ui.thememanager.delete_file')
@patch('openlp.core.ui.thememanager.create_paths')
def test_save_theme_missing_original(self, mocked_paths, mocked_delete, mocked_log_exception):
"""
Test that we revert to the old theme background image if the source is missing
when changing the theme. (User doesn't change background but the original is
missing)
"""
# GIVEN: A new theme manager instance, with invalid files. Setup as if the user
# has left the background the same, or reselected the same path.
# Not using resource dir because I could potentially copy a file
folder_path = Path(mkdtemp())
theme_manager = ThemeManager(None)
theme_manager.old_background_image_path = folder_path / 'old.png'
theme_manager.update_preview_images = MagicMock()
theme_manager.theme_path = MagicMock()
mocked_theme = MagicMock()
mocked_theme.theme_name = 'themename'
mocked_theme.background_filename = folder_path / 'old.png'
mocked_theme.background_source = folder_path / 'non_existent_original.png'
# WHEN: Calling save_theme with a invalid background_filename
# Although all filenames are considered invalid in this test,
# it is important it reverts to the old background path as this in reality is always
# valid unless someone has messed with the cache.
theme_manager.save_theme(mocked_theme)
# THEN: The old background should not have bee deleted
# AND the filename should have been replaced with the old cached background
# AND there is no exception
assert mocked_delete.called is False, 'delete_file should not be called'
assert mocked_theme.background_filename == theme_manager.old_background_image_path, \
'Background path should be reverted'
assert mocked_log_exception.called is False, \
'Should not have been an exception as the file wasn\'t changed'
@patch.object(ThemeManager, 'log_warning')
@patch('openlp.core.ui.thememanager.delete_file')
@patch('openlp.core.ui.thememanager.create_paths')
def test_save_theme_missing_new(self, mocked_paths, mocked_delete, mocked_log_warning):
"""
Test that we log a warning if the new background is missing
"""
# GIVEN: A new theme manager instance, with invalid files. Setup as if the user
# has changed the background to a invalid path.
# Not using resource dir because I could potentially copy a file
folder_path = Path(mkdtemp())
theme_manager = ThemeManager(None)
theme_manager.old_background_image_path = folder_path / 'old.png'
theme_manager.update_preview_images = MagicMock()
theme_manager.theme_path = MagicMock()
mocked_theme = MagicMock()
mocked_theme.theme_name = 'themename'
mocked_theme.background_filename = folder_path / 'new_cached.png'
mocked_theme.background_source = folder_path / 'new_original.png'
# WHEN: Calling save_theme with a invalid background_filename
theme_manager.save_theme(mocked_theme)
# THEN: A warning should have happened due to attempting to copy a missing file
mocked_log_warning.assert_called_once_with('Background does not exist, retaining cached background')
def test_save_theme_special_char_name(self):
"""
Test that we can save themes with special characters in the name
"""
# GIVEN: A new theme manager instance, with mocked theme and thememanager-attributes.
theme_manager = ThemeManager(None)
theme_manager.old_background_image = None
theme_manager.old_background_image_path = None
theme_manager.update_preview_images = MagicMock()
theme_manager.theme_path = Path(self.temp_folder)
mocked_theme = MagicMock()

View File

@ -22,6 +22,7 @@
This module contains tests for the PdfController
"""
import os
import pytest
from pathlib import Path
from shutil import rmtree, which
from tempfile import mkdtemp
@ -173,7 +174,13 @@ class TestPdfController(TestCase, TestMixin):
if exe_path:
self.load_pdf(exe_path)
self.load_pdf_pictures(exe_path)
# PyMuPDF
def test_loading_pdf_using_pymupdf(self):
try:
import fitz # noqa: F401
except ImportError:
pytest.skip('PyMuPDF is not installed')
self.load_pdf(None)
self.load_pdf_pictures(None)

View File

@ -147,7 +147,7 @@ describe("The Display object", function () {
Display._slides["v1"] = 0;
Display.goToSlide("v1");
expect(Reveal.slide).toHaveBeenCalledWith(0);
expect(Reveal.slide).toHaveBeenCalledWith(0, 0);
});
it("should have an alert() method", function () {
@ -458,8 +458,8 @@ describe("Display.addTextSlide", function () {
Display.addTextSlide(verse, text, footer);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
expect($(".slides > section")[0].innerHTML).toEqual(_prepareText(text));
expect($(".slides > section > section").length).toEqual(1);
expect($(".slides > section > section")[0].innerHTML).toEqual(_prepareText(text));
expect(Display.reinit).toHaveBeenCalled();
});
@ -472,8 +472,8 @@ describe("Display.addTextSlide", function () {
Display.addTextSlide(verse, text, footer, false);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
expect($(".slides > section")[0].innerHTML).toEqual(_prepareText(text));
expect($(".slides > section > section").length).toEqual(1);
expect($(".slides > section > section")[0].innerHTML).toEqual(_prepareText(text));
expect(Display.reinit).not.toHaveBeenCalled();
});
@ -484,12 +484,12 @@ describe("Display.addTextSlide", function () {
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", footer, false);
spyOn(Display, "reinit");
Display.addTextSlide(verse, text, true);
Display.addTextSlide(verse, text, footer, true);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
expect($(".slides > section")[0].innerHTML).toEqual(_prepareText(text));
expect(Display.reinit).toHaveBeenCalled();
expect($(".slides > section > section").length).toEqual(1);
expect($(".slides > section > section")[0].innerHTML).toEqual(_prepareText(text));
expect(Display.reinit).toHaveBeenCalledTimes(0);
});
});
@ -526,12 +526,12 @@ describe("Display.setTextSlides", function () {
expect(Display.clearSlides).toHaveBeenCalledTimes(1);
expect(Display._slides["v1"]).toEqual(0);
expect(Display._slides["v2"]).toEqual(1);
expect($(".slides > section").length).toEqual(2);
expect($(".slides > section > section").length).toEqual(2);
expect(Display.reinit).toHaveBeenCalledTimes(1);
expect(Reveal.slide).toHaveBeenCalledWith(0);
expect(Reveal.slide).toHaveBeenCalledWith(0, 0);
});
xit("should correctly set outline width (skipped for now)", function () {
it("should correctly set outline width", function () {
const slides = [
{
"verse": "v1",
@ -549,14 +549,68 @@ describe("Display.setTextSlides", function () {
spyOn(Display, "reinit");
spyOn(Reveal, "slide");
Display.setTextSlides(slides);
Display.setTheme(theme);
Display.setTextSlides(slides);
const slidesDiv = $(".slides")[0];
const slidesDiv = $(".text-slides")[0];
expect(slidesDiv.style['-webkit-text-stroke']).toEqual('42pt red');
expect(slidesDiv.style['padding-left']).toEqual('84pt');
expect(slidesDiv.style['-webkit-text-fill-color']).toEqual('yellow');
})
it("should correctly set text alignment,\
(check the order of alignments in the emuns are the same in both js and python)", function () {
const slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
//
const theme = {
'display_horizontal_align': 3,
'display_vertical_align': 1
};
spyOn(Display, "reinit");
spyOn(Reveal, "slide");
Display.setTheme(theme);
Display.setTextSlides(slides);
const slidesDiv = $(".text-slides")[0];
expect(slidesDiv.style['text-align-last']).toEqual('justify');
expect(slidesDiv.style['justify-content']).toEqual('center');
})
it("should correctly set slide size position to theme size when adding a text slide", function () {
const slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
//
const theme = {
'font_main_y': 789,
'font_main_x': 1000,
'font_main_width': 1230,
'font_main_height': 4560
};
spyOn(Display, "reinit");
spyOn(Reveal, "slide");
Display.setTheme(theme);
Display.setTextSlides(slides);
const slidesDiv = $(".text-slides")[0];
expect(slidesDiv.style['top']).toEqual('789px');
expect(slidesDiv.style['left']).toEqual('1000px');
expect(slidesDiv.style['width']).toEqual('1230px');
expect(slidesDiv.style['height']).toEqual('4560px');
})
});
describe("Display.setImageSlides", function () {
@ -578,12 +632,12 @@ describe("Display.setImageSlides", function () {
expect(Display.clearSlides).toHaveBeenCalledTimes(1);
expect(Display._slides["0"]).toEqual(0);
expect(Display._slides["1"]).toEqual(1);
expect($(".slides > section").length).toEqual(2);
expect($(".slides > section > img").length).toEqual(2);
expect($(".slides > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg")
expect($(".slides > section > img")[0].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
expect($(".slides > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg")
expect($(".slides > section > img")[1].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
expect($(".slides > section > section").length).toEqual(2);
expect($(".slides > section > section > img").length).toEqual(2);
expect($(".slides > section > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg")
expect($(".slides > section > section > img")[0].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
expect($(".slides > section > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg")
expect($(".slides > section > section > img")[1].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
expect(Display.reinit).toHaveBeenCalledTimes(1);
});
});

2595
yarn.lock Normal file

File diff suppressed because it is too large Load Diff