← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~alisonken1/openlp/strings-templates into lp:openlp

 

Ken Roberts has proposed merging lp:~alisonken1/openlp/strings-templates into lp:openlp.

Commit message:
String conversions with templates part 1

Requested reviews:
  Tim Bentley (trb143)
  Raoul Snyman (raoul-snyman)

For more details, see:
https://code.launchpad.net/~alisonken1/openlp/strings-templates/+merge/302228

String conversions with templates part 1

- Fix string format key error in first time wizard from previous string conversion
- Fix projector pjlink1 test to use MagicMock
- Part 1 string conversions where format template is a string variable that's filled later
- Update projectordb test
- Fix a test on htmlbuilder string formatting
- Revert openlp/core/lib/htmlbuilder.py
- Convert renderer to use Template()
- Added test for renderer css
- Merge trunk and fix conflict
- Fix formatting in exceptionform
- Added tests for exceptionsform. formatting
- Fix minor formatting errors/keys
- Merge trunk & fix conflicts
- Qt5 slot updates for projector

--------------------------------
lp:~alisonken1/openlp/strings-templates (revision 2693)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1703/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1614/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1552/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1315/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/905/
[SUCCESS] https://ci.openlp.io/job/Branch-05a-Code_Analysis/973/
[SUCCESS] https://ci.openlp.io/job/Branch-05b-Test_Coverage/841/
[SUCCESS] https://ci.openlp.io/job/Branch-05c-Code_Analysis2/21/

-- 
Your team OpenLP Core is subscribed to branch lp:openlp.
=== removed file '.coveragerc'
--- .coveragerc	2016-02-22 14:25:22 +0000
+++ .coveragerc	1970-01-01 00:00:00 +0000
@@ -1,5 +0,0 @@
-[run]
-source = openlp
-
-[html]
-directory = coverage

=== modified file 'openlp/core/common/uistrings.py'
--- openlp/core/common/uistrings.py	2016-07-24 18:56:13 +0000
+++ openlp/core/common/uistrings.py	2016-08-07 21:16:04 +0000
@@ -81,8 +81,8 @@
         self.Export = translate('OpenLP.Ui', 'Export')
         self.File = translate('OpenLP.Ui', 'File')
         self.FileNotFound = translate('OpenLP.Ui', 'File Not Found')
-        # TODO: Check before converting to python3 string
-        self.FileNotFoundMessage = translate('OpenLP.Ui', 'File %s not found.\nPlease try selecting it individually.')
+        self.FileNotFoundMessage = translate('OpenLP.Ui',
+                                             'File {name} not found.\nPlease try selecting it individually.')
         self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')
         self.Help = translate('OpenLP.Ui', 'Help')
         self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
@@ -141,8 +141,8 @@
         self.Split = translate('OpenLP.Ui', 'Optional &Split')
         self.SplitToolTip = translate('OpenLP.Ui',
                                       'Split a slide into two only if it does not fit on the screen as one slide.')
-        # TODO: Check before converting to python3 string
-        self.StartTimeCode = translate('OpenLP.Ui', 'Start %s')
+        # TODO: WHERE is this used at? cannot find where it's used at in code.
+        self.StartTimeCode = translate('OpenLP.Ui', 'Start {code}')
         self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
         self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
         self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2016-07-01 21:17:20 +0000
+++ openlp/core/lib/__init__.py	2016-08-07 21:16:04 +0000
@@ -323,8 +323,7 @@
         return ''
     elif len(string_list) == 1:
         return string_list[0]
-    # TODO:
-    #   Cannot convert these strings to python3 yet until I can figure out how to mock translate() with the new format
+    # TODO: Verify mocking of translate() test before conversion
     elif len(string_list) == 2:
         return translate('OpenLP.core.lib', '%s and %s',
                          'Locale list separator: 2 items') % (string_list[0], string_list[1])

=== modified file 'openlp/core/lib/filedialog.py'
--- openlp/core/lib/filedialog.py	2016-05-15 17:33:42 +0000
+++ openlp/core/lib/filedialog.py	2016-08-07 21:16:04 +0000
@@ -51,9 +51,8 @@
                 file = parse.unquote(file)
                 if not os.path.exists(file):
                     log.error('File {text} not found.'.format(text=file))
-                    # TODO: Test with UiStrings() before converting to python3 strings
                     QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound,
-                                                      UiStrings().FileNotFoundMessage % file)
+                                                      UiStrings().FileNotFoundMessage.format(name=file))
                     continue
             file_list.append(file)
         return file_list

=== modified file 'openlp/core/lib/projector/pjlink1.py'
--- openlp/core/lib/projector/pjlink1.py	2016-07-21 21:32:25 +0000
+++ openlp/core/lib/projector/pjlink1.py	2016-08-07 21:16:04 +0000
@@ -436,7 +436,7 @@
             return
         return self.process_command(cmd, data)
 
-    @pyqtSlot(int)
+    @pyqtSlot(QAbstractSocket.SocketError)
     def get_error(self, err):
         """
         Process error from SocketError signal.

=== modified file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	2016-07-01 21:17:20 +0000
+++ openlp/core/lib/renderer.py	2016-08-07 21:16:04 +0000
@@ -22,6 +22,7 @@
 
 import re
 
+from string import Template
 from PyQt5 import QtGui, QtCore, QtWebKitWidgets
 
 from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
@@ -370,8 +371,7 @@
         self.web.resize(self.page_width, self.page_height)
         self.web_frame = self.web.page().mainFrame()
         # Adjust width and height to account for shadow. outline done in css.
-        # TODO: Verify before converting to python3 strings
-        html = """<!DOCTYPE html><html><head><script>
+        html = Template("""<!DOCTYPE html><html><head><script>
             function show_text(newtext) {
                 var main = document.getElementById('main');
                 main.innerHTML = newtext;
@@ -380,12 +380,16 @@
                 // returned value).
                 return main.offsetHeight;
             }
-            </script><style>*{margin: 0; padding: 0; border: 0;}
-            #main {position: absolute; top: 0px; %s %s}</style></head><body>
-            <div id="main"></div></body></html>""" % \
-            (build_lyrics_format_css(theme_data, self.page_width, self.page_height),
-             build_lyrics_outline_css(theme_data))
-        self.web.setHtml(html)
+            </script>
+            <style>
+                *{margin: 0; padding: 0; border: 0;}
+                #main {position: absolute; top: 0px; ${format_css} ${outline_css}}
+            </style></head>
+            <body><div id="main"></div></body></html>""")
+        self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
+                                                                            self.page_width,
+                                                                            self.page_height),
+                                         outline_css=build_lyrics_outline_css(theme_data)))
         self.empty_height = self.web_frame.contentsSize().height()
 
     def _paginate_slide(self, lines, line_end):

=== modified file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py	2016-07-23 21:41:24 +0000
+++ openlp/core/lib/theme.py	2016-08-07 21:16:04 +0000
@@ -514,8 +514,8 @@
         theme_strings = []
         for key in dir(self):
             if key[0:1] != '_':
-                # TODO: Verify spacing format before converting to python3 string
-                theme_strings.append('%30s: %s' % (key, getattr(self, key)))
+                # TODO: Due to bound methods returned, I don't know how to write a proper test
+                theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
         return '\n'.join(theme_strings)
 
     def _build_xml_from_attrs(self):

=== modified file 'openlp/core/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py	2016-07-01 21:17:20 +0000
+++ openlp/core/ui/exceptionform.py	2016-08-07 21:16:04 +0000
@@ -90,13 +90,12 @@
         super(ExceptionForm, self).__init__(None, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
         self.setupUi(self)
         self.settings_section = 'crashreport'
-        # TODO: Need to see how to format strings when string with tags is actually a variable
         self.report_text = '**OpenLP Bug Report**\n' \
-            'Version: %s\n\n' \
-            '--- Details of the Exception. ---\n\n%s\n\n ' \
-            '--- Exception Traceback ---\n%s\n' \
-            '--- System information ---\n%s\n' \
-            '--- Library Versions ---\n%s\n'
+            'Version: {version}\n\n' \
+            '--- Details of the Exception. ---\n\n{description}\n\n ' \
+            '--- Exception Traceback ---\n{traceback}\n' \
+            '--- System information ---\n{system}\n' \
+            '--- Library Versions ---\n{libs}\n'
 
     def exec(self):
         """
@@ -132,7 +131,9 @@
                 system += 'Desktop: GNOME\n'
             elif os.environ.get('DESKTOP_SESSION') == 'xfce':
                 system += 'Desktop: Xfce\n'
-        return openlp_version, description, traceback, system, libraries
+        # NOTE: Keys match the expected input for self.report_text.format()
+        return {'version': openlp_version, 'description': description, 'traceback': traceback,
+                'system': system, 'libs': libraries}
 
     def on_save_report_button_clicked(self):
         """
@@ -146,7 +147,9 @@
         if filename:
             filename = str(filename).replace('/', os.path.sep)
             Settings().setValue(self.settings_section + '/last directory', os.path.dirname(filename))
-            report_text = self.report_text % self._create_report()
+            opts = self._create_report()
+            report_text = self.report_text.format(version=opts['version'], description=opts['description'],
+                                                  traceback=opts['traceback'], libs=opts['libs'], system=opts['system'])
             try:
                 report_file = open(filename, 'w')
                 try:
@@ -169,7 +172,7 @@
         content = self._create_report()
         source = ''
         exception = ''
-        for line in content[2].split('\n'):
+        for line in content['traceback'].split('\n'):
             if re.search(r'[/\\]openlp[/\\]', line):
                 source = re.sub(r'.*[/\\]openlp[/\\](.*)".*', r'\1', line)
             if ':' in line:
@@ -177,8 +180,11 @@
         subject = 'Bug report: {error} in {source}'.format(error=exception, source=source)
         mail_urlquery = QtCore.QUrlQuery()
         mail_urlquery.addQueryItem('subject', subject)
-        # TODO: Find out how to format() text that is in a variable
-        mail_urlquery.addQueryItem('body', self.report_text % content)
+        mail_urlquery.addQueryItem('body', self.report_text.format(version=content['version'],
+                                                                   description=content['description'],
+                                                                   traceback=content['traceback'],
+                                                                   system=content['system'],
+                                                                   libs=content['libs']))
         if self.file_attachment:
             mail_urlquery.addQueryItem('attach', self.file_attachment)
         mail_to_url = QtCore.QUrl('mailto:bugs@xxxxxxxxxx')
@@ -208,7 +214,7 @@
                                                                    Settings().value(self.settings_section +
                                                                                     '/last directory'),
                                                                    '{text} (*)'.format(text=UiStrings().AllFiles))
-        log.info('New files(s) %s', str(files))
+        log.info('New files(s) {files}'.format(files=str(files)))
         if files:
             self.file_attachment = str(files)
 

=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py	2016-07-01 21:17:20 +0000
+++ openlp/core/ui/firsttimeform.py	2016-08-07 21:16:04 +0000
@@ -207,8 +207,8 @@
                 trace_error_handler(log)
         self.update_screen_list_combo()
         self.application.process_events()
-        # TODO: Figure out how to use a variable with format()
-        self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading %s...')
+        # TODO: Tested at home
+        self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')
         if self.has_run_wizard:
             self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
             self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active())
@@ -632,7 +632,8 @@
             item = self.songs_list_widget.item(i)
             if item.checkState() == QtCore.Qt.Checked:
                 filename, sha256 = item.data(QtCore.Qt.UserRole)
-                self._increment_progress_bar(self.downloading % filename, 0)
+                # TODO: Tested at home
+                self._increment_progress_bar(self.downloading.format(name=filename), 0)
                 self.previous_size = 0
                 destination = os.path.join(songs_destination, str(filename))
                 if not self.url_get_file('{path}{name}'.format(path=self.songs_url, name=filename),
@@ -644,7 +645,8 @@
             item = bibles_iterator.value()
             if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
                 bible, sha256 = item.data(0, QtCore.Qt.UserRole)
-                self._increment_progress_bar(self.downloading % bible, 0)
+                # TODO: Tested at home
+                self._increment_progress_bar(self.downloading.format(name=bible), 0)
                 self.previous_size = 0
                 if not self.url_get_file('{path}{name}'.format(path=self.bibles_url, name=bible),
                                          os.path.join(bibles_destination, bible),
@@ -656,8 +658,8 @@
             item = self.themes_list_widget.item(i)
             if item.checkState() == QtCore.Qt.Checked:
                 theme, sha256 = item.data(QtCore.Qt.UserRole)
-                # TODO: Verify how to use format() with strings in a variable
-                self._increment_progress_bar(self.downloading % theme, 0)
+                # TODO: Tested at home
+                self._increment_progress_bar(self.downloading.format(name=theme), 0)
                 self.previous_size = 0
                 if not self.url_get_file('{path}{name}'.format(path=self.themes_url, name=theme),
                                          os.path.join(themes_destination, theme),

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2016-07-01 21:17:20 +0000
+++ openlp/core/ui/mainwindow.py	2016-08-07 21:16:04 +0000
@@ -1333,7 +1333,7 @@
         self.recent_files_menu.clear()
         for file_id, filename in enumerate(recent_files_to_display):
             log.debug('Recent file name: {name}'.format(name=filename))
-            # TODO: Verify ''.format() before committing
+            # TODO: Should be good
             action = create_action(self, '',
                                    text='&{n} {name}'.format(n=file_id + 1,
                                                              name=os.path.splitext(os.path.basename(str(filename)))[0]),

=== modified file 'openlp/core/ui/pluginform.py'
--- openlp/core/ui/pluginform.py	2016-05-20 16:22:06 +0000
+++ openlp/core/ui/pluginform.py	2016-08-07 21:16:04 +0000
@@ -60,7 +60,7 @@
         self._clear_details()
         self.programatic_change = True
         plugin_list_width = 0
-        # TODO: See how to use format() with variables
+        # TODO: Tested at home
         for plugin in self.plugin_manager.plugins:
             item = QtWidgets.QListWidgetItem(self.plugin_list_widget)
             # We do this just to make 100% sure the status is an integer as
@@ -68,19 +68,19 @@
             plugin.status = int(plugin.status)
             # Set the little status text in brackets next to the plugin name.
             if plugin.status == PluginStatus.Disabled:
-                status_text = translate('OpenLP.PluginForm', '%s (Disabled)')
+                status_text = translate('OpenLP.PluginForm', '{name} (Disabled)')
             elif plugin.status == PluginStatus.Active:
-                status_text = translate('OpenLP.PluginForm', '%s (Active)')
+                status_text = translate('OpenLP.PluginForm', '{name} (Active)')
             else:
                 # PluginStatus.Inactive
-                status_text = translate('OpenLP.PluginForm', '%s (Inactive)')
-            item.setText(status_text % plugin.name_strings['singular'])
+                status_text = translate('OpenLP.PluginForm', '{name} (Inactive)')
+            item.setText(status_text.format(name=plugin.name_strings['singular']))
             # If the plugin has an icon, set it!
             if plugin.icon:
                 item.setIcon(plugin.icon)
             self.plugin_list_widget.addItem(item)
             plugin_list_width = max(plugin_list_width, self.fontMetrics().width(
-                translate('OpenLP.PluginForm', '%s (Inactive)') % plugin.name_strings['singular']))
+                translate('OpenLP.PluginForm', '{name} (Inactive)').format(name=plugin.name_strings['singular'])))
         self.plugin_list_widget.setFixedWidth(plugin_list_width + self.plugin_list_widget.iconSize().width() + 48)
 
     def _clear_details(self):
@@ -137,13 +137,13 @@
             self.active_plugin.app_startup()
         else:
             self.active_plugin.toggle_status(PluginStatus.Inactive)
-        # TODO: Verify using format() with a variable
-        status_text = translate('OpenLP.PluginForm', '%s (Inactive)')
+        # TODO: Tested at home
+        status_text = translate('OpenLP.PluginForm', '{name} (Inactive)')
         if self.active_plugin.status == PluginStatus.Active:
-            status_text = translate('OpenLP.PluginForm', '%s (Active)')
+            status_text = translate('OpenLP.PluginForm', '{name} (Active)')
         elif self.active_plugin.status == PluginStatus.Inactive:
-            status_text = translate('OpenLP.PluginForm', '%s (Inactive)')
+            status_text = translate('OpenLP.PluginForm', '{name} (Inactive)')
         elif self.active_plugin.status == PluginStatus.Disabled:
-            status_text = translate('OpenLP.PluginForm', '%s (Disabled)')
+            status_text = translate('OpenLP.PluginForm', '{name} (Disabled)')
         self.plugin_list_widget.currentItem().setText(
-            status_text % self.active_plugin.name_strings['singular'])
+            status_text.format(name=self.active_plugin.name_strings['singular']))

=== modified file 'openlp/core/ui/projector/sourceselectform.py'
--- openlp/core/ui/projector/sourceselectform.py	2016-07-01 21:17:20 +0000
+++ openlp/core/ui/projector/sourceselectform.py	2016-08-07 21:16:04 +0000
@@ -30,8 +30,8 @@
 
 from PyQt5 import QtCore, QtWidgets
 from PyQt5.QtCore import pyqtSlot, QSize
-from PyQt5.QtWidgets import QDialog, QButtonGroup, QDialogButtonBox, QFormLayout, QLineEdit, QRadioButton, \
-    QStyle, QStylePainter, QStyleOptionTab, QTabBar, QTabWidget, QVBoxLayout, QWidget
+from PyQt5.QtWidgets import QAbstractButton, QDialog, QButtonGroup, QDialogButtonBox, QFormLayout, QLineEdit, \
+    QRadioButton, QStyle, QStylePainter, QStyleOptionTab, QTabBar, QTabWidget, QVBoxLayout, QWidget
 
 from openlp.core.common import translate, is_macosx
 from openlp.core.lib import build_icon
@@ -452,7 +452,7 @@
         selected = super(SourceSelectSingle, self).exec()
         return selected
 
-    @pyqtSlot(object)
+    @pyqtSlot(QAbstractButton)
     def button_clicked(self, button):
         """
         Checks which button was clicked

=== modified file 'openlp/core/ui/themeform.py'
--- openlp/core/ui/themeform.py	2016-07-01 21:17:20 +0000
+++ openlp/core/ui/themeform.py	2016-08-07 21:16:04 +0000
@@ -464,9 +464,9 @@
         """
         Background video button pushed.
         """
-        # TODO: Check this before converting
-        visible_formats = '(%s)' % '; '.join(VIDEO_EXT)
-        actual_formats = '(%s)' % ' '.join(VIDEO_EXT)
+        # TODO: Should work
+        visible_formats = '({name})'.format(name='; '.join(VIDEO_EXT))
+        actual_formats = '({name})'.format(name=' '.join(VIDEO_EXT))
         video_filter = '{trans} {visible} {actual}'.format(trans=translate('OpenLP', 'Video Files'),
                                                            visible=visible_formats, actual=actual_formats)
         video_filter = '{video};;{ui} (*.*)'.format(video=video_filter, ui=UiStrings().AllFiles)

=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py	2016-07-01 21:17:20 +0000
+++ openlp/core/ui/thememanager.py	2016-08-07 21:16:04 +0000
@@ -769,7 +769,7 @@
                                                                              '{count} time(s) by {plugin}'
                                                                              ).format(name=used_count,
                                                                                       plugin=plugin.name)))
-                        plugin_usage = "%s\n" % plugin_usage
+                        plugin_usage = "{text}\n".format(text=plugin_usage)
                 if plugin_usage:
                     critical_error_message_box(translate('OpenLP.ThemeManager', 'Unable to delete theme'),
                                                translate('OpenLP.ThemeManager',

=== modified file 'openlp/plugins/songs/lib/openlyricsxml.py'
--- openlp/plugins/songs/lib/openlyricsxml.py	2016-08-01 17:42:29 +0000
+++ openlp/plugins/songs/lib/openlyricsxml.py	2016-08-07 21:16:04 +0000
@@ -643,7 +643,7 @@
         # Append text from tail and add formatting end tag.
         # TODO: Verify format() with template variables
         if element.tag == NSMAP % 'tag' and use_endtag:
-            text += '{{{name}}}'.format(name=element.get('name'))
+            text += '{{/{name}}}'.format(name=element.get('name'))
         # Append text from tail.
         if element.tail:
             text += element.tail

=== modified file 'tests/functional/openlp_core_lib/test_file_dialog.py'
--- tests/functional/openlp_core_lib/test_file_dialog.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_core_lib/test_file_dialog.py	2016-08-07 21:16:04 +0000
@@ -60,7 +60,7 @@
         self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [
             '/Valid File', '/url encoded file #1']
         self.mocked_ui_strings().FileNotFound = 'File Not Found'
-        self.mocked_ui_strings().FileNotFoundMessage = 'File %s not found.\nPlease try selecting it individually.'
+        self.mocked_ui_strings().FileNotFoundMessage = 'File {name} not found.\nPlease try selecting it individually.'
 
         # WHEN: FileDialog.getOpenFileNames is called
         result = FileDialog.getOpenFileNames(self.mocked_parent)

=== modified file 'tests/functional/openlp_core_lib/test_projector_pjlink1.py'
--- tests/functional/openlp_core_lib/test_projector_pjlink1.py	2016-06-18 02:45:02 +0000
+++ tests/functional/openlp_core_lib/test_projector_pjlink1.py	2016-08-07 21:16:04 +0000
@@ -29,26 +29,12 @@
 from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_WARMUP, S_ON, \
     S_COOLDOWN, PJLINK_POWR_STATUS
 
-from tests.functional import patch
+from tests.functional import patch, MagicMock
 from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH
 
 pjlink_test = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
 
 
-class DummyTimer(object):
-    '''
-    Dummy class to fake timers
-    '''
-    def __init__(self, *args, **kwargs):
-        pass
-
-    def start(self, *args, **kwargs):
-        pass
-
-    def stop(self, *args, **kwargs):
-        pass
-
-
 class TestPJLink(TestCase):
     """
     Tests for the PJLink module
@@ -308,8 +294,8 @@
         pjlink.other_info = 'ANOTHER TEST'
         pjlink.send_queue = True
         pjlink.send_busy = True
-        pjlink.timer = DummyTimer()
-        pjlink.socket_timer = DummyTimer()
+        pjlink.timer = MagicMock()
+        pjlink.socket_timer = MagicMock()
 
         # WHEN: reset_information() is called
         with patch.object(pjlink.timer, 'stop') as mock_timer:

=== modified file 'tests/functional/openlp_core_lib/test_projectordb.py'
--- tests/functional/openlp_core_lib/test_projectordb.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_core_lib/test_projectordb.py	2016-08-07 21:16:04 +0000
@@ -284,3 +284,16 @@
         self.assertEqual(str(source),
                          '<ProjectorSource(id="1", code="11", text="First RGB source", projector_id="1")>',
                          'ProjectorSource.__repr__)_ should have returned a proper representation string')
+
+    def test_get_projector_by_id_none(self):
+        """
+        Test get_projector_by_id returns None if no db entry
+        """
+        # GIVEN: Test object and data
+        projector = self.projector
+
+        # WHEN: DB search for entry not saved
+        results = projector.get_projector_by_id(dbid=123134556409824506)
+
+        # THEN: Verify return was None
+        self.assertEqual(results, None, 'Returned results should have equaled None')

=== modified file 'tests/functional/openlp_core_lib/test_renderer.py'
--- tests/functional/openlp_core_lib/test_renderer.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_core_lib/test_renderer.py	2016-08-07 21:16:04 +0000
@@ -29,6 +29,7 @@
 from openlp.core.common import Registry
 from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
 from openlp.core.lib.renderer import words_split, get_start_tags
+from openlp.core.lib.theme import ThemeXML
 
 from tests.functional import MagicMock, patch
 
@@ -39,6 +40,24 @@
 }
 
 
+# WARNING: Leave formatting alone - this is how it's returned in renderer.py
+CSS_TEST_ONE = """<!DOCTYPE html><html><head><script>
+            function show_text(newtext) {
+                var main = document.getElementById('main');
+                main.innerHTML = newtext;
+                // We need to be sure that the page is loaded, that is why we
+                // return the element's height (even though we do not use the
+                // returned value).
+                return main.offsetHeight;
+            }
+            </script>
+            <style>
+                *{margin: 0; padding: 0; border: 0;}
+                #main {position: absolute; top: 0px;  FORMAT CSS;   OUTLINE CSS; }
+            </style></head>
+            <body><div id="main"></div></body></html>'"""
+
+
 class TestRenderer(TestCase):
 
     def setUp(self):
@@ -159,3 +178,28 @@
 
         # THEN: The blanks have been removed.
         self.assertListEqual(result_words, expected_words)
+
+    @patch('openlp.core.lib.renderer.QtWebKitWidgets.QWebView')
+    @patch('openlp.core.lib.renderer.build_lyrics_format_css')
+    @patch('openlp.core.lib.renderer.build_lyrics_outline_css')
+    def test_set_text_rectangle(self, mock_outline_css, mock_lyrics_css, mock_webview):
+        """
+        Test set_text_rectangle returns a proper html string
+        """
+        # GIVEN: test object and data
+        mock_lyrics_css.return_value = ' FORMAT CSS; '
+        mock_outline_css.return_value = ' OUTLINE CSS; '
+        theme_data = ThemeXML()
+        theme_data.font_main_name = 'Arial'
+        theme_data.font_main_size = 20
+        theme_data.font_main_color = '#FFFFFF'
+        theme_data.font_main_outline_color = '#FFFFFF'
+        main = QtCore.QRect(10, 10, 1280, 900)
+        foot = QtCore.QRect(10, 1000, 1260, 24)
+        renderer = Renderer()
+
+        # WHEN: Calling method
+        renderer._set_text_rectangle(theme_data=theme_data, rect_main=main, rect_footer=foot)
+
+        # THEN: QtWebKitWidgets should be called with the proper string
+        mock_webview.setHtml.called_with(CSS_TEST_ONE, 'Should be the same')

=== added file 'tests/functional/openlp_core_ui/test_exceptionform.py'
--- tests/functional/openlp_core_ui/test_exceptionform.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_ui/test_exceptionform.py	2016-08-07 21:16:04 +0000
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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; version 2 of the License.                              #
+#                                                                             #
+# 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, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Package to test the openlp.core.ui.exeptionform package.
+"""
+
+import os
+import socket
+import tempfile
+import urllib
+from unittest import TestCase
+from unittest.mock import mock_open
+
+from PyQt5.QtCore import QUrlQuery
+
+from openlp.core.common import Registry
+from openlp.core.ui.firsttimeform import FirstTimeForm
+
+from tests.functional import MagicMock, patch
+from tests.helpers.testmixin import TestMixin
+
+from openlp.core.ui import exceptionform
+
+exceptionform.WEBKIT_VERSION = 'Webkit Test'
+exceptionform.MIGRATE_VERSION = 'Migrate Test'
+exceptionform.CHARDET_VERSION = 'CHARDET Test'
+exceptionform.ENCHANT_VERSION = 'Enchant Test'
+exceptionform.MAKO_VERSION = 'Mako Test'
+exceptionform.ICU_VERSION = 'ICU Test'
+exceptionform.VLC_VERSION = 'VLC Test'
+
+MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of the Exception. ---\n\n'
+                  'Description Test\n\n --- Exception Traceback ---\nopenlp: Traceback Test\n'
+                  '--- System information ---\nPlatform: Nose Test\n\n--- Library Versions ---\n'
+                  'Python: Python Test\nQt5: Qt5 test\nPyQt5: PyQt5 Test\nQtWebkit: Webkit Test\n'
+                  'SQLAlchemy: SqlAlchemy Test\nSQLAlchemy Migrate: Migrate Test\nBeautifulSoup: BeautifulSoup Test\n'
+                  'lxml: ETree Test\nChardet: CHARDET Test\nPyEnchant: Enchant Test\nMako: Mako Test\n'
+                  'pyICU: ICU Test\npyUNO bridge: UNO Bridge Test\nVLC: VLC Test\n\n')
+
+
+@patch("openlp.core.ui.exceptionform.Qt.qVersion")
+@patch("openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl")
+@patch("openlp.core.ui.exceptionform.get_application_version")
+@patch("openlp.core.ui.exceptionform.sqlalchemy")
+@patch("openlp.core.ui.exceptionform.bs4")
+@patch("openlp.core.ui.exceptionform.etree")
+@patch("openlp.core.ui.exceptionform.is_linux")
+@patch("openlp.core.ui.exceptionform.platform.platform")
+@patch("openlp.core.ui.exceptionform.platform.python_version")
+class TestExceptionForm(TestMixin, TestCase):
+    """
+    Test functionality of exception form functions
+    """
+    def __method_template_for_class_patches(self,
+                                            __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__,
+                                            mocked_python_version,
+                                            mocked_platform,
+                                            mocked_is_linux,
+                                            mocked_etree,
+                                            mocked_bs4,
+                                            mocked_sqlalchemy,
+                                            mocked_application_version,
+                                            mocked_openlurl,
+                                            mocked_qversion,
+                                            ):
+        """
+        Template so you don't have to remember the layout of class mock options for methods
+        """
+        mocked_etree.__version__ = 'ETree Test'
+        mocked_bs4.__version__ = 'BeautifulSoup Test'
+        mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
+        mocked_python_version.return_value = 'Python Test'
+        mocked_platform.return_value = 'Nose Test'
+        mocked_qversion.return_value = 'Qt5 test'
+        mocked_is_linux.return_value = False
+        mocked_application_version.return_value = 'Trunk Test'
+
+    def setUp(self):
+        self.setup_application()
+        self.app.setApplicationVersion('0.0')
+        # Set up a fake "set_normal_cursor" method since we're not dealing with an actual OpenLP application object
+        self.app.set_normal_cursor = lambda: None
+        self.app.process_events = lambda: None
+        Registry.create()
+        Registry().register('application', self.app)
+        self.tempfile = os.path.join(tempfile.gettempdir(), 'testfile')
+
+    def tearDown(self):
+        if os.path.isfile(self.tempfile):
+            os.remove(self.tempfile)
+
+    @patch("openlp.core.ui.exceptionform.Ui_ExceptionDialog")
+    @patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog")
+    @patch("openlp.core.ui.exceptionform.QtCore.QUrl")
+    @patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem")
+    @patch("openlp.core.ui.exceptionform.Qt")
+    def test_on_send_report_button_clicked(self,
+                                           mocked_qt,
+                                           mocked_add_query_item,
+                                           mocked_qurl,
+                                           mocked_file_dialog,
+                                           mocked_ui_exception_dialog,
+                                           mocked_python_version,
+                                           mocked_platform,
+                                           mocked_is_linux,
+                                           mocked_etree,
+                                           mocked_bs4,
+                                           mocked_sqlalchemy,
+                                           mocked_application_version,
+                                           mocked_openlurl,
+                                           mocked_qversion,
+                                           ):
+        """
+        Test send report  creates the proper system information text
+        """
+        # GIVEN: Test environment
+        mocked_etree.__version__ = 'ETree Test'
+        mocked_bs4.__version__ = 'BeautifulSoup Test'
+        mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
+        mocked_python_version.return_value = 'Python Test'
+        mocked_platform.return_value = 'Nose Test'
+        mocked_qversion.return_value = 'Qt5 test'
+        mocked_is_linux.return_value = False
+        mocked_application_version.return_value = 'Trunk Test'
+        mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
+        mocked_is_linux.return_value = False
+        mocked_application_version.return_value = 'Trunk Test'
+
+        test_form = exceptionform.ExceptionForm()
+        test_form.file_attachment = None
+
+        with patch.object(test_form, '_pyuno_import') as mock_pyuno:
+            with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback:
+                with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
+                    mock_pyuno.return_value = 'UNO Bridge Test'
+                    mock_traceback.return_value = 'openlp: Traceback Test'
+                    mock_description.return_value = 'Description Test'
+
+                    # WHEN: on_save_report_button_clicked called
+                    test_form.on_send_report_button_clicked()
+
+        # THEN: Verify strings were formatted properly
+        mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT)
+
+    @patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog.getSaveFileName")
+    @patch("openlp.core.ui.exceptionform.Qt")
+    def test_on_save_report_button_clicked(self,
+                                           mocked_qt,
+                                           mocked_save_filename,
+                                           mocked_python_version,
+                                           mocked_platform,
+                                           mocked_is_linux,
+                                           mocked_etree,
+                                           mocked_bs4,
+                                           mocked_sqlalchemy,
+                                           mocked_application_version,
+                                           mocked_openlurl,
+                                           mocked_qversion,
+                                           ):
+        """
+        Test save report saves the correct information to a file
+        """
+        mocked_etree.__version__ = 'ETree Test'
+        mocked_bs4.__version__ = 'BeautifulSoup Test'
+        mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
+        mocked_python_version.return_value = 'Python Test'
+        mocked_platform.return_value = 'Nose Test'
+        mocked_qversion.return_value = 'Qt5 test'
+        mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
+        mocked_is_linux.return_value = False
+        mocked_application_version.return_value = 'Trunk Test'
+        mocked_save_filename.return_value = ['testfile.txt', ]
+
+        test_form = exceptionform.ExceptionForm()
+        test_form.file_attachment = None
+
+        with patch.object(test_form, '_pyuno_import') as mock_pyuno:
+            with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback:
+                with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
+                    with patch("openlp.core.ui.exceptionform.open", mock_open(), create=True) as mocked_open:
+                        mock_pyuno.return_value = 'UNO Bridge Test'
+                        mock_traceback.return_value = 'openlp: Traceback Test'
+                        mock_description.return_value = 'Description Test'
+
+                        # WHEN: on_save_report_button_clicked called
+                        test_form.on_save_report_button_clicked()
+
+        # THEN: Verify proper calls to save file
+        # self.maxDiff = None
+        check_text = "call().write({text})".format(text=MAIL_ITEM_TEXT.__repr__())
+        write_text = "{text}".format(text=mocked_open.mock_calls[1])
+        mocked_open.assert_called_with('testfile.txt', 'w')
+        self.assertEquals(check_text, write_text, "Saved information should match test text")


Follow ups