openlp/openlp/core/utils/__init__.py

452 lines
17 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2012-12-29 09:35:24 +00:00
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
2013-12-24 08:56:50 +00:00
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
2012-11-11 21:16:14 +00:00
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
2012-10-21 13:16:22 +00:00
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# 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 #
###############################################################################
2010-06-10 19:45:02 +00:00
"""
2010-12-10 05:09:03 +00:00
The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
2010-06-10 19:45:02 +00:00
"""
2013-02-02 20:18:34 +00:00
from datetime import datetime
from distutils.version import LooseVersion
2010-07-30 22:48:09 +00:00
import logging
2012-12-04 21:22:08 +00:00
import locale
import os
2010-07-30 22:48:09 +00:00
import re
from subprocess import Popen, PIPE
import sys
2013-10-13 13:51:13 +00:00
import urllib.request
import urllib.error
import urllib.parse
from random import randint
2013-02-27 11:29:24 +00:00
from PyQt4 import QtGui, QtCore
2013-12-13 17:44:05 +00:00
from openlp.core.common import Registry, AppLocation, Settings
2012-05-17 15:13:09 +00:00
2013-08-31 18:17:38 +00:00
if sys.platform != 'win32' and sys.platform != 'darwin':
2011-01-18 04:32:24 +00:00
try:
from xdg import BaseDirectory
XDG_BASE_AVAILABLE = True
except ImportError:
XDG_BASE_AVAILABLE = False
2013-10-13 21:07:28 +00:00
from openlp.core.common import translate
2014-04-12 20:19:22 +00:00
log = logging.getLogger(__name__ + '.__init__')
2014-01-11 21:29:01 +00:00
2011-03-25 16:29:39 +00:00
APPLICATION_VERSION = {}
2011-01-18 04:32:24 +00:00
IMAGES_FILTER = None
ICU_COLLATOR = None
2013-08-31 18:17:38 +00:00
UNO_CONNECTION_TYPE = 'pipe'
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
USER_AGENTS = {
'win32': [
'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36'
],
'darwin': [
2014-03-20 19:10:31 +00:00
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) '
'Chrome/26.0.1410.43 Safari/537.31',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) '
'Chrome/20.0.1132.57 Safari/536.11',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) '
'Chrome/20.0.1132.47 Safari/536.11',
],
'linux2': [
2014-03-20 19:10:31 +00:00
'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 '
'Chrome/25.0.1364.160 Safari/537.22',
'Mozilla/5.0 (X11; CrOS armv7l 2913.260.0) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.99 '
'Safari/537.11',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1389.0 Safari/537.27'
],
'default': [
'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0'
]
}
2009-10-14 17:52:50 +00:00
2013-02-02 20:18:34 +00:00
2010-07-30 22:48:09 +00:00
class VersionThread(QtCore.QThread):
"""
A special Qt thread class to fetch the version of OpenLP from the website.
This is threaded so that it doesn't affect the loading time of OpenLP.
"""
def run(self):
"""
Run the thread.
"""
self.sleep(1)
2013-08-31 18:17:38 +00:00
log.debug('Version thread - run')
2011-03-25 16:29:39 +00:00
app_version = get_application_version()
version = check_latest_version(app_version)
2013-08-31 18:17:38 +00:00
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
Registry().execute('openlp_version_check', '%s' % version)
2010-07-30 22:48:09 +00:00
2011-03-25 16:29:39 +00:00
def get_application_version():
2011-03-24 19:12:27 +00:00
"""
Returns the application version of the running instance of OpenLP::
2014-05-02 06:42:17 +00:00
{'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
2011-03-24 19:12:27 +00:00
"""
global APPLICATION_VERSION
if APPLICATION_VERSION:
return APPLICATION_VERSION
2013-08-31 18:17:38 +00:00
if '--dev-version' in sys.argv or '-d' in sys.argv:
2013-06-06 06:40:10 +00:00
# NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
# there.
# Get the revision of this tree.
2013-08-31 18:17:38 +00:00
bzr = Popen(('bzr', 'revno'), stdout=PIPE)
2013-06-06 06:40:10 +00:00
tree_revision, error = bzr.communicate()
2013-07-19 16:07:33 +00:00
tree_revision = tree_revision.decode()
2013-04-15 21:04:49 +00:00
code = bzr.wait()
if code != 0:
2013-08-31 18:17:38 +00:00
raise Exception('Error running bzr log')
2013-06-06 06:40:10 +00:00
# Get all tags.
2013-08-31 18:17:38 +00:00
bzr = Popen(('bzr', 'tags'), stdout=PIPE)
2013-04-15 21:04:49 +00:00
output, error = bzr.communicate()
code = bzr.wait()
if code != 0:
2013-08-31 18:17:38 +00:00
raise Exception('Error running bzr tags')
tags = list(map(bytes.decode, output.splitlines()))
2013-06-06 06:40:10 +00:00
if not tags:
2013-08-31 18:17:38 +00:00
tag_version = '0.0.0'
tag_revision = '0'
2013-06-06 06:40:10 +00:00
else:
# Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
# another series.
2013-08-31 18:17:38 +00:00
tags = [tag for tag in tags if tag.split()[-1].strip() != '?']
2013-06-06 06:40:10 +00:00
# Get the last tag and split it in a revision and tag name.
tag_version, tag_revision = tags[-1].split()
# If they are equal, then this tree is tarball with the source for the release. We do not want the revision
# number in the full version.
if tree_revision == tag_revision:
2014-07-01 21:10:26 +00:00
full_version = tag_version.decode('utf-8')
2013-06-06 06:40:10 +00:00
else:
2014-07-01 21:10:26 +00:00
full_version = '%s-bzr%s' % (tag_version.decode('utf-8'), tree_revision.decode('utf-8'))
2011-03-24 19:12:27 +00:00
else:
# We're not running the development version, let's use the file.
filepath = AppLocation.get_directory(AppLocation.VersionDir)
2013-08-31 18:17:38 +00:00
filepath = os.path.join(filepath, '.version')
2011-03-24 19:12:27 +00:00
fversion = None
try:
2013-08-31 18:17:38 +00:00
fversion = open(filepath, 'r')
full_version = str(fversion.read()).rstrip()
2011-03-24 19:12:27 +00:00
except IOError:
log.exception('Error in version file.')
2013-08-31 18:17:38 +00:00
full_version = '0.0.0-bzr000'
2011-03-24 19:12:27 +00:00
finally:
if fversion:
fversion.close()
2013-08-31 18:17:38 +00:00
bits = full_version.split('-')
2011-03-24 19:12:27 +00:00
APPLICATION_VERSION = {
2013-08-31 18:17:38 +00:00
'full': full_version,
'version': bits[0],
'build': bits[1] if len(bits) > 1 else None
2011-03-24 19:12:27 +00:00
}
2013-08-31 18:17:38 +00:00
if APPLICATION_VERSION['build']:
log.info('Openlp version %s build %s', APPLICATION_VERSION['version'], APPLICATION_VERSION['build'])
2011-03-24 19:12:27 +00:00
else:
2013-08-31 18:17:38 +00:00
log.info('Openlp version %s' % APPLICATION_VERSION['version'])
2011-03-24 19:12:27 +00:00
return APPLICATION_VERSION
2010-04-27 16:27:57 +00:00
def check_latest_version(current_version):
"""
Check the latest version of OpenLP against the version file on the OpenLP
site.
2014-03-17 19:05:55 +00:00
:param current_version: The current version of OpenLP.
**Rules around versions and version files:**
* If a version number has a build (i.e. -bzr1234), then it is a nightly.
* If a version number's minor version is an odd number, it is a development release.
* If a version number's minor version is an even number, it is a stable release.
"""
2013-08-31 18:17:38 +00:00
version_string = current_version['full']
# set to prod in the distribution config file.
2012-05-17 15:13:09 +00:00
settings = Settings()
2013-08-31 18:17:38 +00:00
settings.beginGroup('core')
last_test = settings.value('last version test')
this_test = str(datetime.now().date())
settings.setValue('last version test', this_test)
2010-04-28 14:17:42 +00:00
settings.endGroup()
2013-02-07 10:16:08 +00:00
# Tell the main window whether there will ever be data to display
2013-08-31 18:17:38 +00:00
Registry().get('main_window').version_update_running = last_test != this_test
2009-11-30 18:29:22 +00:00
if last_test != this_test:
2013-08-31 18:17:38 +00:00
if current_version['build']:
req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt')
else:
2013-08-31 18:17:38 +00:00
version_parts = current_version['version'].split('.')
if int(version_parts[1]) % 2 != 0:
2013-08-31 18:17:38 +00:00
req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt')
else:
2013-08-31 18:17:38 +00:00
req = urllib.request.Request('http://www.openlp.org/files/version.txt')
req.add_header('User-Agent', 'OpenLP/%s' % current_version['full'])
2010-08-26 00:37:25 +00:00
remote_version = None
2009-10-12 04:43:02 +00:00
try:
2013-08-31 18:17:38 +00:00
remote_version = str(urllib.request.urlopen(req, None).read().decode()).strip()
2011-01-18 04:32:24 +00:00
except IOError:
2013-08-31 18:17:38 +00:00
log.exception('Failed to download the latest OpenLP version file')
2010-08-26 00:37:25 +00:00
if remote_version:
version_string = remote_version
2009-10-12 04:43:02 +00:00
return version_string
2010-04-23 16:00:32 +00:00
def add_actions(target, actions):
"""
Adds multiple actions to a menu or toolbar in one command.
2014-03-17 19:05:55 +00:00
:param target: The menu or toolbar to add actions to
:param actions: The actions to be added. An action consisting of the keyword ``None``
2010-04-23 19:42:51 +00:00
will result in a separator being inserted into the target.
2010-04-23 16:00:32 +00:00
"""
for action in actions:
if action is None:
target.addSeparator()
else:
target.addAction(action)
def get_filesystem_encoding():
"""
Returns the name of the encoding used to convert Unicode filenames into system file names.
"""
encoding = sys.getfilesystemencoding()
if encoding is None:
encoding = sys.getdefaultencoding()
return encoding
def get_images_filter():
"""
Returns a filter string for a file dialog containing all the supported image formats.
"""
2011-01-18 04:32:24 +00:00
global IMAGES_FILTER
if not IMAGES_FILTER:
2013-08-31 18:17:38 +00:00
log.debug('Generating images filter.')
formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
visible_formats = '(*.%s)' % '; *.'.join(formats)
actual_formats = '(*.%s)' % ' *.'.join(formats)
IMAGES_FILTER = '%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats)
2011-01-18 04:32:24 +00:00
return IMAGES_FILTER
def is_not_image_file(file_name):
"""
Validate that the file is not an image file.
2014-03-17 19:05:55 +00:00
:param file_name: File name to be checked.
"""
2013-07-13 16:33:32 +00:00
if not file_name:
return True
else:
formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
2013-08-31 18:17:38 +00:00
file_part, file_extension = os.path.splitext(str(file_name))
if file_extension[1:].lower() in formats and os.path.exists(file_name):
return False
return True
def split_filename(path):
2011-01-18 04:32:24 +00:00
"""
Return a list of the parts in a given path.
"""
path = os.path.abspath(path)
if not os.path.isfile(path):
2013-08-31 18:17:38 +00:00
return path, ''
else:
return os.path.split(path)
def clean_filename(filename):
"""
Removes invalid characters from the given ``filename``.
2014-03-17 19:05:55 +00:00
:param filename: The "dirty" file name to clean.
"""
2013-08-31 18:17:38 +00:00
if not isinstance(filename, str):
filename = str(filename, 'utf-8')
return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
2011-01-14 18:58:47 +00:00
def delete_file(file_path_name):
"""
Deletes a file from the system.
2014-03-17 19:05:55 +00:00
:param file_path_name: The file, including path, to delete.
2011-01-14 18:58:47 +00:00
"""
if not file_path_name:
return False
try:
if os.path.exists(file_path_name):
os.remove(file_path_name)
return True
except (IOError, OSError):
log.exception("Unable to delete file %s" % file_path_name)
return False
def _get_user_agent():
"""
Return a user agent customised for the platform the user is on.
"""
browser_list = USER_AGENTS.get(sys.platform, None)
if not browser_list:
browser_list = USER_AGENTS['default']
random_index = randint(0, len(browser_list) - 1)
return browser_list[random_index]
def get_web_page(url, header=None, update_openlp=False):
2011-01-10 01:46:47 +00:00
"""
Attempts to download the webpage at url and returns that page or None.
2014-03-17 19:05:55 +00:00
:param url: The URL to be downloaded.
:param header: An optional HTTP header to pass in the request to the web server.
:param update_openlp: Tells OpenLP to update itself if the page is successfully downloaded.
2011-01-10 01:46:47 +00:00
Defaults to False.
"""
2011-02-25 17:05:01 +00:00
# TODO: Add proxy usage. Get proxy info from OpenLP settings, add to a
2011-01-10 01:46:47 +00:00
# proxy_handler, build into an opener and install the opener into urllib2.
# http://docs.python.org/library/urllib2.html
if not url:
return None
2013-08-31 18:17:38 +00:00
req = urllib.request.Request(url)
if not header or header[0].lower() != 'user-agent':
user_agent = _get_user_agent()
req.add_header('User-Agent', user_agent)
if header:
req.add_header(header[0], header[1])
2011-01-10 01:46:47 +00:00
page = None
2013-08-31 18:17:38 +00:00
log.debug('Downloading URL = %s' % url)
2011-01-10 01:46:47 +00:00
try:
2013-08-31 18:17:38 +00:00
page = urllib.request.urlopen(req)
log.debug('Downloaded URL = %s' % page.geturl())
except urllib.error.URLError:
log.exception('The web page could not be downloaded')
2011-01-10 01:46:47 +00:00
if not page:
return None
if update_openlp:
2013-08-31 18:17:38 +00:00
Registry().get('application').process_events()
2011-03-10 13:15:49 +00:00
log.debug(page)
2011-01-10 01:46:47 +00:00
return page
2011-01-21 22:28:34 +00:00
def get_uno_command():
"""
Returns the UNO command to launch an openoffice.org instance.
"""
2013-08-31 18:17:38 +00:00
COMMAND = 'soffice'
OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard'
if UNO_CONNECTION_TYPE == 'pipe':
CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"'
2011-01-21 22:28:34 +00:00
else:
2013-08-31 18:17:38 +00:00
CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"'
return '%s %s %s' % (COMMAND, OPTIONS, CONNECTION)
2011-01-21 22:28:34 +00:00
2011-01-21 22:28:34 +00:00
def get_uno_instance(resolver):
"""
Returns a running openoffice.org instance.
2014-03-17 19:05:55 +00:00
:param resolver: The UNO resolver to use to find a running instance.
2011-01-21 22:28:34 +00:00
"""
2013-08-31 18:17:38 +00:00
log.debug('get UNO Desktop Openoffice - resolve')
if UNO_CONNECTION_TYPE == 'pipe':
return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
2011-01-21 22:28:34 +00:00
else:
2013-08-31 18:17:38 +00:00
return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
2011-01-21 22:28:34 +00:00
2012-09-16 15:33:05 +00:00
def format_time(text, local_time):
"""
2013-01-20 10:01:51 +00:00
Workaround for Python built-in time formatting function time.strftime().
time.strftime() accepts only ascii characters. This function accepts
unicode string and passes individual % placeholders to time.strftime().
This ensures only ascii characters are passed to time.strftime().
2012-09-16 15:33:05 +00:00
2014-03-17 19:05:55 +00:00
:param text: The text to be processed.
:param local_time: The time to be used to add to the string. This is a time object
"""
def match_formatting(match):
2013-02-02 20:18:34 +00:00
"""
Format the match
"""
2012-09-16 15:33:05 +00:00
return local_time.strftime(match.group())
return re.sub('\%[a-zA-Z]', match_formatting, text)
def get_locale_key(string):
"""
Creates a key for case insensitive, locale aware string sorting.
2013-07-15 18:02:54 +00:00
2014-03-17 19:05:55 +00:00
:param string: The corresponding string.
"""
string = string.lower()
# ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
2014-03-10 19:53:11 +00:00
global ICU_COLLATOR
try:
if ICU_COLLATOR is None:
import icu
from .languagemanager import LanguageManager
language = LanguageManager.get_language()
icu_locale = icu.Locale(language)
ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
return ICU_COLLATOR.getSortKey(string)
except:
return locale.strxfrm(string).encode()
2014-03-17 19:05:55 +00:00
def get_natural_key(string):
"""
Generate a key for locale aware natural string sorting.
Returns a list of string compare keys and integers.
"""
key = DIGITS_OR_NONDIGITS.findall(string)
key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
2013-10-13 13:51:13 +00:00
# Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
2013-07-06 19:49:03 +00:00
# and int.
if string[0].isdigit():
return [b''] + key
2013-04-16 17:54:40 +00:00
return key
2013-08-31 18:17:38 +00:00
from .languagemanager import LanguageManager
from .actions import ActionList
2013-10-13 13:51:13 +00:00
__all__ = ['ActionList', 'LanguageManager', 'get_application_version', 'check_latest_version',
2014-03-17 19:05:55 +00:00
'add_actions', 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance',
'delete_file', 'clean_filename', 'format_time', 'get_locale_key', 'get_natural_key']