Merge pull request #1412 from ostcar/management

Rework of management command
This commit is contained in:
Oskar Hahn 2015-01-23 15:48:14 +01:00
commit 0b29d13dc6
26 changed files with 578 additions and 728 deletions

10
.gitignore vendored
View File

@ -7,16 +7,12 @@
.virtualenv/* .virtualenv/*
.venv/* .venv/*
# Node modules # Javascript tools and libraries
node_modules/* node_modules/*
bower_components/*
# Development user data (settings, database, media, search index, static files) # Development user data (settings, database, media, search index, static files)
settings.py development/*
!tests/settings.py
database.sqlite
media/*
whoosh_index/*
bower_components/*
openslides/static/* openslides/static/*
# Package building/IDE # Package building/IDE

View File

@ -8,6 +8,6 @@ install:
- "node_modules/.bin/bower install" - "node_modules/.bin/bower install"
- "node_modules/.bin/gulp --production" - "node_modules/.bin/gulp --production"
script: 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" - "coverage report -m --fail-under=80"
- "flake8 --max-line-length=150 --statistics openslides tests" - "flake8 --max-line-length=150 --statistics openslides tests"

View File

@ -11,7 +11,7 @@ def test(args=None):
""" """
module = getattr(args, 'module', '') module = getattr(args, 'module', '')
return call("DJANGO_SETTINGS_MODULE='tests.settings' coverage run " 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') @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 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. a plain report and fails under a certain amount of untested lines.
""" """
test()
if plain is None: if plain is None:
plain = getattr(args, 'plain', False) plain = getattr(args, 'plain', False)
if plain: if plain:

View File

@ -1,59 +1,50 @@
#!/usr/bin/env python #!/usr/bin/env python
import argparse
import os import os
import shutil
import sys import sys
from django.conf import ENVIRONMENT_VARIABLE
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
from openslides import get_version from openslides import get_version
from openslides.utils.main import ( from openslides.utils.main import (
detect_openslides_type,
ensure_settings,
get_browser_url,
get_database_path_from_settings,
get_default_settings_path, get_default_settings_path,
get_default_user_data_path,
get_port,
setup_django_settings_module, setup_django_settings_module,
write_settings,
UnknownCommand,
ExceptionArgumentParser,
get_development_settings_path,
start_browser, start_browser,
translate_customizable_strings, is_development)
write_settings)
def main(): def main():
""" """
Main entrance to OpenSlides. Main entrance to OpenSlides.
""" """
# Parse all command line args. parser = get_parser()
args = parse_args() try:
known_args, unknown_args = parser.parse_known_args()
# Setup settings path: Take it either from command line or get default path except UnknownCommand:
if hasattr(args, 'settings') and args.settings: unknown_command = True
settings = args.settings
setup_django_settings_module(settings)
else: else:
if ENVIRONMENT_VARIABLE not in os.environ: unknown_command = False
openslides_type = detect_openslides_type()
settings = get_default_settings_path(openslides_type) if unknown_command:
setup_django_settings_module(settings) # 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: else:
# The environment variable is set, so we do not need to process # Run a command that is defined here
# anything more here. # These are commands that can not rely on an existing settings
settings = None known_args.callback(known_args)
# Process the subcommand's callback
return args.callback(settings, args)
def parse_args(): def get_parser():
""" """
Parses all command line arguments. The subcommand 'django' links to Parses all command line arguments.
Django's command-line utility.
""" """
if len(sys.argv) == 1: if len(sys.argv) == 1 and not is_development():
sys.argv.append('start') sys.argv.append('start')
# Init parser # Init parser
@ -64,7 +55,7 @@ def parse_args():
'That means OpenSlides will setup default settings and ' 'That means OpenSlides will setup default settings and '
'database, start the tornado webserver, launch the ' 'database, start the tornado webserver, launch the '
'default web browser and open the webinterface.') 'default web browser and open the webinterface.')
parser = argparse.ArgumentParser(description=description) parser = ExceptionArgumentParser(description=description)
# Add version argument # Add version argument
parser.add_argument( parser.add_argument(
@ -85,284 +76,88 @@ def parse_args():
subcommand_start = subparsers.add_parser( subcommand_start = subparsers.add_parser(
'start', 'start',
help='Setup settings and database, start tornado webserver, launch the ' help='Setup settings and database, start tornado webserver, launch the '
'default web browser and open the webinterface.') 'default web browser and open the webinterface. The environment '
add_general_arguments(subcommand_start, ('settings', 'user_data_path', 'language', 'address', 'port')) 'variable DJANGO_SETTINGS_MODULE is ignored.')
subcommand_start.add_argument( subcommand_start.add_argument(
'--no-browser', '--no-browser',
action='store_true', action='store_true',
help='Do not launch the default web browser.') 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_start.set_defaults(callback=start)
subcommand_start.add_argument(
# Subcommand runserver '--development',
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',
action='store_true', action='store_true',
help='Launch the default web browser and open the webinterface.') help='Command for development purposes.')
subcommand_runserver.set_defaults(callback=runserver)
# Subcommand syncdb # Subcommand createsettings
subcommand_syncdb = subparsers.add_parser( subcommand_createsettings = subparsers.add_parser(
'syncdb', 'createsettings',
help='Create or update database tables.') help='Create the settings file.')
add_general_arguments(subcommand_syncdb, ('settings', 'user_data_path', 'language')) subcommand_createsettings.set_defaults(callback=createsettings)
subcommand_syncdb.set_defaults(callback=syncdb) 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 return parser
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 def start(args):
subcommand_deletedb = subparsers.add_parser( """
'deletedb', Starts OpenSlides: Runs migrations and runs runserver.
help='Delete the SQLite3 database.') """
add_general_arguments(subcommand_deletedb, ('settings', 'user_data_path')) settings_path = args.settings_path
subcommand_deletedb.set_defaults(callback=deletedb) development = is_development()
# Subcommand create-dev-settings if settings_path is None:
subcommand_create_dev_settings = subparsers.add_parser( if development:
'create-dev-settings', settings_path = get_development_settings_path()
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: else:
if unknown_args: settings_path = get_default_settings_path()
parser.error('Unknown arguments %s found.' % ' '.join(unknown_args))
return known_args # 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 add_general_arguments(subcommand, arguments): def createsettings(args):
""" """
Adds the named arguments to the subcommand. Creates settings for OpenSlides.
""" """
general_arguments = {} settings_path = args.settings_path
openslides_type = detect_openslides_type() 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.'))
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 start(settings, args):
"""
Starts OpenSlides: Runs syncdb and runs runserver (tornado webserver).
"""
ensure_settings(settings, args)
syncdb(settings, args)
args.start_browser = not args.no_browser
runserver(settings, args)
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 = {}
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
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 django_command_line_utility(settings, args): settings_path = write_settings(settings_path, **context)
""" print('Settings created at %s' % settings_path)
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
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -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.')

View File

@ -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.')

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -1,12 +1,9 @@
from django import forms from django import forms
from django.dispatch import Signal
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable
post_database_setup = Signal()
def setup_general_config(sender, **kwargs): def setup_general_config(sender, **kwargs):
""" """

View File

@ -79,6 +79,7 @@ MIDDLEWARE_CLASSES = (
ROOT_URLCONF = 'openslides.urls' ROOT_URLCONF = 'openslides.urls'
INSTALLED_APPS = ( INSTALLED_APPS = (
'openslides.core',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
@ -90,7 +91,6 @@ INSTALLED_APPS = (
'ckeditor', 'ckeditor',
'rest_framework', 'rest_framework',
'openslides.poll', 'openslides.poll',
'openslides.core',
'openslides.account', 'openslides.account',
'openslides.projector', 'openslides.projector',
'openslides.agenda', 'openslides.agenda',

View File

@ -1,4 +1,5 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import post_migrate
class MotionAppConfig(AppConfig): class MotionAppConfig(AppConfig):
@ -12,13 +13,12 @@ class MotionAppConfig(AppConfig):
# Import all required stuff. # Import all required stuff.
from openslides.config.signals import config_signal 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.projector.api import register_slide_model
from .signals import create_builtin_workflows, setup_motion_config from .signals import create_builtin_workflows, setup_motion_config
# Connect signals. # Connect signals.
config_signal.connect(setup_motion_config, dispatch_uid='setup_motion_config') 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. # Register slides.
Motion = self.get_model('Motion') Motion = self.get_model('Motion')

View File

@ -172,8 +172,11 @@ def create_builtin_workflows(sender, **kwargs):
connected to the signal openslides.core.signals.post_database_setup connected to the signal openslides.core.signals.post_database_setup
during app loading. during app loading.
""" """
workflow_1, created = Workflow.objects.get_or_create(name=ugettext_noop('Simple Workflow')) if Workflow.objects.exists():
if created: # If there is at least one workflow, then do nothing.
return
workflow_1 = Workflow.objects.create(name=ugettext_noop('Simple Workflow'))
state_1_1 = State.objects.create(name=ugettext_noop('submitted'), state_1_1 = State.objects.create(name=ugettext_noop('submitted'),
workflow=workflow_1, workflow=workflow_1,
allow_create_poll=True, allow_create_poll=True,
@ -192,8 +195,7 @@ def create_builtin_workflows(sender, **kwargs):
workflow_1.first_state = state_1_1 workflow_1.first_state = state_1_1
workflow_1.save() workflow_1.save()
workflow_2, created = Workflow.objects.get_or_create(name=ugettext_noop('Complex Workflow')) workflow_2 = Workflow.objects.create(name=ugettext_noop('Complex Workflow'))
if created:
state_2_1 = State.objects.create(name=ugettext_noop('published'), state_2_1 = State.objects.create(name=ugettext_noop('published'),
workflow=workflow_2, workflow=workflow_2,
allow_support=True, allow_support=True,

View File

@ -2,6 +2,7 @@ from random import choice
from django.contrib.auth.models import Permission, Group from django.contrib.auth.models import Permission, Group
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_noop
from .models import User from .models import User
@ -49,6 +50,95 @@ def get_registered_group():
return Group.objects.get(pk=2) 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(): def create_or_reset_admin_user():
group_staff = Group.objects.get(pk=4) group_staff = Group.objects.get(pk=4)
try: try:

View File

@ -13,10 +13,9 @@ class UsersAppConfig(AppConfig):
# Import all required stuff. # Import all required stuff.
from django.db.models.signals import post_save from django.db.models.signals import post_save
from openslides.config.signals import config_signal 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.projector.api import register_slide_model
from openslides.utils.rest_api import router 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 from .views import UserViewSet
# Load User model. # Load User model.
@ -24,7 +23,6 @@ class UsersAppConfig(AppConfig):
# Connect signals. # Connect signals.
config_signal.connect(setup_users_config, dispatch_uid='setup_users_config') 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') post_save.connect(user_post_save, sender=User, dispatch_uid='users_user_post_save')
# Register slides. # Register slides.

View File

@ -1,14 +1,9 @@
from django import forms 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 as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable 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): def setup_users_config(sender, **kwargs):
""" """
@ -104,100 +99,6 @@ def setup_users_config(sender, **kwargs):
groups=(group_general, group_pdf)) 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): def user_post_save(sender, instance, *args, **kwargs):
""" """
Receiver function to add a new user to the registered group. It is Receiver function to add a new user to the registered group. It is

View File

@ -70,7 +70,7 @@ class OpenSlidesSockJSConnection(SockJSConnection):
waiter.send(data) waiter.send(data)
def run_tornado(addr, port): def run_tornado(addr, port, *args, **kwargs):
""" """
Starts the tornado webserver as wsgi server for OpenSlides. 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 # Don't try to read the command line args from openslides
parse_command_line(args=[]) 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 # Setup WSGIContainer
app = WSGIContainer(get_wsgi_application()) app = WSGIContainer(get_wsgi_application())
@ -101,7 +93,7 @@ def run_tornado(addr, port):
# Start the application # Start the application
debug = settings.DEBUG 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 = HTTPServer(tornado_app)
server.listen(port=port, address=addr) server.listen(port=port, address=addr)
IOLoop.instance().start() IOLoop.instance().start()

View File

@ -1,11 +1,11 @@
import ctypes import ctypes
import os import os
import socket
import sys import sys
import tempfile import tempfile
import threading import threading
import time import time
import webbrowser import webbrowser
import argparse
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.conf import ENVIRONMENT_VARIABLE 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 activate, check_for_language, get_language
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
DEVELOPMENT_VERSION = 'Development Version'
UNIX_VERSION = 'Unix Version' UNIX_VERSION = 'Unix Version'
WINDOWS_VERSION = 'Windows Version' WINDOWS_VERSION = 'Windows Version'
WINDOWS_PORTABLE_VERSION = 'Windows Portable Version' WINDOWS_PORTABLE_VERSION = 'Windows Portable Version'
@ -30,6 +31,15 @@ class DatabaseInSettingsError(Exception):
pass pass
class UnknownCommand(Exception):
pass
class ExceptionArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise UnknownCommand(message)
def detect_openslides_type(): def detect_openslides_type():
""" """
Returns the type of this OpenSlides version. Returns the type of this OpenSlides version.
@ -50,16 +60,19 @@ def detect_openslides_type():
return 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. Returns the default settings path according to the OpenSlides type.
The argument 'openslides_type' has to be one of the three types mentioned in The argument 'openslides_type' has to be one of the three types mentioned in
openslides.utils.main. openslides.utils.main.
""" """
if openslides_type is None:
openslides_type = detect_openslides_type()
if openslides_type == UNIX_VERSION: if openslides_type == UNIX_VERSION:
parent_directory = os.environ.get( 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: elif openslides_type == WINDOWS_VERSION:
parent_directory = get_win32_app_data_path() parent_directory = get_win32_app_data_path()
elif openslides_type == WINDOWS_PORTABLE_VERSION: 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') 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 Sets the environment variable ENVIRONMENT_VARIABLE, that means
'DJANGO_SETTINGS_MODULE', to the given settings. '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_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: if '.' in settings_module_name:
raise ImproperlyConfigured("'.' is not an allowed character in the settings-file") 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) 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
# Set the environment variable to the settings module
def ensure_settings(settings, args): os.environ[ENVIRONMENT_VARIABLE] = settings_module_name
"""
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)
def get_default_settings_context(user_data_path=None): 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: if openslides_type == UNIX_VERSION:
default_user_data_path = os.environ.get( 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: elif openslides_type == WINDOWS_VERSION:
default_user_data_path = get_win32_app_data_path() default_user_data_path = get_win32_app_data_path()
elif openslides_type == WINDOWS_PORTABLE_VERSION: 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') 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 Creates the settings file at the given path using the given values for the
file template. file template.
Retuns the path to the created settings.
""" """
if settings_path is None:
settings_path = get_default_settings_path()
if template is None: if template is None:
with open(os.path.join(os.path.dirname(__file__), 'settings.py.tpl')) as template_file: with open(os.path.join(os.path.dirname(__file__), 'settings.py.tpl')) as template_file:
template = template_file.read() template = template_file.read()
@ -205,56 +244,16 @@ def write_settings(settings_path, template=None, **context):
# from django.core.management.commands.startproject # from django.core.management.commands.startproject
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
context.setdefault('secret_key', get_random_string(50, chars)) 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 content = template % context
settings_module = os.path.realpath(os.path.dirname(settings_path)) settings_module = os.path.realpath(os.path.dirname(settings_path))
if not os.path.exists(settings_module): if not os.path.exists(settings_module):
os.makedirs(settings_module) os.makedirs(settings_module)
with open(settings_path, 'w') as settings_file: with open(settings_path, 'w') as settings_file:
settings_file.write(content) settings_file.write(content)
return os.path.realpath(settings_path)
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
def start_browser(browser_url): 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, Retrieves the database path out of the settings file. Returns None,
if it is not a SQLite3 database. if it is not a SQLite3 database.
Needed for the backupdb command.
""" """
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
@ -304,3 +305,12 @@ def translate_customizable_strings(language_code):
for name in config.get_all_translatable(): for name in config.get_all_translatable():
config[name] = _(config[name]) config[name] = _(config[name])
activate(current_language) 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

View File

@ -25,7 +25,7 @@ SECRET_KEY = %(secret_key)r
# Use 'DEBUG = True' to get more details for server errors. # Use 'DEBUG = True' to get more details for server errors.
# SECURITY WARNING: Don't run with debug turned on in production! # SECURITY WARNING: Don't run with debug turned on in production!
DEBUG = True DEBUG = %(debug)s
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG

View File

@ -2,18 +2,12 @@ from django.core.management import call_command
from django.test import TestCase as _TestCase from django.test import TestCase as _TestCase
from openslides.config.api import config from openslides.config.api import config
from openslides.core.signals import post_database_setup
class TestCase(_TestCase): class TestCase(_TestCase):
""" """
Overwrites Django's TestCase class to call the post_database_setup Overwrites Django's TestCase class to refreshs the config cache.
signal after the preparation of every test. Also 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): def _post_teardown(self, *args, **kwargs):
return_value = super(TestCase, self)._post_teardown(*args, **kwargs) return_value = super(TestCase, self)._post_teardown(*args, **kwargs)

View File

@ -21,14 +21,6 @@ SECRET_KEY = 'secret'
DEBUG = True DEBUG = True
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
# OpenSlides plugins
# Add plugins to this list.
INSTALLED_PLUGINS += (
'tests.utils',
)
INSTALLED_APPS += INSTALLED_PLUGINS INSTALLED_APPS += INSTALLED_PLUGINS

View File

@ -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)

View File

@ -2,78 +2,76 @@ import os
import sys import sys
from unittest.mock import MagicMock, patch 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.config.api import config
from openslides.utils.main import ( from openslides.utils import main
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.test import TestCase from openslides.utils.test import TestCase
class TestFunctions(TestCase): class TestFunctions(TestCase):
def test_get_default_user_data_path(self): @patch('openslides.utils.main.sys')
self.assertIn(os.path.join('.local', 'share'), get_default_user_data_path(UNIX_VERSION)) 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): @patch('openslides.utils.main.os.path.basename')
self.assertIn( @patch('openslides.utils.main.sys')
os.path.join('.config', 'openslides', 'settings.py'), get_default_settings_path(UNIX_VERSION)) 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') @patch('openslides.utils.main.detect_openslides_type')
def test_get_default_settings_context_portable(self, detect_mock): def test_get_default_settings_context_portable(self, detect_mock):
detect_mock.return_value = WINDOWS_PORTABLE_VERSION detect_mock.return_value = main.WINDOWS_PORTABLE_VERSION
context = get_default_settings_context() context = main.get_default_settings_context()
self.assertEqual(context['openslides_user_data_path'], 'get_win32_portable_user_data_path()') self.assertEqual(context['openslides_user_data_path'], 'get_win32_portable_user_data_path()')
def test_setup_django_settings_module(self): def test_get_default_user_data_path(self):
setup_django_settings_module('test_dir_dhvnghfjdh456fzheg2f/test_path_bngjdhc756dzwncshdfnx.py') self.assertIn(os.path.join('.local', 'share'), main.get_default_user_data_path(main.UNIX_VERSION))
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)
@patch('openslides.utils.main.threading.Thread') @patch('openslides.utils.main.threading.Thread')
@patch('openslides.utils.main.time') @patch('openslides.utils.main.time')
@ -81,53 +79,18 @@ class TestFunctions(TestCase):
def test_start_browser(self, mock_webbrowser, mock_time, mock_Thread): def test_start_browser(self, mock_webbrowser, mock_time, mock_Thread):
browser_mock = MagicMock() browser_mock = MagicMock()
mock_webbrowser.get.return_value = browser_mock mock_webbrowser.get.return_value = browser_mock
start_browser('http://localhost:8234')
main.start_browser('http://localhost:8234')
self.assertTrue(mock_Thread.called) self.assertTrue(mock_Thread.called)
inner_function = mock_Thread.call_args[1]['target'] inner_function = mock_Thread.call_args[1]['target']
inner_function() inner_function()
browser_mock.open.assert_called_with('http://localhost:8234') browser_mock.open.assert_called_with('http://localhost:8234')
def test_get_database_path_from_settings_memory(self): 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): def test_translate_customizable_strings(self):
self.assertEqual(config['event_description'], 'Presentation and assembly system') 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') 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)

View File

@ -3,7 +3,6 @@ from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import clear_url_caches from django.core.urlresolvers import clear_url_caches
from django.db import connection, reset_queries
from django.test import RequestFactory from django.test import RequestFactory
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings 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 openslides.utils.test import TestCase
from . import views as test_views from . import views as test_views
from .models import DummyModel
@override_settings(ROOT_URLCONF='tests.utils.urls') @override_settings(ROOT_URLCONF='tests.utils.urls')
@ -194,15 +192,22 @@ class QuestionViewTest(ViewTestCase):
self.assertIn('the question', question) self.assertIn('the question', question)
class DetailViewTest(ViewTestCase): class SingleObjectMixinTest(TestCase):
def test_get_object_cache(self):
with self.settings(DEBUG=True): @patch('openslides.utils.views.django_views.detail.SingleObjectMixin.get_object')
DummyModel.objects.create(title='title_ooth8she7yos1Oi8Boh3') def test_get_object_cache(self, mock_super_class_get_object):
reset_queries() """
client = Client() Test that the method get_object caches his result.
response = client.get('/dummy_detail_view/1/')
self.assertContains(response, 'title_ooth8she7yos1Oi8Boh3') Tests that get_object from the django view is only called once, even if
self.assertEqual(len(connection.queries), 3) 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): def set_context(sender, request, context, **kwargs):

View File

@ -26,7 +26,4 @@ urlpatterns += patterns(
url(r'^permission_mixin3/$', url(r'^permission_mixin3/$',
views.PermissionMixinView.as_view(required_permission='agenda.can_see_agenda')), views.PermissionMixinView.as_view(required_permission='agenda.can_see_agenda')),
url(r'^dummy_detail_view/(?P<pk>\d+)/$',
views.DummyDetailView.as_view()),
) )

View File

@ -2,8 +2,6 @@ from django.http import HttpResponse
from openslides.utils import views from openslides.utils import views
from .models import DummyModel
class GetAbsoluteUrl(object): class GetAbsoluteUrl(object):
""" """
@ -47,15 +45,3 @@ class UrlMixinView(views.UrlMixin, views.View):
class UrlMixinViewWithObject(views.UrlMixin, views.View): class UrlMixinViewWithObject(views.UrlMixin, views.View):
object = GetAbsoluteUrl() 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