forked from openlp/openlp
Only update presentation thumbnails if needed. Fixes bug 1424330.
Make presentation controller save and read notes and titles in utf-8 encoding. Fixes bug 1424972. Moved string format input outside calls to translate. Fixes bug 1425035. Added option for check of format input strings in translation util script. Removed tab from text since transifex cannot handle it. Some PEP8 fixes (from pep8 1.6.1) Use the language id when importing bibles. bzr-revno: 2521
This commit is contained in:
commit
39ac46efaf
@ -146,7 +146,7 @@ class FormattingTagController(object):
|
||||
end = self.start_html_to_end_html(start_html)
|
||||
if not end_html:
|
||||
if not end:
|
||||
return translate('OpenLP.FormattingTagForm', 'Start tag %s is not valid HTML' % start_html), None
|
||||
return translate('OpenLP.FormattingTagForm', 'Start tag %s is not valid HTML') % start_html, None
|
||||
return None, end
|
||||
return None, None
|
||||
|
||||
@ -166,5 +166,5 @@ class FormattingTagController(object):
|
||||
return None, end
|
||||
if end and end != end_html:
|
||||
return translate('OpenLP.FormattingTagForm',
|
||||
'End tag %s does not match end tag for start tag %s' % (end, start_html)), None
|
||||
'End tag %s does not match end tag for start tag %s') % (end, start_html), None
|
||||
return None, None
|
||||
|
@ -533,7 +533,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
|
||||
self.application.set_normal_cursor()
|
||||
title = translate('OpenLP.ServiceManager', 'Service File(s) Missing')
|
||||
message = translate('OpenLP.ServiceManager',
|
||||
'The following file(s) in the service are missing:\n\t%s\n\n'
|
||||
'The following file(s) in the service are missing: %s\n\n'
|
||||
'These files will be removed if you continue to save.') % "\n\t".join(missing_list)
|
||||
answer = QtGui.QMessageBox.critical(self, title, message,
|
||||
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok |
|
||||
|
@ -717,8 +717,8 @@ class SlideController(DisplayController, RegistryProperties):
|
||||
self.play_slides_loop.setChecked(False)
|
||||
self.play_slides_loop.setIcon(build_icon(':/media/media_time.png'))
|
||||
if item.is_text():
|
||||
if (Settings().value(self.main_window.songs_settings_section + '/display songbar')
|
||||
and not self.song_menu.menu().isEmpty()):
|
||||
if (Settings().value(self.main_window.songs_settings_section + '/display songbar') and
|
||||
not self.song_menu.menu().isEmpty()):
|
||||
self.toolbar.set_widget_visible(['song_menu'], True)
|
||||
if item.is_capable(ItemCapabilities.CanLoop) and len(item.get_frames()) > 1:
|
||||
self.toolbar.set_widget_visible(LOOP_LIST)
|
||||
|
@ -70,8 +70,8 @@ class AlertsManager(OpenLPMixin, RegistryMixin, QtCore.QObject, RegistryProperti
|
||||
"""
|
||||
Format and request the Alert and start the timer.
|
||||
"""
|
||||
if not self.alert_list or (self.live_controller.display.screens.display_count == 1
|
||||
and not Settings().value('core/display on monitor')):
|
||||
if not self.alert_list or (self.live_controller.display.screens.display_count == 1 and
|
||||
not Settings().value('core/display on monitor')):
|
||||
return
|
||||
text = self.alert_list.pop(0)
|
||||
alert_tab = self.parent().settings_tab
|
||||
|
@ -123,8 +123,8 @@ class OpenSongBible(BibleDB):
|
||||
verse_number += 1
|
||||
self.create_verse(db_book.id, chapter_number, verse_number, self.get_text(verse))
|
||||
self.wizard.increment_progress_bar(
|
||||
translate('BiblesPlugin.Opensong', 'Importing %(bookname)s %(chapter)s...' %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number}))
|
||||
translate('BiblesPlugin.Opensong', 'Importing %(bookname)s %(chapter)s...') %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number})
|
||||
self.session.commit()
|
||||
self.application.process_events()
|
||||
except etree.XMLSyntaxError as inst:
|
||||
|
@ -71,6 +71,7 @@ class OSISBible(BibleDB):
|
||||
if not language_id:
|
||||
log.error('Importing books from "%s" failed' % self.filename)
|
||||
return False
|
||||
self.save_meta('language_id', language_id)
|
||||
num_books = int(osis_bible_tree.xpath("count(//ns:div[@type='book'])", namespaces=namespace))
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport',
|
||||
'Removing unused tags (this may take a few minutes)...'))
|
||||
@ -124,7 +125,7 @@ class OSISBible(BibleDB):
|
||||
break
|
||||
# Remove div-tags in the book
|
||||
etree.strip_tags(book, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}div'))
|
||||
book_ref_id = self.get_book_ref_id_by_name(book.get('osisID'), num_books)
|
||||
book_ref_id = self.get_book_ref_id_by_name(book.get('osisID'), num_books, language_id)
|
||||
if not book_ref_id:
|
||||
book_ref_id = self.get_book_ref_id_by_localised_name(book.get('osisID'))
|
||||
if not book_ref_id:
|
||||
@ -154,8 +155,8 @@ class OSISBible(BibleDB):
|
||||
verse_number = verse.get("osisID").split('.')[2]
|
||||
self.create_verse(db_book.id, chapter_number, verse_number, verse.text.strip())
|
||||
self.wizard.increment_progress_bar(
|
||||
translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...' %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number}))
|
||||
translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...') %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number})
|
||||
else:
|
||||
# The chapter tags is used as milestones. For now we assume verses is also milestones
|
||||
chapter_number = 0
|
||||
@ -164,8 +165,8 @@ class OSISBible(BibleDB):
|
||||
and element.get('sID'):
|
||||
chapter_number = element.get("osisID").split('.')[1]
|
||||
self.wizard.increment_progress_bar(
|
||||
translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...' %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number}))
|
||||
translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...') %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number})
|
||||
elif element.tag == '{http://www.bibletechnologies.net/2003/OSIS/namespace}verse' \
|
||||
and element.get('sID'):
|
||||
# If this tag marks the start of a verse, the verse text is between this tag and
|
||||
|
@ -69,6 +69,7 @@ class ZefaniaBible(BibleDB):
|
||||
if not language_id:
|
||||
log.error('Importing books from "%s" failed' % self.filename)
|
||||
return False
|
||||
self.save_meta('language_id', language_id)
|
||||
num_books = int(zefania_bible_tree.xpath("count(//BIBLEBOOK)"))
|
||||
# Strip tags we don't use - keep content
|
||||
etree.strip_tags(zefania_bible_tree, ('STYLE', 'GRAM', 'NOTE', 'SUP', 'XREF'))
|
||||
@ -83,7 +84,7 @@ class ZefaniaBible(BibleDB):
|
||||
if not bname and not bnumber:
|
||||
continue
|
||||
if bname:
|
||||
book_ref_id = self.get_book_ref_id_by_name(bname, num_books)
|
||||
book_ref_id = self.get_book_ref_id_by_name(bname, num_books, language_id)
|
||||
if not book_ref_id:
|
||||
book_ref_id = self.get_book_ref_id_by_localised_name(bname)
|
||||
else:
|
||||
@ -102,8 +103,8 @@ class ZefaniaBible(BibleDB):
|
||||
verse_number = VERS.get("vnumber")
|
||||
self.create_verse(db_book.id, chapter_number, verse_number, VERS.text.replace('<BR/>', '\n'))
|
||||
self.wizard.increment_progress_bar(
|
||||
translate('BiblesPlugin.Zefnia', 'Importing %(bookname)s %(chapter)s...' %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number}))
|
||||
translate('BiblesPlugin.Zefnia', 'Importing %(bookname)s %(chapter)s...') %
|
||||
{'bookname': db_book.name, 'chapter': chapter_number})
|
||||
self.session.commit()
|
||||
self.application.process_events()
|
||||
except Exception as e:
|
||||
|
@ -230,17 +230,26 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
Settings().setValue(self.settings_section + '/presentations files', self.get_file_list())
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
def clean_up_thumbnails(self, filepath):
|
||||
def clean_up_thumbnails(self, filepath, clean_for_update=False):
|
||||
"""
|
||||
Clean up the files created such as thumbnails
|
||||
|
||||
:param filepath: File path of the presention to clean up after
|
||||
:param clean_for_update: Only clean thumbnails if update is needed
|
||||
:return: None
|
||||
"""
|
||||
for cidx in self.controllers:
|
||||
doc = self.controllers[cidx].add_document(filepath)
|
||||
doc.presentation_deleted()
|
||||
doc.close_presentation()
|
||||
root, file_ext = os.path.splitext(filepath)
|
||||
file_ext = file_ext[1:]
|
||||
if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports:
|
||||
doc = self.controllers[cidx].add_document(filepath)
|
||||
if clean_for_update:
|
||||
thumb_path = doc.get_thumbnail_path(1, True)
|
||||
if not thumb_path or os.path.getmtime(thumb_path) < os.path.getmtime(filepath):
|
||||
doc.presentation_deleted()
|
||||
else:
|
||||
doc.presentation_deleted()
|
||||
doc.close_presentation()
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Service, presentation_file=None):
|
||||
|
@ -132,7 +132,7 @@ class PresentationDocument(object):
|
||||
"""
|
||||
The location where thumbnail images will be stored
|
||||
"""
|
||||
# TODO: If statment can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
|
||||
# TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
|
||||
if Settings().value('presentations/thumbnail_scheme') == 'md5':
|
||||
folder = md5_hash(self.file_path.encode('utf-8'))
|
||||
else:
|
||||
@ -143,7 +143,7 @@ class PresentationDocument(object):
|
||||
"""
|
||||
The location where thumbnail images will be stored
|
||||
"""
|
||||
# TODO: If statment can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
|
||||
# TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
|
||||
if Settings().value('presentations/thumbnail_scheme') == 'md5':
|
||||
folder = md5_hash(self.file_path.encode('utf-8'))
|
||||
else:
|
||||
@ -306,7 +306,7 @@ class PresentationDocument(object):
|
||||
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
|
||||
if os.path.exists(titles_file):
|
||||
try:
|
||||
with open(titles_file) as fi:
|
||||
with open(titles_file, encoding='utf-8') as fi:
|
||||
titles = fi.read().splitlines()
|
||||
except:
|
||||
log.exception('Failed to open/read existing titles file')
|
||||
@ -316,7 +316,7 @@ class PresentationDocument(object):
|
||||
note = ''
|
||||
if os.path.exists(notes_file):
|
||||
try:
|
||||
with open(notes_file) as fn:
|
||||
with open(notes_file, encoding='utf-8') as fn:
|
||||
note = fn.read()
|
||||
except:
|
||||
log.exception('Failed to open/read notes file')
|
||||
@ -331,12 +331,12 @@ class PresentationDocument(object):
|
||||
"""
|
||||
if titles:
|
||||
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
|
||||
with open(titles_file, mode='w') as fo:
|
||||
with open(titles_file, mode='wt', encoding='utf-8') as fo:
|
||||
fo.writelines(titles)
|
||||
if notes:
|
||||
for slide_no, note in enumerate(notes, 1):
|
||||
notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
|
||||
with open(notes_file, mode='w') as fn:
|
||||
with open(notes_file, mode='wt', encoding='utf-8') as fn:
|
||||
fn.write(note)
|
||||
|
||||
|
||||
|
@ -144,7 +144,7 @@ class PresentationPlugin(Plugin):
|
||||
files_from_config = Settings().value('presentations/presentations files')
|
||||
for file in files_from_config:
|
||||
try:
|
||||
self.media_item.clean_up_thumbnails(file)
|
||||
self.media_item.clean_up_thumbnails(file, True)
|
||||
except AttributeError:
|
||||
pass
|
||||
self.media_item.list_view.clear()
|
||||
|
@ -179,6 +179,6 @@ class WorshipAssistantImport(SongImport):
|
||||
cleaned_verse_order_list.append(verse)
|
||||
self.verse_order_list = cleaned_verse_order_list
|
||||
if not self.finish():
|
||||
self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d') % index
|
||||
+ (': "' + self.title + '"' if self.title else ''))
|
||||
self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d') % index +
|
||||
(': "' + self.title + '"' if self.title else ''))
|
||||
songs_file.close()
|
||||
|
@ -118,8 +118,8 @@ class ZionWorxImport(SongImport):
|
||||
self.add_verse(verse)
|
||||
title = self.title
|
||||
if not self.finish():
|
||||
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index
|
||||
+ (': "' + title + '"' if title else ''))
|
||||
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index +
|
||||
(': "' + title + '"' if title else ''))
|
||||
|
||||
def _decode(self, str):
|
||||
"""
|
||||
|
@ -53,7 +53,9 @@ from getpass import getpass
|
||||
import base64
|
||||
import json
|
||||
import webbrowser
|
||||
import glob
|
||||
|
||||
from lxml import etree, objectify
|
||||
from optparse import OptionParser
|
||||
from PyQt4 import QtCore
|
||||
|
||||
@ -76,6 +78,7 @@ class Command(object):
|
||||
Prepare = 3
|
||||
Update = 4
|
||||
Generate = 5
|
||||
Check = 6
|
||||
|
||||
|
||||
class CommandStack(object):
|
||||
@ -292,6 +295,38 @@ def create_translation():
|
||||
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.
|
||||
"""
|
||||
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('%'):
|
||||
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('%'):
|
||||
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))
|
||||
|
||||
|
||||
def process_stack(command_stack):
|
||||
"""
|
||||
This method looks at the commands in the command stack, and processes them
|
||||
@ -315,6 +350,8 @@ def process_stack(command_stack):
|
||||
generate_binaries()
|
||||
elif command == Command.Create:
|
||||
create_translation()
|
||||
elif command == Command.Check:
|
||||
check_format_strings()
|
||||
print_quiet('Finished processing commands.')
|
||||
else:
|
||||
print_quiet('No commands to process.')
|
||||
@ -345,6 +382,8 @@ def main():
|
||||
help='show extra information while processing translations')
|
||||
parser.add_option('-q', '--quiet', dest='quiet', action='store_true',
|
||||
help='suppress all output other than errors')
|
||||
parser.add_option('-f', '--check-format-strings', dest='check', action='store_true',
|
||||
help='check that format strings are matching in translations')
|
||||
(options, args) = parser.parse_args()
|
||||
# Create and populate the command stack
|
||||
command_stack = CommandStack()
|
||||
@ -358,6 +397,8 @@ def main():
|
||||
command_stack.append(Command.Update)
|
||||
if options.generate:
|
||||
command_stack.append(Command.Generate)
|
||||
if options.check:
|
||||
command_stack.append(Command.Check)
|
||||
verbose_mode = options.verbose
|
||||
quiet_mode = options.quiet
|
||||
if options.username:
|
||||
|
@ -26,7 +26,7 @@ from unittest import TestCase
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
|
||||
from tests.functional import patch, MagicMock
|
||||
from tests.functional import patch, MagicMock, call
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
@ -84,3 +84,26 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.assertIn('*.pdf', self.media_item.on_new_file_masks, 'The file mask should contain the pdf extension')
|
||||
self.assertIn('*.xps', self.media_item.on_new_file_masks, 'The file mask should contain the xps extension')
|
||||
self.assertIn('*.oxps', self.media_item.on_new_file_masks, 'The file mask should contain the oxps extension')
|
||||
|
||||
def clean_up_thumbnails_test(self):
|
||||
"""
|
||||
Test that the clean_up_thumbnails method works as expected.
|
||||
"""
|
||||
# GIVEN: A mocked controller, and mocked os.path.getmtime
|
||||
mocked_controller = MagicMock()
|
||||
mocked_doc = MagicMock()
|
||||
mocked_controller.add_document.return_value = mocked_doc
|
||||
mocked_controller.supports = ['tmp']
|
||||
self.media_item.controllers = {
|
||||
'Mocked': mocked_controller
|
||||
}
|
||||
presentation_file = 'file.tmp'
|
||||
with patch('openlp.plugins.presentations.lib.mediaitem.os.path.getmtime') as mocked_getmtime:
|
||||
mocked_getmtime.side_effect = [100, 200]
|
||||
|
||||
# WHEN: calling clean_up_thumbnails
|
||||
self.media_item.clean_up_thumbnails(presentation_file, True)
|
||||
|
||||
# THEN: doc.presentation_deleted should have been called since the thumbnails mtime will be greater than
|
||||
# the presentation_file's mtime.
|
||||
mocked_doc.assert_has_calls([call.get_thumbnail_path(1, True), call.presentation_deleted()], True)
|
||||
|
@ -81,9 +81,9 @@ class TestPresentationController(TestCase):
|
||||
self.document.save_titles_and_notes(titles, notes)
|
||||
|
||||
# THEN: the last call to open should have been for slideNotes2.txt
|
||||
mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='w')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='w')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='w')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='wt', encoding='utf-8')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='wt', encoding='utf-8')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='wt', encoding='utf-8')
|
||||
self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened')
|
||||
mocked_open().writelines.assert_called_once_with(['uno', 'dos'])
|
||||
mocked_open().write.assert_called_any('one')
|
||||
@ -126,9 +126,9 @@ class TestPresentationController(TestCase):
|
||||
self.assertIs(type(result_notes), list, 'result_notes should be of type list')
|
||||
self.assertEqual(len(result_notes), 2, 'There should be two items in the notes')
|
||||
self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'titles.txt'))
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'))
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'))
|
||||
mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), encoding='utf-8')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), encoding='utf-8')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), encoding='utf-8')
|
||||
self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked')
|
||||
|
||||
def get_titles_and_notes_with_file_not_found_test(self):
|
||||
|
Loading…
Reference in New Issue
Block a user