From c63b7f995fcba0ffe75ccc3da7186b9cba194260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sun, 8 Sep 2013 14:30:26 +0200 Subject: [PATCH 1/3] Clean up main.py and move code to __main__.py and utils/main.py. --- fabfile.py | 2 +- manage.py | 19 +- openslides/__main__.py | 530 ++++++++++++++++++ .../core/management/commands/backupdb.py | 53 -- .../core/management/commands/runserver.py | 27 - openslides/global_settings.py | 14 +- openslides/main.py | 423 -------------- openslides/participant/api.py | 5 +- openslides/utils/main.py | 114 ++++ setup.py | 2 +- start.py | 13 - 11 files changed, 668 insertions(+), 534 deletions(-) create mode 100644 openslides/__main__.py delete mode 100644 openslides/core/management/commands/backupdb.py delete mode 100644 openslides/core/management/commands/runserver.py delete mode 100644 openslides/main.py create mode 100644 openslides/utils/main.py delete mode 100755 start.py diff --git a/fabfile.py b/fabfile.py index 5ab162176..08a84a310 100644 --- a/fabfile.py +++ b/fabfile.py @@ -23,7 +23,7 @@ def test(module='tests'): environment variable DJANGO_SETTINGS_MODULE is set to 'tests.settings'. """ django.settings_module('tests.settings') - local('coverage run ./manage.py test %s' % module) + local('coverage run ./manage.py django test %s' % module) def coverage_report_plain(): diff --git a/manage.py b/manage.py index 2618206eb..281159766 100644 --- a/manage.py +++ b/manage.py @@ -1,18 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ - Django's execute manager. + Manage script for OpenSlides. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ -import os, sys -from django.core.management import execute_from_command_line -from openslides.main import get_user_config_path, setup_django_environment +import sys + + +from openslides.__main__ import main + if __name__ == "__main__": - if 'DJANGO_SETTINGS_MODULE' not in os.environ: - setup_django_environment( - get_user_config_path('openslides', 'settings.py')) - execute_from_command_line(sys.argv) + if len(sys.argv) == 1: + sys.argv.append('--help') + exit(main()) diff --git a/openslides/__main__.py b/openslides/__main__.py new file mode 100644 index 000000000..6296d3688 --- /dev/null +++ b/openslides/__main__.py @@ -0,0 +1,530 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.__main__ + ~~~~~~~~~~~~~~~~~~~ + + Main script for OpenSlides + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +import argparse +import base64 +import imp +import os +import shutil +import socket +import sys +import time +import threading +import webbrowser + +from django.conf import ENVIRONMENT_VARIABLE +from django.core.management import execute_from_command_line + +from openslides import get_version +from openslides.utils.tornado_webserver import run_tornado +from openslides.utils.main import ( + filesystem2unicode, + detect_openslides_type, + get_win32_app_data_path, + get_win32_portable_path, + UNIX_VERSION, + WINDOWS_VERSION, + WINDOWS_PORTABLE_VERSION) + + +SETTINGS_TEMPLATE = """# -*- coding: utf-8 -*- +# +# Settings file for OpenSlides +# + +%(import_function)s +from openslides.global_settings import * + +# Use 'DEBUG = True' to get more details for server errors. Default for releases: False +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': %(database_path_value)s, + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '', + } +} + +# Set timezone +TIME_ZONE = 'Europe/Berlin' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = %(secret_key)r + +# Add OpenSlides plugins to this list (see example entry in comment) +INSTALLED_PLUGINS = ( +# 'pluginname', +) + +INSTALLED_APPS += INSTALLED_PLUGINS + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = %(media_path_value)s +""" + + +def main(): + """ + Main entrance to OpenSlides. + """ + # Parse all command line args. + args = parse_args() + + # Setup settings path: Take it either from command line or get default path + if not hasattr(args, 'settings') or not args.settings: + openslides_type = detect_openslides_type() + args.settings = get_default_settings_path(openslides_type) + + # Create settings if if still does not exist. + if not os.path.exists(args.settings): + # Setup path for local data (SQLite3 database, media, search index, ...): + # Take it either from command line or get default path + if not hasattr(args, 'localpath') or not args.localpath: + openslides_type = detect_openslides_type() + args.localpath = get_default_local_path(openslides_type) + localpath_values = get_localpath_values(localpath=args.localpath, default=True, openslides_type=openslides_type) + else: + localpath_values = get_localpath_values(localpath=args.localpath, default=False) + create_settings(args.settings, localpath_values) + + # Setup DJANGO_SETTINGS_MODULE environment variable + if 'DJANGO_SETTINGS_MODULE' not in os.environ: + setup_django_settings_module(args.settings) + + # Process the subcommand's callback + return args.callback(args) + + +def parse_args(): + """ + Parses all command line arguments. The subcommand 'django' links to + Django's command-line utility. + """ + if len(sys.argv) == 1: + sys.argv.append('start') + + description = 'Start script for OpenSlides.' + if 'manage.py' not in sys.argv[0]: + description += (' If it is called without any argument, this will be ' + 'treated as if it is called with the "start" subcommand. ' + 'That means OpenSlides will setup settings and database, ' + 'start the tornado webserver, launch the default web ' + 'browser and open the webinterface.') + + parser = argparse.ArgumentParser(description=description) + + parser.add_argument( + '--version', + action='version', + version=get_version(), + help='Show version number and exit.') + + subparsers = parser.add_subparsers( + dest='subcommand', + title='Available subcommands', + description="Type 'python %s --help' for help on a specific subcommand." % parser.prog, + help='You can choose only one subcommand at once.') + + settings_args, settings_kwargs = ( + ('-s', '--settings'), + dict(help='Path to settings file.')) + localpath_args, localpath_kwargs = ( + ('-l', '--localpath'), + dict(help='Path to the directory for local files like SQLite3 database, ' + 'uploaded media and search index. This is only used, when a new ' + 'settings file is created.')) + address_args, address_kwargs = ( + ('-a', '--address',), + dict(default='0.0.0.0', help='IP address to listen on. Default is %(default)s.')) + port_args, port_kwargs = ( + ('-p', '--port'), + dict(type=int, default=80, help='Port to listen on. Default as admin or root is %(default)d, else 8000.')) + + # Subcommand start + subcommand_start = subparsers.add_parser( + 'start', + help='Setup settings and database, start tornado webserver, launch the ' + 'default web browser and open the webinterface.') + subcommand_start.add_argument(*settings_args, **settings_kwargs) + subcommand_start.add_argument(*localpath_args, **localpath_kwargs) + subcommand_start.add_argument(*address_args, **address_kwargs) + subcommand_start.add_argument(*port_args, **port_kwargs) + subcommand_start.set_defaults(callback=start) + + # Subcommand runserver + subcommand_runserver = subparsers.add_parser( + 'runserver', + help='Run OpenSlides using tornado webserver.') + subcommand_runserver.add_argument(*settings_args, **settings_kwargs) + subcommand_runserver.add_argument(*localpath_args, **localpath_kwargs) + subcommand_runserver.add_argument(*address_args, **address_kwargs) + subcommand_runserver.add_argument(*port_args, **port_kwargs) + subcommand_runserver.add_argument( + '--start-browser', + action='store_true', + help='Launch the default web browser and open the webinterface.') + subcommand_runserver.add_argument( + '--no-reload', + action='store_true', + help='Do not reload the webserver if source code changes.') + subcommand_runserver.set_defaults(callback=runserver) + + # Subcommand syncdb + subcommand_syncdb = subparsers.add_parser( + 'syncdb', + help='Create or update database tables.') + subcommand_syncdb.add_argument(*settings_args, **settings_kwargs) + subcommand_syncdb.add_argument(*localpath_args, **localpath_kwargs) + subcommand_syncdb.set_defaults(callback=syncdb) + + # Subcommand createsuperuser + subcommand_createsuperuser = subparsers.add_parser( + 'createsuperuser', + help="Make sure the user 'admin' exists and uses 'admin' as password.") + subcommand_createsuperuser.add_argument(*settings_args, **settings_kwargs) + subcommand_createsuperuser.add_argument(*localpath_args, **localpath_kwargs) + subcommand_createsuperuser.set_defaults(callback=createsuperuser) + + # Subcommand backupdb + subcommand_backupdb = subparsers.add_parser( + 'backupdb', + help='Store a backup copy of the SQLite3 database.') + subcommand_backupdb.add_argument(*settings_args, **settings_kwargs) + subcommand_backupdb.add_argument(*localpath_args, **localpath_kwargs) + subcommand_backupdb.add_argument( + 'path', + help='Path to the backup file. An existing file will be overwritten.') + subcommand_backupdb.set_defaults(callback=backupdb) + + # Subcommand deletedb + subcommand_deletedb = subparsers.add_parser( + 'deletedb', + help='Delete the SQLite3 database.') + subcommand_deletedb.add_argument(*settings_args, **settings_kwargs) + subcommand_deletedb.add_argument(*localpath_args, **localpath_kwargs) + subcommand_deletedb.set_defaults(callback=deletedb) + + # Subcommand django + subcommand_django_command_line_utility = subparsers.add_parser( + 'django', + description="Link to Django's command-line utility. Type 'python %s django help' for more help on this." % parser.prog, + help="Call Django's command-line utility.") + subcommand_django_command_line_utility.set_defaults( + callback=django_command_line_utility, + django_args=['%s' % subcommand_django_command_line_utility.prog]) + + known_args, unknown_args = parser.parse_known_args() + + if known_args.subcommand == 'django': + if not unknown_args: + unknown_args.append('help') + known_args.django_args.extend(unknown_args) + else: + if unknown_args: + parser.error('Unknown arguments %s found.' % ' '.join(unknown_args)) + + return known_args + + +def get_default_settings_path(openslides_type): + """ + Returns the default settings path according to the OpenSlides type. + + The argument 'openslides_type' has to be one of the three types mentioned in + openslides.utils.main. + """ + if openslides_type == UNIX_VERSION: + parent_directory = filesystem2unicode(os.environ.get( + 'XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))) + elif openslides_type == WINDOWS_VERSION: + parent_directory = get_win32_app_data_path() + elif openslides_type == WINDOWS_PORTABLE_VERSION: + parent_directory = get_win32_portable_path() + else: + raise TypeError('%s is not a valid OpenSlides type.' % openslides_type) + return os.path.join(parent_directory, 'openslides', 'settings.py') + + +def get_default_local_path(openslides_type): + """ + Returns the default local path according to the OpenSlides type. + + The argument 'openslides_type' has to be one of the three types mentioned in + openslides.utils.main. + """ + if openslides_type == UNIX_VERSION: + default_local_path = filesystem2unicode(os.environ.get( + 'XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share'))) + elif openslides_type == WINDOWS_VERSION: + default_local_path = get_win32_app_data_path() + elif openslides_type == WINDOWS_PORTABLE_VERSION: + default_local_path = get_win32_portable_path() + else: + raise TypeError('%s is not a valid OpenSlides type.' % openslides_type) + return default_local_path + + +def get_localpath_values(localpath, default=False, openslides_type=None): + """ + Returns the local path values for the new settings file. + + The argument 'localpath' is a path to the directory where OpenSlides should + store the local data like SQLite3 database, media and search index. + + The argument 'default' is a simple flag. If it is True and the OpenSlides + type is the Windows portable version, this function returns callable + functions for the settings file, else it returns string paths. + + The argument 'openslides_type' can to be one of the three types mentioned in + openslides.utils.main. + """ + localpath_values = {} + if default and openslides_type == WINDOWS_PORTABLE_VERSION: + localpath_values['import_function'] = 'from openslides.utils.main import get_portable_paths' + localpath_values['database_path_value'] = "get_portable_paths('database')" + localpath_values['media_path_value'] = "get_portable_paths('media')" + else: + localpath_values['import_function'] = '' + # TODO: Decide whether to use only absolute paths here. + localpath_values['database_path_value'] = "'%s'" % os.path.join(localpath, 'openslides', 'database.sqlite') + # TODO: Decide whether to use only absolute paths here. + localpath_values['media_path_value'] = "'%s'" % os.path.join(localpath, 'openslides', 'media', '') + return localpath_values + + +def create_settings(settings_path, local_path_values): + """ + Creates the settings file at the given path using the given values for the + file template. + """ + settings_module = os.path.realpath(os.path.dirname(settings_path)) + if not os.path.exists(settings_module): + os.makedirs(settings_module) + context = {'secret_key': base64.b64encode(os.urandom(30))} + context.update(local_path_values) + settings_content = SETTINGS_TEMPLATE % context + with open(settings_path, 'w') as settings_file: + settings_file.write(settings_content) + print('Settings file at %s successfully created.' % settings_path) + + +def setup_django_settings_module(settings_path): + """ + Sets the environment variable DJANGO_SETTINGS_MODULE to the given settings. + """ + settings_file = os.path.basename(settings_path) + settings_module_name = "".join(settings_file.split('.')[:-1]) + if '.' in settings_module_name: + print("'.' is not an allowed character in the settings-file") + sys.exit(1) + settings_module_dir = os.path.dirname(settings_path) + sys.path.append(settings_module_dir) + os.environ[ENVIRONMENT_VARIABLE] = '%s' % settings_module_name + + +def start(args): + """ + Starts OpenSlides: Runs syncdb and runs runserver (tornado webserver) with + the flag 'start_browser'. + """ + syncdb(args) + args.start_browser = True + args.no_reload = False + runserver(args) + + +def runserver(args): + """ + Runs tornado webserver. Runs the function start_browser if the respective + argument is given. + """ + port = get_port(address=args.address, port=args.port) + if args.start_browser: + browser_url = get_browser_url(address=args.address, port=port) + start_browser(browser_url) + run_tornado(args.address, port, not args.no_reload) + + +def get_port(address, port): + """ + Returns the port for the server. If port 80 is given, checks if it is + available. If not returns port 8000. + + The argument 'address' should be an IP address. The argument 'port' should + be an integer. + """ + if port == 80: + # test if we can use port 80 + s = socket.socket() + try: + s.bind((address, port)) + s.listen(-1) + except socket.error: + port = 8000 + finally: + s.close() + return port + + +def get_browser_url(address, port): + """ + Returns the url to open the web browser. + + The argument 'address' should be an IP address. The argument 'port' should + be an integer. + """ + browser_url = 'http://' + if address == '0.0.0.0': + browser_url += 'localhost' + else: + browser_url += address + if not port == 80: + browser_url += ":%d" % port + return browser_url + + +def start_browser(browser_url): + """ + Launches the default web browser at the given url and opens the + webinterface. + """ + browser = webbrowser.get() + + def function(): + time.sleep(1) + browser.open(browser_url) + + thread = threading.Thread(target=function) + thread.start() + + +def syncdb(args): + """ + Run syncdb to create or update the database. + """ + # TODO: Check use of filesystem2unicode here. + path = filesystem2unicode(os.path.dirname(get_database_path_from_settings(args.settings))) + if not os.path.exists(path): + os.makedirs(path) + execute_from_command_line(["", "syncdb", "--noinput"]) + + +def get_database_path_from_settings(settings_path): + """ + Retrieves the database path out of the given settings file. Returns None, + if it is not a SQLite3 database. + """ + from django.conf import settings as django_settings + from django.db import DEFAULT_DB_ALIAS + + db_settings = django_settings.DATABASES + default = db_settings.get(DEFAULT_DB_ALIAS) + if not default: + raise Exception("Default databases is not configured") + database_path = default.get('NAME') + if not database_path: + raise Exception('No path specified for default database.') + if default.get('ENGINE') != 'django.db.backends.sqlite3': + database_path = None + return database_path + + +def createsuperuser(args): + """ + Creates or resets the admin user. Returns 0 to show success. + """ + # can't be imported in global scope as it already requires + # the settings module during import + from openslides.participant.api import create_or_reset_admin_user + if create_or_reset_admin_user(): + print('Admin user successfully created.') + else: + print('Admin user successfully reset.') + return 0 + + +def backupdb(args): + """ + Stores a backup copy of the SQlite3 database. + """ + from django.db import connection, transaction + + @transaction.commit_manually + def do_backup(src_path, dest_path): + # perform a simple file-copy backup of the database + # first we need a shared lock on the database, issuing a select() + # will do this for us + cursor = connection.cursor() + cursor.execute("SELECT count(*) from sqlite_master") + # now copy the file + try: + shutil.copy(src_path, dest_path) + except IOError as e: + raise Exception("Database backup failed.") + # and release the lock again + transaction.commit() + + database_path = get_database_path_from_settings(args.settings) + if database_path: + do_backup(database_path, args.path) + print('Database %s successfully stored at %s.' % (database_path, args.path)) + return_value = 0 + else: + print('Error: Default database is not SQLite3. Only SQLite3 databases can currently be backuped.') + return_value = 1 + return return_value + + +def deletedb(args): + """ + Deletes the sqlite3 database. Returns 0 on success, else 1. + """ + database_path = get_database_path_from_settings(args.settings) + if database_path and os.path.exists(database_path): + os.remove(database_path) + print('SQLite3 database file %s successfully deleted.' % database_path) + return_value = 0 + else: + print('SQLite3 database file %s does not exist.' % database_path) + return_value = 1 + return return_value + + +def django_command_line_utility(args): + """ + Runs Django's command line utility. Returns 0 on success, else 1. + """ + if 'runserver' in args.django_args: + command = 'runserver' + elif 'syncdb' in args.django_args: + command = 'syncdb' + elif 'createsuperuser' in args.django_args: + command = 'createsuperuser' + else: + command = None + if command: + print("Error: The command '%s' is disabled for use via Django's command line utility." % command) + return_value = 1 + else: + execute_from_command_line(args.django_args) + return_value = 0 + return return_value + + +if __name__ == "__main__": + exit(main()) diff --git a/openslides/core/management/commands/backupdb.py b/openslides/core/management/commands/backupdb.py deleted file mode 100644 index f853184e5..000000000 --- a/openslides/core/management/commands/backupdb.py +++ /dev/null @@ -1,53 +0,0 @@ -import shutil -from optparse import make_option - -import django.conf -import django.db -import django.db.transaction -from django.core.management.base import CommandError, NoArgsCommand - - -class Command(NoArgsCommand): - help = "Backup the openslides database" - option_list = NoArgsCommand.option_list + ( - make_option( - "--destination", action="store", - help="path to the backup database (will be overwritten)"), - ) - - def handle_noargs(self, *args, **kw): - db_settings = django.conf.settings.DATABASES - default = db_settings.get(django.db.DEFAULT_DB_ALIAS) - if not default: - raise CommandError("Default databases is not configured") - - if default.get("ENGINE") != "django.db.backends.sqlite3": - raise CommandError( - "Only sqlite3 databases can currently be backuped") - - src_path = default.get("NAME") - if not src_path: - raise CommandError("No path specified for default database") - - dest_path = kw.get("destination") - if not dest_path: - raise CommandError("--destination must be specified") - - self.do_backup(src_path, dest_path) - - @django.db.transaction.commit_manually - def do_backup(self, src_path, dest_path): - # perform a simple file-copy backup of the database - # first we need a shared lock on the database, issuing a select() - # will do this for us - cursor = django.db.connection.cursor() - cursor.execute("SELECT count(*) from sqlite_master") - - # now copy the file - try: - shutil.copy(src_path, dest_path) - except IOError as e: - raise CommandError("{0}\nDatabase backup failed!".format(e)) - - # and release the lock again - django.db.transaction.commit() diff --git a/openslides/core/management/commands/runserver.py b/openslides/core/management/commands/runserver.py deleted file mode 100644 index 83af12280..000000000 --- a/openslides/core/management/commands/runserver.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - openslides.utils.management.commands.runserver - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Overrides the Django runserver command to start the tornado webserver. - - :copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS. - :license: GNU GPL, see LICENSE for more details. - -""" - -from django.core.management.base import BaseCommand - -from openslides.main import main - - -class Command(BaseCommand): - """ - Start the application using the tornado webserver - """ - - help = 'Start the application using the tornado webserver' - - def handle(self, *args, **options): - main(manage_runserver=True) diff --git a/openslides/global_settings.py b/openslides/global_settings.py index 373643730..f3e3b806b 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -11,7 +11,7 @@ """ import os -from openslides.main import fs2unicode +from openslides.utils.main import filesystem2unicode SITE_ROOT = os.path.realpath(os.path.dirname(__file__)) @@ -44,7 +44,7 @@ USE_I18N = True USE_L10N = True LOCALE_PATHS = ( - fs2unicode(os.path.join(SITE_ROOT, 'locale')), + filesystem2unicode(os.path.join(SITE_ROOT, 'locale')), ) # URL that handles the media served from MEDIA_ROOT. Make sure to use a @@ -54,7 +54,7 @@ MEDIA_URL = '/media/' # Absolute path to the directory that holds static media from ``collectstatic`` # Example: "/home/media/static.lawrence.com/" -STATIC_ROOT = fs2unicode(os.path.join(SITE_ROOT, '../collected-site-static')) +STATIC_ROOT = filesystem2unicode(os.path.join(SITE_ROOT, '../collected-site-static')) # URL that handles the media served from STATIC_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). @@ -64,12 +64,14 @@ STATIC_URL = '/static/' # Additional directories containing static files (not application specific) # Examples: "/home/media/lawrence.com/extra-static/" STATICFILES_DIRS = ( - fs2unicode(os.path.join(SITE_ROOT, 'static')), + filesystem2unicode(os.path.join(SITE_ROOT, 'static')), ) +#XXX: Note this setting (as well as our workaround finder) +# can be removed again once django-bug-#18404 has been resolved STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'openslides.utils.staticfiles.AppDirectoriesFinder', ) MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' @@ -97,7 +99,7 @@ TEMPLATE_DIRS = ( # "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. - fs2unicode(os.path.join(SITE_ROOT, 'templates')), + filesystem2unicode(os.path.join(SITE_ROOT, 'templates')), ) INSTALLED_APPS = ( diff --git a/openslides/main.py b/openslides/main.py deleted file mode 100644 index 9cbabc18f..000000000 --- a/openslides/main.py +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - openslides.main - ~~~~~~~~~~~~~~~ - - Main script to start and set up OpenSlides. - - :copyright: 2011–2013 by OpenSlides team, see AUTHORS. - :license: GNU GPL, see LICENSE for more details. -""" - -import base64 -import ctypes -import optparse -import os -import socket -import sys -import tempfile -import threading -import time -import webbrowser - -from django.conf import ENVIRONMENT_VARIABLE -from django.core.management import execute_from_command_line - -from openslides import get_version -from openslides.utils.tornado_webserver import run_tornado - - -CONFIG_TEMPLATE = """#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import openslides.main -from openslides.global_settings import * - - -# Use 'DEBUG = True' to get more details for server errors -# (Default for releases: 'False') -DEBUG = False -TEMPLATE_DEBUG = DEBUG - -DBPATH = %(dbpath)s - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': DBPATH, - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', - } -} - -# Set timezone -TIME_ZONE = 'Europe/Berlin' - -# Make this unique, and don't share it with anybody. -SECRET_KEY = %(default_key)r - -# Add OpenSlides plugins to this list (see example entry in comment) -INSTALLED_PLUGINS = ( -# 'pluginname', -) - -INSTALLED_APPS += INSTALLED_PLUGINS - -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = %(media_root_path)s - -# Path to Whoosh search index -HAYSTACK_CONNECTIONS['default']['PATH'] = %(whoosh_index_path)s -""" - -KEY_LENGTH = 30 - -# sentinel used to signal that the database ought to be stored -# relative to the portable's directory -_portable_db_path = object() - - -def process_options(argv=None, manage_runserver=False): - if argv is None: - argv = sys.argv[1:] - - parser = optparse.OptionParser( - description="Run openslides using the tornado webserver") - parser.add_option( - "-a", "--address", - help="IP Address to listen on. Default: 0.0.0.0") - parser.add_option( - "-p", "--port", type="int", - help="Port to listen on. Default: 8000 (start as admin/root: 80)") - parser.add_option( - "--syncdb", action="store_true", - help="Update/create database before starting the server.") - parser.add_option( - "--backupdb", action="store", metavar="BACKUP_PATH", - help="Make a backup copy of the database to BACKUP_PATH.") - parser.add_option( - "--reset-admin", action="store_true", - help="Make sure the user 'admin' exists and uses 'admin' as password.") - parser.add_option( - "-s", "--settings", help="Set the path to the settings file.") - parser.add_option( - "--no-browser", - action="store_false", dest="start_browser", default=True, - help="Do not automatically start the web browser.") - parser.add_option( - "--no-reload", action="store_true", - help="Do not reload the web server.") - parser.add_option( - "--no-run", action="store_true", - help="Do not start the web server.") - parser.add_option( - "--version", action="store_true", - help="Show version and exit.") - - opts, args = parser.parse_args(argv) - - # Do not parse any argv if the script is started via manage.py runserver. - # This simulates the django runserver command - if manage_runserver: - opts.start_browser = False - opts.no_reload = False - return opts - - if opts.version: - print get_version() - exit(0) - - if args: - sys.stderr.write("This command does not take arguments!\n\n") - parser.print_help() - sys.exit(1) - - return opts - - -def main(argv=None, manage_runserver=False): - opts = process_options(argv, manage_runserver) - _main(opts) - - -def win32_portable_main(argv=None): - """special entry point for the win32 portable version""" - - opts = process_options(argv) - - database_path = None - - if opts.settings is None: - portable_dir = get_portable_path() - try: - fd, test_file = tempfile.mkstemp(dir=portable_dir) - except OSError: - portable_dir_writeable = False - else: - portable_dir_writeable = True - os.close(fd) - os.unlink(test_file) - - if portable_dir_writeable: - opts.settings = os.path.join( - portable_dir, "openslides", "settings.py") - database_path = _portable_db_path - - _main(opts, database_path=database_path) - - -def _main(opts, database_path=None): - # Find the path to the settings - settings_path = opts.settings - if settings_path is None: - settings_path = get_user_config_path('openslides', 'settings.py') - - # Create settings if necessary - if not os.path.exists(settings_path): - create_settings(settings_path, database_path) - - # Set the django environment to the settings - setup_django_environment(settings_path) - - # Find url to openslides - addr, port = detect_listen_opts(opts.address, opts.port) - - # Create Database if necessary - if not database_exists() or opts.syncdb: - run_syncdb() - - # Reset Admin - elif opts.reset_admin: - reset_admin_user() - - if opts.backupdb: - backup_database(opts.backupdb) - - if opts.no_run: - return - - # Start OpenSlides - reload = True - if opts.no_reload: - reload = False - - if opts.start_browser: - if opts.address: - prefix = opts.address - else: - prefix = 'localhost' - if port == 80: - suffix = "" - else: - suffix = ":%d" % port - start_browser("http://%s%s" % (prefix, suffix)) - - # Start the server - run_tornado(addr, port, reload) - - -def create_settings(settings_path, database_path=None): - settings_module = os.path.dirname(settings_path) - - if database_path is _portable_db_path: - database_path = get_portable_db_path() - dbpath_value = 'openslides.main.get_portable_db_path()' - media_root_path_value = 'openslides.main.get_portable_media_root_path()' - whoosh_index_path_value = 'openslides.main.get_portable_whoosh_index_path()' - else: - if database_path is None: - database_path = get_user_data_path('openslides', 'database.sqlite') - dbpath_value = repr(fs2unicode(database_path)) - media_root_path_value = repr(fs2unicode(get_user_data_path('openslides', 'media', ''))) - whoosh_index_path_value = repr(fs2unicode(get_user_data_path('openslides', 'whoosh_index', ''))) - - settings_content = CONFIG_TEMPLATE % dict( - default_key=base64.b64encode(os.urandom(KEY_LENGTH)), - dbpath=dbpath_value, - media_root_path=media_root_path_value, - whoosh_index_path=whoosh_index_path_value) - - if not os.path.exists(settings_module): - os.makedirs(settings_module) - - if not os.path.exists(os.path.dirname(database_path)): - os.makedirs(os.path.dirname(database_path)) - - with open(settings_path, 'w') as file: - file.write(settings_content) - - -def setup_django_environment(settings_path): - settings_file = os.path.basename(settings_path) - settings_module_name = "".join(settings_file.split('.')[:-1]) - if '.' in settings_module_name: - print "'.' is not an allowed character in the settings-file" - sys.exit(1) - settings_module_dir = os.path.dirname(settings_path) - sys.path.append(settings_module_dir) - os.environ[ENVIRONMENT_VARIABLE] = '%s' % settings_module_name - - -def detect_listen_opts(address=None, port=None): - if address is None: - address = "0.0.0.0" - - if port is None: - # test if we can use port 80 - s = socket.socket() - port = 80 - try: - s.bind((address, port)) - s.listen(-1) - except socket.error: - port = 8000 - finally: - s.close() - - return address, port - - -def database_exists(): - """Detect if database exists""" - # can't be imported in global scope as they already require - # the settings module during import - from django.db import DatabaseError - from django.core.exceptions import ImproperlyConfigured - from openslides.participant.models import User - - try: - # TODO: Use another model, the User could be deactivated - User.objects.count() - except DatabaseError: - return False - except ImproperlyConfigured: - print "Your settings file seems broken" - sys.exit(0) - else: - return True - - -def run_syncdb(): - # now initialize the database - argv = ["", "syncdb", "--noinput"] - execute_from_command_line(argv) - - -def reset_admin_user(): - # can't be imported in global scope as it already requires - # the settings module during import - from openslides.participant.api import create_or_reset_admin_user - create_or_reset_admin_user() - - -def backup_database(dest_path): - argv = ["", "backupdb", "--destination={0}".format(dest_path)] - execute_from_command_line(argv) - - -def start_browser(url): - browser = webbrowser.get() - - def f(): - time.sleep(1) - browser.open(url) - - t = threading.Thread(target=f) - t.start() - - -def fs2unicode(s): - if isinstance(s, unicode): - return s - fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() - return s.decode(fs_encoding) - - -def get_user_config_path(*args): - if sys.platform == "win32": - return win32_get_app_data_path(*args) - - config_home = os.environ.get( - 'XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) - - return os.path.join(fs2unicode(config_home), *args) - - -def get_user_data_path(*args): - if sys.platform == "win32": - return win32_get_app_data_path(*args) - - data_home = os.environ.get( - 'XDG_DATA_HOME', os.path.join( - os.path.expanduser('~'), '.local', 'share')) - - return os.path.join(fs2unicode(data_home), *args) - - -def is_portable(): - """Return True if openslides is run as portable version""" - - # NOTE: sys.executable is the path of the *interpreter* - # the portable version embeds python so it *is* the interpreter. - # The wrappers generated by pip and co. will spawn - # the usual python(w).exe, so there is no danger of mistaking - # them for the portable even though they may also be called - # openslides.exe - exename = os.path.basename(sys.executable).lower() - return exename == "openslides.exe" - - -def get_portable_path(*args): - # NOTE: sys.executable will be the path to openslides.exe - # since it is essentially a small wrapper that embeds the - # python interpreter - - if not is_portable(): - raise Exception( - "Cannot determine portable path when " - "not running as portable") - - portable_dir = fs2unicode(os.path.dirname(os.path.abspath(sys.executable))) - return os.path.join(portable_dir, *args) - - -def get_portable_db_path(): - return get_portable_path('openslides', 'database.sqlite') - - -def get_portable_media_root_path(): - return get_portable_path('openslides', 'media', '') - - -def get_portable_whoosh_index_path(): - return get_portable_path('openslides', 'whoosh_index', '') - - -def win32_get_app_data_path(*args): - shell32 = ctypes.WinDLL("shell32.dll") - SHGetFolderPath = shell32.SHGetFolderPathW - SHGetFolderPath.argtypes = ( - ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32, - ctypes.c_wchar_p) - SHGetFolderPath.restype = ctypes.c_uint32 - - CSIDL_LOCAL_APPDATA = 0x001c - MAX_PATH = 260 - - buf = ctypes.create_unicode_buffer(MAX_PATH) - res = SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, 0, buf) - if res != 0: - raise Exception("Could not deterime APPDATA path") - - return os.path.join(buf.value, *args) - - -if __name__ == "__main__": - if is_portable(): - win32_portable_main() - else: - main() diff --git a/openslides/participant/api.py b/openslides/participant/api.py index be9eb6771..e757bd119 100644 --- a/openslides/participant/api.py +++ b/openslides/participant/api.py @@ -130,8 +130,11 @@ def create_or_reset_admin_user(): admin = User() admin.username = 'admin' admin.last_name = 'Administrator' - + created = True + else: + created = False admin.default_password = 'admin' admin.set_password(admin.default_password) admin.save() admin.groups.add(group_staff) + return created diff --git a/openslides/utils/main.py b/openslides/utils/main.py new file mode 100644 index 000000000..a17ff4e6f --- /dev/null +++ b/openslides/utils/main.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.utils.main + ~~~~~~~~~~~~~~~~~~~~~ + + Some functions for OpenSlides. + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +import os +import sys +import ctypes + + +UNIX_VERSION = 'Unix Version' +WINDOWS_VERSION = 'Windows Version' +WINDOWS_PORTABLE_VERSION = 'Windows Portable Version' + + +def filesystem2unicode(path): + """ + Transforms a path string to unicode according to the filesystem's encoding. + """ + # TODO: Delete this function after switch to Python 3. + if not isinstance(path, unicode): + filesystem_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() + path = path.decode(filesystem_encoding) + return path + + +def detect_openslides_type(): + """ + Returns the type of this version. + """ + if sys.platform == 'win32': + if os.path.basename(sys.executable).lower() == 'openslides.exe': + # Note: sys.executable is the path of the *interpreter* + # the portable version embeds python so it *is* the interpreter. + # The wrappers generated by pip and co. will spawn the usual + # python(w).exe, so there is no danger of mistaking them + # for the portable even though they may also be called + # openslides.exe + openslides_type = WINDOWS_PORTABLE_VERSION + else: + openslides_type = WINDOWS_VERSION + else: + openslides_type = UNIX_VERSION + return openslides_type + + +def is_portable(): + """ + Helper function just for the GUI. + """ + # TODO: Remove this function. + return detect_openslides_type() == WINDOWS_PORTABLE_VERSION + + +def get_win32_app_data_path(): + """ + Returns the path to Windows' AppData directory. + """ + shell32 = ctypes.WinDLL("shell32.dll") + SHGetFolderPath = shell32.SHGetFolderPathW + SHGetFolderPath.argtypes = ( + ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32, + ctypes.c_wchar_p) + SHGetFolderPath.restype = ctypes.c_uint32 + + CSIDL_LOCAL_APPDATA = 0x001c + MAX_PATH = 260 + + buf = ctypes.create_unicode_buffer(MAX_PATH) + res = SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, 0, buf) + if res != 0: + raise Exception("Could not deterime APPDATA path") + + return buf.value + + +def get_win32_portable_path(): + """ + Returns the path to the Windows portable version. + """ + # NOTE: sys.executable will be the path to openslides.exe + # since it is essentially a small wrapper that embeds the + # python interpreter + portable_path = filesystem2unicode(os.path.dirname(os.path.abspath(sys.executable))) + try: + fd, test_file = tempfile.mkstemp(dir=portable_path) + except OSError: + raise Exception('Portable directory is not writeable. Please choose another directory for settings and local files.') + finally: + os.close(fd) + os.unlink(test_file) + return portable_path + + +def get_portable_paths(name): + """ + Returns the paths for the Windows portable version on runtime for the + SQLite3 database and the media directory. The argument 'name' can be + 'database' or 'media'. + """ + if name == 'database': + path = os.path.join(get_win32_portable_path(), 'openslides', 'database.sqlite') + elif name == 'media': + path = os.path.join(get_win32_portable_path(), 'openslides', 'media', '') + else: + raise TypeError('Unknow type %s' % name) + return path diff --git a/setup.py b/setup.py index 4b1535e6a..c9d6670ba 100644 --- a/setup.py +++ b/setup.py @@ -45,4 +45,4 @@ setup( packages=find_packages(exclude=['tests', 'tests.*']), include_package_data=True, install_requires=install_requires, - entry_points={'console_scripts': ['openslides = openslides.main:main']}) + entry_points={'console_scripts': ['openslides = openslides.__main__:main']}) diff --git a/start.py b/start.py deleted file mode 100755 index 693a543e4..000000000 --- a/start.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - Start script for OpenSlides. - - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. - :license: GNU GPL, see LICENSE for more details. -""" - -from openslides.main import main - -if __name__ == "__main__": - main() From 21a1d77e295c8a7e18da7e8f5090592ad0377b1f Mon Sep 17 00:00:00 2001 From: Andy Kittner Date: Sat, 12 Oct 2013 21:30:34 +0200 Subject: [PATCH 2/3] fix gui/portable for pull request #903 --- extras/openslides_gui/gui.py | 52 +++++++++++++++++++++--------------- openslides/utils/main.py | 9 +++++-- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/extras/openslides_gui/gui.py b/extras/openslides_gui/gui.py index 5919b7c20..a934865e0 100755 --- a/extras/openslides_gui/gui.py +++ b/extras/openslides_gui/gui.py @@ -15,7 +15,17 @@ import threading import wx import openslides -import openslides.main +from openslides.utils.main import ( + filesystem2unicode, + is_portable, + detect_openslides_type, + get_win32_portable_path, +) + +from openslides.__main__ import ( + get_port, + get_default_settings_path +) # NOTE: djangos translation module can't be used here since it requires # a defined settings module @@ -25,7 +35,7 @@ ungettext = lambda msg1, msg2, n: _translations.ungettext(msg1, msg2, n) def get_data_path(*args): - path = openslides.main.fs2unicode(__file__) + path = filesystem2unicode(__file__) return os.path.join(os.path.dirname(path), "data", *args) @@ -81,7 +91,7 @@ class RunCommandControl(wx.Panel): if self.is_alive(): raise ValueError("already running a command") - cmd = [sys.executable, "-u", "-m", "openslides.main"] + cmd = [sys.executable, "-u", "-m", "openslides"] cmd.extend(args) # XXX: subprocess on windows only handles byte strings @@ -336,12 +346,12 @@ class MainWindow(wx.Frame): self.SetIcons(icons) self.server_running = False - if openslides.main.is_portable(): - self.gui_settings_path = openslides.main.get_portable_path( - "openslides", "gui_settings.json") - else: - self.gui_settings_path = openslides.main.get_user_config_path( - "openslides", "gui_settings.json") + openslides_type = detect_openslides_type() + # XXX: this works, but I'd prefer keeping get_user_config_path + # it was much clearer what path was intended IMHO ... + self.gui_settings_path = os.path.join( + os.path.dirname(get_default_settings_path(openslides_type)), + "gui_settings.json") self.backupdb_enabled = False self.backupdb_destination = "" @@ -417,9 +427,8 @@ class MainWindow(wx.Frame): self.bt_server.Bind(wx.EVT_BUTTON, self.on_start_server_clicked) server_box.Add(self.bt_server, flag=wx.EXPAND) - host, port = openslides.main.detect_listen_opts() - self.host = host - self.port = unicode(port) + self.host = "0.0.0.0" + self.port = unicode(get_port(self.host, 80)) # "action" buttons action_vbox = wx.BoxSizer(wx.VERTICAL) @@ -568,9 +577,8 @@ class MainWindow(wx.Frame): def do_backup(self): cmd = [ - sys.executable, "-u", "-m", "openslides.main", - "--no-run", - "--backupdb={0}".format(self.backupdb_destination), + sys.executable, "-u", "-m", "openslides", "backupdb", + self.backupdb_destination, ] p = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -593,11 +601,11 @@ class MainWindow(wx.Frame): def on_syncdb_clicked(self, evt): self.cmd_run_ctrl.append_message(_("Syncing database...")) - self.cmd_run_ctrl.run_command("--no-run", "--syncdb") + self.cmd_run_ctrl.run_command("syncdb") def on_reset_admin_clicked(self, evt): self.cmd_run_ctrl.append_message(_("Resetting admin user...")) - self.cmd_run_ctrl.run_command("--no-run", "--reset-admin") + self.cmd_run_ctrl.run_command("createsuperuser") def on_about_clicked(self, evt): info = wx.AboutDialogInfo() @@ -624,11 +632,13 @@ class MainWindow(wx.Frame): args = ["--port", self._port] else: args = ["--address", self._host, "--port", self._port] - if not self.cb_start_browser.GetValue(): - args.append("--no-browser") + + # XXX: --no-browser is missing + #if not self.cb_start_browser.GetValue(): + # args.append("--no-browser") self.server_running = True - self.cmd_run_ctrl.run_command(*args) + self.cmd_run_ctrl.run_command("start", *args) # initiate backup_timer if backup is enabled self.apply_backup_settings() @@ -702,7 +712,7 @@ def main(): lang = locale.getdefaultlocale()[0] if lang: global _translations - localedir = openslides.main.fs2unicode(openslides.__file__) + localedir = filesystem2unicode(openslides.__file__) localedir = os.path.dirname(localedir) localedir = os.path.join(localedir, "locale") _translations = gettext.translation( diff --git a/openslides/utils/main.py b/openslides/utils/main.py index a17ff4e6f..c258257af 100644 --- a/openslides/utils/main.py +++ b/openslides/utils/main.py @@ -13,12 +13,15 @@ import os import sys import ctypes +import tempfile UNIX_VERSION = 'Unix Version' WINDOWS_VERSION = 'Windows Version' WINDOWS_PORTABLE_VERSION = 'Windows Portable Version' +class PortableDirNotWritable(Exception): + pass def filesystem2unicode(path): """ @@ -92,8 +95,10 @@ def get_win32_portable_path(): try: fd, test_file = tempfile.mkstemp(dir=portable_path) except OSError: - raise Exception('Portable directory is not writeable. Please choose another directory for settings and local files.') - finally: + raise PortableDirNotWritable( + 'Portable directory is not writeable. ' + 'Please choose another directory for settings and local files.') + else: os.close(fd) os.unlink(test_file) return portable_path From a1ad1e4c491671a289d4b1e4d3623af30c66730e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sun, 13 Oct 2013 17:17:56 +0200 Subject: [PATCH 3/3] Some changes in __main__.py and utils.main and other. Reinsert --no-browser option. Move some code to utils.main. Some other small style changes. Fix merge conflict misstakes. Change settings behavior. Add tests. Fix rebase problems. --- extras/openslides_gui/gui.py | 25 ++-- manage.py | 1 - openslides/__main__.py | 267 +++++++++++++++++----------------- openslides/global_settings.py | 4 +- openslides/utils/main.py | 62 ++++++-- tests/utils/test_main.py | 67 +++++++++ 6 files changed, 267 insertions(+), 159 deletions(-) create mode 100644 tests/utils/test_main.py diff --git a/extras/openslides_gui/gui.py b/extras/openslides_gui/gui.py index a934865e0..4d46233c3 100755 --- a/extras/openslides_gui/gui.py +++ b/extras/openslides_gui/gui.py @@ -15,17 +15,15 @@ import threading import wx import openslides + from openslides.utils.main import ( - filesystem2unicode, - is_portable, detect_openslides_type, + filesystem2unicode, + get_default_user_data_path, + get_port, get_win32_portable_path, ) -from openslides.__main__ import ( - get_port, - get_default_settings_path -) # NOTE: djangos translation module can't be used here since it requires # a defined settings module @@ -346,12 +344,14 @@ class MainWindow(wx.Frame): self.SetIcons(icons) self.server_running = False + + # Set path for gui settings to default user data according to the + # OpenSlides type. This does not depend on any argument the user might + # type in. openslides_type = detect_openslides_type() - # XXX: this works, but I'd prefer keeping get_user_config_path - # it was much clearer what path was intended IMHO ... + default_user_data_path = get_default_user_data_path(openslides_type) self.gui_settings_path = os.path.join( - os.path.dirname(get_default_settings_path(openslides_type)), - "gui_settings.json") + default_user_data_path, 'openslides', 'gui_settings.json') self.backupdb_enabled = False self.backupdb_destination = "" @@ -633,9 +633,8 @@ class MainWindow(wx.Frame): else: args = ["--address", self._host, "--port", self._port] - # XXX: --no-browser is missing - #if not self.cb_start_browser.GetValue(): - # args.append("--no-browser") + if not self.cb_start_browser.GetValue(): + args.append("--no-browser") self.server_running = True self.cmd_run_ctrl.run_command("start", *args) diff --git a/manage.py b/manage.py index 281159766..5dd79adc8 100644 --- a/manage.py +++ b/manage.py @@ -9,7 +9,6 @@ import sys - from openslides.__main__ import main diff --git a/openslides/__main__.py b/openslides/__main__.py index 6296d3688..74c1af3c4 100644 --- a/openslides/__main__.py +++ b/openslides/__main__.py @@ -12,28 +12,29 @@ import argparse import base64 -import imp import os import shutil -import socket import sys import time import threading import webbrowser from django.conf import ENVIRONMENT_VARIABLE +from django.core.exceptions import ImproperlyConfigured from django.core.management import execute_from_command_line from openslides import get_version -from openslides.utils.tornado_webserver import run_tornado from openslides.utils.main import ( filesystem2unicode, detect_openslides_type, + get_default_user_data_path, + get_port, get_win32_app_data_path, get_win32_portable_path, UNIX_VERSION, WINDOWS_VERSION, WINDOWS_PORTABLE_VERSION) +from openslides.utils.tornado_webserver import run_tornado SETTINGS_TEMPLATE = """# -*- coding: utf-8 -*- @@ -75,6 +76,9 @@ INSTALLED_APPS += INSTALLED_PLUGINS # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = %(media_path_value)s + +# Path to Whoosh search index +HAYSTACK_CONNECTIONS['default']['PATH'] = %(whoosh_index_path_value)s """ @@ -86,25 +90,35 @@ def main(): args = parse_args() # Setup settings path: Take it either from command line or get default path - if not hasattr(args, 'settings') or not args.settings: - openslides_type = detect_openslides_type() - args.settings = get_default_settings_path(openslides_type) + if hasattr(args, 'settings') and args.settings: + settings = args.settings + setup_django_settings_module(settings) + else: + if ENVIRONMENT_VARIABLE not in os.environ: + openslides_type = detect_openslides_type() + settings = get_default_settings_path(openslides_type) + setup_django_settings_module(settings) + else: + # The environment variable is set, so we do not need to process + # anything more here. + settings = None # Create settings if if still does not exist. - if not os.path.exists(args.settings): - # Setup path for local data (SQLite3 database, media, search index, ...): + if settings and not os.path.exists(settings): + # Setup path for user specific data (SQLite3 database, media, search index, ...): # Take it either from command line or get default path - if not hasattr(args, 'localpath') or not args.localpath: - openslides_type = detect_openslides_type() - args.localpath = get_default_local_path(openslides_type) - localpath_values = get_localpath_values(localpath=args.localpath, default=True, openslides_type=openslides_type) + if hasattr(args, 'user_data_path') and args.user_data_path: + user_data_path_values = get_user_data_path_values( + user_data_path=args.user_data_path, + default=False) else: - localpath_values = get_localpath_values(localpath=args.localpath, default=False) - create_settings(args.settings, localpath_values) - - # Setup DJANGO_SETTINGS_MODULE environment variable - if 'DJANGO_SETTINGS_MODULE' not in os.environ: - setup_django_settings_module(args.settings) + openslides_type = detect_openslides_type() + args.user_data_path = get_default_user_data_path(openslides_type) + user_data_path_values = get_user_data_path_values( + user_data_path=args.user_data_path, + default=True, + openslides_type=openslides_type) + create_settings(settings, user_data_path_values) # Process the subcommand's callback return args.callback(args) @@ -118,62 +132,48 @@ def parse_args(): if len(sys.argv) == 1: sys.argv.append('start') + # Init parser description = 'Start script for OpenSlides.' if 'manage.py' not in sys.argv[0]: description += (' If it is called without any argument, this will be ' 'treated as if it is called with the "start" subcommand. ' - 'That means OpenSlides will setup settings and database, ' - 'start the tornado webserver, launch the default web ' - 'browser and open the webinterface.') - + 'That means OpenSlides will setup default settings and ' + 'database, start the tornado webserver, launch the ' + 'default web browser and open the webinterface.') parser = argparse.ArgumentParser(description=description) + # Add version argument parser.add_argument( '--version', action='version', version=get_version(), help='Show version number and exit.') + # Init subparsers subparsers = parser.add_subparsers( dest='subcommand', title='Available subcommands', - description="Type 'python %s --help' for help on a specific subcommand." % parser.prog, + description="Type 'python %s --help' for help on a " + "specific subcommand." % parser.prog, help='You can choose only one subcommand at once.') - settings_args, settings_kwargs = ( - ('-s', '--settings'), - dict(help='Path to settings file.')) - localpath_args, localpath_kwargs = ( - ('-l', '--localpath'), - dict(help='Path to the directory for local files like SQLite3 database, ' - 'uploaded media and search index. This is only used, when a new ' - 'settings file is created.')) - address_args, address_kwargs = ( - ('-a', '--address',), - dict(default='0.0.0.0', help='IP address to listen on. Default is %(default)s.')) - port_args, port_kwargs = ( - ('-p', '--port'), - dict(type=int, default=80, help='Port to listen on. Default as admin or root is %(default)d, else 8000.')) - # Subcommand start subcommand_start = subparsers.add_parser( 'start', help='Setup settings and database, start tornado webserver, launch the ' 'default web browser and open the webinterface.') - subcommand_start.add_argument(*settings_args, **settings_kwargs) - subcommand_start.add_argument(*localpath_args, **localpath_kwargs) - subcommand_start.add_argument(*address_args, **address_kwargs) - subcommand_start.add_argument(*port_args, **port_kwargs) + add_general_arguments(subcommand_start, ('settings', 'user_data_path', 'address', 'port')) + subcommand_start.add_argument( + '--no-browser', + action='store_true', + help='Do not launch the default web browser.') subcommand_start.set_defaults(callback=start) # Subcommand runserver subcommand_runserver = subparsers.add_parser( 'runserver', help='Run OpenSlides using tornado webserver.') - subcommand_runserver.add_argument(*settings_args, **settings_kwargs) - subcommand_runserver.add_argument(*localpath_args, **localpath_kwargs) - subcommand_runserver.add_argument(*address_args, **address_kwargs) - subcommand_runserver.add_argument(*port_args, **port_kwargs) + add_general_arguments(subcommand_runserver, ('settings', 'user_data_path', 'address', 'port')) subcommand_runserver.add_argument( '--start-browser', action='store_true', @@ -188,24 +188,21 @@ def parse_args(): subcommand_syncdb = subparsers.add_parser( 'syncdb', help='Create or update database tables.') - subcommand_syncdb.add_argument(*settings_args, **settings_kwargs) - subcommand_syncdb.add_argument(*localpath_args, **localpath_kwargs) + add_general_arguments(subcommand_syncdb, ('settings', 'user_data_path')) subcommand_syncdb.set_defaults(callback=syncdb) # Subcommand createsuperuser subcommand_createsuperuser = subparsers.add_parser( 'createsuperuser', help="Make sure the user 'admin' exists and uses 'admin' as password.") - subcommand_createsuperuser.add_argument(*settings_args, **settings_kwargs) - subcommand_createsuperuser.add_argument(*localpath_args, **localpath_kwargs) + add_general_arguments(subcommand_createsuperuser, ('settings', 'user_data_path')) subcommand_createsuperuser.set_defaults(callback=createsuperuser) # Subcommand backupdb subcommand_backupdb = subparsers.add_parser( 'backupdb', - help='Store a backup copy of the SQLite3 database.') - subcommand_backupdb.add_argument(*settings_args, **settings_kwargs) - subcommand_backupdb.add_argument(*localpath_args, **localpath_kwargs) + help='Store a backup copy of the SQLite3 database at the given path.') + add_general_arguments(subcommand_backupdb, ('settings', 'user_data_path')) subcommand_backupdb.add_argument( 'path', help='Path to the backup file. An existing file will be overwritten.') @@ -215,14 +212,14 @@ def parse_args(): subcommand_deletedb = subparsers.add_parser( 'deletedb', help='Delete the SQLite3 database.') - subcommand_deletedb.add_argument(*settings_args, **settings_kwargs) - subcommand_deletedb.add_argument(*localpath_args, **localpath_kwargs) + add_general_arguments(subcommand_deletedb, ('settings', 'user_data_path')) subcommand_deletedb.set_defaults(callback=deletedb) # Subcommand django subcommand_django_command_line_utility = subparsers.add_parser( 'django', - description="Link to Django's command-line utility. Type 'python %s django help' for more help on this." % parser.prog, + description="Link to Django's command-line utility. Type " + "'python %s django help' for more help on this." % parser.prog, help="Call Django's command-line utility.") subcommand_django_command_line_utility.set_defaults( callback=django_command_line_utility, @@ -241,6 +238,45 @@ def parse_args(): return known_args +def add_general_arguments(subcommand, arguments): + """ + Adds the named arguments to the subcommand. + """ + general_arguments = {} + openslides_type = detect_openslides_type() + + general_arguments['settings'] = ( + ('-s', '--settings'), + dict(help="Path to settings file. If this isn't provided, the " + "%s environment variable will be used. " + "If this isn't provided too, a default path according to the " + "OpenSlides type will be used. At the moment it is %s" % ( + ENVIRONMENT_VARIABLE, + get_default_settings_path(openslides_type)))) + general_arguments['user_data_path'] = ( + ('-d', '--user-data-path'), + dict(help='Path to the directory for user specific data files like SQLite3 ' + 'database, uploaded media and search index. This is only used, ' + 'when a new settings file is created. The given path is only ' + 'written into the new settings file. Default according to the ' + 'OpenSlides is at the moment %s' % get_default_user_data_path(openslides_type))) + general_arguments['address'] = ( + ('-a', '--address',), + dict(default='0.0.0.0', help='IP address to listen on. Default is %(default)s.')) + general_arguments['port'] = ( + ('-p', '--port'), + dict(type=int, + default=80, + help='Port to listen on. Default as admin or root is %(default)d, else 8000.')) + + for argument in arguments: + try: + args, kwargs = general_arguments[argument] + except KeyError: + raise TypeError('The argument %s is not a valid general argument.' % argument) + subcommand.add_argument(*args, **kwargs) + + def get_default_settings_path(openslides_type): """ Returns the default settings path according to the OpenSlides type. @@ -260,54 +296,44 @@ def get_default_settings_path(openslides_type): return os.path.join(parent_directory, 'openslides', 'settings.py') -def get_default_local_path(openslides_type): +def get_user_data_path_values(user_data_path, default=False, openslides_type=None): """ - Returns the default local path according to the OpenSlides type. + Returns a dictionary of the user specific data path values for the new + settings file. - The argument 'openslides_type' has to be one of the three types mentioned in - openslides.utils.main. - """ - if openslides_type == UNIX_VERSION: - default_local_path = filesystem2unicode(os.environ.get( - 'XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share'))) - elif openslides_type == WINDOWS_VERSION: - default_local_path = get_win32_app_data_path() - elif openslides_type == WINDOWS_PORTABLE_VERSION: - default_local_path = get_win32_portable_path() - else: - raise TypeError('%s is not a valid OpenSlides type.' % openslides_type) - return default_local_path - - -def get_localpath_values(localpath, default=False, openslides_type=None): - """ - Returns the local path values for the new settings file. - - The argument 'localpath' is a path to the directory where OpenSlides should - store the local data like SQLite3 database, media and search index. + The argument 'user_data_path' is a path to the directory where OpenSlides + should store the user specific data like SQLite3 database, media and search + index. The argument 'default' is a simple flag. If it is True and the OpenSlides - type is the Windows portable version, this function returns callable - functions for the settings file, else it returns string paths. + type is the Windows portable version, the returned dictionary contains + strings of callable functions for the settings file, else it contains + string paths. The argument 'openslides_type' can to be one of the three types mentioned in openslides.utils.main. """ - localpath_values = {} + user_data_path_values = {} if default and openslides_type == WINDOWS_PORTABLE_VERSION: - localpath_values['import_function'] = 'from openslides.utils.main import get_portable_paths' - localpath_values['database_path_value'] = "get_portable_paths('database')" - localpath_values['media_path_value'] = "get_portable_paths('media')" + user_data_path_values['import_function'] = 'from openslides.utils.main import get_portable_paths' + user_data_path_values['database_path_value'] = "get_portable_paths('database')" + user_data_path_values['media_path_value'] = "get_portable_paths('media')" + user_data_path_values['whoosh_index_path_value'] = "get_portable_paths('whoosh_index')" else: - localpath_values['import_function'] = '' + user_data_path_values['import_function'] = '' # TODO: Decide whether to use only absolute paths here. - localpath_values['database_path_value'] = "'%s'" % os.path.join(localpath, 'openslides', 'database.sqlite') + user_data_path_values['database_path_value'] = "'%s'" % os.path.join( + user_data_path, 'openslides', 'database.sqlite') # TODO: Decide whether to use only absolute paths here. - localpath_values['media_path_value'] = "'%s'" % os.path.join(localpath, 'openslides', 'media', '') - return localpath_values + user_data_path_values['media_path_value'] = "'%s'" % os.path.join( + user_data_path, 'openslides', 'media', '') + # TODO: Decide whether to use only absolute paths here. + user_data_path_values['whoosh_index_path_value'] = "'%s'" % os.path.join( + user_data_path, 'openslides', 'whoosh_index', '') + return user_data_path_values -def create_settings(settings_path, local_path_values): +def create_settings(settings_path, user_data_path_values): """ Creates the settings file at the given path using the given values for the file template. @@ -316,7 +342,7 @@ def create_settings(settings_path, local_path_values): if not os.path.exists(settings_module): os.makedirs(settings_module) context = {'secret_key': base64.b64encode(os.urandom(30))} - context.update(local_path_values) + context.update(user_data_path_values) settings_content = SETTINGS_TEMPLATE % context with open(settings_path, 'w') as settings_file: settings_file.write(settings_content) @@ -325,25 +351,24 @@ def create_settings(settings_path, local_path_values): def setup_django_settings_module(settings_path): """ - Sets the environment variable DJANGO_SETTINGS_MODULE to the given settings. + Sets the environment variable ENVIRONMENT_VARIABLE, that means + 'DJANGO_SETTINGS_MODULE', to the given settings. """ settings_file = os.path.basename(settings_path) - settings_module_name = "".join(settings_file.split('.')[:-1]) + settings_module_name = ".".join(settings_file.split('.')[:-1]) if '.' in settings_module_name: - print("'.' is not an allowed character in the settings-file") - sys.exit(1) - settings_module_dir = os.path.dirname(settings_path) - sys.path.append(settings_module_dir) + raise ImproperlyConfigured("'.' is not an allowed character in the settings-file") + settings_module_dir = os.path.dirname(settings_path) # TODO: Use absolute path here or not? + sys.path.insert(0, settings_module_dir) os.environ[ENVIRONMENT_VARIABLE] = '%s' % settings_module_name def start(args): """ - Starts OpenSlides: Runs syncdb and runs runserver (tornado webserver) with - the flag 'start_browser'. + Starts OpenSlides: Runs syncdb and runs runserver (tornado webserver). """ syncdb(args) - args.start_browser = True + args.start_browser = not args.no_browser args.no_reload = False runserver(args) @@ -360,27 +385,6 @@ def runserver(args): run_tornado(args.address, port, not args.no_reload) -def get_port(address, port): - """ - Returns the port for the server. If port 80 is given, checks if it is - available. If not returns port 8000. - - The argument 'address' should be an IP address. The argument 'port' should - be an integer. - """ - if port == 80: - # test if we can use port 80 - s = socket.socket() - try: - s.bind((address, port)) - s.listen(-1) - except socket.error: - port = 8000 - finally: - s.close() - return port - - def get_browser_url(address, port): """ Returns the url to open the web browser. @@ -418,15 +422,16 @@ def syncdb(args): Run syncdb to create or update the database. """ # TODO: Check use of filesystem2unicode here. - path = filesystem2unicode(os.path.dirname(get_database_path_from_settings(args.settings))) + path = filesystem2unicode(os.path.dirname(get_database_path_from_settings())) if not os.path.exists(path): os.makedirs(path) execute_from_command_line(["", "syncdb", "--noinput"]) + return 0 -def get_database_path_from_settings(settings_path): +def get_database_path_from_settings(): """ - Retrieves the database path out of the given settings file. Returns None, + Retrieves the database path out of the settings file. Returns None, if it is not a SQLite3 database. """ from django.conf import settings as django_settings @@ -460,7 +465,7 @@ def createsuperuser(args): def backupdb(args): """ - Stores a backup copy of the SQlite3 database. + Stores a backup copy of the SQlite3 database. Returns 0 on success, else 1. """ from django.db import connection, transaction @@ -474,18 +479,19 @@ def backupdb(args): # now copy the file try: shutil.copy(src_path, dest_path) - except IOError as e: + except IOError: raise Exception("Database backup failed.") # and release the lock again transaction.commit() - database_path = get_database_path_from_settings(args.settings) + database_path = get_database_path_from_settings() if database_path: do_backup(database_path, args.path) print('Database %s successfully stored at %s.' % (database_path, args.path)) return_value = 0 else: - print('Error: Default database is not SQLite3. Only SQLite3 databases can currently be backuped.') + print('Error: Default database is not SQLite3. Only SQLite3 databases ' + 'can currently be backuped.') return_value = 1 return return_value @@ -494,7 +500,7 @@ def deletedb(args): """ Deletes the sqlite3 database. Returns 0 on success, else 1. """ - database_path = get_database_path_from_settings(args.settings) + database_path = get_database_path_from_settings() if database_path and os.path.exists(database_path): os.remove(database_path) print('SQLite3 database file %s successfully deleted.' % database_path) @@ -518,7 +524,8 @@ def django_command_line_utility(args): else: command = None if command: - print("Error: The command '%s' is disabled for use via Django's command line utility." % command) + print("Error: The command '%s' is disabled in OpenSlides for use via Django's " + "command line utility." % command) return_value = 1 else: execute_from_command_line(args.django_args) diff --git a/openslides/global_settings.py b/openslides/global_settings.py index f3e3b806b..b1d533af0 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -67,11 +67,9 @@ STATICFILES_DIRS = ( filesystem2unicode(os.path.join(SITE_ROOT, 'static')), ) -#XXX: Note this setting (as well as our workaround finder) -# can be removed again once django-bug-#18404 has been resolved STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', - 'openslides.utils.staticfiles.AppDirectoriesFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' diff --git a/openslides/utils/main.py b/openslides/utils/main.py index c258257af..2a2c7dfe9 100644 --- a/openslides/utils/main.py +++ b/openslides/utils/main.py @@ -10,9 +10,10 @@ :license: GNU GPL, see LICENSE for more details. """ -import os -import sys import ctypes +import os +import socket +import sys import tempfile @@ -20,9 +21,11 @@ UNIX_VERSION = 'Unix Version' WINDOWS_VERSION = 'Windows Version' WINDOWS_PORTABLE_VERSION = 'Windows Portable Version' + class PortableDirNotWritable(Exception): pass + def filesystem2unicode(path): """ Transforms a path string to unicode according to the filesystem's encoding. @@ -36,7 +39,7 @@ def filesystem2unicode(path): def detect_openslides_type(): """ - Returns the type of this version. + Returns the type of this OpenSlides version. """ if sys.platform == 'win32': if os.path.basename(sys.executable).lower() == 'openslides.exe': @@ -54,12 +57,24 @@ def detect_openslides_type(): return openslides_type -def is_portable(): +def get_default_user_data_path(openslides_type): """ - Helper function just for the GUI. + Returns the default path for user specific data according to the OpenSlides + type. + + The argument 'openslides_type' has to be one of the three types mentioned + in openslides.utils.main. """ - # TODO: Remove this function. - return detect_openslides_type() == WINDOWS_PORTABLE_VERSION + if openslides_type == UNIX_VERSION: + default_user_data_path = filesystem2unicode(os.environ.get( + 'XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share'))) + elif openslides_type == WINDOWS_VERSION: + default_user_data_path = get_win32_app_data_path() + elif openslides_type == WINDOWS_PORTABLE_VERSION: + default_user_data_path = get_win32_portable_path() + else: + raise TypeError('%s is not a valid OpenSlides type.' % openslides_type) + return default_user_data_path def get_win32_app_data_path(): @@ -79,7 +94,7 @@ def get_win32_app_data_path(): buf = ctypes.create_unicode_buffer(MAX_PATH) res = SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, 0, buf) if res != 0: - raise Exception("Could not deterime APPDATA path") + raise Exception("Could not determine Windows' APPDATA path") return buf.value @@ -97,7 +112,7 @@ def get_win32_portable_path(): except OSError: raise PortableDirNotWritable( 'Portable directory is not writeable. ' - 'Please choose another directory for settings and local files.') + 'Please choose another directory for settings and data files.') else: os.close(fd) os.unlink(test_file) @@ -107,13 +122,36 @@ def get_win32_portable_path(): def get_portable_paths(name): """ Returns the paths for the Windows portable version on runtime for the - SQLite3 database and the media directory. The argument 'name' can be - 'database' or 'media'. + SQLite3 database, the media directory and the search index. The argument + 'name' can be 'database', 'media' or 'whoosh_index'. """ if name == 'database': path = os.path.join(get_win32_portable_path(), 'openslides', 'database.sqlite') elif name == 'media': path = os.path.join(get_win32_portable_path(), 'openslides', 'media', '') + elif name == 'whoosh_index': + path = os.path.join(get_win32_portable_path(), 'openslides', 'whoosh_index', '') else: - raise TypeError('Unknow type %s' % name) + raise TypeError('Unknown type %s' % name) return path + + +def get_port(address, port): + """ + Returns the port for the server. If port 80 is given, checks if it is + available. If not returns port 8000. + + The argument 'address' should be an IP address. The argument 'port' should + be an integer. + """ + if port == 80: + # test if we can use port 80 + s = socket.socket() + try: + s.bind((address, port)) + s.listen(-1) + except socket.error: + port = 8000 + finally: + s.close() + return port diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py new file mode 100644 index 000000000..2361b10b3 --- /dev/null +++ b/tests/utils/test_main.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + Tests for openslides.utils.main + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +import os +import sys + +from django.core.exceptions import ImproperlyConfigured + +from openslides.__main__ import ( + get_default_settings_path, + get_browser_url, + get_user_data_path_values, + setup_django_settings_module) +from openslides.utils.test import TestCase +from openslides.utils.main import ( + get_default_user_data_path, + UNIX_VERSION, + WINDOWS_PORTABLE_VERSION) + + +class TestFunctions(TestCase): + def test_get_default_user_data_path(self): + self.assertTrue('.local/share' in get_default_user_data_path(UNIX_VERSION)) + + def test_get_default_settings_path(self): + self.assertTrue('.config/openslides/settings.py' in get_default_settings_path(UNIX_VERSION)) + + def test_get_user_data_path_values_case_one(self): + self.assertEqual( + get_user_data_path_values('test_path_dfhvndshfgsef', default=False), + {'import_function': '', + 'database_path_value': "'test_path_dfhvndshfgsef/openslides/database.sqlite'", + 'media_path_value': "'test_path_dfhvndshfgsef/openslides/media/'", + 'whoosh_index_path_value': "'test_path_dfhvndshfgsef/openslides/whoosh_index/'"}) + + def test_get_user_data_path_values_case_two(self): + self.assertEqual( + get_user_data_path_values('test_path_dfhvndshfgsef', default=True, openslides_type=WINDOWS_PORTABLE_VERSION), + {'import_function': 'from openslides.utils.main import get_portable_paths', + 'database_path_value': "get_portable_paths('database')", + 'media_path_value': "get_portable_paths('media')", + 'whoosh_index_path_value': "get_portable_paths('whoosh_index')"}) + + def test_setup_django_settings_module(self): + setup_django_settings_module('test_dir_dhvnghfjdh456fzheg2f/test_path_bngjdhc756dzwncshdfnx.py') + self.assertEqual(os.environ['DJANGO_SETTINGS_MODULE'], 'test_path_bngjdhc756dzwncshdfnx') + self.assertEqual(sys.path[0], 'test_dir_dhvnghfjdh456fzheg2f') + + def test_setup_django_settings_module_error(self): + self.assertRaisesMessage( + ImproperlyConfigured, + "'.' is not an allowed character in the settings-file", + setup_django_settings_module, + 'wrong.file.py') + + def test_get_browser_url(self): + self.assertEqual(get_browser_url('123.456.789.365', 6789), 'http://123.456.789.365:6789') + self.assertEqual(get_browser_url('123.456.789.365', 80), 'http://123.456.789.365') + self.assertEqual(get_browser_url('0.0.0.0', 6789), 'http://localhost:6789') + self.assertEqual(get_browser_url('0.0.0.0', 80), 'http://localhost')