This commit is contained in:
Raoul Snyman 2019-08-26 21:41:56 -07:00
commit e53f61b7f3
75 changed files with 399 additions and 319967 deletions

View File

@ -8,7 +8,7 @@
*.nja
*.orig
*.pyc
*.qm
resources/i18n/*.ts
*.rej
*.ropeproject
*.~\?~

View File

@ -16,3 +16,5 @@ include copyright.txt
include LICENSE
include README.txt
include openlp/.version
include package.json
include karma.conf.js

View File

@ -26,18 +26,21 @@ module.exports = function(config) {
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
"display.js": ["coverage"]
// "display.js": ["coverage"]
},
// test results reporter to use
// possible values: "dots", "progress"
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["progress", "coverage"],
reporters: ["dots", "junit"],
// configure the coverateReporter
coverageReporter: {
/* coverageReporter: {
type : "html",
dir : "htmlcov/"
}, */
junitReporter: {
outputFile: "test-results.xml"
},
// web server port
@ -60,11 +63,11 @@ module.exports = function(config) {
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["PhantomJS"],
browsers: ["Chromium"],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous

View File

@ -172,6 +172,21 @@ class SlideLimits(object):
Next = 3
class Singleton(type):
"""
Provide a `Singleton` metaclass https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python
"""
_instances = {}
def __call__(cls, *args, **kwargs):
"""
Create a new instance if one does not already exist.
"""
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
def de_hump(name):
"""
Change any Camel Case string to python string
@ -385,7 +400,7 @@ def get_images_filter():
global IMAGES_FILTER
if not IMAGES_FILTER:
log.debug('Generating images filter.')
formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
formats = list(map(bytes.decode, map(bytes, QtGui.QImageReader.supportedImageFormats())))
visible_formats = '(*.{text})'.format(text='; *.'.join(formats))
actual_formats = '(*.{text})'.format(text=' *.'.join(formats))
IMAGES_FILTER = '{text} {visible} {actual}'.format(text=translate('OpenLP', 'Image Files'),

View File

@ -260,7 +260,7 @@ class ActionList(object):
return
# We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
# which is only done when we convert the strings in this way (QKeySequencet -> uncode).
shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
shortcuts = list(map(QtGui.QKeySequence.toString, map(QtGui.QKeySequence, shortcuts)))
# Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
# after removing the (initial) primary shortcut due to conflicts.
if len(shortcuts) == 2:

View File

@ -29,7 +29,7 @@ from collections import namedtuple
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import is_macosx, is_win
from openlp.core.common import Singleton, is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.settings import Settings
@ -327,22 +327,11 @@ class LanguageManager(object):
return LanguageManager.__qm_list__
class UiStrings(object):
class UiStrings(metaclass=Singleton):
"""
Provide standard strings for objects to use.
"""
__instance__ = None
def __new__(cls):
"""
Override the default object creation method to return a single instance.
"""
if not cls.__instance__:
cls.__instance__ = super().__new__(cls)
cls.__instance__.load()
return cls.__instance__
def load(self):
def __init__(self):
"""
These strings should need a good reason to be retranslated elsewhere.
Should some/more/less of these have an & attached?
@ -436,6 +425,7 @@ class UiStrings(object):
self.ResetLiveBG = translate('OpenLP.Ui', 'Reset live background.')
self.RequiredShowInFooter = translate('OpenLP.Ui', 'Required, this will be displayed in footer.')
self.Seconds = translate('OpenLP.Ui', 's', 'The abbreviated unit for seconds')
self.SaveAndClose = translate('OpenLP.ui', translate('SongsPlugin.EditSongForm', '&Save && Close'))
self.SaveAndPreview = translate('OpenLP.Ui', 'Save && Preview')
self.Search = translate('OpenLP.Ui', 'Search')
self.SearchThemes = translate('OpenLP.Ui', 'Search Themes...', 'Search bar place holder text ')

View File

@ -23,29 +23,19 @@
Provide Registry Services
"""
import logging
import sys
from openlp.core.common import de_hump, trace_error_handler
from openlp.core.common import Singleton, de_hump, trace_error_handler
log = logging.getLogger(__name__)
class Registry(object):
class Registry(metaclass=Singleton):
"""
This is the Component Registry. It is a singleton object and is used to provide a look up service for common
objects.
"""
log.info('Registry loaded')
__instance__ = None
def __new__(cls):
"""
Re-implement the __new__ method to make sure we create a true singleton.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
@classmethod
def create(cls):
@ -57,20 +47,9 @@ class Registry(object):
registry.service_list = {}
registry.functions_list = {}
registry.working_flags = {}
# Allow the tests to remove Registry entries but not the live system
registry.running_under_test = 'nose' in sys.argv[0] or 'pytest' in sys.argv[0]
registry.initialising = True
return registry
@classmethod
def destroy(cls):
"""
Destroy the Registry.
"""
if cls.__instance__.running_under_test:
del cls.__instance__
cls.__instance__ = None
def get(self, key):
"""
Extracts the registry value from the list based on the key passed in

View File

@ -90,12 +90,13 @@ def upgrade_screens(number, x_position, y_position, height, width, can_override,
number: {
'number': number,
geometry_key: {
'x': x_position,
'y': y_position,
'height': height,
'width': width
'x': int(x_position),
'y': int(y_position),
'height': int(height),
'width': int(width)
},
'is_display': is_display_screen
'is_display': is_display_screen,
'is_primary': can_override
}
}
@ -309,7 +310,7 @@ class Settings(QtCore.QSettings):
('songuasge/db hostname', 'songusage/db hostname', []),
('songuasge/db database', 'songusage/db database', []),
('presentations / Powerpoint Viewer', '', []),
(['core/monitor', 'core/x position', 'core/y position', 'core/height', 'core/width', 'core/override',
(['core/monitor', 'core/x position', 'core/y position', 'core/height', 'core/width', 'core/override position',
'core/display on monitor'], 'core/screens', [(upgrade_screens, [1, 0, 0, None, None, False, False])]),
('bibles/proxy name', '', []), # Just remove these bible proxy settings. They weren't used in 2.4!
('bibles/proxy address', '', []),

View File

@ -281,8 +281,9 @@ var Display = {
* Checks if the present slide content fits within the slide
*/
doesContentFit: function () {
console.debug("scrollHeight: " + $(".slides")[0].scrollHeight + ", clientHeight: " + $(".slides")[0].clientHeight);
return $(".slides")[0].clientHeight >= $(".slides")[0].scrollHeight;
var currSlide = $(".slides")[0];
console.debug("scrollHeight: " + currSlide.scrollHeight + ", clientHeight: " + currSlide.clientHeight);
return currSlide.clientHeight >= currSlide.scrollHeight;
},
/**
* Generate the OpenLP startup splashscreen
@ -333,7 +334,7 @@ var Display = {
/**
* Set fullscreen image from base64 data
* @param {string} bg_color - The background color
* @param {string} image - Path to the image
* @param {string} image_data - base64 encoded image data
*/
setFullscreenImageFromData: function(bg_color, image_data) {
Display.clearSlides();
@ -372,9 +373,8 @@ var Display = {
* @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 {bool} [reinit=true] - Re-initialize Reveal. Defaults to true.
*/
addTextSlide: function (verse, text, footer_text) {
addTextSlide: function (verse, text, footerText) {
var html = _prepareText(text);
if (this._slides.hasOwnProperty(verse)) {
var slide = $("#" + verse)[0];
@ -390,11 +390,9 @@ var Display = {
slidesDiv.appendChild(slide);
var slides = $(".slides > section");
this._slides[verse] = slides.length - 1;
console.debug(" footer_text: " + footer_text);
var footerDiv = $(".footer")[0];
footerDiv.innerHTML = footer_text;
if (footerText) {
$(".footer")[0].innerHTML = footerText;
}
}
if ((arguments.length > 3) && (arguments[3] === true)) {
this.reinit();
@ -426,9 +424,10 @@ var Display = {
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.setAttribute("style", "height: 100%; width: 100%;");
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);
Display._slides[index.toString()] = index;
@ -476,25 +475,28 @@ var Display = {
* Play a video
*/
playVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].play();
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].play();
}
},
/**
* Pause a video
*/
pauseVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].pause();
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].pause();
}
},
/**
* Stop a video
*/
stopVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].pause();
$("#video")[0].currentTime = 0.0;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].pause();
videoElem[0].currentTime = 0.0;
}
},
/**
@ -502,8 +504,9 @@ var Display = {
* @param seconds The position in seconds to seek to
*/
seekVideo: function (seconds) {
if ($("#video").length == 1) {
$("#video")[0].currentTime = seconds;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].currentTime = seconds;
}
},
/**
@ -511,8 +514,9 @@ var Display = {
* @param rate A Double of the rate. 1.0 => 100% speed, 0.75 => 75% speed, 1.25 => 125% speed, etc.
*/
setPlaybackRate: function (rate) {
if ($("#video").length == 1) {
$("#video")[0].playbackRate = rate;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].playbackRate = rate;
}
},
/**
@ -520,24 +524,27 @@ var Display = {
* @param level The volume level from 0 to 100.
*/
setVideoVolume: function (level) {
if ($("#video").length == 1) {
$("#video")[0].volume = level / 100.0;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].volume = level / 100.0;
}
},
/**
* Mute the volume
*/
toggleVideoMute: function () {
if ($("#video").length == 1) {
$("#video")[0].muted = !$("#video")[0].muted;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].muted = !videoElem[0].muted;
}
},
/**
* Clear the background audio playlist
*/
clearPlaylist: function () {
if ($("#background-audio").length == 1) {
var audio = $("#background-audio")[0];
var backgroundAudoElem = $("#background-audio");
if (backgroundAudoElem.length == 1) {
var audio = backgroundAudoElem[0];
/* audio.playList */
}
},
@ -619,7 +626,6 @@ var Display = {
},
setTheme: function (theme) {
this._theme = theme;
var slidesDiv = $(".slides")
// Set the background
var globalBackground = $("#global-background")[0];
var backgroundStyle = {};

View File

@ -47,7 +47,7 @@ log = logging.getLogger(__name__)
SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
r'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?')
r'([\u0080-\uFFFF\w\s\.\,\!\?\;\:\|\"\'\-\_]*)(\Z)?')
CHORD_TEMPLATE = '<span class="chordline">{chord}</span>'
FIRST_CHORD_TEMPLATE = '<span class="chordline firstchordline">{chord}</span>'
CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}'
@ -482,6 +482,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
:param theme_data: The theme to generated a preview for.
:param force_page: Flag to tell message lines per page need to be generated.
:param generate_screenshot: Do I need to generate a screen shot?
:rtype: QtGui.QPixmap
"""
# save value for use in format_slide

View File

@ -28,6 +28,7 @@ from functools import cmp_to_key
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import Singleton
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
@ -133,8 +134,13 @@ class Screen(object):
self.number = int(screen_dict['number'])
self.is_display = screen_dict['is_display']
self.is_primary = screen_dict['is_primary']
self.geometry = QtCore.QRect(screen_dict['geometry']['x'], screen_dict['geometry']['y'],
screen_dict['geometry']['width'], screen_dict['geometry']['height'])
try:
self.geometry = QtCore.QRect(screen_dict['geometry']['x'], screen_dict['geometry']['y'],
screen_dict['geometry']['width'], screen_dict['geometry']['height'])
except KeyError:
# Preserve the current values as this has come from the settings update which does not have
# the geometry information
pass
if 'custom_geometry' in screen_dict:
self.custom_geometry = QtCore.QRect(screen_dict['custom_geometry']['x'],
screen_dict['custom_geometry']['y'],
@ -142,24 +148,15 @@ class Screen(object):
screen_dict['custom_geometry']['height'])
class ScreenList(object):
class ScreenList(metaclass=Singleton):
"""
Wrapper to handle the parameters of the display screen.
To get access to the screen list call ``ScreenList()``.
"""
log.info('Screen loaded')
__instance__ = None
screens = []
def __new__(cls):
"""
Re-implement __new__ to create a true singleton.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
def __iter__(self):
"""
Convert this object into an iterable, so that we can iterate over it instead of the inner list

View File

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

View File

@ -28,6 +28,7 @@ contained within the openlp.core module.
"""
import logging
from openlp.core.common import Singleton
from openlp.core.common.registry import Registry
from openlp.core.common.mixins import LogMixin
from openlp.core.lib.plugin import PluginStatus
@ -52,17 +53,7 @@ class StateModule(LogMixin):
self.text = None
class State(LogMixin):
__instance__ = None
def __new__(cls):
"""
Re-implement the __new__ method to make sure we create a true singleton.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
class State(LogMixin, metaclass=Singleton):
def load_settings(self):
self.modules = {}

View File

@ -24,10 +24,11 @@ The :mod:`openlp.core.threading` module contains some common threading code
"""
from PyQt5 import QtCore
from openlp.core.common.mixins import LogMixin
from openlp.core.common.registry import Registry
class ThreadWorker(QtCore.QObject):
class ThreadWorker(QtCore.QObject, LogMixin):
"""
The :class:`~openlp.core.threading.ThreadWorker` class provides a base class for all worker objects
"""

View File

@ -179,7 +179,7 @@ class UiAboutDialog(object):
hu = translate('OpenLP.AboutForm', 'Hungarian (hu)')
ind = translate('OpenLP.AboutForm', 'Indonesian (id)')
ja = translate('OpenLP.AboutForm', 'Japanese (ja)')
nb = translate('OpenLP.AboutForm', 'Norwegian Bokm\xe5l (nb)')
nb = translate('OpenLP.AboutForm', 'Norwegian Bokmal (nb)')
nl = translate('OpenLP.AboutForm', 'Dutch (nl)')
pl = translate('OpenLP.AboutForm', 'Polish (pl)')
ptbr = translate('OpenLP.AboutForm', 'Portuguese, Brazil (pt_BR)')

View File

@ -81,7 +81,7 @@ class AdvancedTab(SettingsTab):
self.ui_layout.addRow(self.media_plugin_check_box)
self.hide_mouse_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
self.ui_layout.addWidget(self.hide_mouse_check_box)
self.ui_layout.addRow(self.hide_mouse_check_box)
self.double_click_live_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.double_click_live_check_box.setObjectName('double_click_live_check_box')
self.ui_layout.addRow(self.double_click_live_check_box)

View File

@ -27,6 +27,7 @@ import logging
import qtawesome as qta
from PyQt5 import QtGui, QtWidgets
from openlp.core.common import Singleton
from openlp.core.common.applocation import AppLocation
from openlp.core.lib import build_icon
@ -34,22 +35,11 @@ from openlp.core.lib import build_icon
log = logging.getLogger(__name__)
class UiIcons(object):
class UiIcons(metaclass=Singleton):
"""
Provide standard icons for objects to use.
"""
__instance__ = None
def __new__(cls):
"""
Override the default object creation method to return a single instance.
"""
if not cls.__instance__:
cls.__instance__ = super().__new__(cls)
cls.__instance__.load()
return cls.__instance__
def load(self):
def __init__(self):
"""
These are the font icons used in the code.
"""
@ -165,6 +155,7 @@ class UiIcons(object):
'volunteer': {'icon': 'fa.group'}
}
self.load_icons(icon_list)
self.main_icon = build_icon(':/icon/openlp-logo.svg')
def load_icons(self, icon_list):
"""
@ -184,7 +175,6 @@ class UiIcons(object):
setattr(self, key, qta.icon('fa.plus-circle', color='red'))
except Exception:
setattr(self, key, qta.icon('fa.plus-circle', color='red'))
self.main_icon = build_icon(':/icon/openlp-logo.svg')
@staticmethod
def _print_icons():

View File

@ -635,7 +635,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
# if self.live_controller.display.isVisible():
# self.live_controller.display.setFocus()
self.activateWindow()
if self.application.args:
# We have -disable-web-security added by our code.
# If a file is passed in we will have that as well so count of 2
# If not we need to see if we want to use the previous file.so count of 1
if self.application.args and len(self.application.args) > 1:
self.open_cmd_line_files(self.application.args)
elif Settings().value(self.general_settings_section + '/auto open'):
self.service_manager_contents.load_last_file()

View File

@ -219,13 +219,13 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
verse_def = None
verse_html = None
for slide in item.get_frames():
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['printing_html']:
if not verse_def or verse_def != slide['verse'] or verse_html == slide['text']:
text_div = self._add_element('div', parent=div, class_id='itemText')
elif 'chordspacing' not in slide['printing_html']:
elif 'chordspacing' not in slide['text']:
self._add_element('br', parent=text_div)
self._add_element('span', slide['printing_html'], text_div)
verse_def = slide['verseTag']
verse_html = slide['printing_html']
self._add_element('span', slide['text'], text_div)
verse_def = slide['verse']
verse_html = slide['text']
# Break the page before the div element.
if index != 0 and self.page_break_after_text.isChecked():
div.set('class', 'item newPage')

View File

@ -53,7 +53,6 @@ class ScreensTab(SettingsTab):
self.setObjectName('self')
self.tab_layout = QtWidgets.QVBoxLayout(self)
self.tab_layout.setObjectName('tab_layout')
self.screen_selection_widget = ScreenSelectionWidget(self, ScreenList())
self.tab_layout.addWidget(self.screen_selection_widget)
self.generic_group_box = QtWidgets.QGroupBox(self)
@ -63,13 +62,11 @@ class ScreensTab(SettingsTab):
self.display_on_monitor_check.setObjectName('monitor_combo_box')
self.generic_group_layout.addWidget(self.display_on_monitor_check)
self.tab_layout.addWidget(self.generic_group_box)
Registry().register_function('config_screen_changed', self.screen_selection_widget.load)
self.retranslate_ui()
def retranslate_ui(self):
self.setWindowTitle(translate('self', 'self')) # TODO: ???
self.generic_group_box.setTitle(translate('OpenLP.ScreensTab', 'Generic screen settings'))
self.display_on_monitor_check.setText(translate('OpenLP.ScreensTab', 'Display if a single screen'))

View File

@ -34,6 +34,7 @@ from tempfile import NamedTemporaryFile
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.state import State
from openlp.core.common import ThemeLevel, delete_file
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.applocation import AppLocation
@ -828,7 +829,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.auto_start_action.setIcon(UiIcons().inactive)
self.auto_start_action.setText(translate('OpenLP.ServiceManager', '&Auto Start - inactive'))
if service_item['service_item'].is_text():
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
if plugin.name == 'custom' and plugin.status == PluginStatus.Active:
self.create_custom_action.setVisible(True)
break

View File

@ -30,6 +30,7 @@ from xml.etree.ElementTree import XML, ElementTree
from PyQt5 import QtCore, QtWidgets
from openlp.core.state import State
from openlp.core.common import delete_file
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, get_locale_key, translate
@ -293,7 +294,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
old_theme_data = self.get_theme_data(old_theme_name)
self.clone_theme_data(old_theme_data, new_theme_name)
self.delete_theme(old_theme_name)
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
if plugin.uses_theme(old_theme_name):
plugin.rename_theme(old_theme_name, new_theme_name)
self.renderer.set_theme(self.get_theme_data(new_theme_name))
@ -612,7 +613,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
critical_error_message_box(
translate('OpenLP.ThemeManager', 'Import Error'),
translate('OpenLP.ThemeManager', 'There was a problem imoorting {file_name}.\n\nIt is corrupt,'
translate('OpenLP.ThemeManager', 'There was a problem importing {file_name}.\n\nIt is corrupt, '
'inaccessible or not a valid theme.').format(file_name=file_path))
finally:
if not abort_import:
@ -771,7 +772,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
# check for use in the system else where.
if test_plugin:
plugin_usage = ""
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
used_count = plugin.uses_theme(theme)
if used_count:
plugin_usage = "{plug}{text}".format(plug=plugin_usage,

View File

@ -210,9 +210,13 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
pixmap = QtGui.QPixmap(str(slide['image']))
else:
pixmap = QtGui.QPixmap(str(slide['path']))
if pixmap.height() > 0:
pixmap_ratio = pixmap.width() / pixmap.height()
else:
pixmap_ratio = 1
label.setPixmap(pixmap)
container = QtWidgets.QWidget()
layout = AspectRatioLayout(container, self.screen_ratio)
layout = AspectRatioLayout(container, pixmap_ratio)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(label)
container.setLayout(layout)

View File

@ -26,6 +26,7 @@ plugin.
import logging
import re
from openlp.core.common import Singleton
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
@ -64,20 +65,10 @@ class LanguageSelection(object):
English = 2
class BibleStrings(object):
class BibleStrings(metaclass=Singleton):
"""
Provide standard strings for objects to use.
"""
__instance__ = None
def __new__(cls):
"""
Override the default object creation method to return a single instance.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
def __init__(self):
"""
These strings should need a good reason to be retranslated elsewhere.
@ -336,11 +327,13 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
log.debug('Matched reference {text}'.format(text=reference))
book = match.group('book')
if not book_ref_id:
book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection)
book_ref_ids = bible.get_book_ref_id_by_localised_name(book, language_selection)
elif not bible.get_book_by_book_ref_id(book_ref_id):
return []
else:
book_ref_ids = [book_ref_id]
# We have not found the book so do not continue
if not book_ref_id:
if not book_ref_ids:
return []
ranges = match.group('ranges')
range_list = get_reference_match('range_separator').split(ranges)
@ -381,22 +374,23 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
to_chapter = to_verse
to_verse = None
# Append references to the list
if has_range:
if not from_verse:
from_verse = 1
if not to_verse:
to_verse = -1
if to_chapter and to_chapter > from_chapter:
ref_list.append((book_ref_id, from_chapter, from_verse, -1))
for i in range(from_chapter + 1, to_chapter):
ref_list.append((book_ref_id, i, 1, -1))
ref_list.append((book_ref_id, to_chapter, 1, to_verse))
elif to_verse >= from_verse or to_verse == -1:
ref_list.append((book_ref_id, from_chapter, from_verse, to_verse))
elif from_verse:
ref_list.append((book_ref_id, from_chapter, from_verse, from_verse))
else:
ref_list.append((book_ref_id, from_chapter, 1, -1))
for book_ref_id in book_ref_ids:
if has_range:
if not from_verse:
from_verse = 1
if not to_verse:
to_verse = -1
if to_chapter and to_chapter > from_chapter:
ref_list.append((book_ref_id, from_chapter, from_verse, -1))
for i in range(from_chapter + 1, to_chapter):
ref_list.append((book_ref_id, i, 1, -1))
ref_list.append((book_ref_id, to_chapter, 1, to_verse))
elif to_verse >= from_verse or to_verse == -1:
ref_list.append((book_ref_id, from_chapter, from_verse, to_verse))
elif from_verse:
ref_list.append((book_ref_id, from_chapter, from_verse, from_verse))
else:
ref_list.append((book_ref_id, from_chapter, 1, -1))
return ref_list
else:
log.debug('Invalid reference: {text}'.format(text=reference))

View File

@ -281,13 +281,14 @@ class BibleDB(Manager):
log.debug('BibleDB.get_book("{book}")'.format(book=book))
return self.get_object_filtered(Book, Book.name.like(book + '%'))
def get_books(self):
def get_books(self, book=None):
"""
A wrapper so both local and web bibles have a get_books() method that
manager can call. Used in the media manager advanced search tab.
"""
log.debug('BibleDB.get_books()')
return self.get_all_objects(Book, order_by_ref=Book.id)
log.debug('BibleDB.get_books("{book}")'.format(book=book))
filter = Book.name.like(book + '%') if book else None
return self.get_all_objects(Book, filter_clause=filter, order_by_ref=Book.id)
def get_book_by_book_ref_id(self, ref_id):
"""
@ -300,39 +301,35 @@ class BibleDB(Manager):
def get_book_ref_id_by_localised_name(self, book, language_selection):
"""
Return the id of a named book.
Return the ids of a matching named book.
:param book: The name of the book, according to the selected language.
:param language_selection: The language selection the user has chosen in the settings section of the Bible.
:rtype: list[int]
"""
log.debug('get_book_ref_id_by_localised_name("{book}", "{lang}")'.format(book=book, lang=language_selection))
from openlp.plugins.bibles.lib import LanguageSelection, BibleStrings
book_names = BibleStrings().BookNames
# escape reserved characters
book_escaped = book
for character in RESERVED_CHARACTERS:
book_escaped = book_escaped.replace(character, '\\' + character)
book_escaped = book.replace(character, '\\' + character)
regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), re.IGNORECASE)
if language_selection == LanguageSelection.Bible:
db_book = self.get_book(book)
if db_book:
return db_book.book_reference_id
elif language_selection == LanguageSelection.Application:
books = [key for key in list(book_names.keys()) if regex_book.match(str(book_names[key]))]
books = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f]
for value in books:
if self.get_book_by_book_ref_id(value['id']):
return value['id']
elif language_selection == LanguageSelection.English:
books = BiblesResourcesDB.get_books_like(book)
if books:
book_list = [value for value in books if regex_book.match(value['name'])]
if not book_list:
book_list = books
for value in book_list:
if self.get_book_by_book_ref_id(value['id']):
return value['id']
return False
db_books = self.get_books(book)
return [db_book.book_reference_id for db_book in db_books]
else:
book_list = []
if language_selection == LanguageSelection.Application:
books = [key for key in list(book_names.keys()) if regex_book.match(book_names[key])]
book_list = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f]
elif language_selection == LanguageSelection.English:
books = BiblesResourcesDB.get_books_like(book)
if books:
book_list = [value for value in books if regex_book.match(value['name'])]
if not book_list:
book_list = books
return [value['id'] for value in book_list if self.get_book_by_book_ref_id(value['id'])]
return []
def get_verses(self, reference_list, show_error=True):
"""

View File

@ -240,8 +240,10 @@ class BibleManager(LogMixin, RegistryProperties):
book=book,
chapter=chapter))
language_selection = self.get_language_selection(bible)
book_ref_id = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection)
return self.db_cache[bible].get_verse_count(book_ref_id, chapter)
book_ref_ids = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection)
if book_ref_ids:
return self.db_cache[bible].get_verse_count(book_ref_ids[0], chapter)
return 0
def get_verse_count_by_book_ref_id(self, bible, book_ref_id, chapter):
"""

View File

@ -97,6 +97,7 @@ class Ui_CustomEditDialog(object):
self.preview_button = QtWidgets.QPushButton()
self.button_box = create_button_box(custom_edit_dialog, 'button_box', ['cancel', 'save'],
[self.preview_button])
self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save)
self.dialog_layout.addWidget(self.button_box)
self.retranslate_ui(custom_edit_dialog)
@ -112,3 +113,4 @@ class Ui_CustomEditDialog(object):
self.theme_label.setText(translate('CustomPlugin.EditCustomForm', 'The&me:'))
self.credit_label.setText(translate('CustomPlugin.EditCustomForm', '&Credits:'))
self.preview_button.setText(UiStrings().SaveAndPreview)
self.save_button.setText(UiStrings().SaveAndClose)

View File

@ -349,7 +349,7 @@ class CustomMediaItem(MediaManagerItem):
custom.credits = ''
custom_xml = CustomXMLBuilder()
for (idx, slide) in enumerate(item.slides):
custom_xml.add_verse_to_lyrics('custom', str(idx + 1), slide['raw_slide'])
custom_xml.add_verse_to_lyrics('custom', str(idx + 1), slide['text'])
custom.text = str(custom_xml.extract_xml(), 'utf-8')
self.plugin.db_manager.save_object(custom)
self.on_search_text_button_clicked()

View File

@ -291,6 +291,7 @@ class Ui_EditSongDialog(object):
self.warning_label.setObjectName('warning_label')
self.bottom_layout.addWidget(self.warning_label)
self.button_box = create_button_box(edit_song_dialog, 'button_box', ['cancel', 'save'])
self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save)
self.bottom_layout.addWidget(self.button_box)
self.dialog_layout.addLayout(self.bottom_layout)
self.retranslate_ui(edit_song_dialog)
@ -341,6 +342,7 @@ class Ui_EditSongDialog(object):
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> Not all of the verses are in use.')
self.no_verse_order_entered_warning = \
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.')
self.save_button.setText(UiStrings().SaveAndPreview)
def create_combo_box(parent, name, editable=True):

View File

@ -29,6 +29,7 @@ from shutil import copyfile
from PyQt5 import QtCore, QtWidgets
from openlp.core.state import State
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
from openlp.core.common.mixins import RegistryProperties
@ -416,7 +417,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
Load the media files into a combobox.
"""
self.from_media_button.setVisible(False)
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
if plugin.name == 'media' and plugin.status == PluginStatus.Active:
self.from_media_button.setVisible(True)
self.media_form.populate_files(plugin.media_item.get_list(MediaType.Audio))

View File

@ -374,7 +374,9 @@ def init_schema(url):
mapper(SongBookEntry, songs_songbooks_table, properties={
'songbook': relation(Book)
})
mapper(Book, song_books_table)
mapper(Book, song_books_table, properties={
'songs': relation(Song, secondary=songs_songbooks_table)
})
mapper(MediaFile, media_files_table)
mapper(Song, songs_table, properties={
# Use the authors_songs relation when you need access to the 'author_type' attribute

View File

@ -146,7 +146,9 @@ class CCLIFileImport(SongImport):
"""
log.debug('USR file text: {text}'.format(text=text_list))
song_author = ''
song_fields = ''
song_topics = ''
song_words = ''
for line in text_list:
if line.startswith('[S '):
ccli, line = line.split(']', 1)

View File

@ -87,6 +87,7 @@ class DreamBeamImport(SongImport):
if self.stop_import_flag:
return
self.set_defaults()
author_copyright = ''
parser = etree.XMLParser(remove_blank_text=True)
try:
with file_path.open('r') as xml_file:
@ -142,7 +143,7 @@ class DreamBeamImport(SongImport):
author_copyright = song_xml.Text2.Text.text
if author_copyright:
author_copyright = str(author_copyright)
if author_copyright.find(str(SongStrings.CopyrightSymbol)) >= 0:
if author_copyright.find(SongStrings.CopyrightSymbol) >= 0:
self.add_copyright(author_copyright)
else:
self.parse_author(author_copyright)

View File

@ -137,9 +137,11 @@ class EasySlidesImport(SongImport):
except UnicodeDecodeError:
log.exception('Unicode decode error while decoding Contents')
self._success = False
return
except AttributeError:
log.exception('no Contents')
self._success = False
return
lines = lyrics.split('\n')
# we go over all lines first, to determine information,
# which tells us how to parse verses later

View File

@ -268,13 +268,13 @@ class EasyWorshipSongImport(SongImport):
self.db_set_record_struct(field_descriptions)
# Pick out the field description indexes we will need
try:
success = True
fi_title = self.db_find_field(b'Title')
fi_author = self.db_find_field(b'Author')
fi_copy = self.db_find_field(b'Copyright')
fi_admin = self.db_find_field(b'Administrator')
fi_words = self.db_find_field(b'Words')
fi_ccli = self.db_find_field(b'Song Number')
success = True
except IndexError:
# This is the wrong table
success = False

View File

@ -128,7 +128,7 @@ class SongBeamerImport(SongImport):
# The encoding should only be ANSI (cp1252), UTF-8, Unicode, Big-Endian-Unicode.
# So if it doesn't start with 'u' we default to cp1252. See:
# https://forum.songbeamer.com/viewtopic.php?p=419&sid=ca4814924e37c11e4438b7272a98b6f2
if not self.input_file_encoding.lower().startswith('u'):
if self.input_file_encoding and not self.input_file_encoding.lower().startswith('u'):
self.input_file_encoding = 'cp1252'
with file_path.open(encoding=self.input_file_encoding) as song_file:
song_data = song_file.readlines()

View File

@ -34,7 +34,7 @@ class SongStrings(object):
Author = translate('OpenLP.Ui', 'Author', 'Singular')
Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
CopyrightSymbol = '\xa9'
SongBook = translate('OpenLP.Ui', 'Songbook', 'Singular')
SongBooks = translate('OpenLP.Ui', 'Songbooks', 'Plural')
SongIncomplete = translate('OpenLP.Ui', 'Title and/or verses not found')

View File

@ -9,17 +9,16 @@
"dependencies": {
"jasmine-core": "^2.6.4",
"karma": "^3.1.4",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.2.0",
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.4",
"phantomjs-prebuilt": "^2.1.16"
"karma-junit-reporter": "^1.2.0",
"karma-log-reporter": "0.0.4"
},
"scripts": {
"test": "karma start"
"test": "karma start --single-run"
},
"author": "OpenLP Developers",
"license": "GPL-3.0-or-later",
"devDependencies": {
"karma-log-reporter": "0.0.4"
}
"license": "GPL-3.0-or-later"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

9
scripts/.tx/config Normal file
View File

@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[openlp.openlp-30x]
file_filter = ../resources/i18n/<lang>.ts
minimum_perc = 0
source_file = ../resources/i18n/en.ts
source_lang = en
type = qt

46
scripts/pull_translations Executable file
View File

@ -0,0 +1,46 @@
#!/bin/sh
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
#
# This script automates the update of the translations on OpenLP.
#
# It uses the tx client from Transifex for all the heavy lifting
# All download *.ts files are converted to *.qm files which are used by
# OpenLP.
#
###############################################################################
pwd=`pwd`
result=${PWD##*/}; echo $result
if [ $result != 'scripts' ] ; then
echo 'This script must be run from the scripts directory'
exit
fi
rm ../resources/i18n/*.ts
echo
echo Downloading the translated files
echo
tx pull -a --minimum-perc=45
echo Translation update complete

52
scripts/push_translations Executable file
View File

@ -0,0 +1,52 @@
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
#
# This script automates the update of the translations on OpenLP.
#
# It uses the tx client from Transifex for all the heavy lifting
# All download *.ts files are converted to *.qm files which are used by
# OpenLP.
#
###############################################################################
pwd=`pwd`
result=${PWD##*/}; echo $result
if [ $result != 'scripts' ] ; then
echo 'This script must be run from the scripts directory'
exit
fi
echo
echo Generation translation control file
echo
rm ../resources/i18n/*.ts
python3 $pwd/translation_utils.py -p
echo Creating base translation file
cd ..
pylupdate5 -verbose -noobsolete openlp.pro
cd scripts
echo Check of invalid characters in push file
grep -axv '.*' ../resources/i18n/en.ts
tx push -s
echo New translation file pushed.

View File

@ -22,210 +22,29 @@
##########################################################################
"""
This script is used to maintain the translation files in OpenLP. It downloads
the latest translation files from the Transifex translation server, updates the
local translation files from both the source code and the files from Transifex,
and can also generate the compiled translation files.
Create New Language
-------------------
To create a new language, simply run this script with the ``-c`` command line
option::
@:~$ ./translation_utils.py -c
Update Translation Files
------------------------
The best way to update the translations is to download the files from Transifex,
and then update the local files using both the downloaded files and the source.
This is done easily via the ``-d``, ``-p`` and ``-u`` options::
@:~$ ./translation_utils.py -dpu
This script is used to maintain the translation files in OpenLP.
It generates the base en.ts file used to drive all translations
on Transifex.
For more details on the translation process see the Translation pages on the
Wiki
"""
import base64
import glob
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
import webbrowser
from argparse import ArgumentParser
from getpass import getpass
from lxml import objectify
from PyQt5 import QtCore
SERVER_URL = 'http://www.transifex.com/api/2/project/openlp/resource/openlp-26x/'
IGNORED_PATHS = ['scripts']
IGNORED_PATHS = ['scripts', 'tests']
IGNORED_FILES = ['setup.py']
verbose_mode = False
quiet_mode = False
username = ''
password = ''
class Command(object):
"""
Provide an enumeration of commands.
"""
Download = 1
Create = 2
Prepare = 3
Update = 4
Generate = 5
Check = 6
class CommandStack(object):
"""
This class provides an iterable stack.
"""
def __init__(self):
self.current_index = 0
self.data = []
def __len__(self):
return len(self.data)
def __getitem__(self, index):
if index not in self.data:
return None
elif self.data[index].get('arguments'):
return self.data[index]['command'], self.data[index]['arguments']
else:
return self.data[index]['command']
def __iter__(self):
return self
def __next__(self):
if self.current_index == len(self.data):
raise StopIteration
else:
current_item = self.data[self.current_index]['command']
self.current_index += 1
return current_item
def append(self, command, **kwargs):
data = {'command': command}
if 'arguments' in kwargs:
data['arguments'] = kwargs['arguments']
self.data.append(data)
def reset(self):
self.current_index = 0
def arguments(self):
if self.data[self.current_index - 1].get('arguments'):
return self.data[self.current_index - 1]['arguments']
else:
return []
def __repr__(self):
results = []
for item in self.data:
if item.get('arguments'):
results.append(str((item['command'], item['arguments'])))
else:
results.append(str((item['command'], )))
return '[%s]' % ', '.join(results)
def print_quiet(text, linefeed=True):
"""
This method checks to see if we are in quiet mode, and if not prints ``text`` out.
:param text: The text to print.
:param linefeed: Linefeed required
"""
global quiet_mode
if not quiet_mode:
if linefeed:
print(text)
else:
print(text, end=' ')
def print_verbose(text):
"""
This method checks to see if we are in verbose mode, and if so prints ``text`` out.
:param text: The text to print.
"""
global verbose_mode, quiet_mode
if not quiet_mode and verbose_mode:
print(' %s' % text)
def run(command):
"""
This method runs an external application.
:param command: The command to run.
"""
print_verbose(command)
process = QtCore.QProcess()
process.start(command)
while process.waitForReadyRead():
print_verbose('ReadyRead: %s' % process.readAll())
print_verbose('Error(s):\n%s' % process.readAllStandardError())
print_verbose('Output:\n%s' % process.readAllStandardOutput())
def download_translations():
"""
This method downloads the translation files from the Pootle server.
**Note:** URLs and headers need to remain strings, not unicode.
"""
global username, password
print_quiet('Download translation files from Transifex')
if not username:
username = input(' Transifex username: ')
if not password:
password = getpass(' Transifex password: ')
# First get the list of languages
base64string = base64.encodebytes(('%s:%s' % (username, password)).encode())[:-1]
auth_header = 'Basic %s' % base64string.decode()
request = urllib.request.Request(SERVER_URL + '?details')
request.add_header('Authorization', auth_header)
print_verbose('Downloading list of languages from: %s' % SERVER_URL)
try:
json_response = urllib.request.urlopen(request)
except urllib.error.HTTPError:
print_quiet('Username or password incorrect.')
return False
json_dict = json.loads(json_response.read().decode())
languages = [lang['code'] for lang in json_dict['available_languages']]
for language in languages:
lang_url = SERVER_URL + 'translation/%s/?file' % language
request = urllib.request.Request(lang_url)
request.add_header('Authorization', auth_header)
filename = os.path.join(os.path.abspath('..'), 'resources', 'i18n', language + '.ts')
print_verbose('Get Translation File: %s' % filename)
response = urllib.request.urlopen(request)
fd = open(filename, 'wb')
fd.write(response.read())
fd.close()
print_quiet(' Done.')
return True
def prepare_project():
"""
This method creates the project file needed to update the translation files and compile them into .qm files.
"""
print_quiet('Generating the openlp.pro file')
print('Generating the openlp.pro file')
lines = []
start_dir = os.path.abspath('..')
start_dir = start_dir + os.sep
print_verbose('Starting directory: %s' % start_dir)
print('Starting directory: %s' % start_dir)
for root, dirs, files in os.walk(start_dir):
for file in files:
path = root.replace(start_dir, '').replace('\\', '/')
@ -250,180 +69,14 @@ def prepare_project():
line = '%s/%s' % (path, file)
else:
line = file
print_verbose('Parsing "%s"' % line)
print('Parsing "%s"' % line)
lines.append('SOURCES += %s' % line)
elif file.endswith('.ts'):
line = '%s/%s' % (path, file)
print_verbose('Parsing "%s"' % line)
lines.append('TRANSLATIONS += %s' % line)
lines.append('TRANSLATIONS += resources/i18n/en.ts')
lines.sort()
file = open(os.path.join(start_dir, 'openlp.pro'), 'w')
file.write('\n'.join(lines))
file.close()
print_quiet(' Done.')
def update_translations():
print_quiet('Update the translation files')
if not os.path.exists(os.path.join(os.path.abspath('..'), 'openlp.pro')):
print('You have not generated a project file yet, please run this script with the -p option.')
return
else:
os.chdir(os.path.abspath('..'))
run('pylupdate5 -verbose -noobsolete openlp.pro')
os.chdir(os.path.abspath('scripts'))
def generate_binaries():
print_quiet('Generate the related *.qm files')
if not os.path.exists(os.path.join(os.path.abspath('..'), 'openlp.pro')):
print('You have not generated a project file yet, please run this script with the -p option. It is also ' +
'recommended that you this script with the -u option to update the translation files as well.')
return
else:
os.chdir(os.path.abspath('..'))
run('lrelease openlp.pro')
print_quiet(' Done.')
def create_translation():
"""
This method opens a browser to the OpenLP project page at Transifex so
that the user can request a new language.
"""
print_quiet('Please request a new language at the OpenLP project on Transifex.')
webbrowser.open('https://www.transifex.net/projects/p/openlp/resource/ents/')
print_quiet('Opening browser to OpenLP project...')
def check_format_strings():
"""
This method runs through the ts-files and looks for mismatches between format strings in the original text
and in the translations.
"""
is_ok = True
path = os.path.join(os.path.abspath('..'), 'resources', 'i18n', '*.ts')
file_list = glob.glob(path)
for filename in file_list:
print_quiet('Checking %s' % filename)
file = open(filename, 'rb')
tree = objectify.parse(file)
root = tree.getroot()
for tag in root.iter('message'):
location = tag.location.get('filename')
line = tag.location.get('line')
org_text = tag.source.text
translation = tag.translation.text
if not translation:
for num in tag.iter('numerusform'):
print_verbose('parsed numerusform: location: %s, source: %s, translation: %s' % (
location, org_text, num.text))
if num and org_text.count('%') != num.text.count('%'):
is_ok = False
print_quiet(
'ERROR: Translation from %s at line %s has a mismatch of format input:\n%s\n%s\n' % (
location, line, org_text, num.text))
else:
print_verbose('parsed: location: %s, source: %s, translation: %s' % (location, org_text, translation))
if org_text.count('%') != translation.count('%'):
is_ok = False
print_quiet('ERROR: Translation from %s at line %s has a mismatch of format input:\n%s\n%s\n' % (
location, line, org_text, translation))
return is_ok
def process_stack(command_stack):
"""
This method looks at the commands in the command stack, and processes them
in the order they are in the stack.
``command_stack``
The command stack to process.
"""
is_success = True
if command_stack:
print_quiet('Processing %d commands...' % len(command_stack))
for command in command_stack:
print_quiet('%d.' % (command_stack.current_index), False)
if command == Command.Download:
if not download_translations():
return
elif command == Command.Prepare:
prepare_project()
elif command == Command.Update:
update_translations()
elif command == Command.Generate:
generate_binaries()
elif command == Command.Create:
create_translation()
elif command == Command.Check:
is_success = check_format_strings()
print_quiet('Finished processing commands.')
else:
print_quiet('No commands to process.')
return is_success
def main():
global verbose_mode, quiet_mode, username, password
# Set up command line options.
usage = '%(prog)s [options]\nOptions are parsed in the order they are ' + \
'listed below. If no options are given, "-dpug" will be used.\n\n' + \
'This script is used to manage OpenLP\'s translation files.'
parser = ArgumentParser(usage=usage)
parser.add_argument('-U', '--username', dest='username', metavar='USERNAME',
help='Transifex username, used for authentication')
parser.add_argument('-P', '--password', dest='password', metavar='PASSWORD',
help='Transifex password, used for authentication')
parser.add_argument('-d', '--download-ts', dest='download',
action='store_true', help='download language files from Transifex')
parser.add_argument('-c', '--create', dest='create', action='store_true',
help='go to Transifex to request a new translation file')
parser.add_argument('-p', '--prepare', dest='prepare', action='store_true',
help='generate a project file, used to update the translations')
parser.add_argument('-u', '--update', action='store_true', dest='update',
help='update translation files (needs a project file)')
parser.add_argument('-g', '--generate', dest='generate', action='store_true',
help='compile .ts files into .qm files')
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
help='show extra information while processing translations')
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
help='suppress all output other than errors')
parser.add_argument('-f', '--check-format-strings', dest='check', action='store_true',
help='check that format strings are matching in translations')
args = parser.parse_args()
# Create and populate the command stack
command_stack = CommandStack()
if args.download:
command_stack.append(Command.Download)
if args.create:
command_stack.append(Command.Create, arguments=[args.create])
if args.prepare:
command_stack.append(Command.Prepare)
if args.update:
command_stack.append(Command.Update)
if args.generate:
command_stack.append(Command.Generate)
if args.check:
command_stack.append(Command.Check)
verbose_mode = args.verbose
quiet_mode = args.quiet
if args.username:
username = args.username
if args.password:
password = args.password
if not command_stack:
command_stack.append(Command.Download)
command_stack.append(Command.Prepare)
command_stack.append(Command.Update)
command_stack.append(Command.Generate)
# Process the commands
return process_stack(command_stack)
if __name__ == '__main__':
if os.path.split(os.path.abspath('.'))[1] != 'scripts':
print('You need to run this script from the scripts directory.')
else:
if not main():
sys.exit(1)
prepare_project()

View File

@ -26,7 +26,7 @@ from pathlib import Path
from unittest import TestCase
from unittest.mock import MagicMock, call, patch
from openlp.core.common import clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \
from openlp.core.common import Singleton, clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \
normalize_str, path_to_module, trace_error_handler
@ -163,6 +163,48 @@ class TestCommonFunctions(TestCase):
mocked_logger.error.assert_called_with(
'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test')
def test_singleton_metaclass_multiple_init(self):
"""
Test that a class using the Singleton Metaclass is only initialised once despite being called several times and
that the same instance is returned each time..
"""
# GIVEN: The Singleton Metaclass and a test class using it
class SingletonClass(metaclass=Singleton):
def __init__(self):
pass
with patch.object(SingletonClass, '__init__', return_value=None) as patched_init:
# WHEN: Initialising the class multiple times
inst_1 = SingletonClass()
inst_2 = SingletonClass()
# THEN: The __init__ method of the SingletonClass should have only been called once, and both returned values
# should be the same instance.
assert inst_1 is inst_2
assert patched_init.call_count == 1
def test_singleton_metaclass_multiple_classes(self):
"""
Test that multiple classes using the Singleton Metaclass return the different an appropriate instances.
"""
# GIVEN: Two different classes using the Singleton Metaclass
class SingletonClass1(metaclass=Singleton):
def __init__(self):
pass
class SingletonClass2(metaclass=Singleton):
def __init__(self):
pass
# WHEN: Initialising both classes
s_c1 = SingletonClass1()
s_c2 = SingletonClass2()
# THEN: The instances should be an instance of the appropriate class
assert isinstance(s_c1, SingletonClass1)
assert isinstance(s_c2, SingletonClass2)
def test_is_win(self):
"""
Test the is_win() function

View File

@ -182,4 +182,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 47 == len(theme.__dict__), 'The theme should have 47 attributes'
assert 48 == len(theme.__dict__), 'The theme should have 48 attributes'

View File

@ -33,7 +33,7 @@ from tests.helpers.testmixin import TestMixin
class TestIcons(TestCase, TestMixin):
@patch('openlp.core.ui.icons.UiIcons.load')
@patch('openlp.core.ui.icons.UiIcons.__init__', return_value=None)
def test_simple_icon(self, _):
# GIVEN: an basic set of icons
icons = UiIcons()

View File

@ -1,3 +1,12 @@
function _createDiv(attrs) {
var div = document.createElement("div");
for (key in attrs) {
div.setAttribute(key, attrs[key]);
}
document.body.appendChild(div);
return div;
}
describe("The enumeration object", function () {
it("BackgroundType should exist", function () {
expect(BackgroundType).toBeDefined();
@ -22,9 +31,7 @@ describe("The enumeration object", function () {
describe("The function", function () {
it("$() should return the right element", function () {
var div = document.createElement("div");
div.setAttribute("id", "dollar-test");
document.body.appendChild(div);
var div = _createDiv({"id": "dollar-test"});
expect($("#dollar-test")[0]).toBe(div);
});
@ -39,10 +46,8 @@ describe("The function", function () {
});
it("_getStyle should return the correct style on an element", function () {
var div = document.createElement("div");
var div = _createDiv({"id": "style-test"});
div.style.setProperty("width", "100px");
div.setAttribute("id", "style-test");
document.body.appendChild(div);
expect(_getStyle($("#style-test")[0], "width")).toBe("100px");
});
@ -120,10 +125,8 @@ describe("The Display object", function () {
expect(Display.clearSlides).toBeDefined();
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
var slidesDiv = _createDiv({"class": "slides"});
slidesDiv.innerHTML = "<section><p></p></section>";
document.body.appendChild(slidesDiv);
Display.clearSlides();
expect($(".slides")[0].innerHTML).toEqual("");
@ -143,17 +146,18 @@ describe("The Display object", function () {
describe("Display.addTextSlide", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
_createDiv({"class": "slides"});
_createDiv({"class": "footer"});
Display._slides = {};
});
it("should add a new slide", function () {
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
var verse = "v1",
text = "Amazing grace,\nhow sweet the sound",
footer = "Public Domain";
spyOn(Display, "reinit");
Display.addTextSlide(verse, text);
Display.addTextSlide(verse, text, footer);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
@ -162,10 +166,12 @@ describe("Display.addTextSlide", function () {
});
it("should add a new slide without calling reinit()", function () {
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
var verse = "v1",
text = "Amazing grace,\nhow sweet the sound",
footer = "Public Domain";
spyOn(Display, "reinit");
Display.addTextSlide(verse, text, false);
Display.addTextSlide(verse, text, footer, false);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
@ -174,8 +180,10 @@ describe("Display.addTextSlide", function () {
});
it("should update an existing slide", function () {
var verse = "v1", text = "Amazing grace, how sweet the sound\nThat saved a wretch like me";
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", false);
var verse = "v1",
text = "Amazing grace, how sweet the sound\nThat saved a wretch like me",
footer = "Public Domain";
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", footer, false);
spyOn(Display, "reinit");
Display.addTextSlide(verse, text, true);
@ -190,18 +198,9 @@ describe("Display.addTextSlide", function () {
describe("Display.setTextSlides", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
var background = document.createElement("div");
background.setAttribute("id", "global-background");
document.body.appendChild(background);
var footer = document.createElement("div");
footer.setAttribute("class", "footer");
document.body.appendChild(footer);
_createDiv({"class": "slides"});
_createDiv({"class": "footer"});
_createDiv({"id": "global-background"});
Display._slides = {};
});
@ -210,12 +209,14 @@ describe("Display.setTextSlides", function () {
{
"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"
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
},
{
"verse": "v2",
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
"How precious did that grace appear,\nthe hour I first believed."
"How precious did that grace appear,\nthe hour I first believed.",
"footer": "Public Domain"
}
];
spyOn(Display, "clearSlides");
@ -232,29 +233,27 @@ describe("Display.setTextSlides", function () {
expect(Reveal.slide).toHaveBeenCalledWith(0);
});
it("should correctly set outline width", function () {
xit("should correctly set outline width (skipped for now)", 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"
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
const theme = {
'font_main_color': 'yellow',
'font_main_outline': true,
'font_main_outline_size': 42,
'font_main_outline_color': 'red'
};
spyOn(Display, "reinit");
Display.setTextSlides(slides);
Display.setTheme(theme);
const slidesDiv = $(".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');
@ -264,12 +263,9 @@ describe("Display.setTextSlides", function () {
describe("Display.setImageSlides", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
var backgroundDiv = document.createElement("div");
backgroundDiv.setAttribute("id", "global-background");
document.body.appendChild(backgroundDiv);
_createDiv({"class": "slides"});
_createDiv({"class": "footer"});
_createDiv({"id": "global-background"});
Display._slides = {};
});
@ -286,7 +282,9 @@ describe("Display.setImageSlides", function () {
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(Display.reinit).toHaveBeenCalledTimes(1);
});
});
@ -294,12 +292,8 @@ describe("Display.setImageSlides", function () {
describe("Display.setVideo", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
var backgroundDiv = document.createElement("div");
backgroundDiv.setAttribute("id", "global-background");
document.body.appendChild(backgroundDiv);
_createDiv({"class": "slides"});
_createDiv({"id": "global-background"});
Display._slides = {};
});