Merge trunk updates

This commit is contained in:
John Lines 2019-09-03 11:20:46 +01:00
commit 53bf0fe420
60 changed files with 1105 additions and 320453 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

@ -374,7 +374,7 @@ var Display = {
* @param {string} text - The HTML for the verse, e.g. "line1<br>line2"
* @param {string} footer_text - The HTML for the footer"
*/
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;

View File

@ -67,6 +67,15 @@ FOOTER_COPYRIGHT = 'Public Domain'
CCLI_NO = '123456'
def remove_chords(text):
"""
Remove chords from the text
:param text: Text to be cleaned
"""
return re.sub(r'\[.+?\]', r'', text)
def remove_tags(text, can_remove_chords=False):
"""
Remove Tags from text for display
@ -82,7 +91,7 @@ def remove_tags(text, can_remove_chords=False):
text = text.replace(tag['end tag'], '')
# Remove ChordPro tags
if can_remove_chords:
text = re.sub(r'\[.+?\]', r'', text)
text = remove_chords(text)
return text
@ -377,6 +386,8 @@ def render_tags(text, can_render_chords=False, is_printing=False):
text = render_chords_for_printing(text, '{br}')
else:
text = render_chords(text)
else:
text = remove_chords(text)
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html'])
@ -503,6 +514,26 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
self.force_page = False
return None
def get_theme(self, item):
"""
:param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object
:return string: The name of the theme to be used
"""
# Just assume we use the global theme.
theme_name = Registry().get('theme_manager').global_theme
# The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
# service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
# theme.
if self.theme_level != ThemeLevel.Global:
# When the theme level is at Service and we actually have a service theme then use it.
if self.theme_level == ThemeLevel.Service:
theme_name = Registry().get('service_manager').service_theme
# If we have Item level and have an item theme then use it.
if self.theme_level == ThemeLevel.Song and item.theme:
theme_name = item.theme
return theme_name
def format_slide(self, text, item):
"""
Calculate how much text can fit on a slide.
@ -515,7 +546,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
QtWidgets.QApplication.instance().processEvents()
self.log_debug('format slide')
if item:
theme_name = item.theme if item.theme else Registry().get('theme_manager').global_theme
theme_name = self.get_theme(item)
theme_data = Registry().get('theme_manager').get_theme_data(theme_name)
self.theme_height = theme_data.font_main_height
# Set theme for preview

View File

@ -67,7 +67,7 @@ def database_exists(url):
create_database(engine.url)
database_exists(engine.url) #=> True
Borrowed from SQLAlchemy_Utils (v0.32.14 )since we only need this one function.
Borrowed from SQLAlchemy_Utils (v0.32.14) since we only need this one function.
"""
url = copy(make_url(url))

View File

@ -39,7 +39,7 @@ from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings
from openlp.core.display.render import remove_tags, render_tags
from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing
from openlp.core.lib import ItemCapabilities
from openlp.core.ui.icons import UiIcons
@ -74,6 +74,7 @@ class ServiceItem(RegistryProperties):
self.name = plugin.name
self._rendered_slides = None
self._display_slides = None
self._print_slides = None
self.title = ''
self.slides = []
self.processor = None
@ -185,7 +186,7 @@ class ServiceItem(RegistryProperties):
self._rendered_slides.append(rendered_slide)
display_slide = {
'title': raw_slide['title'],
'text': remove_tags(page),
'text': remove_tags(page, can_remove_chords=True),
'verse': verse_tag,
}
self._display_slides.append(display_slide)
@ -209,6 +210,34 @@ class ServiceItem(RegistryProperties):
self._create_slides()
return self._display_slides
@property
def print_slides(self):
"""
Render the frames for printing and return them
:param can_render_chords: bool Whether or not to render the chords
"""
if not self._print_slides:
self._print_slides = []
previous_pages = {}
index = 0
for raw_slide in self.slides:
verse_tag = raw_slide['verse']
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
pages = previous_pages[verse_tag][1]
else:
pages = self.renderer.format_slide(raw_slide['text'], self)
previous_pages[verse_tag] = (raw_slide, pages)
for page in pages:
slide = {
'title': raw_slide['title'],
'text': render_chords_for_printing(remove_tags(page), '\n'),
'verse': index,
'footer': self.raw_footer,
}
self._print_slides.append(slide)
return self._print_slides
def add_from_image(self, path, title, background=None, thumbnail=None):
"""
Add an image slide to the service item.
@ -321,6 +350,13 @@ class ServiceItem(RegistryProperties):
'display_title': slide['display_title'], 'notes': slide['notes']})
return {'header': service_header, 'data': service_data}
def render_text_items(self):
"""
This method forces the display to be regenerated
"""
self._display_slides = []
self._rendered_slides = []
def set_from_service(self, service_item, path=None):
"""
This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the
@ -503,7 +539,6 @@ class ServiceItem(RegistryProperties):
:param row: The service item slide to be returned
"""
if self.service_item_type == ServiceItemType.Text:
# return self.display_frames[row]['html'].split('\n')[0]
return self.rendered_slides[row]['text']
elif self.service_item_type == ServiceItemType.Image:
return self.slides[row]['path']

File diff suppressed because it is too large Load Diff

View File

@ -50,19 +50,20 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
Set up the dialog. This method is mocked out in tests.
"""
self.setup_ui(self)
self.button_box.buttons()[0].setFocus()
application_version = get_version()
about_text = self.about_text_edit.toPlainText()
about_text = about_text.replace('<version>', application_version['version'])
about_text = self.about_text_edit.toHtml()
about_text = about_text.replace('{version}', application_version['version'])
if application_version['build']:
build_text = translate('OpenLP.AboutForm', ' build {version}').format(version=application_version['build'])
else:
build_text = ''
about_text = about_text.replace('<revision>', build_text)
self.about_text_edit.setPlainText(about_text)
self.volunteer_button.clicked.connect(self.on_volunteer_button_clicked)
about_text = about_text.replace('{revision}', build_text)
self.about_text_edit.setHtml(about_text)
self.contribute_button.clicked.connect(self.on_contribute_button_clicked)
def on_volunteer_button_clicked(self):
def on_contribute_button_clicked(self):
"""
Launch a web browser and go to the contribute page on the site.
"""
webbrowser.open_new('http://openlp.org/en/contribute')
webbrowser.open_new('http://openlp.org/contribute')

View File

@ -546,7 +546,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
Wait for the threads
"""
# Sometimes the threads haven't finished, let's wait for them
wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
wait_dialog = QtWidgets.QProgressDialog(translate('OpenLP.MainWindow', 'Waiting for some things to finish...'),
'', 0, 0, self)
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
wait_dialog.setAutoClose(False)
wait_dialog.setCancelButton(None)

View File

@ -218,7 +218,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
if item.is_text():
verse_def = None
verse_html = None
for slide in item.get_frames():
for slide in item.print_slides:
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['text']:

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

@ -46,7 +46,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon
from openlp.core.lib.exceptions import ValidationError
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem, ServiceItemType
from openlp.core.lib.ui import create_widget_action, critical_error_message_box, find_and_set_in_combo_box
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT
@ -749,9 +749,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if theme:
find_and_set_in_combo_box(self.theme_combo_box, theme, set_missing=False)
if theme == self.theme_combo_box.currentText():
# TODO: Use a local display widget
# self.preview_display.set_theme(get_theme_from_name(theme))
pass
self.service_theme = theme
else:
if self._save_lite:
service_item.set_from_service(item)
@ -1197,9 +1195,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
service_item_from_item = item['service_item']
tree_widget_item = QtWidgets.QTreeWidgetItem(self.service_manager_list)
if service_item_from_item.is_valid:
if service_item_from_item.notes:
icon = service_item_from_item.icon.pixmap(80, 80).toImage()
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
if service_item_from_item.notes:
overlay = UiIcons().notes.pixmap(40, 40).toImage()
overlay = overlay.scaled(40, 40, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
painter = QtGui.QPainter(icon)
@ -1207,8 +1205,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
painter.end()
tree_widget_item.setIcon(0, build_icon(icon))
elif service_item_from_item.temporary_edit:
icon = service_item_from_item.icon.pixmap(80, 80).toImage()
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
overlay = QtGui.QImage(UiIcons().upload)
overlay = overlay.scaled(40, 40, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
painter = QtGui.QPainter(icon)
@ -1242,13 +1238,14 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
tree_widget_item.setData(0, QtCore.Qt.UserRole, item['order'])
tree_widget_item.setSelected(item['selected'])
# Add the children to their parent tree_widget_item.
for slide_index, slide in enumerate(service_item_from_item.slides):
for slide_index, slide in enumerate(service_item_from_item.get_frames()):
child = QtWidgets.QTreeWidgetItem(tree_widget_item)
# prefer to use a display_title
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
text = slide['display_title'].replace('\n', ' ')
else:
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle) or \
service_item_from_item.service_item_type == ServiceItemType.Image:
text = slide['title'].replace('\n', ' ')
else:
text = service_item_from_item.get_rendered_frame(slide_index)
child.setText(0, text[:40])
child.setData(0, QtCore.Qt.UserRole, slide_index)
if service_item == item_index:
@ -1275,8 +1272,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
:param current_index: The combo box index for the selected item
"""
self.service_theme = self.theme_combo_box.currentText()
# TODO: Use a local display widget
# self.preview_display.set_theme(get_theme_from_name(theme))
Settings().setValue(self.main_window.service_manager_settings_section + '/service theme', self.service_theme)
self.regenerate_service_items(True)
@ -1335,7 +1330,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
"""
for item_count, item in enumerate(self.service_items):
if item['service_item'].edit_id == new_item.edit_id and item['service_item'].name == new_item.name:
new_item.render()
new_item.create_slides()
new_item.merge(item['service_item'])
item['service_item'] = new_item
self.repaint_service_list(item_count + 1, 0)
@ -1368,7 +1363,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(s_item, child)
self.live_controller.replace_service_manager_item(item)
else:
# item.render()
item.render_text_items()
# nothing selected for dnd
if self.drop_position == -1:
if isinstance(item, list):
@ -1617,8 +1612,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
theme_group.addAction(create_widget_action(self.theme_menu, theme, text=theme, checked=False,
triggers=self.on_theme_change_action))
find_and_set_in_combo_box(self.theme_combo_box, self.service_theme)
# TODO: Sort this out
# self.renderer.set_service_theme(self.service_theme)
self.regenerate_service_items()
def on_theme_change_action(self):

View File

@ -613,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:

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

@ -27,6 +27,7 @@ from Pyro4 import Proxy
from openlp.core.common import delete_file, is_macosx
from openlp.core.common.applocation import AppLocation
from openlp.core.common.mixins import LogMixin
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry
from openlp.core.display.screens import ScreenList
@ -47,7 +48,7 @@ log = logging.getLogger(__name__)
register_classes()
class MacLOController(PresentationController):
class MacLOController(PresentationController, LogMixin):
"""
Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver,
starts the LibreOfficeServer, and then controls MacLO via Pyro4.

View File

@ -291,7 +291,6 @@ 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)
@ -342,7 +341,6 @@ 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

@ -28,6 +28,7 @@ import re
from openlp.core.common.settings import Settings
from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.db import AuthorType
log = logging.getLogger(__name__)
@ -39,6 +40,7 @@ class ChordProImport(SongImport):
This importer is based on the information available on these webpages:
- https://www.chordpro.org
- http://webchord.sourceforge.net/tech.html
- http://www.vromans.org/johan/projects/Chordii/chordpro/
- http://www.tenbyten.com/software/songsgen/help/HtmlHelp/files_reference.htm
@ -73,6 +75,29 @@ class ChordProImport(SongImport):
self.title = tag_value
elif tag_name in ['subtitle', 'su', 'st']:
self.alternate_title = tag_value
elif tag_name == 'composer':
self.parse_author(tag_value, AuthorType.Music)
elif tag_name in ['lyricist', 'artist', 'author']: # author is not an official directive
self.parse_author(tag_value, AuthorType.Words)
elif tag_name == 'meta':
meta_tag_name, meta_tag_value = tag_value.split(' ', 1)
# Skip, if no value
if not meta_tag_value:
continue
# The meta-tag can contain anything. We check for the ones above and a few more
if meta_tag_name in ['title', 't']:
self.title = meta_tag_value
elif meta_tag_name in ['subtitle', 'su', 'st']:
self.alternate_title = meta_tag_value
elif meta_tag_name == 'composer':
self.parse_author(meta_tag_value, AuthorType.Music)
elif meta_tag_name in ['lyricist', 'artist', 'author']:
self.parse_author(meta_tag_value, AuthorType.Words)
elif meta_tag_name in ['topic', 'topics']:
for topic in meta_tag_value.split(','):
self.topics.append(topic.strip())
elif 'ccli' in meta_tag_name:
self.ccli_number = meta_tag_value
elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
# Detect if the comment is used as a chorus repeat marker
if tag_value.lower().startswith('chorus'):
@ -156,6 +181,13 @@ class ChordProImport(SongImport):
'songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
# if no title was in directives, get it from the first line
if not self.title:
(verse_def, verse_text, lang) = self.verses[0]
# strip any chords from the title
self.title = re.sub(r'\[.*?\]', '', verse_text.split('\n')[0])
# strip the last char if it a punctuation
self.title = re.sub(r'[^\w\s]$', '', self.title)
if not self.finish():
self.log_error(song_file.name)

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

View File

@ -97,7 +97,7 @@ MODULES = [
OPTIONAL_MODULES = [
('qdarkstyle', '(dark style support)'),
('mysql.connector', '(MySQL support)'),
('pymysql', '(MySQL support)'),
('pyodbc', '(ODBC support)'),
('psycopg2', '(PostgreSQL support)'),
('enchant', '(spell checker)'),

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

@ -191,7 +191,7 @@ using a computer and a data projector.""",
extras_require={
'agpl-pdf': ['PyMuPDF'],
'darkstyle': ['QDarkStyle'],
'mysql': ['mysql-connector-python'],
'mysql': ['pymysql'],
'odbc': ['pyodbc'],
'postgresql': ['psycopg2'],
'spellcheck': ['pyenchant >= 1.6'],

View File

@ -22,10 +22,20 @@
"""
Test the :mod:`~openlp.core.display.render` package.
"""
from unittest.mock import patch
import sys
from unittest import TestCase
from unittest.mock import MagicMock, patch
from openlp.core.common import ThemeLevel
from tests.helpers.testmixin import TestMixin
from PyQt5 import QtWidgets
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
from openlp.core.common.registry import Registry
from openlp.core.display.render import compare_chord_lyric_width, find_formatting_tags, remove_tags, render_chords, \
render_chords_for_printing, render_tags
render_chords_for_printing, render_tags, ThemePreviewRenderer
from openlp.core.lib.formattingtags import FormattingTags
@ -208,3 +218,101 @@ def test_find_formatting_tags():
# THEN: The list of active tags should contain only 'st'
assert active_tags == ['st'], 'The list of active tags should contain only "st"'
class TestThemePreviewRenderer(TestMixin, TestCase):
def setUp(self):
"""
Set up the components need for all tests.
"""
# Create the Registry
self.application = QtWidgets.QApplication.instance()
Registry.create()
self.application.setOrganizationName('OpenLP-tests')
self.application.setOrganizationDomain('openlp.org')
def test_get_theme_global(self):
"""
Test the return of the global theme if set to Global level
"""
# GIVEN: A set up with a Global Theme and settings at Global
mocked_theme_manager = MagicMock()
mocked_theme_manager.global_theme = 'my_global_theme'
Registry().register('theme_manager', mocked_theme_manager)
with patch('openlp.core.display.webengine.WebEngineView'), \
patch('PyQt5.QtWidgets.QVBoxLayout'):
tpr = ThemePreviewRenderer()
tpr.theme_level = ThemeLevel.Global
# WHEN: I Request the theme to Use
theme = tpr.get_theme(MagicMock())
# THEN: The list of active tags should contain only 'st'
assert theme == mocked_theme_manager.global_theme, 'The Theme returned is not that of the global theme'
def test_get_theme_service(self):
"""
Test the return of the global theme if set to Global level
"""
# GIVEN: A set up with a Global Theme and settings at Global
mocked_theme_manager = MagicMock()
mocked_theme_manager.global_theme = 'my_global_theme'
mocked_service_manager = MagicMock()
mocked_service_manager.service_theme = 'my_service_theme'
Registry().register('theme_manager', mocked_theme_manager)
Registry().register('service_manager', mocked_service_manager)
with patch('openlp.core.display.webengine.WebEngineView'), \
patch('PyQt5.QtWidgets.QVBoxLayout'):
tpr = ThemePreviewRenderer()
tpr.theme_level = ThemeLevel.Service
# WHEN: I Request the theme to Use
theme = tpr.get_theme(MagicMock())
# THEN: The list of active tags should contain only 'st'
assert theme == mocked_service_manager.service_theme, 'The Theme returned is not that of the Service theme'
def test_get_theme_item_level_none(self):
"""
Test the return of the global theme if set to Global level
"""
# GIVEN: A set up with a Global Theme and settings at Global
mocked_theme_manager = MagicMock()
mocked_theme_manager.global_theme = 'my_global_theme'
mocked_service_manager = MagicMock()
mocked_service_manager.service_theme = 'my_service_theme'
mocked_item = MagicMock()
mocked_item.theme = None
Registry().register('theme_manager', mocked_theme_manager)
Registry().register('service_manager', mocked_service_manager)
with patch('openlp.core.display.webengine.WebEngineView'), \
patch('PyQt5.QtWidgets.QVBoxLayout'):
tpr = ThemePreviewRenderer()
tpr.theme_level = ThemeLevel.Song
# WHEN: I Request the theme to Use
theme = tpr.get_theme(mocked_item)
# THEN: The list of active tags should contain only 'st'
assert theme == mocked_theme_manager.global_theme, 'The Theme returned is not that of the global theme'
def test_get_theme_item_level_set(self):
"""
Test the return of the global theme if set to Global level
"""
# GIVEN: A set up with a Global Theme and settings at Global
mocked_theme_manager = MagicMock()
mocked_theme_manager.global_theme = 'my_global_theme'
mocked_service_manager = MagicMock()
mocked_service_manager.service_theme = 'my_service_theme'
mocked_item = MagicMock()
mocked_item.theme = "my_item_theme"
Registry().register('theme_manager', mocked_theme_manager)
Registry().register('service_manager', mocked_service_manager)
with patch('openlp.core.display.webengine.WebEngineView'), \
patch('PyQt5.QtWidgets.QVBoxLayout'):
tpr = ThemePreviewRenderer()
tpr.theme_level = ThemeLevel.Song
# WHEN: I Request the theme to Use
theme = tpr.get_theme(mocked_item)
# THEN: The list of active tags should contain only 'st'
assert theme == mocked_item.theme, 'The Theme returned is not that of the item theme'

View File

@ -30,10 +30,10 @@ from openlp.core.ui.aboutform import AboutForm
from tests.helpers.testmixin import TestMixin
class TestFirstTimeForm(TestCase, TestMixin):
class TestAboutForm(TestCase, TestMixin):
@patch('openlp.core.ui.aboutform.webbrowser')
def test_on_volunteer_button_clicked(self, mocked_webbrowser):
def test_on_contribute_button_clicked(self, mocked_webbrowser):
"""
Test that clicking on the "Volunteer" button opens a web page.
"""
@ -41,10 +41,10 @@ class TestFirstTimeForm(TestCase, TestMixin):
about_form = AboutForm(None)
# WHEN: The "Volunteer" button is "clicked"
about_form.on_volunteer_button_clicked()
about_form.on_contribute_button_clicked()
# THEN: A web browser is opened
mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute')
mocked_webbrowser.open_new.assert_called_with('http://openlp.org/contribute')
@patch('openlp.core.ui.aboutform.get_version')
def test_about_form_build_number(self, mocked_get_version):
@ -66,11 +66,11 @@ class TestFirstTimeForm(TestCase, TestMixin):
Test that the copyright date is included correctly
"""
# GIVEN: A correct application date
date_string = "2004-%s" % datetime.date.today().year
date_string = '2004-{year}'.format(year=datetime.date.today().year)
# WHEN: The about form is created
about_form = AboutForm(None)
license_text = about_form.license_text_edit.toPlainText()
about_text = about_form.about_text_edit.toPlainText()
# THEN: The date should be in the text twice.
assert license_text.count(date_string, 0) == 2, "The text string should be added twice to the license string"
assert about_text.count(date_string, 0) == 1, "The text string should be added twice to the license string"

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 = {};
});

View File

@ -1,5 +1,7 @@
{title:Swing Low Sweet Chariot}
{st:Traditional}
{lyricist:Wallis Willis}
{meta:composer Wallis Willis}
{start_of_chorus}
Swing [D]low, sweet [G]chari[D]ot,

View File

@ -1,6 +1,10 @@
{
"title": "Swing Low Sweet Chariot",
"alternative_title": "Traditional",
"authors": [
["Wallis Willis", "words"],
["Wallis Willis", "music"]
],
"verses": [
[
"Swing [D]low, sweet [G]chari[D]ot,\nComin' for to carry me [A7]home.\nSwing [D7]low, sweet [G]chari[D]ot,\nComin' for to [A7]carry me [D]home.",