From 35024764cf38cb19395bd773d29f5b8d463f3024 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Fri, 16 Jan 2015 14:18:34 +0100 Subject: [PATCH] Rework of management command --- .gitignore | 10 +- .travis.yml | 2 +- make/commands.py | 3 +- openslides/__main__.py | 377 ++++-------------- .../core/management/commands/backupdb.py | 42 ++ .../management/commands/createsuperuser.py | 15 + .../core/management/commands/migrate.py | 29 ++ .../core/management/commands/runserver.py | 78 ++++ openslides/core/management/commands/syncdb.py | 23 -- openslides/core/signals.py | 3 - openslides/global_settings.py | 2 +- openslides/motion/apps.py | 4 +- openslides/motion/signals.py | 140 +++---- openslides/users/api.py | 90 +++++ openslides/users/apps.py | 4 +- openslides/users/signals.py | 99 ----- openslides/utils/autoupdate.py | 12 +- openslides/utils/main.py | 138 ++++--- openslides/utils/settings.py.tpl | 2 +- openslides/utils/test.py | 8 +- tests/settings.py | 8 - tests/utils/models.py | 8 - tests/utils/test_main.py | 165 +++----- tests/utils/test_views.py | 27 +- tests/utils/urls.py | 3 - tests/utils/views.py | 14 - 26 files changed, 578 insertions(+), 728 deletions(-) create mode 100644 openslides/core/management/commands/backupdb.py create mode 100644 openslides/core/management/commands/createsuperuser.py create mode 100644 openslides/core/management/commands/migrate.py create mode 100644 openslides/core/management/commands/runserver.py delete mode 100644 openslides/core/management/commands/syncdb.py delete mode 100644 tests/utils/models.py diff --git a/.gitignore b/.gitignore index 5745da234..a1dbe723a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,16 +7,12 @@ .virtualenv/* .venv/* -# Node modules +# Javascript tools and libraries node_modules/* +bower_components/* # Development user data (settings, database, media, search index, static files) -settings.py -!tests/settings.py -database.sqlite -media/* -whoosh_index/* -bower_components/* +development/* openslides/static/* # Package building/IDE diff --git a/.travis.yml b/.travis.yml index ddd54dfd3..54b756b6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ install: - "node_modules/.bin/bower install" - "node_modules/.bin/gulp --production" script: - - "DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py django test" + - "DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test" - "coverage report -m --fail-under=80" - "flake8 --max-line-length=150 --statistics openslides tests" diff --git a/make/commands.py b/make/commands.py index 236d3a27e..01772ecfa 100644 --- a/make/commands.py +++ b/make/commands.py @@ -11,7 +11,7 @@ def test(args=None): """ module = getattr(args, 'module', '') return call("DJANGO_SETTINGS_MODULE='tests.settings' coverage run " - "./manage.py django test %s" % module) + "./manage.py test %s" % module) @argument('--plain', action='store_true') @@ -23,7 +23,6 @@ def coverage(args=None, plain=None): By default it creates a html report. With the argument --plain, it creates a plain report and fails under a certain amount of untested lines. """ - test() if plain is None: plain = getattr(args, 'plain', False) if plain: diff --git a/openslides/__main__.py b/openslides/__main__.py index e67bebf4b..4a485ea50 100644 --- a/openslides/__main__.py +++ b/openslides/__main__.py @@ -1,59 +1,50 @@ #!/usr/bin/env python -import argparse import os -import shutil import sys -from django.conf import ENVIRONMENT_VARIABLE from django.core.management import execute_from_command_line from openslides import get_version from openslides.utils.main import ( - detect_openslides_type, - ensure_settings, - get_browser_url, - get_database_path_from_settings, get_default_settings_path, - get_default_user_data_path, - get_port, setup_django_settings_module, + write_settings, + UnknownCommand, + ExceptionArgumentParser, + get_development_settings_path, start_browser, - translate_customizable_strings, - write_settings) + is_development) 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 hasattr(args, 'settings') and args.settings: - settings = args.settings - setup_django_settings_module(settings) + parser = get_parser() + try: + known_args, unknown_args = parser.parse_known_args() + except UnknownCommand: + unknown_command = True 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 + unknown_command = False - # Process the subcommand's callback - return args.callback(settings, args) + if unknown_command: + # Run a command, that is defined by the django management api + development = is_development() + setup_django_settings_module(development=development) + execute_from_command_line(sys.argv) + else: + # Run a command that is defined here + # These are commands that can not rely on an existing settings + known_args.callback(known_args) -def parse_args(): +def get_parser(): """ - Parses all command line arguments. The subcommand 'django' links to - Django's command-line utility. + Parses all command line arguments. """ - if len(sys.argv) == 1: + if len(sys.argv) == 1 and not is_development(): sys.argv.append('start') # Init parser @@ -64,7 +55,7 @@ def parse_args(): '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) + parser = ExceptionArgumentParser(description=description) # Add version argument parser.add_argument( @@ -85,284 +76,88 @@ def parse_args(): subcommand_start = subparsers.add_parser( 'start', help='Setup settings and database, start tornado webserver, launch the ' - 'default web browser and open the webinterface.') - add_general_arguments(subcommand_start, ('settings', 'user_data_path', 'language', 'address', 'port')) + 'default web browser and open the webinterface. The environment ' + 'variable DJANGO_SETTINGS_MODULE is ignored.') subcommand_start.add_argument( '--no-browser', action='store_true', help='Do not launch the default web browser.') + subcommand_start.add_argument( + '--settings_path', + action='store', + default=None, + help='The used settings file. The file is created, if it does not exist.') subcommand_start.set_defaults(callback=start) - - # Subcommand runserver - subcommand_runserver = subparsers.add_parser( - 'runserver', - help='Run OpenSlides using tornado webserver. The database tables must ' - 'be created before. Use syncdb subcommand for this.') - add_general_arguments(subcommand_runserver, ('settings', 'user_data_path', 'address', 'port')) - subcommand_runserver.add_argument( - '--start-browser', + subcommand_start.add_argument( + '--development', action='store_true', - help='Launch the default web browser and open the webinterface.') - subcommand_runserver.set_defaults(callback=runserver) + help='Command for development purposes.') - # Subcommand syncdb - subcommand_syncdb = subparsers.add_parser( - 'syncdb', - help='Create or update database tables.') - add_general_arguments(subcommand_syncdb, ('settings', 'user_data_path', 'language')) - subcommand_syncdb.set_defaults(callback=syncdb) + # Subcommand createsettings + subcommand_createsettings = subparsers.add_parser( + 'createsettings', + help='Create the settings file.') + subcommand_createsettings.set_defaults(callback=createsettings) + subcommand_createsettings.add_argument( + '--settings_path', + action='store', + default=None, + help='The used settings file. The file is created, even if it exists.') + subcommand_createsettings.add_argument( + '--development', + action='store_true', + help='Command for development purposes.') - # Subcommand createsuperuser - subcommand_createsuperuser = subparsers.add_parser( - 'createsuperuser', - help="Make sure the user 'admin' exists and uses 'admin' as password. " - "The database tables must be created before. Use syncdb subcommand for this.") - 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 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.') - subcommand_backupdb.set_defaults(callback=backupdb) - - # Subcommand deletedb - subcommand_deletedb = subparsers.add_parser( - 'deletedb', - help='Delete the SQLite3 database.') - add_general_arguments(subcommand_deletedb, ('settings', 'user_data_path')) - subcommand_deletedb.set_defaults(callback=deletedb) - - # Subcommand create-dev-settings - subcommand_create_dev_settings = subparsers.add_parser( - 'create-dev-settings', - help='Create a settings file at current working directory for development use.') - subcommand_create_dev_settings.set_defaults(callback=create_dev_settings) - - # Subcommand django - subcommand_django_command_line_utility = subparsers.add_parser( - 'django', - description="Link to Django's command-line utility. Type " - "'%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 + return parser -def add_general_arguments(subcommand, arguments): +def start(args): """ - Adds the named arguments to the subcommand. + Starts OpenSlides: Runs migrations and runs runserver. """ - general_arguments = {} - openslides_type = detect_openslides_type() + settings_path = args.settings_path + development = is_development() - general_arguments['settings'] = ( - ('-s', '--settings'), - dict(help="Path to settings file. The file must be a python module. " - "If if isn't provided, the %s environment variable will be " - "used. If the environment variable 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. It 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 type is at the moment %s' % get_default_user_data_path(openslides_type))) - general_arguments['language'] = ( - ('-l', '--language'), - dict(help='Language code. All customizable strings will be translated ' - 'during database setup. See https://www.transifex.com/projects/p/openslides/ ' - 'for supported languages.')) - 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.')) + if settings_path is None: + if development: + settings_path = get_development_settings_path() + else: + settings_path = get_default_settings_path() - 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) + # Write settings if it does not exists. + if not os.path.isfile(settings_path): + createsettings(args) + + # Set the django setting module and run migrations + # A manual given environment variable will be overwritten + setup_django_settings_module(settings_path, development=development) + + execute_from_command_line(['manage.py', 'migrate']) + + if not args.no_browser: + start_browser('http://0.0.0.0:8000') + + # Start the webserver + execute_from_command_line(['manage.py', 'runserver', '0.0.0.0:8000']) -def start(settings, args): +def createsettings(args): """ - Starts OpenSlides: Runs syncdb and runs runserver (tornado webserver). + Creates settings for OpenSlides. """ - ensure_settings(settings, args) - syncdb(settings, args) - args.start_browser = not args.no_browser - runserver(settings, args) + settings_path = args.settings_path + development = is_development() + context = {} + if development: + if settings_path is None: + settings_path = get_development_settings_path() + context = { + 'openslides_user_data_path': repr(os.path.join(os.getcwd(), 'development')), + 'debug': 'True'} -def runserver(settings, args): - """ - Runs tornado webserver. Runs the function start_browser if the respective - argument is given. - """ - ensure_settings(settings, args) - 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) - - # Now the settings is available and the function can be imported. - # TODO: only start tornado when it is used as wsgi server - from openslides.utils.autoupdate import run_tornado - run_tornado(args.address, port) - - -def syncdb(settings, args): - """ - Run syncdb to create or update the database. - """ - ensure_settings(settings, args) - db_file = get_database_path_from_settings() - if db_file is not None: - db_dir = os.path.dirname(db_file) - if not os.path.exists(db_dir): - os.makedirs(db_dir) - if not os.path.exists(db_file): - print('Clearing old search index...') - execute_from_command_line(["", "clear_index", "--noinput"]) - execute_from_command_line(["", "syncdb", "--noinput"]) - if args.language: - translate_customizable_strings(args.language) - return 0 - - -def createsuperuser(settings, args): - """ - Creates or resets the admin user. Returns 0 to show success. - """ - ensure_settings(settings, args) - # can't be imported in global scope as it already requires - # the settings module during import - from openslides.users.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(settings, args): - """ - Stores a backup copy of the SQlite3 database. Returns 0 on success, else 1. - """ - ensure_settings(settings, args) - - from django.db import connection, transaction - - @transaction.atomic - 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: - raise Exception("Database backup failed.") - - 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.') - return_value = 1 - return return_value - - -def deletedb(settings, args): - """ - Deletes the sqlite3 database. Returns 0 on success, else 1. - """ - ensure_settings(settings, args) - 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) - execute_from_command_line(["", "clear_index", "--noinput"]) - print('Whoosh search index successfully cleared.') - return_value = 0 - else: - print('SQLite3 database file %s does not exist.' % database_path) - return_value = 1 - return return_value - - -def create_dev_settings(settings, args): - """ - Creates a settings file at the currect working directory for development use. - """ - settings = os.path.join(os.getcwd(), 'settings.py') - if not os.path.exists(settings): - context = {} - context['openslides_user_data_path'] = repr(os.getcwd()) - context['import_function'] = '' - context['debug'] = 'True' - write_settings(settings, **context) - print('Settings file at %s successfully created.' % settings) - return_value = 0 - else: - print('Error: Settings file %s already exists.' % settings) - return_value = 1 - return return_value - - -def django_command_line_utility(settings, 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 in OpenSlides for use via Django's " - "command line utility." % command) - return_value = 1 - else: - ensure_settings(settings, args) - execute_from_command_line(args.django_args) - return_value = 0 - return return_value + settings_path = write_settings(settings_path, **context) + print('Settings created at %s' % settings_path) if __name__ == "__main__": diff --git a/openslides/core/management/commands/backupdb.py b/openslides/core/management/commands/backupdb.py new file mode 100644 index 000000000..21755a8e8 --- /dev/null +++ b/openslides/core/management/commands/backupdb.py @@ -0,0 +1,42 @@ +from optparse import make_option # TODO: Use argpase in Django 1.8 +import shutil + +from django.core.management.base import NoArgsCommand, CommandError +from django.db import connection, transaction + +from openslides.utils.main import get_database_path_from_settings + + +class Command(NoArgsCommand): + """ + Commands to create or reset the adminuser + """ + option_list = NoArgsCommand.option_list + ( + make_option('--path', dest='path'), + ) + + def handle_noargs(self, **options): + path = options.get('path') + + @transaction.atomic + 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: + # TODO: use the IOError message as message for the user + raise CommandError("Database backup failed.") + + database_path = get_database_path_from_settings() + if database_path: + do_backup(database_path, path) + self.stdout.write('Database %s successfully stored at %s.' % (database_path, path)) + else: + raise CommandError( + 'Default database is not SQLite3. Only SQLite3 databases' + 'can currently be backuped.') diff --git a/openslides/core/management/commands/createsuperuser.py b/openslides/core/management/commands/createsuperuser.py new file mode 100644 index 000000000..b65082189 --- /dev/null +++ b/openslides/core/management/commands/createsuperuser.py @@ -0,0 +1,15 @@ +from django.core.management.base import NoArgsCommand + +from openslides.users.api import create_or_reset_admin_user + + +class Command(NoArgsCommand): + """ + Commands to create or reset the adminuser + """ + + def handle_noargs(self, **options): + if create_or_reset_admin_user(): + self.stdout.write('Admin user successfully created.') + else: + self.stdout.write('Admin user successfully reset.') diff --git a/openslides/core/management/commands/migrate.py b/openslides/core/management/commands/migrate.py new file mode 100644 index 000000000..9219b5d51 --- /dev/null +++ b/openslides/core/management/commands/migrate.py @@ -0,0 +1,29 @@ +import os + +from django.core.management.commands.migrate import Command as _Command + +from openslides.users.api import create_builtin_groups_and_admin + + +class Command(_Command): + """ + Migration command that does the same like the django migration command but + calles also creates the default groups + """ + # TODO: Try to get rid of this code. The problem are the ContentType + # and Permission objects, which are created in the post_migrate signal, but + # we need to things later. + + def handle(self, *args, **options): + from django.conf import settings + # Creates the folder for a sqlite database if necessary + if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3': + try: + os.makedirs(settings.OPENSLIDES_USER_DATA_PATH) + except (FileExistsError, AttributeError): + # If the folder already exist or the settings OPENSLIDES_USER_DATA_PATH + # is unknown, then do nothing + pass + + super().handle(*args, **options) + create_builtin_groups_and_admin() diff --git a/openslides/core/management/commands/runserver.py b/openslides/core/management/commands/runserver.py new file mode 100644 index 000000000..f96cc9978 --- /dev/null +++ b/openslides/core/management/commands/runserver.py @@ -0,0 +1,78 @@ +from datetime import datetime +import sys +import socket +import errno + +from django.core.management.commands.runserver import Command as _Command +from django.core.exceptions import ImproperlyConfigured +from django.utils import translation +from django.utils.encoding import force_text + +from openslides.utils.autoupdate import run_tornado + + +class Command(_Command): + """ + Runserver command from django core, but starts the tornado webserver. + + Only the line to run tornado has changed from the django default + implementation. + """ + # TODO: do not start tornado when the settings says so + + def inner_run(self, *args, **options): + from django.conf import settings + # From the base class: + self.stdout.write("Performing system checks...\n\n") + self.validate(display_num_errors=True) + + try: + self.check_migrations() + except ImproperlyConfigured: + pass + + now = datetime.now().strftime('%B %d, %Y - %X') + + shutdown_message = options.get('shutdown_message', '') + quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C' + + self.stdout.write(( + "%(started_at)s\n" + "Django version %(version)s, using settings %(settings)r\n" + "Starting development server at http://%(addr)s:%(port)s/\n" + "Quit the server with %(quit_command)s.\n" + ) % { + "started_at": now, + "version": self.get_version(), + "settings": settings.SETTINGS_MODULE, + "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr, + "port": self.port, + "quit_command": quit_command, + }) + + translation.activate(settings.LANGUAGE_CODE) + + try: + handler = self.get_handler(*args, **options) + run_tornado( + self.addr, + int(self.port), + handler, + ipv6=self.use_ipv6) + except socket.error as e: + # Use helpful error messages instead of ugly tracebacks. + ERRORS = { + errno.EACCES: "You don't have permission to access that port.", + errno.EADDRINUSE: "That port is already in use.", + errno.EADDRNOTAVAIL: "That IP address can't be assigned-to.", + } + try: + error_text = ERRORS[e.errno] + except KeyError: + error_text = force_text(e) + self.stderr.write("Error: %s" % error_text) + sys.exit(0) + except KeyboardInterrupt: + if shutdown_message: + self.stdout.write(shutdown_message) + sys.exit(0) diff --git a/openslides/core/management/commands/syncdb.py b/openslides/core/management/commands/syncdb.py deleted file mode 100644 index 928d2fc4f..000000000 --- a/openslides/core/management/commands/syncdb.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.core.management.commands.syncdb import Command as _Command - -from openslides.core.signals import post_database_setup - - -class Command(_Command): - """ - Setup the database and sends the signal post_database_setup. - """ - def handle_noargs(self, *args, **kwargs): - """ - Calls Django's syncdb command but always in non-interactive mode. After - this it sends our post_database_setup signal. - """ - interactive = kwargs.get('interactive', False) - kwargs['interactive'] = False - return_value = super(Command, self).handle_noargs(*args, **kwargs) - post_database_setup.send(sender=self) - - if interactive: - print('Interactive mode (e. g. creating a superuser) is not possibile ' - 'in OpenSlides. A superuser is automaticly created.') - return return_value diff --git a/openslides/core/signals.py b/openslides/core/signals.py index 15bb08f0e..54407e902 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -1,12 +1,9 @@ from django import forms -from django.dispatch import Signal from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable -post_database_setup = Signal() - def setup_general_config(sender, **kwargs): """ diff --git a/openslides/global_settings.py b/openslides/global_settings.py index 7da0ea348..cdc692038 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -79,6 +79,7 @@ MIDDLEWARE_CLASSES = ( ROOT_URLCONF = 'openslides.urls' INSTALLED_APPS = ( + 'openslides.core', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -90,7 +91,6 @@ INSTALLED_APPS = ( 'ckeditor', 'rest_framework', 'openslides.poll', - 'openslides.core', 'openslides.account', 'openslides.projector', 'openslides.agenda', diff --git a/openslides/motion/apps.py b/openslides/motion/apps.py index e5ecdfe71..9a15b4ccb 100644 --- a/openslides/motion/apps.py +++ b/openslides/motion/apps.py @@ -1,4 +1,5 @@ from django.apps import AppConfig +from django.db.models.signals import post_migrate class MotionAppConfig(AppConfig): @@ -12,13 +13,12 @@ class MotionAppConfig(AppConfig): # Import all required stuff. from openslides.config.signals import config_signal - from openslides.core.signals import post_database_setup from openslides.projector.api import register_slide_model from .signals import create_builtin_workflows, setup_motion_config # Connect signals. config_signal.connect(setup_motion_config, dispatch_uid='setup_motion_config') - post_database_setup.connect(create_builtin_workflows, dispatch_uid='motion_create_builtin_workflows') + post_migrate.connect(create_builtin_workflows, dispatch_uid='motion_create_builtin_workflows') # Register slides. Motion = self.get_model('Motion') diff --git a/openslides/motion/signals.py b/openslides/motion/signals.py index 07507e9b4..e420470fc 100644 --- a/openslides/motion/signals.py +++ b/openslides/motion/signals.py @@ -172,73 +172,75 @@ def create_builtin_workflows(sender, **kwargs): connected to the signal openslides.core.signals.post_database_setup during app loading. """ - workflow_1, created = Workflow.objects.get_or_create(name=ugettext_noop('Simple Workflow')) - if created: - state_1_1 = State.objects.create(name=ugettext_noop('submitted'), - workflow=workflow_1, - allow_create_poll=True, - allow_support=True, - allow_submitter_edit=True) - state_1_2 = State.objects.create(name=ugettext_noop('accepted'), - workflow=workflow_1, - action_word=ugettext_noop('Accept')) - state_1_3 = State.objects.create(name=ugettext_noop('rejected'), - workflow=workflow_1, - action_word=ugettext_noop('Reject')) - state_1_4 = State.objects.create(name=ugettext_noop('not decided'), - workflow=workflow_1, - action_word=ugettext_noop('Do not decide')) - state_1_1.next_states.add(state_1_2, state_1_3, state_1_4) - workflow_1.first_state = state_1_1 - workflow_1.save() + if Workflow.objects.exists(): + # If there is at least one workflow, then do nothing. + return - workflow_2, created = Workflow.objects.get_or_create(name=ugettext_noop('Complex Workflow')) - if created: - state_2_1 = State.objects.create(name=ugettext_noop('published'), - workflow=workflow_2, - allow_support=True, - allow_submitter_edit=True, - dont_set_identifier=True) - state_2_2 = State.objects.create(name=ugettext_noop('permitted'), - workflow=workflow_2, - action_word=ugettext_noop('Permit'), - allow_create_poll=True, - allow_submitter_edit=True, - versioning=True, - leave_old_version_active=True) - state_2_3 = State.objects.create(name=ugettext_noop('accepted'), - workflow=workflow_2, - action_word=ugettext_noop('Accept'), - versioning=True) - state_2_4 = State.objects.create(name=ugettext_noop('rejected'), - workflow=workflow_2, - action_word=ugettext_noop('Reject'), - versioning=True) - state_2_5 = State.objects.create(name=ugettext_noop('withdrawed'), - workflow=workflow_2, - action_word=ugettext_noop('Withdraw'), - versioning=True) - state_2_6 = State.objects.create(name=ugettext_noop('adjourned'), - workflow=workflow_2, - action_word=ugettext_noop('Adjourn'), - versioning=True) - state_2_7 = State.objects.create(name=ugettext_noop('not concerned'), - workflow=workflow_2, - action_word=ugettext_noop('Do not concern'), - versioning=True) - state_2_8 = State.objects.create(name=ugettext_noop('commited a bill'), - workflow=workflow_2, - action_word=ugettext_noop('Commit a bill'), - versioning=True) - state_2_9 = State.objects.create(name=ugettext_noop('needs review'), - workflow=workflow_2, - action_word=ugettext_noop('Needs review'), - versioning=True) - state_2_10 = State.objects.create(name=ugettext_noop('rejected (not authorized)'), - workflow=workflow_2, - action_word=ugettext_noop('Reject (not authorized)'), - versioning=True) - state_2_1.next_states.add(state_2_2, state_2_5, state_2_10) - state_2_2.next_states.add(state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9) - workflow_2.first_state = state_2_1 - workflow_2.save() + workflow_1 = Workflow.objects.create(name=ugettext_noop('Simple Workflow')) + state_1_1 = State.objects.create(name=ugettext_noop('submitted'), + workflow=workflow_1, + allow_create_poll=True, + allow_support=True, + allow_submitter_edit=True) + state_1_2 = State.objects.create(name=ugettext_noop('accepted'), + workflow=workflow_1, + action_word=ugettext_noop('Accept')) + state_1_3 = State.objects.create(name=ugettext_noop('rejected'), + workflow=workflow_1, + action_word=ugettext_noop('Reject')) + state_1_4 = State.objects.create(name=ugettext_noop('not decided'), + workflow=workflow_1, + action_word=ugettext_noop('Do not decide')) + state_1_1.next_states.add(state_1_2, state_1_3, state_1_4) + workflow_1.first_state = state_1_1 + workflow_1.save() + + workflow_2 = Workflow.objects.create(name=ugettext_noop('Complex Workflow')) + state_2_1 = State.objects.create(name=ugettext_noop('published'), + workflow=workflow_2, + allow_support=True, + allow_submitter_edit=True, + dont_set_identifier=True) + state_2_2 = State.objects.create(name=ugettext_noop('permitted'), + workflow=workflow_2, + action_word=ugettext_noop('Permit'), + allow_create_poll=True, + allow_submitter_edit=True, + versioning=True, + leave_old_version_active=True) + state_2_3 = State.objects.create(name=ugettext_noop('accepted'), + workflow=workflow_2, + action_word=ugettext_noop('Accept'), + versioning=True) + state_2_4 = State.objects.create(name=ugettext_noop('rejected'), + workflow=workflow_2, + action_word=ugettext_noop('Reject'), + versioning=True) + state_2_5 = State.objects.create(name=ugettext_noop('withdrawed'), + workflow=workflow_2, + action_word=ugettext_noop('Withdraw'), + versioning=True) + state_2_6 = State.objects.create(name=ugettext_noop('adjourned'), + workflow=workflow_2, + action_word=ugettext_noop('Adjourn'), + versioning=True) + state_2_7 = State.objects.create(name=ugettext_noop('not concerned'), + workflow=workflow_2, + action_word=ugettext_noop('Do not concern'), + versioning=True) + state_2_8 = State.objects.create(name=ugettext_noop('commited a bill'), + workflow=workflow_2, + action_word=ugettext_noop('Commit a bill'), + versioning=True) + state_2_9 = State.objects.create(name=ugettext_noop('needs review'), + workflow=workflow_2, + action_word=ugettext_noop('Needs review'), + versioning=True) + state_2_10 = State.objects.create(name=ugettext_noop('rejected (not authorized)'), + workflow=workflow_2, + action_word=ugettext_noop('Reject (not authorized)'), + versioning=True) + state_2_1.next_states.add(state_2_2, state_2_5, state_2_10) + state_2_2.next_states.add(state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9) + workflow_2.first_state = state_2_1 + workflow_2.save() diff --git a/openslides/users/api.py b/openslides/users/api.py index 0b0b34b8f..21b0ca0c4 100644 --- a/openslides/users/api.py +++ b/openslides/users/api.py @@ -2,6 +2,7 @@ from random import choice from django.contrib.auth.models import Permission, Group from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext_noop from .models import User @@ -49,6 +50,95 @@ def get_registered_group(): return Group.objects.get(pk=2) +def create_builtin_groups_and_admin(): + """ + Creates the builtin groups: Anonymous, Registered, Delegates and Staff. + + Creates the builtin user: admin. + """ + # Check whether the group pks 1 to 4 are free + if Group.objects.filter(pk__in=range(1, 5)).exists(): + # Do completely nothing if there are already some of our groups in the database. + return + + # Anonymous (pk 1) and Registered (pk 2) + ct_core = ContentType.objects.get(app_label='core', model='customslide') + perm_11 = Permission.objects.get(content_type=ct_core, codename='can_see_projector') + perm_12 = Permission.objects.get(content_type=ct_core, codename='can_see_dashboard') + + ct_agenda = ContentType.objects.get(app_label='agenda', model='item') + ct_speaker = ContentType.objects.get(app_label='agenda', model='speaker') + perm_13 = Permission.objects.get(content_type=ct_agenda, codename='can_see_agenda') + perm_14 = Permission.objects.get(content_type=ct_agenda, codename='can_see_orga_items') + can_speak = Permission.objects.get(content_type=ct_speaker, codename='can_be_speaker') + + ct_motion = ContentType.objects.get(app_label='motion', model='motion') + perm_15 = Permission.objects.get(content_type=ct_motion, codename='can_see_motion') + + ct_assignment = ContentType.objects.get(app_label='assignment', model='assignment') + perm_16 = Permission.objects.get(content_type=ct_assignment, codename='can_see_assignment') + + ct_users = ContentType.objects.get(app_label='users', model='user') + perm_users_can_see_name = Permission.objects.get(content_type=ct_users, codename='can_see_name') + perm_users_can_see_extra_data = Permission.objects.get(content_type=ct_users, codename='can_see_extra_data') + + ct_mediafile = ContentType.objects.get(app_label='mediafile', model='mediafile') + perm_18 = Permission.objects.get(content_type=ct_mediafile, codename='can_see') + + base_permission_list = ( + perm_11, + perm_12, + perm_13, + perm_14, + perm_15, + perm_16, + perm_users_can_see_name, + perm_users_can_see_extra_data, + perm_18) + + group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous'), pk=1) + group_anonymous.permissions.add(*base_permission_list) + group_registered = Group.objects.create(name=ugettext_noop('Registered'), pk=2) + group_registered.permissions.add(can_speak, *base_permission_list) + + # Delegates (pk 3) + perm_31 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion') + perm_32 = Permission.objects.get(content_type=ct_motion, codename='can_support_motion') + perm_33 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_other') + perm_34 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_self') + perm_35 = Permission.objects.get(content_type=ct_mediafile, codename='can_upload') + + group_delegates = Group.objects.create(name=ugettext_noop('Delegates'), pk=3) + group_delegates.permissions.add(perm_31, perm_32, perm_33, perm_34, perm_35) + + # Staff (pk 4) + perm_41 = Permission.objects.get(content_type=ct_agenda, codename='can_manage_agenda') + perm_42 = Permission.objects.get(content_type=ct_motion, codename='can_manage_motion') + perm_43 = Permission.objects.get(content_type=ct_assignment, codename='can_manage_assignment') + perm_44 = Permission.objects.get(content_type=ct_users, codename='can_manage') + perm_45 = Permission.objects.get(content_type=ct_core, codename='can_manage_projector') + perm_46 = Permission.objects.get(content_type=ct_core, codename='can_use_chat') + perm_47 = Permission.objects.get(content_type=ct_mediafile, codename='can_manage') + + ct_config = ContentType.objects.get(app_label='config', model='configstore') + perm_48 = Permission.objects.get(content_type=ct_config, codename='can_manage') + + ct_tag = ContentType.objects.get(app_label='core', model='tag') + can_manage_tags = Permission.objects.get(content_type=ct_tag, codename='can_manage_tags') + + group_staff = Group.objects.create(name=ugettext_noop('Staff'), pk=4) + # add delegate permissions (without can_support_motion) + group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35) + # add staff permissions + group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48, can_manage_tags) + # add can_see_name and can_see_extra_data permissions + # TODO: Remove this redundancy after cleanup of the permission system. + group_staff.permissions.add(perm_users_can_see_name, perm_users_can_see_extra_data) + + # Admin user + create_or_reset_admin_user() + + def create_or_reset_admin_user(): group_staff = Group.objects.get(pk=4) try: diff --git a/openslides/users/apps.py b/openslides/users/apps.py index 4c4b65539..0d3231554 100644 --- a/openslides/users/apps.py +++ b/openslides/users/apps.py @@ -13,10 +13,9 @@ class UsersAppConfig(AppConfig): # Import all required stuff. from django.db.models.signals import post_save from openslides.config.signals import config_signal - from openslides.core.signals import post_database_setup from openslides.projector.api import register_slide_model from openslides.utils.rest_api import router - from .signals import create_builtin_groups_and_admin, setup_users_config, user_post_save + from .signals import setup_users_config, user_post_save from .views import UserViewSet # Load User model. @@ -24,7 +23,6 @@ class UsersAppConfig(AppConfig): # Connect signals. config_signal.connect(setup_users_config, dispatch_uid='setup_users_config') - post_database_setup.connect(create_builtin_groups_and_admin, dispatch_uid='users_create_builtin_groups_and_admin') post_save.connect(user_post_save, sender=User, dispatch_uid='users_user_post_save') # Register slides. diff --git a/openslides/users/signals.py b/openslides/users/signals.py index 3089c6f93..722ea542e 100644 --- a/openslides/users/signals.py +++ b/openslides/users/signals.py @@ -1,14 +1,9 @@ from django import forms -from django.contrib.auth.models import Permission -from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable -from .api import create_or_reset_admin_user -from .models import Group - def setup_users_config(sender, **kwargs): """ @@ -104,100 +99,6 @@ def setup_users_config(sender, **kwargs): groups=(group_general, group_pdf)) -def create_builtin_groups_and_admin(sender, **kwargs): - """ - Receiver function to builtin groups and the admin user. - - Creates the builtin groups: Anonymous, Registered, Delegates and Staff. - - Creates the builtin user: admin. - - It is connected to the signal - openslides.core.signals.post_database_setup during app loading. - """ - # Check whether the group pks 1 to 4 are free - if Group.objects.filter(pk__in=range(1, 5)).exists(): - # Do completely nothing if there are already some of our groups in the database. - return - - # Anonymous (pk 1) and Registered (pk 2) - ct_core = ContentType.objects.get(app_label='core', model='customslide') - perm_11 = Permission.objects.get(content_type=ct_core, codename='can_see_projector') - perm_12 = Permission.objects.get(content_type=ct_core, codename='can_see_dashboard') - - ct_agenda = ContentType.objects.get(app_label='agenda', model='item') - ct_speaker = ContentType.objects.get(app_label='agenda', model='speaker') - perm_13 = Permission.objects.get(content_type=ct_agenda, codename='can_see_agenda') - perm_14 = Permission.objects.get(content_type=ct_agenda, codename='can_see_orga_items') - can_speak = Permission.objects.get(content_type=ct_speaker, codename='can_be_speaker') - - ct_motion = ContentType.objects.get(app_label='motion', model='motion') - perm_15 = Permission.objects.get(content_type=ct_motion, codename='can_see_motion') - - ct_assignment = ContentType.objects.get(app_label='assignment', model='assignment') - perm_16 = Permission.objects.get(content_type=ct_assignment, codename='can_see_assignment') - - ct_users = ContentType.objects.get(app_label='users', model='user') - perm_users_can_see_name = Permission.objects.get(content_type=ct_users, codename='can_see_name') - perm_users_can_see_extra_data = Permission.objects.get(content_type=ct_users, codename='can_see_extra_data') - - ct_mediafile = ContentType.objects.get(app_label='mediafile', model='mediafile') - perm_18 = Permission.objects.get(content_type=ct_mediafile, codename='can_see') - - base_permission_list = ( - perm_11, - perm_12, - perm_13, - perm_14, - perm_15, - perm_16, - perm_users_can_see_name, - perm_users_can_see_extra_data, - perm_18) - - group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous'), pk=1) - group_anonymous.permissions.add(*base_permission_list) - group_registered = Group.objects.create(name=ugettext_noop('Registered'), pk=2) - group_registered.permissions.add(can_speak, *base_permission_list) - - # Delegates (pk 3) - perm_31 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion') - perm_32 = Permission.objects.get(content_type=ct_motion, codename='can_support_motion') - perm_33 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_other') - perm_34 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_self') - perm_35 = Permission.objects.get(content_type=ct_mediafile, codename='can_upload') - - group_delegates = Group.objects.create(name=ugettext_noop('Delegates'), pk=3) - group_delegates.permissions.add(perm_31, perm_32, perm_33, perm_34, perm_35) - - # Staff (pk 4) - perm_41 = Permission.objects.get(content_type=ct_agenda, codename='can_manage_agenda') - perm_42 = Permission.objects.get(content_type=ct_motion, codename='can_manage_motion') - perm_43 = Permission.objects.get(content_type=ct_assignment, codename='can_manage_assignment') - perm_44 = Permission.objects.get(content_type=ct_users, codename='can_manage') - perm_45 = Permission.objects.get(content_type=ct_core, codename='can_manage_projector') - perm_46 = Permission.objects.get(content_type=ct_core, codename='can_use_chat') - perm_47 = Permission.objects.get(content_type=ct_mediafile, codename='can_manage') - - ct_config = ContentType.objects.get(app_label='config', model='configstore') - perm_48 = Permission.objects.get(content_type=ct_config, codename='can_manage') - - ct_tag = ContentType.objects.get(app_label='core', model='tag') - can_manage_tags = Permission.objects.get(content_type=ct_tag, codename='can_manage_tags') - - group_staff = Group.objects.create(name=ugettext_noop('Staff'), pk=4) - # add delegate permissions (without can_support_motion) - group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35) - # add staff permissions - group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48, can_manage_tags) - # add can_see_name and can_see_extra_data permissions - # TODO: Remove this redundancy after cleanup of the permission system. - group_staff.permissions.add(perm_users_can_see_name, perm_users_can_see_extra_data) - - # Admin user - create_or_reset_admin_user() - - def user_post_save(sender, instance, *args, **kwargs): """ Receiver function to add a new user to the registered group. It is diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index d961273ea..bc5af4679 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -70,7 +70,7 @@ class OpenSlidesSockJSConnection(SockJSConnection): waiter.send(data) -def run_tornado(addr, port): +def run_tornado(addr, port, *args, **kwargs): """ Starts the tornado webserver as wsgi server for OpenSlides. @@ -79,14 +79,6 @@ def run_tornado(addr, port): # Don't try to read the command line args from openslides parse_command_line(args=[]) - # Print listening address and port to command line - if addr == '0.0.0.0': - url_string = "the machine's local ip address" - else: - url_string = 'http://%s:%s' % (addr, port) - # TODO: don't use print, use django logging - print("Starting OpenSlides' tornado webserver listening to %(url_string)s" % {'url_string': url_string}) - # Setup WSGIContainer app = WSGIContainer(get_wsgi_application()) @@ -101,7 +93,7 @@ def run_tornado(addr, port): # Start the application debug = settings.DEBUG - tornado_app = Application(sock_js_router.urls + chatbox_socket_js_router.urls + other_urls, debug=debug) + tornado_app = Application(sock_js_router.urls + chatbox_socket_js_router.urls + other_urls, autoreload=debug, debug=debug) server = HTTPServer(tornado_app) server.listen(port=port, address=addr) IOLoop.instance().start() diff --git a/openslides/utils/main.py b/openslides/utils/main.py index f8bda0967..e9920f0d7 100644 --- a/openslides/utils/main.py +++ b/openslides/utils/main.py @@ -1,11 +1,11 @@ import ctypes import os -import socket import sys import tempfile import threading import time import webbrowser +import argparse from django.core.exceptions import ImproperlyConfigured from django.conf import ENVIRONMENT_VARIABLE @@ -13,6 +13,7 @@ from django.utils.crypto import get_random_string from django.utils.translation import activate, check_for_language, get_language from django.utils.translation import ugettext as _ +DEVELOPMENT_VERSION = 'Development Version' UNIX_VERSION = 'Unix Version' WINDOWS_VERSION = 'Windows Version' WINDOWS_PORTABLE_VERSION = 'Windows Portable Version' @@ -30,6 +31,15 @@ class DatabaseInSettingsError(Exception): pass +class UnknownCommand(Exception): + pass + + +class ExceptionArgumentParser(argparse.ArgumentParser): + def error(self, message): + raise UnknownCommand(message) + + def detect_openslides_type(): """ Returns the type of this OpenSlides version. @@ -50,16 +60,19 @@ def detect_openslides_type(): return openslides_type -def get_default_settings_path(openslides_type): +def get_default_settings_path(openslides_type=None): """ 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 is None: + openslides_type = detect_openslides_type() + if openslides_type == UNIX_VERSION: parent_directory = os.environ.get( - 'XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) + 'XDG_CONFIG_HOME', os.path.expanduser('~/.config')) elif openslides_type == WINDOWS_VERSION: parent_directory = get_win32_app_data_path() elif openslides_type == WINDOWS_PORTABLE_VERSION: @@ -69,31 +82,52 @@ def get_default_settings_path(openslides_type): return os.path.join(parent_directory, 'openslides', 'settings.py') -def setup_django_settings_module(settings_path): +def get_development_settings_path(): + """ + Returns the path to a local development settings. + + On Unix systems: 'development/settings.py' + """ + return os.path.join('development', 'settings.py') + + +def setup_django_settings_module(settings_path=None, development=None): """ Sets the environment variable ENVIRONMENT_VARIABLE, that means 'DJANGO_SETTINGS_MODULE', to the given settings. + + If no settings_path is given and the environment variable is already set, + then this function does nothing. + + If the argument settings_path is set, then the environment variable is + always overwritten. """ + if settings_path is None and os.environ.get(ENVIRONMENT_VARIABLE, None): + return + + if settings_path is None: + if development: + settings_path = get_development_settings_path() + else: + settings_path = get_default_settings_path() + settings_file = os.path.basename(settings_path) settings_module_name = ".".join(settings_file.split('.')[:-1]) if '.' in settings_module_name: 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? + + # Change the python path. Also set the environment variable python path, so + # change of the python path also works after a reload + settings_module_dir = os.path.abspath(os.path.dirname(settings_path)) sys.path.insert(0, settings_module_dir) - os.environ[ENVIRONMENT_VARIABLE] = '%s' % settings_module_name + try: + os.environ['PYTHONPATH'] = os.pathsep.join((settings_module_dir, os.environ['PYTHONPATH'])) + except KeyError: + # The environment variable is empty + os.environ['PYTHONPATH'] = settings_module_dir - -def ensure_settings(settings, args): - """ - Create settings if a settings path is given and this file still does not exist. - """ - if settings and not os.path.exists(settings): - if not hasattr(args, 'user_data_path'): - context = get_default_settings_context() - else: - context = get_default_settings_context(args.user_data_path) - write_settings(settings, **context) - print('Settings file at %s successfully created.' % settings) + # Set the environment variable to the settings module + os.environ[ENVIRONMENT_VARIABLE] = settings_module_name def get_default_settings_context(user_data_path=None): @@ -132,7 +166,7 @@ def get_default_user_data_path(openslides_type): """ if openslides_type == UNIX_VERSION: default_user_data_path = os.environ.get( - 'XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share')) + 'XDG_DATA_HOME', 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: @@ -192,11 +226,16 @@ def get_win32_portable_user_data_path(): return os.path.join(get_win32_portable_path(), 'openslides') -def write_settings(settings_path, template=None, **context): +def write_settings(settings_path=None, template=None, **context): """ Creates the settings file at the given path using the given values for the file template. + + Retuns the path to the created settings. """ + if settings_path is None: + settings_path = get_default_settings_path() + if template is None: with open(os.path.join(os.path.dirname(__file__), 'settings.py.tpl')) as template_file: template = template_file.read() @@ -205,56 +244,16 @@ def write_settings(settings_path, template=None, **context): # from django.core.management.commands.startproject chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' context.setdefault('secret_key', get_random_string(50, chars)) + for key, value in get_default_settings_context().items(): + context.setdefault(key, value) + content = template % context settings_module = os.path.realpath(os.path.dirname(settings_path)) if not os.path.exists(settings_module): os.makedirs(settings_module) with open(settings_path, 'w') as settings_file: settings_file.write(content) - - -def get_port(address, port): - """ - Checks if the port for the server is available and returns it the port. If - it is port 80, try also port 8000. - - The argument 'address' should be an IP address. The argument 'port' should - be an integer. - """ - s = socket.socket() - try: - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind((address, port)) - s.listen(-1) - except socket.error: - error = True - else: - error = False - finally: - s.close() - if error: - if port == 80: - port = get_port(address, 8000) - else: - raise PortIsBlockedError('Port %d is not available. Try another port using the --port option.' % port) - 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 + return os.path.realpath(settings_path) def start_browser(browser_url): @@ -277,6 +276,8 @@ def get_database_path_from_settings(): """ Retrieves the database path out of the settings file. Returns None, if it is not a SQLite3 database. + + Needed for the backupdb command. """ from django.conf import settings as django_settings from django.db import DEFAULT_DB_ALIAS @@ -304,3 +305,12 @@ def translate_customizable_strings(language_code): for name in config.get_all_translatable(): config[name] = _(config[name]) activate(current_language) + + +def is_development(): + """ + Returns True if the command is called for development. + + This is the case if manage.py is used, or when the --development flag is set. + """ + return True if '--development' in sys.argv or 'manage.py' in sys.argv[0] else False diff --git a/openslides/utils/settings.py.tpl b/openslides/utils/settings.py.tpl index b288f3574..8018f60a2 100644 --- a/openslides/utils/settings.py.tpl +++ b/openslides/utils/settings.py.tpl @@ -25,7 +25,7 @@ SECRET_KEY = %(secret_key)r # Use 'DEBUG = True' to get more details for server errors. # SECURITY WARNING: Don't run with debug turned on in production! -DEBUG = True +DEBUG = %(debug)s TEMPLATE_DEBUG = DEBUG diff --git a/openslides/utils/test.py b/openslides/utils/test.py index 7fecf094d..39ff72c71 100644 --- a/openslides/utils/test.py +++ b/openslides/utils/test.py @@ -2,18 +2,12 @@ from django.core.management import call_command from django.test import TestCase as _TestCase from openslides.config.api import config -from openslides.core.signals import post_database_setup class TestCase(_TestCase): """ - Overwrites Django's TestCase class to call the post_database_setup - signal after the preparation of every test. Also refreshs the config cache. + Overwrites Django's TestCase class to refreshs the config cache. """ - def _pre_setup(self, *args, **kwargs): - return_value = super(TestCase, self)._pre_setup(*args, **kwargs) - post_database_setup.send(sender=self) - return return_value def _post_teardown(self, *args, **kwargs): return_value = super(TestCase, self)._post_teardown(*args, **kwargs) diff --git a/tests/settings.py b/tests/settings.py index 09f9234db..8aa5bec62 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -21,14 +21,6 @@ SECRET_KEY = 'secret' DEBUG = True TEMPLATE_DEBUG = DEBUG - -# OpenSlides plugins -# Add plugins to this list. - -INSTALLED_PLUGINS += ( - 'tests.utils', -) - INSTALLED_APPS += INSTALLED_PLUGINS diff --git a/tests/utils/models.py b/tests/utils/models.py deleted file mode 100644 index cd5c87cb9..000000000 --- a/tests/utils/models.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.db import models - - -class DummyModel(models.Model): - """ - Dummy model to test some model views. - """ - title = models.CharField(max_length=255) diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py index 5113b5216..b2df4d48b 100644 --- a/tests/utils/test_main.py +++ b/tests/utils/test_main.py @@ -2,78 +2,76 @@ import os import sys from unittest.mock import MagicMock, patch -from django.core.exceptions import ImproperlyConfigured - -from openslides.__main__ import ( - add_general_arguments, - django_command_line_utility, - start, - syncdb) from openslides.config.api import config -from openslides.utils.main import ( - get_browser_url, - get_database_path_from_settings, - get_default_settings_context, - get_default_settings_path, - get_default_user_data_path, - get_port, - PortIsBlockedError, - setup_django_settings_module, - start_browser, - translate_customizable_strings, - UNIX_VERSION, - WINDOWS_PORTABLE_VERSION) +from openslides.utils import main from openslides.utils.test import TestCase class TestFunctions(TestCase): - def test_get_default_user_data_path(self): - self.assertIn(os.path.join('.local', 'share'), get_default_user_data_path(UNIX_VERSION)) + @patch('openslides.utils.main.sys') + def test_detect_openslides_type_unix(self, mock_sys): + """ + Tests the return value on a unix system. + """ + mock_sys.platform = 'linux' + self.assertEqual(main.detect_openslides_type(), main.UNIX_VERSION) - def test_get_default_settings_path(self): - self.assertIn( - os.path.join('.config', 'openslides', 'settings.py'), get_default_settings_path(UNIX_VERSION)) + @patch('openslides.utils.main.os.path.basename') + @patch('openslides.utils.main.sys') + def test_detect_openslides_type_win_portable(self, mock_sys, mock_os): + """ + Tests the return value on a windows portable system. + """ + mock_sys.platform = 'win32' + mock_os.return_value = 'openslides.exe' + self.assertEqual(main.detect_openslides_type(), main.WINDOWS_PORTABLE_VERSION) + + @patch('openslides.utils.main.os.path.basename') + @patch('openslides.utils.main.sys') + def test_detect_openslides_type_win(self, mock_sys, mock_os): + """ + Tests the return value on a windows system. + """ + mock_sys.platform = 'win32' + mock_os.return_value = 'python' + self.assertEqual(main.detect_openslides_type(), main.WINDOWS_VERSION) + + @patch('openslides.utils.main.detect_openslides_type') + @patch('openslides.utils.main.os.path.expanduser') + def test_get_default_settings_path_unix(self, mock_expanduser, mock_detect): + mock_expanduser.return_value = '/home/test/.config' + self.assertEqual(main.get_default_settings_path(main.UNIX_VERSION), + '/home/test/.config/openslides/settings.py') + + @patch('openslides.utils.main.get_win32_app_data_path') + def test_get_default_settings_path_win(self, mock_win): + mock_win.return_value = 'win32' + self.assertEqual(main.get_default_settings_path(main.WINDOWS_VERSION), + 'win32/openslides/settings.py') + + @patch('openslides.utils.main.get_win32_portable_path') + def test_get_default_settings_path_portable(self, mock_portable): + mock_portable.return_value = 'portable' + self.assertEqual(main.get_default_settings_path(main.WINDOWS_PORTABLE_VERSION), + 'portable/openslides/settings.py') + + def test_get_development_settings_path(self): + self.assertEqual(main.get_development_settings_path(), os.sep.join(('development', 'settings.py'))) + + def test_setup_django_settings_module(self): + main.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], os.path.abspath('test_dir_dhvnghfjdh456fzheg2f')) @patch('openslides.utils.main.detect_openslides_type') def test_get_default_settings_context_portable(self, detect_mock): - detect_mock.return_value = WINDOWS_PORTABLE_VERSION - context = get_default_settings_context() + detect_mock.return_value = main.WINDOWS_PORTABLE_VERSION + context = main.get_default_settings_context() self.assertEqual(context['openslides_user_data_path'], 'get_win32_portable_user_data_path()') - 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') - - def test_get_port(self): - class MyException(Exception): - pass - - def block_some_ports(mock): - if not 8000 == mock_socket.socket().bind.call_args[0][0][1]: - raise MyException - - # Test open port - self.assertEqual(get_port('localhost', 8234), 8234) - # Test blocked ports - with patch('openslides.utils.main.socket') as mock_socket: - mock_socket.error = MyException - mock_socket.socket().listen = MagicMock(side_effect=block_some_ports) - self.assertEqual(get_port('localhost', 80), 8000) - self.assertRaises(PortIsBlockedError, get_port, 'localhost', 81) + def test_get_default_user_data_path(self): + self.assertIn(os.path.join('.local', 'share'), main.get_default_user_data_path(main.UNIX_VERSION)) @patch('openslides.utils.main.threading.Thread') @patch('openslides.utils.main.time') @@ -81,53 +79,18 @@ class TestFunctions(TestCase): def test_start_browser(self, mock_webbrowser, mock_time, mock_Thread): browser_mock = MagicMock() mock_webbrowser.get.return_value = browser_mock - start_browser('http://localhost:8234') + + main.start_browser('http://localhost:8234') + self.assertTrue(mock_Thread.called) inner_function = mock_Thread.call_args[1]['target'] inner_function() browser_mock.open.assert_called_with('http://localhost:8234') def test_get_database_path_from_settings_memory(self): - self.assertEqual(get_database_path_from_settings(), ':memory:') + self.assertEqual(main.get_database_path_from_settings(), ':memory:') def test_translate_customizable_strings(self): self.assertEqual(config['event_description'], 'Presentation and assembly system') - translate_customizable_strings('de') + main.translate_customizable_strings('de') self.assertEqual(config['event_description'], u'Präsentations- und Versammlungssystem') - - -class TestOtherFunctions(TestCase): - """ - Tests functions in openslides.__main__ - """ - def test_add_general_arguments_wrong_arg(self): - self.assertRaisesMessage( - TypeError, - 'The argument invalid_argument is not a valid general argument.', - add_general_arguments, - None, - ['invalid_argument']) - - @patch('openslides.__main__.syncdb') - @patch('openslides.__main__.runserver') - def test_start(self, mock_runserver, mock_syncdb): - mock_args = MagicMock() - start(settings=None, args=mock_args) - self.assertTrue(mock_syncdb.called) - mock_runserver.assert_called_with(None, mock_args) - - @patch('openslides.__main__.os.path.exists') - @patch('openslides.__main__.os.makedirs') - @patch('openslides.__main__.execute_from_command_line') - def test_syncdb(self, mock_execute_from_command_line, mock_os, mock_exists): - mock_exists.return_value = True - mock_args = MagicMock() - mock_args.language = None - syncdb(settings=None, args=mock_args) - self.assertTrue(mock_execute_from_command_line.called) - - @patch('openslides.__main__.execute_from_command_line') - def test_django_command_line_utility(self, mock_execute_from_command_line): - mock_args = MagicMock() - django_command_line_utility(settings=None, args=mock_args) - self.assertTrue(mock_execute_from_command_line.called) diff --git a/tests/utils/test_views.py b/tests/utils/test_views.py index 66515ff5a..850cd7fec 100644 --- a/tests/utils/test_views.py +++ b/tests/utils/test_views.py @@ -3,7 +3,6 @@ from unittest.mock import patch from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import clear_url_caches -from django.db import connection, reset_queries from django.test import RequestFactory from django.test.client import Client from django.test.utils import override_settings @@ -13,7 +12,6 @@ from openslides.utils.signals import template_manipulation from openslides.utils.test import TestCase from . import views as test_views -from .models import DummyModel @override_settings(ROOT_URLCONF='tests.utils.urls') @@ -194,15 +192,22 @@ class QuestionViewTest(ViewTestCase): self.assertIn('the question', question) -class DetailViewTest(ViewTestCase): - def test_get_object_cache(self): - with self.settings(DEBUG=True): - DummyModel.objects.create(title='title_ooth8she7yos1Oi8Boh3') - reset_queries() - client = Client() - response = client.get('/dummy_detail_view/1/') - self.assertContains(response, 'title_ooth8she7yos1Oi8Boh3') - self.assertEqual(len(connection.queries), 3) +class SingleObjectMixinTest(TestCase): + + @patch('openslides.utils.views.django_views.detail.SingleObjectMixin.get_object') + def test_get_object_cache(self, mock_super_class_get_object): + """ + Test that the method get_object caches his result. + + Tests that get_object from the django view is only called once, even if + get_object on our class is called twice. + """ + view = views.SingleObjectMixin() + + view.get_object() + view.get_object() + + mock_super_class_get_object.assert_called_once_with() def set_context(sender, request, context, **kwargs): diff --git a/tests/utils/urls.py b/tests/utils/urls.py index 1f7a87312..66c9d434f 100644 --- a/tests/utils/urls.py +++ b/tests/utils/urls.py @@ -26,7 +26,4 @@ urlpatterns += patterns( url(r'^permission_mixin3/$', views.PermissionMixinView.as_view(required_permission='agenda.can_see_agenda')), - - url(r'^dummy_detail_view/(?P\d+)/$', - views.DummyDetailView.as_view()), ) diff --git a/tests/utils/views.py b/tests/utils/views.py index c9e7c4579..8af2be1ac 100644 --- a/tests/utils/views.py +++ b/tests/utils/views.py @@ -2,8 +2,6 @@ from django.http import HttpResponse from openslides.utils import views -from .models import DummyModel - class GetAbsoluteUrl(object): """ @@ -47,15 +45,3 @@ class UrlMixinView(views.UrlMixin, views.View): class UrlMixinViewWithObject(views.UrlMixin, views.View): object = GetAbsoluteUrl() - - -class DummyDetailView(views.DetailView): - model = DummyModel - - def get_context_data(self, **context): - context = super(DummyDetailView, self).get_context_data(**context) - # Just call get_object() some times to test the cache - self.get_object() - self.get_object() - self.get_object() - return context