From 800055a5eaff28f65afe2bd7fb2a420804255ffc Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Sun, 6 Jan 2019 16:15:56 +0100 Subject: [PATCH 1/2] Format code with black --- .travis.yml | 3 +-- make/commands.py | 9 +++++---- requirements/development.txt | 1 + setup.cfg | 3 +++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index fbd5a0633..09da80096 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ matrix: - pip install --upgrade .[big_mode] - pip freeze script: - - flake8 openslides tests - - isort --check-only --diff --recursive openslides tests - python -m mypy openslides/ tests/ - python -W ignore -m pytest --cov --cov-fail-under=70 @@ -36,6 +34,7 @@ matrix: script: - flake8 openslides tests - isort --check-only --diff --recursive openslides tests + - black --check --diff --py36 openslides tests - python -m mypy openslides/ tests/ - python -W ignore -m pytest --cov --cov-fail-under=70 diff --git a/make/commands.py b/make/commands.py index a190688c3..5e7f359fb 100644 --- a/make/commands.py +++ b/make/commands.py @@ -65,7 +65,7 @@ def min_requirements(args=None): print(' '.join(get_lowest_versions(args.requirements))) -@command('clear', +@command('clean', help='Deletes unneeded files and folders') def clean(args=None): """ @@ -76,7 +76,8 @@ def clean(args=None): call('find -type d -empty -delete') -@command('isort', - help='Sorts all imports in all python files.') +@command('format', + help='Format code with isort and black') def isort(args=None): - return call('isort --recursive openslides tests') + call('isort --recursive openslides tests') + call('black --py36 openslides tests') diff --git a/requirements/development.txt b/requirements/development.txt index 870c431d1..e03a713f6 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -1,3 +1,4 @@ +black coverage flake8 isort diff --git a/setup.cfg b/setup.cfg index 815a1050d..af6fe7b2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,9 @@ include_trailing_comma = true multi_line_output = 3 lines_after_imports = 2 combine_as_imports = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 known_first_party = openslides known_third_party = pytest From eddbd86d3afc4106cd1fec7c9099fde9cdfa064c Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Sun, 6 Jan 2019 16:22:33 +0100 Subject: [PATCH 2/2] Run black --- make/__main__.py | 2 +- make/commands.py | 50 +- make/parser.py | 7 +- openslides/__init__.py | 10 +- openslides/__main__.py | 170 +- openslides/agenda/__init__.py | 2 +- openslides/agenda/access_permissions.py | 56 +- openslides/agenda/apps.py | 28 +- openslides/agenda/config_variables.py | 129 +- openslides/agenda/migrations/0001_initial.py | 116 +- .../agenda/migrations/0002_item_duration.py | 25 +- .../migrations/0003_auto_20170818_1202.py | 35 +- .../agenda/migrations/0004_speaker_marked.py | 10 +- .../migrations/0005_auto_20180815_1109.py | 49 +- openslides/agenda/models.py | 184 ++- openslides/agenda/projector.py | 19 +- openslides/agenda/serializers.py | 50 +- openslides/agenda/signals.py | 18 +- openslides/agenda/views.py | 229 ++- openslides/assignments/__init__.py | 2 +- openslides/assignments/access_permissions.py | 26 +- openslides/assignments/apps.py | 39 +- openslides/assignments/config_variables.py | 146 +- .../assignments/migrations/0001_initial.py | 216 ++- .../0002_assignmentpoll_pollmethod.py | 15 +- .../migrations/0003_candidate_weight.py | 12 +- .../migrations/0004_auto_20180703_1523.py | 12 +- .../migrations/0005_auto_20180822_1042.py | 48 +- openslides/assignments/models.py | 201 ++- openslides/assignments/projector.py | 21 +- openslides/assignments/serializers.py | 180 ++- openslides/assignments/signals.py | 7 +- openslides/assignments/views.py | 187 ++- openslides/core/__init__.py | 2 +- openslides/core/access_permissions.py | 12 +- openslides/core/apps.py | 105 +- openslides/core/config.py | 166 +- openslides/core/config_variables.py | 532 ++++--- .../core/management/commands/backupdb.py | 20 +- .../core/management/commands/changeconfig.py | 23 +- .../commands/insecurechangepassword.py | 15 +- .../core/management/commands/migrate.py | 4 +- openslides/core/migrations/0001_initial.py | 143 +- .../core/migrations/0002_misc_features.py | 300 ++-- .../migrations/0003_auto_20161217_1158.py | 16 +- .../migrations/0004_auto_20170215_1624.py | 16 +- .../migrations/0005_auto_20170412_1258.py | 28 +- .../migrations/0006_auto_20180123_0903.py | 12 +- .../migrations/0007_auto_20180130_1400.py | 29 +- .../migrations/0008_changed_logo_fields.py | 28 +- .../migrations/0009_auto_20181118_2126.py | 70 +- openslides/core/models.py | 165 +- openslides/core/projector.py | 17 +- openslides/core/serializers.py | 54 +- openslides/core/signals.py | 21 +- openslides/core/urls.py | 14 +- openslides/core/views.py | 457 +++--- openslides/core/websocket.py | 73 +- openslides/global_settings.py | 116 +- openslides/mediafiles/__init__.py | 2 +- openslides/mediafiles/access_permissions.py | 16 +- openslides/mediafiles/apps.py | 19 +- .../mediafiles/migrations/0001_initial.py | 44 +- .../migrations/0002_mediafile_private.py | 25 +- openslides/mediafiles/models.py | 50 +- openslides/mediafiles/projector.py | 7 +- openslides/mediafiles/serializers.py | 38 +- openslides/mediafiles/signals.py | 7 +- openslides/mediafiles/views.py | 52 +- openslides/motions/__init__.py | 2 +- openslides/motions/access_permissions.py | 72 +- openslides/motions/apps.py | 74 +- openslides/motions/config_variables.py | 411 ++--- openslides/motions/exceptions.py | 1 + openslides/motions/migrations/0001_initial.py | 412 +++-- .../motions/migrations/0002_misc_features.py | 176 ++- .../migrations/0003_motion_comments.py | 26 +- ...nchangerecommendation_other_description.py | 10 +- .../migrations/0005_auto_20180202_1318.py | 46 +- .../migrations/0006_submitter_model.py | 55 +- .../0007_motionversion_amendment_data.py | 10 +- .../migrations/0008_auto_20180702_1128.py | 32 +- ...09_motionversion_modified_final_version.py | 10 +- .../migrations/0010_auto_20180822_1042.py | 34 +- .../motions/migrations/0011_motion_version.py | 82 +- .../migrations/0012_motion_comments.py | 193 +-- .../0013_motion_sorting_and_statute.py | 59 +- ...014_motionchangerecommendation_internal.py | 10 +- .../migrations/0015_metadata_permission.py | 26 +- .../0016_merge_amendment_into_final.py | 10 +- .../0017_remove_state_action_word.py | 11 +- openslides/motions/models.py | 387 +++-- openslides/motions/projector.py | 22 +- openslides/motions/serializers.py | 346 ++-- openslides/motions/signals.py | 179 ++- openslides/motions/views.py | 789 ++++++---- openslides/poll/majority.py | 8 +- openslides/poll/models.py | 70 +- openslides/poll/serializers.py | 12 +- openslides/routing.py | 14 +- openslides/topics/__init__.py | 2 +- openslides/topics/access_permissions.py | 3 +- openslides/topics/apps.py | 12 +- openslides/topics/migrations/0001_initial.py | 31 +- openslides/topics/models.py | 15 +- openslides/topics/projector.py | 11 +- openslides/topics/serializers.py | 51 +- openslides/topics/signals.py | 7 +- openslides/topics/views.py | 5 +- openslides/urls.py | 18 +- openslides/urls_apps.py | 4 +- openslides/users/__init__.py | 2 +- openslides/users/access_permissions.py | 52 +- openslides/users/apps.py | 37 +- openslides/users/config_variables.py | 161 +- .../commands/createopenslidesuser.py | 42 +- .../management/commands/createsuperuser.py | 7 +- openslides/users/migrations/0001_initial.py | 115 +- .../0002_user_misc_default_groups.py | 31 +- openslides/users/migrations/0003_group.py | 44 +- .../users/migrations/0004_personalnote.py | 45 +- .../migrations/0005_personalnote_rework.py | 44 +- .../users/migrations/0006_user_email.py | 12 +- .../users/migrations/0007_superadmin.py | 22 +- openslides/users/models.py | 188 ++- openslides/users/projector.py | 7 +- openslides/users/serializers.py | 115 +- openslides/users/signals.py | 202 +-- openslides/users/urls.py | 33 +- openslides/users/views.py | 334 ++-- openslides/utils/access_permissions.py | 10 +- openslides/utils/arguments.py | 2 +- openslides/utils/auth.py | 63 +- openslides/utils/autoupdate.py | 80 +- openslides/utils/cache.py | 141 +- openslides/utils/cache_providers.py | 189 ++- openslides/utils/consumers.py | 71 +- openslides/utils/main.py | 130 +- openslides/utils/middleware.py | 15 +- openslides/utils/migrations.py | 17 +- openslides/utils/models.py | 26 +- openslides/utils/plugins.py | 18 +- openslides/utils/projector.py | 10 +- openslides/utils/redis.py | 5 +- openslides/utils/rest_api.py | 62 +- openslides/utils/utils.py | 15 +- openslides/utils/validate.py | 61 +- openslides/utils/websocket.py | 72 +- tests/conftest.py | 21 +- tests/example_data_generator/__init__.py | 2 +- tests/example_data_generator/apps.py | 8 +- .../commands/create-example-data.py | 194 ++- tests/integration/agenda/test_models.py | 2 +- tests/integration/agenda/test_viewset.py | 337 ++-- tests/integration/assignments/test_viewset.py | 282 ++-- tests/integration/core/test_views.py | 240 +-- tests/integration/core/test_viewset.py | 13 +- tests/integration/helpers.py | 55 +- tests/integration/mediafiles/test_viewset.py | 7 +- tests/integration/motions/test_views.py | 7 +- tests/integration/motions/test_viewset.py | 1385 ++++++++++------- tests/integration/test_plugin/__init__.py | 8 +- tests/integration/test_plugin/apps.py | 5 +- tests/integration/topics/test_viewset.py | 21 +- tests/integration/users/test_views.py | 36 +- tests/integration/users/test_viewset.py | 413 ++--- tests/integration/utils/test_consumers.py | 352 +++-- tests/old/agenda/test_list_of_speakers.py | 24 +- tests/old/config/test_config.py | 108 +- tests/old/motions/test_models.py | 50 +- tests/old/utils/test_main.py | 90 +- tests/settings.py | 24 +- tests/unit/agenda/test_models.py | 12 +- tests/unit/agenda/test_views.py | 66 +- tests/unit/config/test_api.py | 23 +- tests/unit/core/test_views.py | 22 +- tests/unit/core/test_websocket.py | 29 +- tests/unit/motions/test_models.py | 16 +- tests/unit/motions/test_views.py | 7 +- tests/unit/users/test_models.py | 124 +- tests/unit/users/test_serializers.py | 16 +- tests/unit/utils/cache_provider.py | 43 +- tests/unit/utils/test_cache.py | 337 ++-- tests/unit/utils/test_utils.py | 8 +- tests/unit/utils/test_validate.py | 5 +- tests/unit/utils/test_views.py | 12 +- 186 files changed, 9061 insertions(+), 6570 deletions(-) diff --git a/make/__main__.py b/make/__main__.py index 690dee641..3963f77bd 100644 --- a/make/__main__.py +++ b/make/__main__.py @@ -5,7 +5,7 @@ from parser import parser if len(sys.argv) < 2: - args = parser.parse_args(['--help']) + args = parser.parse_args(["--help"]) else: args = parser.parse_args() diff --git a/make/commands.py b/make/commands.py index 5e7f359fb..880f7c52a 100644 --- a/make/commands.py +++ b/make/commands.py @@ -2,47 +2,48 @@ from parser import command, argument, call import yaml import requirements -FAIL = '\033[91m' -SUCCESS = '\033[92m' -RESET = '\033[0m' +FAIL = "\033[91m" +SUCCESS = "\033[92m" +RESET = "\033[0m" -@command('check', help='Checks for pep8 errors in openslides and tests') +@command("check", help="Checks for pep8 errors in openslides and tests") def check(args=None): """ Checks for pep8 and other code styling conventions. """ - value = call('flake8 --max-line-length=150 --statistics openslides tests') - value += call('python -m mypy openslides/ tests/') + value = call("flake8 --max-line-length=150 --statistics openslides tests") + value += call("python -m mypy openslides/ tests/") return value -@command('travis', help='Runs the code that travis does') +@command("travis", help="Runs the code that travis does") def travis(args=None): """ Runs all commands that travis tests. """ return_codes = [] - with open('.travis.yml') as f: + with open(".travis.yml") as f: travis = yaml.load(f) - for line in travis['script']: - print('Run: {}'.format(line)) + for line in travis["script"]: + print("Run: {}".format(line)) return_code = call(line) return_codes.append(return_code) if return_code: - print(FAIL + 'fail!\n' + RESET) + print(FAIL + "fail!\n" + RESET) else: - print(SUCCESS + 'success!\n' + RESET) + print(SUCCESS + "success!\n" + RESET) # Retuns True if one command exited with a different statuscode then 1 return bool(list(filter(bool, return_codes))) -@argument('-r', '--requirements', nargs='?', - default='requirements.txt') -@command('min_requirements', - help='Prints a pip line to install the minimum supported versions of ' - 'the requirements.') +@argument("-r", "--requirements", nargs="?", default="requirements.txt") +@command( + "min_requirements", + help="Prints a pip line to install the minimum supported versions of " + "the requirements.", +) def min_requirements(args=None): """ Prints a pip install command to install the minimal supported versions of a @@ -54,6 +55,7 @@ def min_requirements(args=None): pip install $(python make min_requirements) """ + def get_lowest_versions(requirements_file): with open(requirements_file) as f: for req in requirements.parse(f): @@ -62,22 +64,20 @@ def min_requirements(args=None): if spec == ">=": yield "{}=={}".format(req.name, version) - print(' '.join(get_lowest_versions(args.requirements))) + print(" ".join(get_lowest_versions(args.requirements))) -@command('clean', - help='Deletes unneeded files and folders') +@command("clean", help="Deletes unneeded files and folders") def clean(args=None): """ Deletes all .pyc and .orig files and empty folders. """ call('find -name "*.pyc" -delete') call('find -name "*.orig" -delete') - call('find -type d -empty -delete') + call("find -type d -empty -delete") -@command('format', - help='Format code with isort and black') +@command("format", help="Format code with isort and black") def isort(args=None): - call('isort --recursive openslides tests') - call('black --py36 openslides tests') + call("isort --recursive openslides tests") + call("black --py36 openslides tests") diff --git a/make/parser.py b/make/parser.py index 25fd46618..32f8e5cb5 100644 --- a/make/parser.py +++ b/make/parser.py @@ -1,7 +1,7 @@ from argparse import ArgumentParser from subprocess import call as _call -parser = ArgumentParser(description='Development scripts for OpenSlides') +parser = ArgumentParser(description="Development scripts for OpenSlides") subparsers = parser.add_subparsers() @@ -12,6 +12,7 @@ def command(*args, **kwargs): The arguments to this decorator are used as arguments for the argparse command. """ + class decorator: def __init__(self, func): self.parser = subparsers.add_parser(*args, **kwargs) @@ -34,11 +35,15 @@ def argument(*args, **kwargs): Does only work if the decorated function was decorated with the command-decorator before. """ + def decorator(func): func.parser.add_argument(*args, **kwargs) + def wrapper(*func_args, **func_kwargs): return func(*func_args, **func_kwargs) + return wrapper + return decorator diff --git a/openslides/__init__.py b/openslides/__init__.py index 8ada5daea..0b91bc707 100644 --- a/openslides/__init__.py +++ b/openslides/__init__.py @@ -1,7 +1,7 @@ -__author__ = 'OpenSlides Team ' -__description__ = 'Presentation and assembly system' -__version__ = '3.0-dev' -__license__ = 'MIT' -__url__ = 'https://openslides.org' +__author__ = "OpenSlides Team " +__description__ = "Presentation and assembly system" +__version__ = "3.0-dev" +__license__ = "MIT" +__url__ = "https://openslides.org" args = None diff --git a/openslides/__main__.py b/openslides/__main__.py index 805bf4635..9cda91fe5 100644 --- a/openslides/__main__.py +++ b/openslides/__main__.py @@ -42,7 +42,7 @@ def main(): else: # Check for unknown_args. if unknown_args: - parser.error('Unknown arguments {}'.format(' '.join(unknown_args))) + parser.error("Unknown arguments {}".format(" ".join(unknown_args))) # Save arguments, if one wants to access them later. arguments.set_arguments(known_args) @@ -59,11 +59,11 @@ def get_parser(): if len(sys.argv) == 1: # Use start subcommand if called by openslides console script without # any other arguments. - sys.argv.append('start') + sys.argv.append("start") # Init parser - description = 'Start script for OpenSlides.' - if 'manage.py' not in sys.argv[0]: + description = "Start script for OpenSlides." + if "manage.py" not in sys.argv[0]: description += """ If it is called without any argument, this will be treated as if it is called with the 'start' subcommand. That means @@ -77,109 +77,116 @@ def get_parser(): (without the two hyphen-minus characters) to list them all. Type '%(prog)s help ' for help on a specific subcommand. """ - parser = ExceptionArgumentParser( - description=description, - epilog=epilog) + parser = ExceptionArgumentParser(description=description, epilog=epilog) # Add version argument parser.add_argument( - '--version', - action='version', + "--version", + action="version", version=openslides.__version__, - help='Show version number and exit.') + help="Show version number and exit.", + ) # Init subparsers subparsers = parser.add_subparsers( - dest='subcommand', - title='Available subcommands', + dest="subcommand", + title="Available subcommands", description="Type '%s --help' for help on a " - "specific subcommand." % parser.prog, # type: ignore - help='You can choose only one subcommand at once.', - metavar='') + "specific subcommand." % parser.prog, # type: ignore + help="You can choose only one subcommand at once.", + metavar="", + ) # Subcommand start start_help = ( - 'Setup settings and database, start webserver, launch the ' - 'default web browser and open the webinterface. The environment ' - 'variable DJANGO_SETTINGS_MODULE is ignored.') + "Setup settings and database, start webserver, launch the " + "default web browser and open the webinterface. The environment " + "variable DJANGO_SETTINGS_MODULE is ignored." + ) subcommand_start = subparsers.add_parser( - 'start', - description=start_help, - help=start_help) + "start", description=start_help, help=start_help + ) subcommand_start.set_defaults(callback=start) subcommand_start.add_argument( - '--no-browser', - action='store_true', - help='Do not launch the default web browser.') + "--no-browser", + action="store_true", + help="Do not launch the default web browser.", + ) subcommand_start.add_argument( - '--debug-email', - action='store_true', - help='Change the email backend to console output.') + "--debug-email", + action="store_true", + help="Change the email backend to console output.", + ) subcommand_start.add_argument( - '--no-template-caching', - action='store_true', + "--no-template-caching", + action="store_true", default=False, - help='Disables caching of templates.') + help="Disables caching of templates.", + ) subcommand_start.add_argument( - '--host', - action='store', - default='0.0.0.0', - help='IP address to listen on. Default is 0.0.0.0.') + "--host", + action="store", + default="0.0.0.0", + help="IP address to listen on. Default is 0.0.0.0.", + ) subcommand_start.add_argument( - '--port', - action='store', - default='8000', - help='Port to listen on. Default is 8000.') + "--port", + action="store", + default="8000", + help="Port to listen on. Default is 8000.", + ) subcommand_start.add_argument( - '--settings_dir', - action='store', - default=None, - help='The settings directory.') + "--settings_dir", action="store", default=None, help="The settings directory." + ) subcommand_start.add_argument( - '--settings_filename', - action='store', - default='settings.py', - help='The used settings file name. The file is created, if it does not exist.') + "--settings_filename", + action="store", + default="settings.py", + help="The used settings file name. The file is created, if it does not exist.", + ) subcommand_start.add_argument( - '--local-installation', - action='store_true', - help='Store settings and user files in a local directory.') + "--local-installation", + action="store_true", + help="Store settings and user files in a local directory.", + ) # Subcommand createsettings - createsettings_help = 'Creates the settings file.' + createsettings_help = "Creates the settings file." subcommand_createsettings = subparsers.add_parser( - 'createsettings', - description=createsettings_help, - help=createsettings_help) + "createsettings", description=createsettings_help, help=createsettings_help + ) subcommand_createsettings.set_defaults(callback=createsettings) subcommand_createsettings.add_argument( - '--settings_dir', - action='store', + "--settings_dir", + action="store", default=None, - help='The used settings file directory. All settings files are created, even if they exist.') + help="The used settings file directory. All settings files are created, even if they exist.", + ) subcommand_createsettings.add_argument( - '--settings_filename', - action='store', - default='settings.py', - help='The used settings file name. The file is created, if it does not exist.') + "--settings_filename", + action="store", + default="settings.py", + help="The used settings file name. The file is created, if it does not exist.", + ) subcommand_createsettings.add_argument( - '--local-installation', - action='store_true', - help='Store settings and user files in a local directory.') + "--local-installation", + action="store_true", + help="Store settings and user files in a local directory.", + ) # Help text for several Django subcommands django_subcommands = ( - ('backupdb', 'Backups the SQLite3 database.'), - ('createsuperuser', 'Creates or resets the admin user.'), - ('migrate', 'Updates database schema.'), - ('runserver', 'Starts the Tornado webserver.'), + ("backupdb", "Backups the SQLite3 database."), + ("createsuperuser", "Creates or resets the admin user."), + ("migrate", "Updates database schema."), + ("runserver", "Starts the Tornado webserver."), ) for django_subcommand, help_text in django_subcommands: subparsers._choices_actions.append( # type: ignore subparsers._ChoicesPseudoAction( # type: ignore - django_subcommand, - (), - help_text)) + django_subcommand, (), help_text + ) + ) return parser @@ -188,8 +195,10 @@ def start(args): """ Starts OpenSlides: Runs migrations and runs runserver. """ - raise OpenSlidesError('The start command does not work anymore. ' + - 'Please use `createsettings`, `migrate` and `runserver`.') + raise OpenSlidesError( + "The start command does not work anymore. " + + "Please use `createsettings`, `migrate` and `runserver`." + ) settings_dir = args.settings_dir settings_filename = args.settings_filename local_installation = is_local_installation() @@ -212,10 +221,10 @@ def start(args): from django.conf import settings if args.debug_email: - settings.EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Migrate database - call_command('migrate') + call_command("migrate") # Open the browser if not args.no_browser: @@ -229,8 +238,8 @@ def start(args): # # Use flag --insecure to serve static files even if DEBUG is False. call_command( - 'runserver', - '{}:{}'.format(args.host, args.port), + "runserver", + "{}:{}".format(args.host, args.port), noreload=False, # Means True, see above. insecure=True, ) @@ -248,11 +257,14 @@ def createsettings(args): if settings_dir is None: settings_dir = get_local_settings_dir() context = { - 'openslides_user_data_dir': repr(os.path.join(os.getcwd(), 'personal_data', 'var')), - 'debug': 'True'} + "openslides_user_data_dir": repr( + os.path.join(os.getcwd(), "personal_data", "var") + ), + "debug": "True", + } settings_path = write_settings(settings_dir, args.settings_filename, **context) - print('Settings created at %s' % settings_path) + print("Settings created at %s" % settings_path) if __name__ == "__main__": diff --git a/openslides/agenda/__init__.py b/openslides/agenda/__init__.py index 44b39b695..14ec41a0a 100644 --- a/openslides/agenda/__init__.py +++ b/openslides/agenda/__init__.py @@ -1 +1 @@ -default_app_config = 'openslides.agenda.apps.AgendaAppConfig' +default_app_config = "openslides.agenda.apps.AgendaAppConfig" diff --git a/openslides/agenda/access_permissions.py b/openslides/agenda/access_permissions.py index dee6ffbcc..f0dd12eb5 100644 --- a/openslides/agenda/access_permissions.py +++ b/openslides/agenda/access_permissions.py @@ -8,14 +8,14 @@ class ItemAccessPermissions(BaseAccessPermissions): """ Access permissions container for Item and ItemViewSet. """ - base_permission = 'agenda.can_see' + + base_permission = "agenda.can_see" # TODO: In the following method we use full_data['is_hidden'] and # full_data['is_internal'] but this can be out of date. async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Returns the restricted serialized data for the instance prepared for the user. @@ -25,6 +25,7 @@ class ItemAccessPermissions(BaseAccessPermissions): We remove comments for non admins/managers and a lot of fields of internal items for users without permission to see internal items. """ + def filtered_data(full_data, blocked_keys): """ Returns a new dict like full_data but with all blocked_keys removed. @@ -33,47 +34,56 @@ class ItemAccessPermissions(BaseAccessPermissions): return {key: full_data[key] for key in whitelist} # Parse data. - if full_data and await async_has_perm(user_id, 'agenda.can_see'): - if await async_has_perm(user_id, 'agenda.can_manage') and await async_has_perm(user_id, 'agenda.can_see_internal_items'): + if full_data and await async_has_perm(user_id, "agenda.can_see"): + if await async_has_perm( + user_id, "agenda.can_manage" + ) and await async_has_perm(user_id, "agenda.can_see_internal_items"): # Managers with special permission can see everything. data = full_data - elif await async_has_perm(user_id, 'agenda.can_see_internal_items'): + elif await async_has_perm(user_id, "agenda.can_see_internal_items"): # Non managers with special permission can see everything but # comments and hidden items. - data = [full for full in full_data if not full['is_hidden']] # filter hidden items - blocked_keys = ('comment',) - data = [filtered_data(full, blocked_keys) for full in data] # remove blocked_keys + data = [ + full for full in full_data if not full["is_hidden"] + ] # filter hidden items + blocked_keys = ("comment",) + data = [ + filtered_data(full, blocked_keys) for full in data + ] # remove blocked_keys else: # Users without special permission for internal items. # In internal and hidden case managers and non managers see only some fields # so that list of speakers is provided regardless. Hidden items can only be seen by managers. # We know that full_data has at least one entry which can be used to parse the keys. - blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set(( - 'id', - 'title', - 'speakers', - 'speaker_list_closed', - 'content_object')) + blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set( + ("id", "title", "speakers", "speaker_list_closed", "content_object") + ) # In non internal case managers see everything and non managers see # everything but comments. - if await async_has_perm(user_id, 'agenda.can_manage'): + if await async_has_perm(user_id, "agenda.can_manage"): blocked_keys_non_internal_hidden_case: Iterable[str] = [] can_see_hidden = True else: - blocked_keys_non_internal_hidden_case = ('comment',) + blocked_keys_non_internal_hidden_case = ("comment",) can_see_hidden = False data = [] for full in full_data: - if full['is_hidden'] and can_see_hidden: + if full["is_hidden"] and can_see_hidden: # Same filtering for internal and hidden items - data.append(filtered_data(full, blocked_keys_internal_hidden_case)) - elif full['is_internal']: - data.append(filtered_data(full, blocked_keys_internal_hidden_case)) + data.append( + filtered_data(full, blocked_keys_internal_hidden_case) + ) + elif full["is_internal"]: + data.append( + filtered_data(full, blocked_keys_internal_hidden_case) + ) else: # agenda item - data.append(filtered_data(full, blocked_keys_non_internal_hidden_case)) + data.append( + filtered_data(full, blocked_keys_non_internal_hidden_case) + ) else: data = [] diff --git a/openslides/agenda/apps.py b/openslides/agenda/apps.py index 5f2fe966d..f8a3c2122 100644 --- a/openslides/agenda/apps.py +++ b/openslides/agenda/apps.py @@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements class AgendaAppConfig(AppConfig): - name = 'openslides.agenda' - verbose_name = 'OpenSlides Agenda' + name = "openslides.agenda" + verbose_name = "OpenSlides Agenda" angular_site_module = True angular_projector_module = True @@ -20,7 +20,8 @@ class AgendaAppConfig(AppConfig): from .signals import ( get_permission_change_data, listen_to_related_object_post_delete, - listen_to_related_object_post_save) + listen_to_related_object_post_save, + ) from .views import ItemViewSet from . import serializers # noqa from ..utils.access_permissions import required_user @@ -31,22 +32,27 @@ class AgendaAppConfig(AppConfig): # Connect signals. post_save.connect( listen_to_related_object_post_save, - dispatch_uid='listen_to_related_object_post_save') + dispatch_uid="listen_to_related_object_post_save", + ) pre_delete.connect( listen_to_related_object_post_delete, - dispatch_uid='listen_to_related_object_post_delete') + dispatch_uid="listen_to_related_object_post_delete", + ) permission_change.connect( - get_permission_change_data, - dispatch_uid='agenda_get_permission_change_data') + get_permission_change_data, dispatch_uid="agenda_get_permission_change_data" + ) # Register viewsets. - router.register(self.get_model('Item').get_collection_string(), ItemViewSet) + router.register(self.get_model("Item").get_collection_string(), ItemViewSet) # register required_users - required_user.add_collection_string(self.get_model('Item').get_collection_string(), required_users) + required_user.add_collection_string( + self.get_model("Item").get_collection_string(), required_users + ) def get_config_variables(self): from .config_variables import get_config_variables + return get_config_variables() def get_startup_elements(self): @@ -54,11 +60,11 @@ class AgendaAppConfig(AppConfig): Yields all Cachables required on startup i. e. opening the websocket connection. """ - yield self.get_model('Item') + yield self.get_model("Item") def required_users(element: Dict[str, Any]) -> Set[int]: """ Returns all user ids that are displayed as speaker in the given element. """ - return set(speaker['user_id'] for speaker in element['speakers']) + return set(speaker["user_id"] for speaker in element["speakers"]) diff --git a/openslides/agenda/config_variables.py b/openslides/agenda/config_variables.py index d52db6f08..7eb67727f 100644 --- a/openslides/agenda/config_variables.py +++ b/openslides/agenda/config_variables.py @@ -10,97 +10,108 @@ def get_config_variables(): It has to be evaluated during app loading (see apps.py). """ yield ConfigVariable( - name='agenda_enable_numbering', - label='Enable numbering for agenda items', - input_type='boolean', + name="agenda_enable_numbering", + label="Enable numbering for agenda items", + input_type="boolean", default_value=True, weight=200, - group='Agenda', - subgroup='General') + group="Agenda", + subgroup="General", + ) yield ConfigVariable( - name='agenda_number_prefix', - default_value='', - label='Numbering prefix for agenda items', - help_text='This prefix will be set if you run the automatic agenda numbering.', + name="agenda_number_prefix", + default_value="", + label="Numbering prefix for agenda items", + help_text="This prefix will be set if you run the automatic agenda numbering.", weight=210, - group='Agenda', - subgroup='General', - validators=(MaxLengthValidator(20),)) + group="Agenda", + subgroup="General", + validators=(MaxLengthValidator(20),), + ) yield ConfigVariable( - name='agenda_numeral_system', - default_value='arabic', - input_type='choice', - label='Numeral system for agenda items', + name="agenda_numeral_system", + default_value="arabic", + input_type="choice", + label="Numeral system for agenda items", choices=( - {'value': 'arabic', 'display_name': 'Arabic'}, - {'value': 'roman', 'display_name': 'Roman'}), + {"value": "arabic", "display_name": "Arabic"}, + {"value": "roman", "display_name": "Roman"}, + ), weight=215, - group='Agenda', - subgroup='General') + group="Agenda", + subgroup="General", + ) yield ConfigVariable( - name='agenda_start_event_date_time', + name="agenda_start_event_date_time", default_value=None, - input_type='datetimepicker', - label='Begin of event', - help_text='Input format: DD.MM.YYYY HH:MM', + input_type="datetimepicker", + label="Begin of event", + help_text="Input format: DD.MM.YYYY HH:MM", weight=220, - group='Agenda', - subgroup='General') + group="Agenda", + subgroup="General", + ) yield ConfigVariable( - name='agenda_hide_internal_items_on_projector', + name="agenda_hide_internal_items_on_projector", default_value=True, - input_type='boolean', - label='Hide internal items when projecting subitems', + input_type="boolean", + label="Hide internal items when projecting subitems", weight=225, - group='Agenda', - subgroup='General') + group="Agenda", + subgroup="General", + ) yield ConfigVariable( - name='agenda_new_items_default_visibility', - default_value='2', - input_type='choice', + name="agenda_new_items_default_visibility", + default_value="2", + input_type="choice", choices=( - {'value': '1', 'display_name': 'Public item'}, - {'value': '2', 'display_name': 'Internal item'}, - {'value': '3', 'display_name': 'Hidden item'}), - label='Default visibility for new agenda items (except topics)', + {"value": "1", "display_name": "Public item"}, + {"value": "2", "display_name": "Internal item"}, + {"value": "3", "display_name": "Hidden item"}, + ), + label="Default visibility for new agenda items (except topics)", weight=227, - group='Agenda', - subgroup='General') + group="Agenda", + subgroup="General", + ) # List of speakers yield ConfigVariable( - name='agenda_show_last_speakers', + name="agenda_show_last_speakers", default_value=1, - input_type='integer', - label='Number of last speakers to be shown on the projector', + input_type="integer", + label="Number of last speakers to be shown on the projector", weight=230, - group='Agenda', - subgroup='List of speakers', - validators=(MinValueValidator(0),)) + group="Agenda", + subgroup="List of speakers", + validators=(MinValueValidator(0),), + ) yield ConfigVariable( - name='agenda_countdown_warning_time', + name="agenda_countdown_warning_time", default_value=0, - input_type='integer', - label='Show orange countdown in the last x seconds of speaking time', - help_text='Enter duration in seconds. Choose 0 to disable warning color.', + input_type="integer", + label="Show orange countdown in the last x seconds of speaking time", + help_text="Enter duration in seconds. Choose 0 to disable warning color.", weight=235, - group='Agenda', - subgroup='List of speakers', - validators=(MinValueValidator(0),)) + group="Agenda", + subgroup="List of speakers", + validators=(MinValueValidator(0),), + ) yield ConfigVariable( - name='agenda_couple_countdown_and_speakers', + name="agenda_couple_countdown_and_speakers", default_value=False, - input_type='boolean', - label='Couple countdown with the list of speakers', - help_text='[Begin speech] starts the countdown, [End speech] stops the countdown.', + input_type="boolean", + label="Couple countdown with the list of speakers", + help_text="[Begin speech] starts the countdown, [End speech] stops the countdown.", weight=240, - group='Agenda', - subgroup='List of speakers') + group="Agenda", + subgroup="List of speakers", + ) diff --git a/openslides/agenda/migrations/0001_initial.py b/openslides/agenda/migrations/0001_initial.py index b5b91f0d7..70619b21d 100644 --- a/openslides/agenda/migrations/0001_initial.py +++ b/openslides/agenda/migrations/0001_initial.py @@ -14,55 +14,109 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), + ("contenttypes", "0002_remove_content_type_name"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Item', + name="Item", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('item_number', models.CharField(blank=True, max_length=255)), - ('comment', models.TextField(blank=True, null=True)), - ('closed', models.BooleanField(default=False)), - ('type', models.IntegerField(choices=[(1, 'Agenda item'), (2, 'Hidden item')], default=2)), - ('duration', models.CharField(blank=True, max_length=5, null=True)), - ('weight', models.IntegerField(default=10000)), - ('object_id', models.PositiveIntegerField(blank=True, null=True)), - ('speaker_list_closed', models.BooleanField(default=False)), - ('content_type', models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.ContentType')), - ('parent', models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='agenda.Item')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("item_number", models.CharField(blank=True, max_length=255)), + ("comment", models.TextField(blank=True, null=True)), + ("closed", models.BooleanField(default=False)), + ( + "type", + models.IntegerField( + choices=[(1, "Agenda item"), (2, "Hidden item")], default=2 + ), + ), + ("duration", models.CharField(blank=True, max_length=5, null=True)), + ("weight", models.IntegerField(default=10000)), + ("object_id", models.PositiveIntegerField(blank=True, null=True)), + ("speaker_list_closed", models.BooleanField(default=False)), + ( + "content_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="contenttypes.ContentType", + ), + ), + ( + "parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="children", + to="agenda.Item", + ), + ), ], options={ - 'permissions': ( - ('can_see', 'Can see agenda'), - ('can_manage', 'Can manage agenda'), - ('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda')), - 'default_permissions': (), + "permissions": ( + ("can_see", "Can see agenda"), + ("can_manage", "Can manage agenda"), + ( + "can_see_hidden_items", + "Can see hidden items and time scheduling of agenda", + ), + ), + "default_permissions": (), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='Speaker', + name="Speaker", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('begin_time', models.DateTimeField(null=True)), - ('end_time', models.DateTimeField(null=True)), - ('weight', models.IntegerField(null=True)), - ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='speakers', to='agenda.Item')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("begin_time", models.DateTimeField(null=True)), + ("end_time", models.DateTimeField(null=True)), + ("weight", models.IntegerField(null=True)), + ( + "item", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="speakers", + to="agenda.Item", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'permissions': (('can_be_speaker', 'Can put oneself on the list of speakers'),), - 'default_permissions': (), + "permissions": ( + ("can_be_speaker", "Can put oneself on the list of speakers"), + ), + "default_permissions": (), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.AlterUniqueTogether( - name='item', - unique_together=set([('content_type', 'object_id')]), + name="item", unique_together=set([("content_type", "object_id")]) ), ] diff --git a/openslides/agenda/migrations/0002_item_duration.py b/openslides/agenda/migrations/0002_item_duration.py index 17b1cbc25..17b9ed531 100644 --- a/openslides/agenda/migrations/0002_item_duration.py +++ b/openslides/agenda/migrations/0002_item_duration.py @@ -11,7 +11,7 @@ def convert_duration(apps, schema_editor): IntegerField. It uses the temporary field for proper renaming the field in the end. """ - Item = apps.get_model('agenda', 'Item') + Item = apps.get_model("agenda", "Item") for item in Item.objects.all(): duration = item.duration item.duration_tmp = None @@ -20,7 +20,7 @@ def convert_duration(apps, schema_editor): item.duration_tmp = int(duration) elif isinstance(duration, str): # Assuming format (h)h:(m)m. If not, new value is None. - split = duration.split(':') + split = duration.split(":") if len(split) == 2 and is_int(split[0]) and is_int(split[1]): # Calculate new duration: hours * 60 + minutes. item.duration_tmp = int(split[0]) * 60 + int(split[1]) @@ -41,26 +41,17 @@ def is_int(s): class Migration(migrations.Migration): - dependencies = [ - ('agenda', '0001_initial'), - ] + dependencies = [("agenda", "0001_initial")] operations = [ migrations.AddField( - model_name='item', - name='duration_tmp', + model_name="item", + name="duration_tmp", field=models.IntegerField(blank=True, null=True), ), - migrations.RunPython( - convert_duration - ), - migrations.RemoveField( - model_name='item', - name='duration', - ), + migrations.RunPython(convert_duration), + migrations.RemoveField(model_name="item", name="duration"), migrations.RenameField( - model_name='item', - old_name='duration_tmp', - new_name='duration', + model_name="item", old_name="duration_tmp", new_name="duration" ), ] diff --git a/openslides/agenda/migrations/0003_auto_20170818_1202.py b/openslides/agenda/migrations/0003_auto_20170818_1202.py index 940051b43..091f40818 100644 --- a/openslides/agenda/migrations/0003_auto_20170818_1202.py +++ b/openslides/agenda/migrations/0003_auto_20170818_1202.py @@ -11,24 +11,31 @@ from openslides.utils.migrations import ( class Migration(migrations.Migration): - dependencies = [ - ('agenda', '0002_item_duration'), - ] + dependencies = [("agenda", "0002_item_duration")] operations = [ migrations.AlterModelOptions( - name='item', + name="item", options={ - 'default_permissions': (), - 'permissions': ( - ('can_see', 'Can see agenda'), - ('can_manage', 'Can manage agenda'), - ('can_manage_list_of_speakers', 'Can manage list of speakers'), - ('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda') - ) + "default_permissions": (), + "permissions": ( + ("can_see", "Can see agenda"), + ("can_manage", "Can manage agenda"), + ("can_manage_list_of_speakers", "Can manage list of speakers"), + ( + "can_see_hidden_items", + "Can see hidden items and time scheduling of agenda", + ), + ), }, ), - migrations.RunPython(add_permission_to_groups_based_on_existing_permission( - 'can_manage', 'item', 'agenda', 'can_manage_list_of_speakers', 'Can manage list of speakers' - )), + migrations.RunPython( + add_permission_to_groups_based_on_existing_permission( + "can_manage", + "item", + "agenda", + "can_manage_list_of_speakers", + "Can manage list of speakers", + ) + ), ] diff --git a/openslides/agenda/migrations/0004_speaker_marked.py b/openslides/agenda/migrations/0004_speaker_marked.py index 5bc19fccf..c935c3d51 100644 --- a/openslides/agenda/migrations/0004_speaker_marked.py +++ b/openslides/agenda/migrations/0004_speaker_marked.py @@ -7,14 +7,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('agenda', '0003_auto_20170818_1202'), - ] + dependencies = [("agenda", "0003_auto_20170818_1202")] operations = [ migrations.AddField( - model_name='speaker', - name='marked', + model_name="speaker", + name="marked", field=models.BooleanField(default=False), - ), + ) ] diff --git a/openslides/agenda/migrations/0005_auto_20180815_1109.py b/openslides/agenda/migrations/0005_auto_20180815_1109.py index 841ee9104..f6ba78f91 100644 --- a/openslides/agenda/migrations/0005_auto_20180815_1109.py +++ b/openslides/agenda/migrations/0005_auto_20180815_1109.py @@ -11,44 +11,47 @@ from openslides.utils.migrations import ( def delete_old_can_see_hidden_permission(apps, schema_editor): - perm = Permission.objects.filter(codename='can_see_hidden_items') + perm = Permission.objects.filter(codename="can_see_hidden_items") if len(perm): perm = perm.delete() class Migration(migrations.Migration): - dependencies = [ - ('agenda', '0004_speaker_marked'), - ] + dependencies = [("agenda", "0004_speaker_marked")] operations = [ migrations.AlterModelOptions( - name='item', + name="item", options={ - 'default_permissions': (), - 'permissions': ( - ('can_see', 'Can see agenda'), - ('can_manage', 'Can manage agenda'), - ('can_manage_list_of_speakers', 'Can manage list of speakers'), - ('can_see_internal_items', 'Can see internal items and time scheduling of agenda') - ) + "default_permissions": (), + "permissions": ( + ("can_see", "Can see agenda"), + ("can_manage", "Can manage agenda"), + ("can_manage_list_of_speakers", "Can manage list of speakers"), + ( + "can_see_internal_items", + "Can see internal items and time scheduling of agenda", + ), + ), }, ), migrations.AlterField( - model_name='item', - name='type', + model_name="item", + name="type", field=models.IntegerField( - choices=[ - (1, 'Agenda item'), - (2, 'Internal item'), - (3, 'Hidden item') - ], - default=3 + choices=[(1, "Agenda item"), (2, "Internal item"), (3, "Hidden item")], + default=3, ), ), - migrations.RunPython(add_permission_to_groups_based_on_existing_permission( - 'can_see_hidden_items', 'item', 'agenda', 'can_see_internal_items', 'Can see internal items and time scheduling of agenda' - )), + migrations.RunPython( + add_permission_to_groups_based_on_existing_permission( + "can_see_hidden_items", + "item", + "agenda", + "can_see_internal_items", + "Can see internal items and time scheduling of agenda", + ) + ), migrations.RunPython(delete_old_can_see_hidden_permission), ] diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index afca04c4a..1d98dc276 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -24,13 +24,14 @@ class ItemManager(models.Manager): Customized model manager with special methods for agenda tree and numbering. """ + def get_full_queryset(self): """ Returns the normal queryset with all items. In the background all speakers and related items (topics, motions, assignments) are prefetched from the database. """ - return self.get_queryset().prefetch_related('speakers', 'content_object') + return self.get_queryset().prefetch_related("speakers", "content_object") def get_only_non_public_items(self): """ @@ -45,14 +46,17 @@ class ItemManager(models.Manager): Generator that yields a list of items and their children. """ for item in items: - if parent_is_not_public or item.type in (item.INTERNAL_ITEM, item.HIDDEN_ITEM): + if parent_is_not_public or item.type in ( + item.INTERNAL_ITEM, + item.HIDDEN_ITEM, + ): item_is_not_public = True yield item else: item_is_not_public = False yield from yield_items( - item_children[item.pk], - parent_is_not_public=item_is_not_public) + item_children[item.pk], parent_is_not_public=item_is_not_public + ) yield from yield_items(root_items) @@ -64,7 +68,7 @@ class ItemManager(models.Manager): If only_item_type is given, the tree hides items with other types and all of their children. """ - queryset = self.order_by('weight') + queryset = self.order_by("weight") item_children: Dict[int, List[Item]] = defaultdict(list) root_items = [] for item in queryset: @@ -88,7 +92,9 @@ class ItemManager(models.Manager): If include_content is True, the yielded dictonaries have no key 'id' but a key 'item' with the entire object. """ - root_items, item_children = self.get_root_and_children(only_item_type=only_item_type) + root_items, item_children = self.get_root_and_children( + only_item_type=only_item_type + ) def get_children(items): """ @@ -98,7 +104,9 @@ class ItemManager(models.Manager): if include_content: yield dict(item=item, children=get_children(item_children[item.pk])) else: - yield dict(id=item.pk, children=get_children(item_children[item.pk])) + yield dict( + id=item.pk, children=get_children(item_children[item.pk]) + ) yield from get_children(root_items) @@ -110,6 +118,7 @@ class ItemManager(models.Manager): The tree has to be a nested object. For example: [{"id": 1}, {"id": 2, "children": [{"id": 3}]}] """ + def walk_items(tree, parent=None): """ Generator that returns each item in the tree as tuple. @@ -118,15 +127,17 @@ class ItemManager(models.Manager): weight of the item. """ for weight, element in enumerate(tree): - yield (element['id'], parent, weight) - yield from walk_items(element.get('children', []), element['id']) + yield (element["id"], parent, weight) + yield from walk_items(element.get("children", []), element["id"]) touched_items: Set[int] = set() db_items = dict((item.pk, item) for item in Item.objects.all()) for item_id, parent_id, weight in walk_items(tree): # Check that the item is only once in the tree to prevent invalid trees if item_id in touched_items: - raise ValueError("Item {} is more then once in the tree.".format(item_id)) + raise ValueError( + "Item {} is more then once in the tree.".format(item_id) + ) touched_items.add(item_id) try: @@ -143,36 +154,40 @@ class ItemManager(models.Manager): db_item.save() @transaction.atomic - def number_all(self, numeral_system='arabic'): + def number_all(self, numeral_system="arabic"): """ Auto numbering of the agenda according to the numeral_system. Manually added item numbers will be overwritten. """ + def walk_tree(tree, number=None): for index, tree_element in enumerate(tree): # Calculate number of visable agenda items. - if numeral_system == 'roman' and number is None: + if numeral_system == "roman" and number is None: item_number = to_roman(index + 1) else: item_number = str(index + 1) if number is not None: - item_number = '.'.join((number, item_number)) + item_number = ".".join((number, item_number)) # Add prefix. - if config['agenda_number_prefix']: - item_number_tmp = "%s %s" % (config['agenda_number_prefix'], item_number) + if config["agenda_number_prefix"]: + item_number_tmp = "%s %s" % ( + config["agenda_number_prefix"], + item_number, + ) else: item_number_tmp = item_number # Save the new value and go down the tree. - tree_element['item'].item_number = item_number_tmp - tree_element['item'].save() - walk_tree(tree_element['children'], item_number) + tree_element["item"].item_number = item_number_tmp + tree_element["item"].save() + walk_tree(tree_element["children"], item_number) # Start numbering visable agenda items. walk_tree(self.get_tree(only_item_type=Item.AGENDA_ITEM, include_content=True)) # Reset number of hidden items. for item in self.get_only_non_public_items(): - item.item_number = '' + item.item_number = "" item.save() @@ -180,18 +195,20 @@ class Item(RESTModelMixin, models.Model): """ An Agenda Item """ + access_permissions = ItemAccessPermissions() objects = ItemManager() - can_see_permission = 'agenda.can_see' + can_see_permission = "agenda.can_see" AGENDA_ITEM = 1 INTERNAL_ITEM = 2 HIDDEN_ITEM = 3 ITEM_TYPE = ( - (AGENDA_ITEM, ugettext_lazy('Agenda item')), - (INTERNAL_ITEM, ugettext_lazy('Internal item')), - (HIDDEN_ITEM, ugettext_lazy('Hidden item'))) + (AGENDA_ITEM, ugettext_lazy("Agenda item")), + (INTERNAL_ITEM, ugettext_lazy("Internal item")), + (HIDDEN_ITEM, ugettext_lazy("Hidden item")), + ) item_number = models.CharField(blank=True, max_length=255) """ @@ -208,9 +225,7 @@ class Item(RESTModelMixin, models.Model): Flag, if the item is finished. """ - type = models.IntegerField( - choices=ITEM_TYPE, - default=HIDDEN_ITEM) + type = models.IntegerField(choices=ITEM_TYPE, default=HIDDEN_ITEM) """ Type of the agenda item. @@ -223,11 +238,12 @@ class Item(RESTModelMixin, models.Model): """ parent = models.ForeignKey( - 'self', + "self", on_delete=models.SET_NULL, null=True, blank=True, - related_name='children') + related_name="children", + ) """ The parent item in the agenda tree. """ @@ -238,10 +254,8 @@ class Item(RESTModelMixin, models.Model): """ content_type = models.ForeignKey( - ContentType, - on_delete=models.SET_NULL, - null=True, - blank=True) + ContentType, on_delete=models.SET_NULL, null=True, blank=True + ) """ Field for generic relation to a related object. Type of the object. """ @@ -256,8 +270,7 @@ class Item(RESTModelMixin, models.Model): Field for generic relation to a related object. General field to the related object. """ - speaker_list_closed = models.BooleanField( - default=False) + speaker_list_closed = models.BooleanField(default=False) """ True, if the list of speakers is closed. """ @@ -265,11 +278,15 @@ class Item(RESTModelMixin, models.Model): class Meta: default_permissions = () permissions = ( - ('can_see', 'Can see agenda'), - ('can_manage', 'Can manage agenda'), - ('can_manage_list_of_speakers', 'Can manage list of speakers'), - ('can_see_internal_items', 'Can see internal items and time scheduling of agenda')) - unique_together = ('content_type', 'object_id') + ("can_see", "Can see agenda"), + ("can_manage", "Can manage agenda"), + ("can_manage_list_of_speakers", "Can manage list of speakers"), + ( + "can_see_internal_items", + "Can see internal items and time scheduling of agenda", + ), + ) + unique_together = ("content_type", "object_id") def __str__(self): return self.title @@ -280,10 +297,11 @@ class Item(RESTModelMixin, models.Model): list of speakers projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='agenda/list-of-speakers', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="agenda/list-of-speakers", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) @property def title(self): @@ -293,8 +311,10 @@ class Item(RESTModelMixin, models.Model): try: return self.content_object.get_agenda_title() except AttributeError: - raise NotImplementedError('You have to provide a get_agenda_title ' - 'method on your related model.') + raise NotImplementedError( + "You have to provide a get_agenda_title " + "method on your related model." + ) @property def title_with_type(self): @@ -304,8 +324,10 @@ class Item(RESTModelMixin, models.Model): try: return self.content_object.get_agenda_title_with_type() except AttributeError: - raise NotImplementedError('You have to provide a get_agenda_title_with_type ' - 'method on your related model.') + raise NotImplementedError( + "You have to provide a get_agenda_title_with_type " + "method on your related model." + ) def is_internal(self): """ @@ -314,8 +336,9 @@ class Item(RESTModelMixin, models.Model): Attention! This executes one query for each ancestor of the item. """ - return (self.type == self.INTERNAL_ITEM or - (self.parent is not None and self.parent.is_internal())) + return self.type == self.INTERNAL_ITEM or ( + self.parent is not None and self.parent.is_internal() + ) def is_hidden(self): """ @@ -324,15 +347,16 @@ class Item(RESTModelMixin, models.Model): Attention! This executes one query for each ancestor of the item. """ - return (self.type == self.HIDDEN_ITEM or - (self.parent is not None and self.parent.is_hidden())) + return self.type == self.HIDDEN_ITEM or ( + self.parent is not None and self.parent.is_hidden() + ) def get_next_speaker(self): """ Returns the speaker object of the speaker who is next. """ try: - return self.speakers.filter(begin_time=None).order_by('weight')[0] + return self.speakers.filter(begin_time=None).order_by("weight")[0] except IndexError: # The list of speakers is empty. return None @@ -342,6 +366,7 @@ class SpeakerManager(models.Manager): """ Manager for Speaker model. Provides a customized add method. """ + def add(self, user, item, skip_autoupdate=False): """ Customized manager method to prevent anonymous users to be on the @@ -350,12 +375,15 @@ class SpeakerManager(models.Manager): """ if self.filter(user=user, item=item, begin_time=None).exists(): raise OpenSlidesError( - _('{user} is already on the list of speakers.').format(user=user)) + _("{user} is already on the list of speakers.").format(user=user) + ) if isinstance(user, AnonymousUser): raise OpenSlidesError( - _('An anonymous user can not be on lists of speakers.')) - weight = (self.filter(item=item).aggregate( - models.Max('weight'))['weight__max'] or 0) + _("An anonymous user can not be on lists of speakers.") + ) + weight = ( + self.filter(item=item).aggregate(models.Max("weight"))["weight__max"] or 0 + ) speaker = self.model(item=item, user=user, weight=weight + 1) speaker.save(force_insert=True, skip_autoupdate=skip_autoupdate) return speaker @@ -368,17 +396,12 @@ class Speaker(RESTModelMixin, models.Model): objects = SpeakerManager() - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) """ ForeinKey to the user who speaks. """ - item = models.ForeignKey( - Item, - on_delete=models.CASCADE, - related_name='speakers') + item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="speakers") """ ForeinKey to the agenda item to which the user want to speak. """ @@ -405,9 +428,7 @@ class Speaker(RESTModelMixin, models.Model): class Meta: default_permissions = () - permissions = ( - ('can_be_speaker', 'Can put oneself on the list of speakers'), - ) + permissions = (("can_be_speaker", "Can put oneself on the list of speakers"),) def __str__(self): return str(self.user) @@ -420,8 +441,11 @@ class Speaker(RESTModelMixin, models.Model): speaking, end his speech. """ try: - current_speaker = (Speaker.objects.filter(item=self.item, end_time=None) - .exclude(begin_time=None).get()) + current_speaker = ( + Speaker.objects.filter(item=self.item, end_time=None) + .exclude(begin_time=None) + .get() + ) except Speaker.DoesNotExist: pass else: @@ -431,15 +455,21 @@ class Speaker(RESTModelMixin, models.Model): self.weight = None self.begin_time = timezone.now() self.save() # Here, the item is saved and causes an autoupdate. - if config['agenda_couple_countdown_and_speakers']: - countdown, created = Countdown.objects.get_or_create(pk=1, defaults={ - 'default_time': config['projector_default_countdown'], - 'countdown_time': config['projector_default_countdown']}) + if config["agenda_couple_countdown_and_speakers"]: + countdown, created = Countdown.objects.get_or_create( + pk=1, + defaults={ + "default_time": config["projector_default_countdown"], + "countdown_time": config["projector_default_countdown"], + }, + ) if not created: - countdown.control(action='reset', skip_autoupdate=True) - countdown.control(action='start', skip_autoupdate=True) + countdown.control(action="reset", skip_autoupdate=True) + countdown.control(action="start", skip_autoupdate=True) - inform_changed_data(countdown) # Here, the autoupdate for the countdown is triggered. + inform_changed_data( + countdown + ) # Here, the autoupdate for the countdown is triggered. def end_speech(self, skip_autoupdate=False): """ @@ -447,13 +477,13 @@ class Speaker(RESTModelMixin, models.Model): """ self.end_time = timezone.now() self.save(skip_autoupdate=skip_autoupdate) - if config['agenda_couple_countdown_and_speakers']: + if config["agenda_couple_countdown_and_speakers"]: try: countdown = Countdown.objects.get(pk=1) except Countdown.DoesNotExist: pass # Do not create a new countdown on stop action else: - countdown.control(action='reset', skip_autoupdate=skip_autoupdate) + countdown.control(action="reset", skip_autoupdate=skip_autoupdate) def get_root_rest_element(self): """ diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py index a29af1055..ffcdeb6be 100644 --- a/openslides/agenda/projector.py +++ b/openslides/agenda/projector.py @@ -16,14 +16,15 @@ class ItemListSlide(ProjectorElement): Additionally set 'tree' to True to get also children of children. """ - name = 'agenda/item-list' + + name = "agenda/item-list" def check_data(self): - pk = self.config_entry.get('id') + pk = self.config_entry.get("id") if pk is not None: # Children slide. if not Item.objects.filter(pk=pk).exists(): - raise ProjectorException('Item does not exist.') + raise ProjectorException("Item does not exist.") class ListOfSpeakersSlide(ProjectorElement): @@ -31,21 +32,23 @@ class ListOfSpeakersSlide(ProjectorElement): Slide definitions for Item model. This is only for list of speakers slide. You have to set 'id'. """ - name = 'agenda/list-of-speakers' + + name = "agenda/list-of-speakers" def check_data(self): - if not Item.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('Item does not exist.') + if not Item.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("Item does not exist.") def update_data(self): - return {'agenda_item_id': self.config_entry.get('id')} + return {"agenda_item_id": self.config_entry.get("id")} class CurrentListOfSpeakersSlide(ProjectorElement): """ Slide for the current list of speakers. """ - name = 'agenda/current-list-of-speakers' + + name = "agenda/current-list-of-speakers" def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index edcb49ea1..7ebe047a4 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -7,16 +7,17 @@ class SpeakerSerializer(ModelSerializer): """ Serializer for agenda.models.Speaker objects. """ + class Meta: model = Speaker fields = ( - 'id', - 'user', - 'begin_time', - 'end_time', - 'weight', - 'marked', - 'item', # js-data needs the item-id in the nested object to define relations. + "id", + "user", + "begin_time", + "end_time", + "weight", + "marked", + "item", # js-data needs the item-id in the nested object to define relations. ) @@ -24,36 +25,39 @@ class RelatedItemRelatedField(RelatedField): """ A custom field to use for the content_object generic relationship. """ + def to_representation(self, value): """ Returns info concerning the related object extracted from the api URL of this object. """ - return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()} + return {"collection": value.get_collection_string(), "id": value.get_rest_pk()} class ItemSerializer(ModelSerializer): """ Serializer for agenda.models.Item objects. """ + content_object = RelatedItemRelatedField(read_only=True) speakers = SpeakerSerializer(many=True, read_only=True) class Meta: model = Item fields = ( - 'id', - 'item_number', - 'title', - 'title_with_type', - 'comment', - 'closed', - 'type', - 'is_internal', - 'is_hidden', - 'duration', - 'speakers', - 'speaker_list_closed', - 'content_object', - 'weight', - 'parent',) + "id", + "item_number", + "title", + "title_with_type", + "comment", + "closed", + "type", + "is_internal", + "is_hidden", + "duration", + "speakers", + "speaker_list_closed", + "content_object", + "weight", + "parent", + ) diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py index 9411da5d0..823bb6419 100644 --- a/openslides/agenda/signals.py +++ b/openslides/agenda/signals.py @@ -16,18 +16,18 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs): Do not run caching and autoupdate if the instance has a key skip_autoupdate in the agenda_item_update_information container. """ - if hasattr(instance, 'get_agenda_title'): + if hasattr(instance, "get_agenda_title"): if created: attrs = {} - for attr in ('type', 'parent_id', 'comment', 'duration', 'weight'): + for attr in ("type", "parent_id", "comment", "duration", "weight"): if instance.agenda_item_update_information.get(attr): attrs[attr] = instance.agenda_item_update_information.get(attr) Item.objects.create(content_object=instance, **attrs) # If the object is created, the related_object has to be sent again. - if not instance.agenda_item_update_information.get('skip_autoupdate'): + if not instance.agenda_item_update_information.get("skip_autoupdate"): inform_changed_data(instance) - elif not instance.agenda_item_update_information.get('skip_autoupdate'): + elif not instance.agenda_item_update_information.get("skip_autoupdate"): # If the object has changed, then also the agenda item has to be sent. inform_changed_data(instance.agenda_item) @@ -37,7 +37,7 @@ def listen_to_related_object_post_delete(sender, instance, **kwargs): Receiver function to delete agenda items. It is connected to the signal django.db.models.signals.post_delete during app loading. """ - if hasattr(instance, 'get_agenda_title'): + if hasattr(instance, "get_agenda_title"): content_type = ContentType.objects.get_for_model(instance) try: # Attention: This delete() call is also necessary to remove @@ -53,10 +53,12 @@ def get_permission_change_data(sender, permissions, **kwargs): Yields all necessary collections if 'agenda.can_see' or 'agenda.can_see_internal_items' permissions changes. """ - agenda_app = apps.get_app_config(app_label='agenda') + agenda_app = apps.get_app_config(app_label="agenda") for permission in permissions: # There could be only one 'agenda.can_see' and then we want to return data. - if (permission.content_type.app_label == agenda_app.label - and permission.codename in ('can_see', 'can_see_internal_items')): + if ( + permission.content_type.app_label == agenda_app.label + and permission.codename in ("can_see", "can_see_internal_items") + ): yield from agenda_app.get_startup_elements() break diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 2cbdbef2b..93b1028f2 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -24,12 +24,14 @@ from .models import Item, Speaker # Viewsets for the REST API + class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet): """ API endpoint for agenda items. There are some views, see check_view_permissions. """ + access_permissions = ItemAccessPermissions() queryset = Item.objects.all() @@ -37,22 +39,26 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('metadata', 'manage_speaker', 'tree'): - result = has_perm(self.request.user, 'agenda.can_see') + elif self.action in ("metadata", "manage_speaker", "tree"): + result = has_perm(self.request.user, "agenda.can_see") # For manage_speaker and tree requests the rest of the check is # done in the specific method. See below. - elif self.action in ('partial_update', 'update', 'sort', 'assign'): - result = (has_perm(self.request.user, 'agenda.can_see') and - has_perm(self.request.user, 'agenda.can_see_internal_items') and - has_perm(self.request.user, 'agenda.can_manage')) - elif self.action in ('speak', 'sort_speakers'): - result = (has_perm(self.request.user, 'agenda.can_see') and - has_perm(self.request.user, 'agenda.can_manage_list_of_speakers')) - elif self.action in ('numbering', ): - result = (has_perm(self.request.user, 'agenda.can_see') and - has_perm(self.request.user, 'agenda.can_manage')) + elif self.action in ("partial_update", "update", "sort", "assign"): + result = ( + has_perm(self.request.user, "agenda.can_see") + and has_perm(self.request.user, "agenda.can_see_internal_items") + and has_perm(self.request.user, "agenda.can_manage") + ) + elif self.action in ("speak", "sort_speakers"): + result = has_perm(self.request.user, "agenda.can_see") and has_perm( + self.request.user, "agenda.can_manage_list_of_speakers" + ) + elif self.action in ("numbering",): + result = has_perm(self.request.user, "agenda.can_see") and has_perm( + self.request.user, "agenda.can_manage" + ) else: result = False return result @@ -82,7 +88,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV return response - @detail_route(methods=['POST', 'PATCH', 'DELETE']) + @detail_route(methods=["POST", "PATCH", "DELETE"]) def manage_speaker(self, request, pk=None): """ Special view endpoint to add users to the list of speakers or remove @@ -103,55 +109,61 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV # Retrieve item. item = self.get_object() - if request.method == 'POST': + if request.method == "POST": # Retrieve user_id - user_id = request.data.get('user') + user_id = request.data.get("user") # Check permissions and other conditions. Get user instance. if user_id is None: # Add oneself - if not has_perm(self.request.user, 'agenda.can_be_speaker'): + if not has_perm(self.request.user, "agenda.can_be_speaker"): self.permission_denied(request) if item.speaker_list_closed: - raise ValidationError({'detail': _('The list of speakers is closed.')}) + raise ValidationError( + {"detail": _("The list of speakers is closed.")} + ) user = self.request.user else: # Add someone else. - if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'): + if not has_perm( + self.request.user, "agenda.can_manage_list_of_speakers" + ): self.permission_denied(request) try: user = get_user_model().objects.get(pk=int(user_id)) except (ValueError, get_user_model().DoesNotExist): - raise ValidationError({'detail': _('User does not exist.')}) + raise ValidationError({"detail": _("User does not exist.")}) # Try to add the user. This ensurse that a user is not twice in the # list of coming speakers. try: Speaker.objects.add(user, item) except OpenSlidesError as e: - raise ValidationError({'detail': str(e)}) - message = _('User %s was successfully added to the list of speakers.') % user + raise ValidationError({"detail": str(e)}) + message = ( + _("User %s was successfully added to the list of speakers.") % user + ) # Send new speaker via autoupdate because users without permission # to see users may not have it but can get it now. inform_changed_data([user]) # Toggle 'marked' for the speaker - elif request.method == 'PATCH': + elif request.method == "PATCH": # Check permissions - if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'): + if not has_perm(self.request.user, "agenda.can_manage_list_of_speakers"): self.permission_denied(request) # Retrieve user_id - user_id = request.data.get('user') + user_id = request.data.get("user") try: user = get_user_model().objects.get(pk=int(user_id)) except (ValueError, get_user_model().DoesNotExist): - raise ValidationError({'detail': _('User does not exist.')}) + raise ValidationError({"detail": _("User does not exist.")}) - marked = request.data.get('marked') + marked = request.data.get("marked") if not isinstance(marked, bool): - raise ValidationError({'detail': _('Marked has to be a bool.')}) + raise ValidationError({"detail": _("Marked has to be a bool.")}) queryset = Speaker.objects.filter(item=item, user=user) try: @@ -160,37 +172,46 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV # there is only one speaker instance or none. speaker = queryset.get() except Speaker.DoesNotExist: - raise ValidationError({'detail': _('The user is not in the list of speakers.')}) + raise ValidationError( + {"detail": _("The user is not in the list of speakers.")} + ) else: speaker.marked = marked speaker.save() if speaker.marked: - message = _('You are successfully marked the speaker.') + message = _("You are successfully marked the speaker.") else: - message = _('You are successfully unmarked the speaker.') + message = _("You are successfully unmarked the speaker.") else: # request.method == 'DELETE' - speaker_ids = request.data.get('speaker') + speaker_ids = request.data.get("speaker") # Check permissions and other conditions. Get speaker instance. if speaker_ids is None: # Remove oneself queryset = Speaker.objects.filter( - item=item, user=self.request.user).exclude(weight=None) + item=item, user=self.request.user + ).exclude(weight=None) try: # We assume that there aren't multiple entries because this # is forbidden by the Manager's add method. We assume that # there is only one speaker instance or none. speaker = queryset.get() except Speaker.DoesNotExist: - raise ValidationError({'detail': _('You are not on the list of speakers.')}) + raise ValidationError( + {"detail": _("You are not on the list of speakers.")} + ) else: speaker.delete() - message = _('You are successfully removed from the list of speakers.') + message = _( + "You are successfully removed from the list of speakers." + ) else: # Remove someone else. - if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'): + if not has_perm( + self.request.user, "agenda.can_manage_list_of_speakers" + ): self.permission_denied(request) if type(speaker_ids) is int: speaker_ids = [speaker_ids] @@ -209,15 +230,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV inform_changed_data(item) if deleted_speaker_count > 1: - message = str(deleted_speaker_count) + ' ' + _('speakers have been removed from the list of speakers.') + message = ( + str(deleted_speaker_count) + + " " + + _("speakers have been removed from the list of speakers.") + ) elif deleted_speaker_count == 1: - message = _('User %s has been removed from the list of speakers.') % deleted_speaker_name + message = ( + _("User %s has been removed from the list of speakers.") + % deleted_speaker_name + ) else: - message = _('No speakers have been removed from the list of speakers.') + message = _( + "No speakers have been removed from the list of speakers." + ) # Initiate response. - return Response({'detail': message}) + return Response({"detail": message}) - @detail_route(methods=['PUT', 'DELETE']) + @detail_route(methods=["PUT", "DELETE"]) def speak(self, request, pk=None): """ Special view endpoint to begin and end speech of speakers. Send PUT @@ -227,20 +257,22 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV # Retrieve item. item = self.get_object() - if request.method == 'PUT': + if request.method == "PUT": # Retrieve speaker_id - speaker_id = request.data.get('speaker') + speaker_id = request.data.get("speaker") if speaker_id is None: speaker = item.get_next_speaker() if speaker is None: - raise ValidationError({'detail': _('The list of speakers is empty.')}) + raise ValidationError( + {"detail": _("The list of speakers is empty.")} + ) else: try: speaker = Speaker.objects.get(pk=int(speaker_id)) except (ValueError, Speaker.DoesNotExist): - raise ValidationError({'detail': _('Speaker does not exist.')}) + raise ValidationError({"detail": _("Speaker does not exist.")}) speaker.begin_speech() - message = _('User is now speaking.') + message = _("User is now speaking.") else: # request.method == 'DELETE' @@ -248,17 +280,27 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV # We assume that there aren't multiple entries because this # is forbidden by the Model's begin_speech method. We assume that # there is only one speaker instance or none. - current_speaker = Speaker.objects.filter(item=item, end_time=None).exclude(begin_time=None).get() + current_speaker = ( + Speaker.objects.filter(item=item, end_time=None) + .exclude(begin_time=None) + .get() + ) except Speaker.DoesNotExist: raise ValidationError( - {'detail': _('There is no one speaking at the moment according to %(item)s.') % {'item': item}}) + { + "detail": _( + "There is no one speaking at the moment according to %(item)s." + ) + % {"item": item} + } + ) current_speaker.end_speech() - message = _('The speech is finished now.') + message = _("The speech is finished now.") # Initiate response. - return Response({'detail': message}) + return Response({"detail": message}) - @detail_route(methods=['POST']) + @detail_route(methods=["POST"]) def sort_speakers(self, request, pk=None): """ Special view endpoint to sort the list of speakers. @@ -269,10 +311,9 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV item = self.get_object() # Check data - speaker_ids = request.data.get('speakers') + speaker_ids = request.data.get("speakers") if not isinstance(speaker_ids, list): - raise ValidationError( - {'detail': _('Invalid data.')}) + raise ValidationError({"detail": _("Invalid data.")}) # Get all speakers speakers = {} @@ -283,8 +324,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV valid_speakers = [] for speaker_id in speaker_ids: if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None: - raise ValidationError( - {'detail': _('Invalid data.')}) + raise ValidationError({"detail": _("Invalid data.")}) valid_speakers.append(speakers[speaker_id]) weight = 0 with transaction.atomic(): @@ -297,50 +337,57 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV inform_changed_data(item) # Initiate response. - return Response({'detail': _('List of speakers successfully sorted.')}) + return Response({"detail": _("List of speakers successfully sorted.")}) - @list_route(methods=['post']) + @list_route(methods=["post"]) def numbering(self, request): """ Auto numbering of the agenda according to the config. Manually added item numbers will be overwritten. """ - if not config['agenda_enable_numbering']: - raise ValidationError({'detail': _('Numbering of agenda items is deactivated.')}) + if not config["agenda_enable_numbering"]: + raise ValidationError( + {"detail": _("Numbering of agenda items is deactivated.")} + ) - Item.objects.number_all(numeral_system=config['agenda_numeral_system']) - return Response({'detail': _('The agenda has been numbered.')}) + Item.objects.number_all(numeral_system=config["agenda_numeral_system"]) + return Response({"detail": _("The agenda has been numbered.")}) - @list_route(methods=['post']) + @list_route(methods=["post"]) def sort(self, request): """ Sort agenda items. Also checks parent field to prevent hierarchical loops. """ - nodes = request.data.get('nodes', []) - parent_id = request.data.get('parent_id') + nodes = request.data.get("nodes", []) + parent_id = request.data.get("parent_id") items = [] with transaction.atomic(): for index, node in enumerate(nodes): - item = Item.objects.get(pk=node['id']) + item = Item.objects.get(pk=node["id"]) item.parent_id = parent_id item.weight = index item.save(skip_autoupdate=True) items.append(item) # Now check consistency. TODO: Try to use less DB queries. - item = Item.objects.get(pk=node['id']) + item = Item.objects.get(pk=node["id"]) ancestor = item.parent while ancestor is not None: if ancestor == item: - raise ValidationError({'detail': _( - 'There must not be a hierarchical loop. Please reload the page.')}) + raise ValidationError( + { + "detail": _( + "There must not be a hierarchical loop. Please reload the page." + ) + } + ) ancestor = ancestor.parent inform_changed_data(items) - return Response({'detail': _('The agenda has been sorted.')}) + return Response({"detail": _("The agenda has been sorted.")}) - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.atomic def assign(self, request): """ @@ -359,9 +406,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV "items": { "description": "An array of agenda item ids where the items should be assigned to the new parent id.", "type": "array", - "items": { - "type": "integer", - }, + "items": {"type": "integer"}, "minItems": 1, "uniqueItems": True, }, @@ -377,13 +422,19 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV try: jsonschema.validate(request.data, schema) except jsonschema.ValidationError as err: - raise ValidationError({'detail': str(err)}) + raise ValidationError({"detail": str(err)}) # Check parent item try: - parent = Item.objects.get(pk=request.data['parent_id']) + parent = Item.objects.get(pk=request.data["parent_id"]) except Item.DoesNotExist: - raise ValidationError({'detail': 'Parent item {} does not exist'.format(request.data['parent_id'])}) + raise ValidationError( + { + "detail": "Parent item {} does not exist".format( + request.data["parent_id"] + ) + } + ) # Collect ancestors ancestors = [parent.pk] @@ -394,16 +445,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV # First validate all items before changeing them. items = [] - for item_id in request.data['items']: + for item_id in request.data["items"]: # Prevent hierarchical loops. if item_id in ancestors: - raise ValidationError({'detail': 'Assigning item {} to one of its children is not possible.'.format(item_id)}) + raise ValidationError( + { + "detail": "Assigning item {} to one of its children is not possible.".format( + item_id + ) + } + ) # Check every item try: items.append(Item.objects.get(pk=item_id)) except Item.DoesNotExist: - raise ValidationError({'detail': 'Item {} does not exist'.format(item_id)}) + raise ValidationError( + {"detail": "Item {} does not exist".format(item_id)} + ) # OK, assign new parents. for item in items: @@ -415,6 +474,10 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV inform_changed_data(items) # Send response. - return Response({ - 'detail': _('{number} items successfully assigned.').format(number=len(items)), - }) + return Response( + { + "detail": _("{number} items successfully assigned.").format( + number=len(items) + ) + } + ) diff --git a/openslides/assignments/__init__.py b/openslides/assignments/__init__.py index b26bfa2fe..9d068ad59 100644 --- a/openslides/assignments/__init__.py +++ b/openslides/assignments/__init__.py @@ -1 +1 @@ -default_app_config = 'openslides.assignments.apps.AssignmentsAppConfig' +default_app_config = "openslides.assignments.apps.AssignmentsAppConfig" diff --git a/openslides/assignments/access_permissions.py b/openslides/assignments/access_permissions.py index 7cadf883c..f37ed22d6 100644 --- a/openslides/assignments/access_permissions.py +++ b/openslides/assignments/access_permissions.py @@ -8,31 +8,35 @@ class AssignmentAccessPermissions(BaseAccessPermissions): """ Access permissions container for Assignment and AssignmentViewSet. """ - base_permission = 'assignments.can_see' + + base_permission = "assignments.can_see" async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Returns the restricted serialized data for the instance prepared for the user. Removes unpublished polls for non admins so that they only get a result like the AssignmentShortSerializer would give them. """ # Parse data. - if await async_has_perm(user_id, 'assignments.can_see') and await async_has_perm(user_id, 'assignments.can_manage'): + if await async_has_perm( + user_id, "assignments.can_see" + ) and await async_has_perm(user_id, "assignments.can_manage"): data = full_data - elif await async_has_perm(user_id, 'assignments.can_see'): + elif await async_has_perm(user_id, "assignments.can_see"): # Exclude unpublished poll votes. data = [] for full in full_data: full_copy = full.copy() - polls = full_copy['polls'] + polls = full_copy["polls"] for poll in polls: - if not poll['published']: - for option in poll['options']: - option['votes'] = [] # clear votes for not published polls - poll['has_votes'] = False # A user should see, if there are votes. + if not poll["published"]: + for option in poll["options"]: + option["votes"] = [] # clear votes for not published polls + poll[ + "has_votes" + ] = False # A user should see, if there are votes. data.append(full_copy) else: data = [] diff --git a/openslides/assignments/apps.py b/openslides/assignments/apps.py index 8db2e9138..1f2ad5b95 100644 --- a/openslides/assignments/apps.py +++ b/openslides/assignments/apps.py @@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements class AssignmentsAppConfig(AppConfig): - name = 'openslides.assignments' - verbose_name = 'OpenSlides Assignments' + name = "openslides.assignments" + verbose_name = "OpenSlides Assignments" angular_site_module = True angular_projector_module = True @@ -28,17 +28,23 @@ class AssignmentsAppConfig(AppConfig): # Connect signals. permission_change.connect( get_permission_change_data, - dispatch_uid='assignments_get_permission_change_data') + dispatch_uid="assignments_get_permission_change_data", + ) # Register viewsets. - router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet) - router.register('assignments/poll', AssignmentPollViewSet) + router.register( + self.get_model("Assignment").get_collection_string(), AssignmentViewSet + ) + router.register("assignments/poll", AssignmentPollViewSet) # Register required_users - required_user.add_collection_string(self.get_model('Assignment').get_collection_string(), required_users) + required_user.add_collection_string( + self.get_model("Assignment").get_collection_string(), required_users + ) def get_config_variables(self): from .config_variables import get_config_variables + return get_config_variables() def get_startup_elements(self): @@ -46,18 +52,15 @@ class AssignmentsAppConfig(AppConfig): Yields all Cachables required on startup i. e. opening the websocket connection. """ - yield self.get_model('Assignment') + yield self.get_model("Assignment") def get_angular_constants(self): - assignment = self.get_model('Assignment') - Item = TypedDict('Item', {'value': int, 'display_name': str}) + assignment = self.get_model("Assignment") + Item = TypedDict("Item", {"value": int, "display_name": str}) phases: List[Item] = [] for phase in assignment.PHASES: - phases.append({ - 'value': phase[0], - 'display_name': phase[1], - }) - return {'AssignmentPhases': phases} + phases.append({"value": phase[0], "display_name": phase[1]}) + return {"AssignmentPhases": phases} def required_users(element: Dict[str, Any]) -> Set[int]: @@ -65,7 +68,9 @@ def required_users(element: Dict[str, Any]) -> Set[int]: Returns all user ids that are displayed as candidates (including poll options) in the assignment element. """ - candidates = set(related_user['user_id'] for related_user in element['assignment_related_users']) - for poll in element['polls']: - candidates.update(option['candidate_id'] for option in poll['options']) + candidates = set( + related_user["user_id"] for related_user in element["assignment_related_users"] + ) + for poll in element["polls"]: + candidates.update(option["candidate_id"] for option in poll["options"]) return candidates diff --git a/openslides/assignments/config_variables.py b/openslides/assignments/config_variables.py index 778c70f72..8a2033413 100644 --- a/openslides/assignments/config_variables.py +++ b/openslides/assignments/config_variables.py @@ -13,96 +13,118 @@ def get_config_variables(): """ # Ballot and ballot papers yield ConfigVariable( - name='assignments_poll_vote_values', - default_value='auto', - input_type='choice', - label='Election method', + name="assignments_poll_vote_values", + default_value="auto", + input_type="choice", + label="Election method", choices=( - {'value': 'auto', 'display_name': 'Automatic assign of method'}, - {'value': 'votes', 'display_name': 'Always one option per candidate'}, - {'value': 'yesnoabstain', 'display_name': 'Always Yes-No-Abstain per candidate'}, - {'value': 'yesno', 'display_name': 'Always Yes/No per candidate'}), + {"value": "auto", "display_name": "Automatic assign of method"}, + {"value": "votes", "display_name": "Always one option per candidate"}, + { + "value": "yesnoabstain", + "display_name": "Always Yes-No-Abstain per candidate", + }, + {"value": "yesno", "display_name": "Always Yes/No per candidate"}, + ), weight=410, - group='Elections', - subgroup='Ballot and ballot papers') + group="Elections", + subgroup="Ballot and ballot papers", + ) yield ConfigVariable( - name='assignments_poll_100_percent_base', - default_value='YES_NO_ABSTAIN', - input_type='choice', - label='The 100-%-base of an election result consists of', + name="assignments_poll_100_percent_base", + default_value="YES_NO_ABSTAIN", + input_type="choice", + label="The 100-%-base of an election result consists of", choices=( - {'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain per candidate'}, - {'value': 'YES_NO', 'display_name': 'Yes/No per candidate'}, - {'value': 'VALID', 'display_name': 'All valid ballots'}, - {'value': 'CAST', 'display_name': 'All casted ballots'}, - {'value': 'DISABLED', 'display_name': 'Disabled (no percents)'}), - help_text=('For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base ' - 'depends on the election method: If there is only one option per candidate, ' - 'the sum of all votes of all candidates is 100 %. Otherwise for each ' - 'candidate the sum of all votes is 100 %.'), + {"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain per candidate"}, + {"value": "YES_NO", "display_name": "Yes/No per candidate"}, + {"value": "VALID", "display_name": "All valid ballots"}, + {"value": "CAST", "display_name": "All casted ballots"}, + {"value": "DISABLED", "display_name": "Disabled (no percents)"}, + ), + help_text=( + "For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base " + "depends on the election method: If there is only one option per candidate, " + "the sum of all votes of all candidates is 100 %. Otherwise for each " + "candidate the sum of all votes is 100 %." + ), weight=420, - group='Elections', - subgroup='Ballot and ballot papers') + group="Elections", + subgroup="Ballot and ballot papers", + ) # TODO: Add server side validation of the choices. yield ConfigVariable( - name='assignments_poll_default_majority_method', - default_value=majorityMethods[0]['value'], - input_type='choice', + name="assignments_poll_default_majority_method", + default_value=majorityMethods[0]["value"], + input_type="choice", choices=majorityMethods, - label='Required majority', - help_text='Default method to check whether a candidate has reached the required majority.', + label="Required majority", + help_text="Default method to check whether a candidate has reached the required majority.", weight=425, - group='Elections', - subgroup='Ballot and ballot papers') + group="Elections", + subgroup="Ballot and ballot papers", + ) yield ConfigVariable( - name='assignments_add_candidates_to_list_of_speakers', + name="assignments_add_candidates_to_list_of_speakers", default_value=True, - input_type='boolean', - label='Put all candidates on the list of speakers', + input_type="boolean", + label="Put all candidates on the list of speakers", weight=428, - group='Elections', - subgroup='Ballot and ballot papers') + group="Elections", + subgroup="Ballot and ballot papers", + ) yield ConfigVariable( - name='assignments_pdf_ballot_papers_selection', - default_value='CUSTOM_NUMBER', - input_type='choice', - label='Number of ballot papers (selection)', + name="assignments_pdf_ballot_papers_selection", + default_value="CUSTOM_NUMBER", + input_type="choice", + label="Number of ballot papers (selection)", choices=( - {'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'}, - {'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': 'Number of all participants'}, - {'value': 'CUSTOM_NUMBER', 'display_name': 'Use the following custom number'}), + {"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"}, + { + "value": "NUMBER_OF_ALL_PARTICIPANTS", + "display_name": "Number of all participants", + }, + { + "value": "CUSTOM_NUMBER", + "display_name": "Use the following custom number", + }, + ), weight=430, - group='Elections', - subgroup='Ballot and ballot papers') + group="Elections", + subgroup="Ballot and ballot papers", + ) yield ConfigVariable( - name='assignments_pdf_ballot_papers_number', + name="assignments_pdf_ballot_papers_number", default_value=8, - input_type='integer', - label='Custom number of ballot papers', + input_type="integer", + label="Custom number of ballot papers", weight=440, - group='Elections', - subgroup='Ballot and ballot papers', - validators=(MinValueValidator(1),)) + group="Elections", + subgroup="Ballot and ballot papers", + validators=(MinValueValidator(1),), + ) # PDF yield ConfigVariable( - name='assignments_pdf_title', - default_value='Elections', - label='Title for PDF document (all elections)', + name="assignments_pdf_title", + default_value="Elections", + label="Title for PDF document (all elections)", weight=460, - group='Elections', - subgroup='PDF') + group="Elections", + subgroup="PDF", + ) yield ConfigVariable( - name='assignments_pdf_preamble', - default_value='', - label='Preamble text for PDF document (all elections)', + name="assignments_pdf_preamble", + default_value="", + label="Preamble text for PDF document (all elections)", weight=470, - group='Elections', - subgroup='PDF') + group="Elections", + subgroup="PDF", + ) diff --git a/openslides/assignments/migrations/0001_initial.py b/openslides/assignments/migrations/0001_initial.py index 2e0d9d655..60c1dc8e3 100644 --- a/openslides/assignments/migrations/0001_initial.py +++ b/openslides/assignments/migrations/0001_initial.py @@ -15,104 +15,196 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('core', '0001_initial'), + ("core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Assignment', + name="Assignment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('description', models.TextField(blank=True)), - ('open_posts', models.PositiveSmallIntegerField()), - ('poll_description_default', models.CharField(blank=True, max_length=79)), - ('phase', models.IntegerField(choices=[(0, 'Searching for candidates'), (1, 'Voting'), (2, 'Finished')], default=0)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("description", models.TextField(blank=True)), + ("open_posts", models.PositiveSmallIntegerField()), + ( + "poll_description_default", + models.CharField(blank=True, max_length=79), + ), + ( + "phase", + models.IntegerField( + choices=[ + (0, "Searching for candidates"), + (1, "Voting"), + (2, "Finished"), + ], + default=0, + ), + ), ], options={ - 'verbose_name': 'Election', - 'default_permissions': (), - 'permissions': ( - ('can_see', 'Can see elections'), - ('can_nominate_other', 'Can nominate another participant'), - ('can_nominate_self', 'Can nominate oneself'), - ('can_manage', 'Can manage elections')), - 'ordering': ('title',), + "verbose_name": "Election", + "default_permissions": (), + "permissions": ( + ("can_see", "Can see elections"), + ("can_nominate_other", "Can nominate another participant"), + ("can_nominate_self", "Can nominate oneself"), + ("can_manage", "Can manage elections"), + ), + "ordering": ("title",), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='AssignmentOption', + name="AssignmentOption", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "candidate", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='AssignmentPoll', + name="AssignmentPoll", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), - ('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), - ('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), - ('published', models.BooleanField(default=False)), - ('yesnoabstain', models.BooleanField(default=False)), - ('description', models.CharField(blank=True, max_length=79)), - ('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='assignments.Assignment')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "votesvalid", + openslides.utils.models.MinMaxIntegerField(blank=True, null=True), + ), + ( + "votesinvalid", + openslides.utils.models.MinMaxIntegerField(blank=True, null=True), + ), + ( + "votescast", + openslides.utils.models.MinMaxIntegerField(blank=True, null=True), + ), + ("published", models.BooleanField(default=False)), + ("yesnoabstain", models.BooleanField(default=False)), + ("description", models.CharField(blank=True, max_length=79)), + ( + "assignment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="polls", + to="assignments.Assignment", + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='AssignmentRelatedUser', + name="AssignmentRelatedUser", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('elected', models.BooleanField(default=False)), - ('assignment', models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, related_name='assignment_related_users', to='assignments.Assignment')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("elected", models.BooleanField(default=False)), + ( + "assignment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="assignment_related_users", + to="assignments.Assignment", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='AssignmentVote', + name="AssignmentVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('weight', models.IntegerField(default=1, null=True)), - ('value', models.CharField(max_length=255, null=True)), - ('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='assignments.AssignmentOption')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("weight", models.IntegerField(default=1, null=True)), + ("value", models.CharField(max_length=255, null=True)), + ( + "option", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="votes", + to="assignments.AssignmentOption", + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.AddField( - model_name='assignmentoption', - name='poll', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='assignments.AssignmentPoll'), + model_name="assignmentoption", + name="poll", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="options", + to="assignments.AssignmentPoll", + ), ), migrations.AddField( - model_name='assignment', - name='related_users', - field=models.ManyToManyField(through='assignments.AssignmentRelatedUser', to=settings.AUTH_USER_MODEL), + model_name="assignment", + name="related_users", + field=models.ManyToManyField( + through="assignments.AssignmentRelatedUser", to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='assignment', - name='tags', - field=models.ManyToManyField(blank=True, to='core.Tag'), + model_name="assignment", + name="tags", + field=models.ManyToManyField(blank=True, to="core.Tag"), ), migrations.AlterUniqueTogether( - name='assignmentrelateduser', - unique_together=set([('assignment', 'user')]), + name="assignmentrelateduser", unique_together=set([("assignment", "user")]) ), ] diff --git a/openslides/assignments/migrations/0002_assignmentpoll_pollmethod.py b/openslides/assignments/migrations/0002_assignmentpoll_pollmethod.py index 61846ad89..29946d947 100644 --- a/openslides/assignments/migrations/0002_assignmentpoll_pollmethod.py +++ b/openslides/assignments/migrations/0002_assignmentpoll_pollmethod.py @@ -7,18 +7,13 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assignments', '0001_initial'), - ] + dependencies = [("assignments", "0001_initial")] operations = [ - migrations.RemoveField( - model_name='assignmentpoll', - name='yesnoabstain', - ), + migrations.RemoveField(model_name="assignmentpoll", name="yesnoabstain"), migrations.AddField( - model_name='assignmentpoll', - name='pollmethod', - field=models.CharField(default='yna', max_length=5), + model_name="assignmentpoll", + name="pollmethod", + field=models.CharField(default="yna", max_length=5), ), ] diff --git a/openslides/assignments/migrations/0003_candidate_weight.py b/openslides/assignments/migrations/0003_candidate_weight.py index 51ef01e21..7aa25b94a 100644 --- a/openslides/assignments/migrations/0003_candidate_weight.py +++ b/openslides/assignments/migrations/0003_candidate_weight.py @@ -7,19 +7,17 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assignments', '0002_assignmentpoll_pollmethod'), - ] + dependencies = [("assignments", "0002_assignmentpoll_pollmethod")] operations = [ migrations.AddField( - model_name='assignmentrelateduser', - name='weight', + model_name="assignmentrelateduser", + name="weight", field=models.IntegerField(default=0), ), migrations.AddField( - model_name='assignmentoption', - name='weight', + model_name="assignmentoption", + name="weight", field=models.IntegerField(default=0), ), ] diff --git a/openslides/assignments/migrations/0004_auto_20180703_1523.py b/openslides/assignments/migrations/0004_auto_20180703_1523.py index 23d73f129..c39b884ac 100644 --- a/openslides/assignments/migrations/0004_auto_20180703_1523.py +++ b/openslides/assignments/migrations/0004_auto_20180703_1523.py @@ -9,19 +9,17 @@ from openslides.utils.models import MinMaxIntegerField class Migration(migrations.Migration): - dependencies = [ - ('assignments', '0003_candidate_weight'), - ] + dependencies = [("assignments", "0003_candidate_weight")] operations = [ migrations.AddField( - model_name='assignmentpoll', - name='votesabstain', + model_name="assignmentpoll", + name="votesabstain", field=MinMaxIntegerField(null=True, blank=True, min_value=-2), ), migrations.AddField( - model_name='assignmentpoll', - name='votesno', + model_name="assignmentpoll", + name="votesno", field=MinMaxIntegerField(null=True, blank=True, min_value=-2), ), ] diff --git a/openslides/assignments/migrations/0005_auto_20180822_1042.py b/openslides/assignments/migrations/0005_auto_20180822_1042.py index 2618075c5..518f40c81 100644 --- a/openslides/assignments/migrations/0005_auto_20180822_1042.py +++ b/openslides/assignments/migrations/0005_auto_20180822_1042.py @@ -8,69 +8,73 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assignments', '0004_auto_20180703_1523'), - ] + dependencies = [("assignments", "0004_auto_20180703_1523")] operations = [ migrations.AlterField( - model_name='assignmentpoll', - name='votescast', + model_name="assignmentpoll", + name="votescast", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='assignmentpoll', - name='votesinvalid', + model_name="assignmentpoll", + name="votesinvalid", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='assignmentpoll', - name='votesvalid', + model_name="assignmentpoll", + name="votesvalid", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='assignmentvote', - name='weight', + model_name="assignmentvote", + name="weight", field=models.DecimalField( decimal_places=6, - default=Decimal('1'), + default=Decimal("1"), max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='assignmentpoll', - name='votesabstain', + model_name="assignmentpoll", + name="votesabstain", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='assignmentpoll', - name='votesno', + model_name="assignmentpoll", + name="votesno", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), ] diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index 53b894221..42ffb2182 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -31,16 +31,13 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model): """ assignment = models.ForeignKey( - 'Assignment', - on_delete=models.CASCADE, - related_name='assignment_related_users') + "Assignment", on_delete=models.CASCADE, related_name="assignment_related_users" + ) """ ForeinKey to the assignment. """ - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) """ ForeinKey to the user who is related to the assignment. """ @@ -57,7 +54,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model): class Meta: default_permissions = () - unique_together = ('assignment', 'user') + unique_together = ("assignment", "user") def __str__(self): return "%s <-> %s" % (self.assignment, self.user) @@ -73,6 +70,7 @@ class AssignmentManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all assignments. In the background @@ -80,18 +78,17 @@ class AssignmentManager(models.Manager): polls are prefetched from the database. """ return self.get_queryset().prefetch_related( - 'related_users', - 'agenda_items', - 'polls', - 'tags') + "related_users", "agenda_items", "polls", "tags" + ) class Assignment(RESTModelMixin, models.Model): """ Model for assignments. """ + access_permissions = AssignmentAccessPermissions() - can_see_permission = 'assignments.can_see' + can_see_permission = "assignments.can_see" objects = AssignmentManager() @@ -100,19 +97,17 @@ class Assignment(RESTModelMixin, models.Model): PHASE_FINISHED = 2 PHASES = ( - (PHASE_SEARCH, 'Searching for candidates'), - (PHASE_VOTING, 'Voting'), - (PHASE_FINISHED, 'Finished'), + (PHASE_SEARCH, "Searching for candidates"), + (PHASE_VOTING, "Voting"), + (PHASE_FINISHED, "Finished"), ) - title = models.CharField( - max_length=100) + title = models.CharField(max_length=100) """ Title of the assignment. """ - description = models.TextField( - blank=True) + description = models.TextField(blank=True) """ Text to describe the assignment. """ @@ -122,23 +117,19 @@ class Assignment(RESTModelMixin, models.Model): The number of members to be elected. """ - poll_description_default = models.CharField( - max_length=79, - blank=True) + poll_description_default = models.CharField(max_length=79, blank=True) """ Default text for the poll description. """ - phase = models.IntegerField( - choices=PHASES, - default=PHASE_SEARCH) + phase = models.IntegerField(choices=PHASES, default=PHASE_SEARCH) """ Phase in which the assignment is. """ related_users = models.ManyToManyField( - settings.AUTH_USER_MODEL, - through='AssignmentRelatedUser') + settings.AUTH_USER_MODEL, through="AssignmentRelatedUser" + ) """ Users that are candidates or elected. @@ -152,18 +143,18 @@ class Assignment(RESTModelMixin, models.Model): # In theory there could be one then more agenda_item. But we support only # one. See the property agenda_item. - agenda_items = GenericRelation(Item, related_name='assignments') + agenda_items = GenericRelation(Item, related_name="assignments") class Meta: default_permissions = () permissions = ( - ('can_see', 'Can see elections'), - ('can_nominate_other', 'Can nominate another participant'), - ('can_nominate_self', 'Can nominate oneself'), - ('can_manage', 'Can manage elections'), + ("can_see", "Can see elections"), + ("can_nominate_other", "Can nominate another participant"), + ("can_nominate_self", "Can nominate oneself"), + ("can_manage", "Can manage elections"), ) - ordering = ('title', ) - verbose_name = ugettext_noop('Election') + ordering = ("title",) + verbose_name = ugettext_noop("Election") def __str__(self): return self.title @@ -174,26 +165,25 @@ class Assignment(RESTModelMixin, models.Model): assignment projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='assignments/assignment', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO fix typing + skip_autoupdate=skip_autoupdate, name="assignments/assignment", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) @property def candidates(self): """ Queryset that represents the candidates for the assignment. """ - return self.related_users.filter( - assignmentrelateduser__elected=False) + return self.related_users.filter(assignmentrelateduser__elected=False) @property def elected(self): """ Queryset that represents all elected users for the assignment. """ - return self.related_users.filter( - assignmentrelateduser__elected=True) + return self.related_users.filter(assignmentrelateduser__elected=True) def is_candidate(self, user): """ @@ -215,22 +205,22 @@ class Assignment(RESTModelMixin, models.Model): """ Adds the user as candidate. """ - weight = self.assignment_related_users.aggregate( - models.Max('weight'))['weight__max'] or 0 - defaults = { - 'elected': False, - 'weight': weight + 1} + weight = ( + self.assignment_related_users.aggregate(models.Max("weight"))["weight__max"] + or 0 + ) + defaults = {"elected": False, "weight": weight + 1} related_user, __ = self.assignment_related_users.update_or_create( - user=user, - defaults=defaults) + user=user, defaults=defaults + ) def set_elected(self, user): """ Makes user an elected user for this assignment. """ related_user, __ = self.assignment_related_users.update_or_create( - user=user, - defaults={'elected': True}) + user=user, defaults={"elected": True} + ) def delete_related_user(self, user): """ @@ -258,39 +248,43 @@ class Assignment(RESTModelMixin, models.Model): candidates = self.candidates.all() # Find out the method of the election - if config['assignments_poll_vote_values'] == 'votes': - pollmethod = 'votes' - elif config['assignments_poll_vote_values'] == 'yesnoabstain': - pollmethod = 'yna' - elif config['assignments_poll_vote_values'] == 'yesno': - pollmethod = 'yn' + if config["assignments_poll_vote_values"] == "votes": + pollmethod = "votes" + elif config["assignments_poll_vote_values"] == "yesnoabstain": + pollmethod = "yna" + elif config["assignments_poll_vote_values"] == "yesno": + pollmethod = "yn" else: # config['assignments_poll_vote_values'] == 'auto' # candidates <= available posts -> yes/no/abstain if len(candidates) <= (self.open_posts - self.elected.count()): - pollmethod = 'yna' + pollmethod = "yna" else: - pollmethod = 'votes' + pollmethod = "votes" # Create the poll with the candidates. poll = self.polls.create( - description=self.poll_description_default, - pollmethod=pollmethod) + description=self.poll_description_default, pollmethod=pollmethod + ) options = [] - related_users = AssignmentRelatedUser.objects.filter(assignment__id=self.id).exclude(elected=True) + related_users = AssignmentRelatedUser.objects.filter( + assignment__id=self.id + ).exclude(elected=True) for related_user in related_users: - options.append({ - 'candidate': related_user.user, - 'weight': related_user.weight}) + options.append( + {"candidate": related_user.user, "weight": related_user.weight} + ) poll.set_options(options, skip_autoupdate=True) inform_changed_data(self) # Add all candidates to list of speakers of related agenda item # TODO: Try to do this in a bulk create - if config['assignments_add_candidates_to_list_of_speakers']: + if config["assignments_add_candidates_to_list_of_speakers"]: for candidate in self.candidates: try: - Speaker.objects.add(candidate, self.agenda_item, skip_autoupdate=True) + Speaker.objects.add( + candidate, self.agenda_item, skip_autoupdate=True + ) except OpenSlidesError: # The Speaker is already on the list. Do nothing. # TODO: Find a smart way not to catch the error concerning AnonymousUser. @@ -349,7 +343,7 @@ class Assignment(RESTModelMixin, models.Model): Return a title for the agenda with the appended assignment verbose name. Note: It has to be the same return value like in JavaScript. """ - return '%s (%s)' % (self.get_agenda_title(), _(self._meta.verbose_name)) + return "%s (%s)" % (self.get_agenda_title(), _(self._meta.verbose_name)) @property def agenda_item(self): @@ -370,9 +364,8 @@ class Assignment(RESTModelMixin, models.Model): class AssignmentVote(RESTModelMixin, BaseVote): option = models.ForeignKey( - 'AssignmentOption', - on_delete=models.CASCADE, - related_name='votes') + "AssignmentOption", on_delete=models.CASCADE, related_name="votes" + ) class Meta: default_permissions = () @@ -386,12 +379,9 @@ class AssignmentVote(RESTModelMixin, BaseVote): class AssignmentOption(RESTModelMixin, BaseOption): poll = models.ForeignKey( - 'AssignmentPoll', - on_delete=models.CASCADE, - related_name='options') - candidate = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + "AssignmentPoll", on_delete=models.CASCADE, related_name="options" + ) + candidate = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) weight = models.IntegerField(default=0) vote_class = AssignmentVote @@ -411,26 +401,32 @@ class AssignmentOption(RESTModelMixin, BaseOption): # TODO: remove the type-ignoring in the next line, after this is solved: # https://github.com/python/mypy/issues/3855 -class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore - PublishPollMixin, BasePoll): +class AssignmentPoll( # type: ignore + RESTModelMixin, CollectDefaultVotesMixin, PublishPollMixin, BasePoll +): option_class = AssignmentOption assignment = models.ForeignKey( - Assignment, - on_delete=models.CASCADE, - related_name='polls') - pollmethod = models.CharField( - max_length=5, - default='yna') - description = models.CharField( - max_length=79, - blank=True) + Assignment, on_delete=models.CASCADE, related_name="polls" + ) + pollmethod = models.CharField(max_length=5, default="yna") + description = models.CharField(max_length=79, blank=True) - votesabstain = models.DecimalField(null=True, blank=True, validators=[ - MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) + votesabstain = models.DecimalField( + null=True, + blank=True, + validators=[MinValueValidator(Decimal("-2"))], + max_digits=15, + decimal_places=6, + ) """ General abstain votes, used for pollmethod 'votes' """ - votesno = models.DecimalField(null=True, blank=True, validators=[ - MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) + votesno = models.DecimalField( + null=True, + blank=True, + validators=[MinValueValidator(Decimal("-2"))], + max_digits=15, + decimal_places=6, + ) """ General no votes, used for pollmethod 'votes' """ class Meta: @@ -443,27 +439,30 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore """ Projector.remove_any( skip_autoupdate=skip_autoupdate, - name='assignments/assignment', + name="assignments/assignment", id=self.assignment.pk, - poll=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO: fix typing + poll=self.pk, + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) def get_assignment(self): return self.assignment def get_vote_values(self): - if self.pollmethod == 'yna': - return ['Yes', 'No', 'Abstain'] - elif self.pollmethod == 'yn': - return ['Yes', 'No'] + if self.pollmethod == "yna": + return ["Yes", "No", "Abstain"] + elif self.pollmethod == "yn": + return ["Yes", "No"] else: - return ['Votes'] + return ["Votes"] def get_ballot(self): return self.assignment.polls.filter(id__lte=self.pk).count() def get_percent_base_choice(self): - return config['assignments_poll_100_percent_base'] + return config["assignments_poll_100_percent_base"] def get_root_rest_element(self): """ diff --git a/openslides/assignments/projector.py b/openslides/assignments/projector.py index e3345d14a..507bbc1a6 100644 --- a/openslides/assignments/projector.py +++ b/openslides/assignments/projector.py @@ -11,30 +11,33 @@ class AssignmentSlide(ProjectorElement): You can send a poll id to get a poll slide. """ - name = 'assignments/assignment' + + name = "assignments/assignment" def check_data(self): - if not Assignment.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('Election does not exist.') - poll_id = self.config_entry.get('poll') + if not Assignment.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("Election does not exist.") + poll_id = self.config_entry.get("poll") if poll_id: # Poll slide. try: poll = AssignmentPoll.objects.get(pk=poll_id) except AssignmentPoll.DoesNotExist: - raise ProjectorException('Poll does not exist.') - if poll.assignment_id != self.config_entry.get('id'): - raise ProjectorException('Assignment id and poll do not belong together.') + raise ProjectorException("Poll does not exist.") + if poll.assignment_id != self.config_entry.get("id"): + raise ProjectorException( + "Assignment id and poll do not belong together." + ) def update_data(self): data = None try: - assignment = Assignment.objects.get(pk=self.config_entry.get('id')) + assignment = Assignment.objects.get(pk=self.config_entry.get("id")) except Assignment.DoesNotExist: # Assignment does not exist, so just do nothing. pass else: - data = {'agenda_item_id': assignment.agenda_item_id} + data = {"agenda_item_id": assignment.agenda_item_id} return data diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py index 6b2cb1b2b..7188062ae 100644 --- a/openslides/assignments/serializers.py +++ b/openslides/assignments/serializers.py @@ -28,8 +28,10 @@ def posts_validator(data): """ Validator for open posts. It checks that the values for the open posts are greater than 0. """ - if (data['open_posts'] and data['open_posts'] is not None and data['open_posts'] < 1): - raise ValidationError({'detail': _('Value for {} must be greater than 0').format('open_posts')}) + if data["open_posts"] and data["open_posts"] is not None and data["open_posts"] < 1: + raise ValidationError( + {"detail": _("Value for {} must be greater than 0").format("open_posts")} + ) return data @@ -37,35 +39,39 @@ class AssignmentRelatedUserSerializer(ModelSerializer): """ Serializer for assignment.models.AssignmentRelatedUser objects. """ + class Meta: model = AssignmentRelatedUser fields = ( - 'id', - 'user', - 'elected', - 'assignment', - 'weight') # js-data needs the assignment-id in the nested object to define relations. + "id", + "user", + "elected", + "assignment", + "weight", + ) # js-data needs the assignment-id in the nested object to define relations. class AssignmentVoteSerializer(ModelSerializer): """ Serializer for assignment.models.AssignmentVote objects. """ + class Meta: model = AssignmentVote - fields = ('weight', 'value',) + fields = ("weight", "value") class AssignmentOptionSerializer(ModelSerializer): """ Serializer for assignment.models.AssignmentOption objects. """ + votes = AssignmentVoteSerializer(many=True, read_only=True) is_elected = SerializerMethodField() class Meta: model = AssignmentOption - fields = ('id', 'candidate', 'is_elected', 'votes', 'poll', 'weight') + fields = ("id", "candidate", "is_elected", "votes", "poll", "weight") def get_is_elected(self, obj): """ @@ -78,6 +84,7 @@ class FilterPollListSerializer(ListSerializer): """ Customized serializer to filter polls (exclude unpublished). """ + def to_representation(self, data): """ List of object instances -> List of dicts of primitive datatypes. @@ -86,7 +93,9 @@ class FilterPollListSerializer(ListSerializer): """ # Dealing with nested relationships, data can be a Manager, # so, first get a queryset from the Manager if needed - iterable = data.filter(published=True) if isinstance(data, models.Manager) else data + iterable = ( + data.filter(published=True) if isinstance(data, models.Manager) else data + ) return [self.child.to_representation(item) for item in iterable] @@ -96,31 +105,35 @@ class AssignmentAllPollSerializer(ModelSerializer): Serializes all polls. """ + options = AssignmentOptionSerializer(many=True, read_only=True) votes = ListField( child=DictField( - child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)), + child=DecimalField(max_digits=15, decimal_places=6, min_value=-2) + ), write_only=True, - required=False) + required=False, + ) has_votes = SerializerMethodField() class Meta: model = AssignmentPoll fields = ( - 'id', - 'pollmethod', - 'description', - 'published', - 'options', - 'votesabstain', - 'votesno', - 'votesvalid', - 'votesinvalid', - 'votescast', - 'votes', - 'has_votes', - 'assignment') # js-data needs the assignment-id in the nested object to define relations. - read_only_fields = ('pollmethod',) + "id", + "pollmethod", + "description", + "published", + "options", + "votesabstain", + "votesno", + "votesvalid", + "votesinvalid", + "votescast", + "votes", + "has_votes", + "assignment", + ) # js-data needs the assignment-id in the nested object to define relations. + read_only_fields = ("pollmethod",) validators = (default_votes_validator,) def get_has_votes(self, obj): @@ -144,30 +157,45 @@ class AssignmentAllPollSerializer(ModelSerializer): "votes": [{"Votes": 10}, {"Votes": 0}] """ # Update votes. - votes = validated_data.get('votes') + votes = validated_data.get("votes") if votes: options = list(instance.get_options()) if len(votes) != len(options): - raise ValidationError({ - 'detail': _('You have to submit data for %d candidates.') % len(options)}) + raise ValidationError( + { + "detail": _("You have to submit data for %d candidates.") + % len(options) + } + ) for index, option in enumerate(options): if len(votes[index]) != len(instance.get_vote_values()): - raise ValidationError({ - 'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())}) + raise ValidationError( + { + "detail": _("You have to submit data for %d vote values.") + % len(instance.get_vote_values()) + } + ) for vote_value, vote_weight in votes[index].items(): if vote_value not in instance.get_vote_values(): - raise ValidationError({ - 'detail': _('Vote value %s is invalid.') % vote_value}) - instance.set_vote_objects_with_values(option, votes[index], skip_autoupdate=True) + raise ValidationError( + {"detail": _("Vote value %s is invalid.") % vote_value} + ) + instance.set_vote_objects_with_values( + option, votes[index], skip_autoupdate=True + ) # Update remaining writeable fields. - instance.description = validated_data.get('description', instance.description) - instance.published = validated_data.get('published', instance.published) - instance.votesabstain = validated_data.get('votesabstain', instance.votesabstain) - instance.votesno = validated_data.get('votesno', instance.votesno) - instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid) - instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid) - instance.votescast = validated_data.get('votescast', instance.votescast) + instance.description = validated_data.get("description", instance.description) + instance.published = validated_data.get("published", instance.published) + instance.votesabstain = validated_data.get( + "votesabstain", instance.votesabstain + ) + instance.votesno = validated_data.get("votesno", instance.votesno) + instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid) + instance.votesinvalid = validated_data.get( + "votesinvalid", instance.votesinvalid + ) + instance.votescast = validated_data.get("votescast", instance.votescast) instance.save() return instance @@ -178,52 +206,60 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer): Serializes only short polls (excluded unpublished polls). """ + class Meta: list_serializer_class = FilterPollListSerializer model = AssignmentPoll fields = ( - 'id', - 'pollmethod', - 'description', - 'published', - 'options', - 'votesabstain', - 'votesno', - 'votesvalid', - 'votesinvalid', - 'votescast', - 'has_votes',) + "id", + "pollmethod", + "description", + "published", + "options", + "votesabstain", + "votesno", + "votesvalid", + "votesinvalid", + "votescast", + "has_votes", + ) class AssignmentFullSerializer(ModelSerializer): """ Serializer for assignment.models.Assignment objects. With all polls. """ - assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True) + + assignment_related_users = AssignmentRelatedUserSerializer( + many=True, read_only=True + ) polls = AssignmentAllPollSerializer(many=True, read_only=True) - agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3) + agenda_type = IntegerField( + write_only=True, required=False, min_value=1, max_value=3 + ) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) class Meta: model = Assignment fields = ( - 'id', - 'title', - 'description', - 'open_posts', - 'phase', - 'assignment_related_users', - 'poll_description_default', - 'polls', - 'agenda_item_id', - 'agenda_type', - 'agenda_parent_id', - 'tags',) + "id", + "title", + "description", + "open_posts", + "phase", + "assignment_related_users", + "poll_description_default", + "polls", + "agenda_item_id", + "agenda_type", + "agenda_parent_id", + "tags", + ) validators = (posts_validator,) def validate(self, data): - if 'description' in data: - data['description'] = validate_html(data['description']) + if "description" in data: + data["description"] = validate_html(data["description"]) return data def create(self, validated_data): @@ -231,10 +267,10 @@ class AssignmentFullSerializer(ModelSerializer): Customized create method. Set information about related agenda item into agenda_item_update_information container. """ - agenda_type = validated_data.pop('agenda_type', None) - agenda_parent_id = validated_data.pop('agenda_parent_id', None) + agenda_type = validated_data.pop("agenda_type", None) + agenda_parent_id = validated_data.pop("agenda_parent_id", None) assignment = Assignment(**validated_data) - assignment.agenda_item_update_information['type'] = agenda_type - assignment.agenda_item_update_information['parent_id'] = agenda_parent_id + assignment.agenda_item_update_information["type"] = agenda_type + assignment.agenda_item_update_information["parent_id"] = agenda_parent_id assignment.save() return assignment diff --git a/openslides/assignments/signals.py b/openslides/assignments/signals.py index 22182ba51..98b03be55 100644 --- a/openslides/assignments/signals.py +++ b/openslides/assignments/signals.py @@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs): """ Yields all necessary collections if 'assignments.can_see' permission changes. """ - assignments_app = apps.get_app_config(app_label='assignments') + assignments_app = apps.get_app_config(app_label="assignments") for permission in permissions: # There could be only one 'assignment.can_see' and then we want to return data. - if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see': + if ( + permission.content_type.app_label == assignments_app.label + and permission.codename == "can_see" + ): yield from assignments_app.get_startup_elements() diff --git a/openslides/assignments/views.py b/openslides/assignments/views.py index ef6750960..6f07b7f0a 100644 --- a/openslides/assignments/views.py +++ b/openslides/assignments/views.py @@ -21,6 +21,7 @@ from .serializers import AssignmentAllPollSerializer # Viewsets for the REST API + class AssignmentViewSet(ModelViewSet): """ API endpoint for assignments. @@ -29,6 +30,7 @@ class AssignmentViewSet(ModelViewSet): partial_update, update, destroy, candidature_self, candidature_other, mark_elected and create_poll. """ + access_permissions = AssignmentAccessPermissions() queryset = Assignment.objects.all() @@ -36,26 +38,36 @@ class AssignmentViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': + elif self.action == "metadata": # Everybody is allowed to see the metadata. result = True - elif self.action in ('create', 'partial_update', 'update', 'destroy', - 'mark_elected', 'create_poll', 'sort_related_users'): - result = (has_perm(self.request.user, 'assignments.can_see') and - has_perm(self.request.user, 'assignments.can_manage')) - elif self.action == 'candidature_self': - result = (has_perm(self.request.user, 'assignments.can_see') and - has_perm(self.request.user, 'assignments.can_nominate_self')) - elif self.action == 'candidature_other': - result = (has_perm(self.request.user, 'assignments.can_see') and - has_perm(self.request.user, 'assignments.can_nominate_other')) + elif self.action in ( + "create", + "partial_update", + "update", + "destroy", + "mark_elected", + "create_poll", + "sort_related_users", + ): + result = has_perm(self.request.user, "assignments.can_see") and has_perm( + self.request.user, "assignments.can_manage" + ) + elif self.action == "candidature_self": + result = has_perm(self.request.user, "assignments.can_see") and has_perm( + self.request.user, "assignments.can_nominate_self" + ) + elif self.action == "candidature_other": + result = has_perm(self.request.user, "assignments.can_see") and has_perm( + self.request.user, "assignments.can_nominate_other" + ) else: result = False return result - @detail_route(methods=['post', 'delete']) + @detail_route(methods=["post", "delete"]) def candidature_self(self, request, pk=None): """ View to nominate self as candidate (POST) or withdraw own @@ -63,18 +75,26 @@ class AssignmentViewSet(ModelViewSet): """ assignment = self.get_object() if assignment.is_elected(request.user): - raise ValidationError({'detail': _('You are already elected.')}) - if request.method == 'POST': + raise ValidationError({"detail": _("You are already elected.")}) + if request.method == "POST": message = self.nominate_self(request, assignment) else: # request.method == 'DELETE' message = self.withdraw_self(request, assignment) - return Response({'detail': message}) + return Response({"detail": message}) def nominate_self(self, request, assignment): if assignment.phase == assignment.PHASE_FINISHED: - raise ValidationError({'detail': _('You can not candidate to this election because it is finished.')}) - if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'): + raise ValidationError( + { + "detail": _( + "You can not candidate to this election because it is finished." + ) + } + ) + if assignment.phase == assignment.PHASE_VOTING and not has_perm( + request.user, "assignments.can_manage" + ): # To nominate self during voting you have to be a manager. self.permission_denied(request) # If the request.user is already a candidate he can nominate himself nevertheless. @@ -82,19 +102,29 @@ class AssignmentViewSet(ModelViewSet): # Send new candidate via autoupdate because users without permission # to see users may not have it but can get it now. inform_changed_data([request.user]) - return _('You were nominated successfully.') + return _("You were nominated successfully.") def withdraw_self(self, request, assignment): # Withdraw candidature. if assignment.phase == assignment.PHASE_FINISHED: - raise ValidationError({'detail': _('You can not withdraw your candidature to this election because it is finished.')}) - if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'): + raise ValidationError( + { + "detail": _( + "You can not withdraw your candidature to this election because it is finished." + ) + } + ) + if assignment.phase == assignment.PHASE_VOTING and not has_perm( + request.user, "assignments.can_manage" + ): # To withdraw self during voting you have to be a manager. self.permission_denied(request) if not assignment.is_candidate(request.user): - raise ValidationError({'detail': _('You are not a candidate of this election.')}) + raise ValidationError( + {"detail": _("You are not a candidate of this election.")} + ) assignment.delete_related_user(request.user) - return _('You have withdrawn your candidature successfully.') + return _("You have withdrawn your candidature successfully.") def get_user_from_request_data(self, request): """ @@ -103,20 +133,26 @@ class AssignmentViewSet(ModelViewSet): self.mark_elected can play with it. """ if not isinstance(request.data, dict): - detail = _('Invalid data. Expected dictionary, got %s.') % type(request.data) - raise ValidationError({'detail': detail}) - user_str = request.data.get('user', '') + detail = _("Invalid data. Expected dictionary, got %s.") % type( + request.data + ) + raise ValidationError({"detail": detail}) + user_str = request.data.get("user", "") try: user_pk = int(user_str) except ValueError: - raise ValidationError({'detail': _('Invalid data. Expected something like {"user": }.')}) + raise ValidationError( + {"detail": _('Invalid data. Expected something like {"user": }.')} + ) try: user = get_user_model().objects.get(pk=user_pk) except get_user_model().DoesNotExist: - raise ValidationError({'detail': _('Invalid data. User %d does not exist.') % user_pk}) + raise ValidationError( + {"detail": _("Invalid data. User %d does not exist.") % user_pk} + ) return user - @detail_route(methods=['post', 'delete']) + @detail_route(methods=["post", "delete"]) def candidature_other(self, request, pk=None): """ View to nominate other users (POST) or delete their candidature @@ -124,43 +160,51 @@ class AssignmentViewSet(ModelViewSet): """ user = self.get_user_from_request_data(request) assignment = self.get_object() - if request.method == 'POST': + if request.method == "POST": message = self.nominate_other(request, user, assignment) else: # request.method == 'DELETE' message = self.delete_other(request, user, assignment) - return Response({'detail': message}) + return Response({"detail": message}) def nominate_other(self, request, user, assignment): if assignment.is_elected(user): - raise ValidationError({'detail': _('User %s is already elected.') % user}) + raise ValidationError({"detail": _("User %s is already elected.") % user}) if assignment.phase == assignment.PHASE_FINISHED: - detail = _('You can not nominate someone to this election because it is finished.') - raise ValidationError({'detail': detail}) - if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'): + detail = _( + "You can not nominate someone to this election because it is finished." + ) + raise ValidationError({"detail": detail}) + if assignment.phase == assignment.PHASE_VOTING and not has_perm( + request.user, "assignments.can_manage" + ): # To nominate another user during voting you have to be a manager. self.permission_denied(request) if assignment.is_candidate(user): - raise ValidationError({'detail': _('User %s is already nominated.') % user}) + raise ValidationError({"detail": _("User %s is already nominated.") % user}) assignment.set_candidate(user) # Send new candidate via autoupdate because users without permission # to see users may not have it but can get it now. inform_changed_data(user) - return _('User %s was nominated successfully.') % user + return _("User %s was nominated successfully.") % user def delete_other(self, request, user, assignment): # To delete candidature status you have to be a manager. - if not has_perm(request.user, 'assignments.can_manage'): + if not has_perm(request.user, "assignments.can_manage"): self.permission_denied(request) if assignment.phase == assignment.PHASE_FINISHED: - detail = _("You can not delete someone's candidature to this election because it is finished.") - raise ValidationError({'detail': detail}) + detail = _( + "You can not delete someone's candidature to this election because it is finished." + ) + raise ValidationError({"detail": detail}) if not assignment.is_candidate(user) and not assignment.is_elected(user): - raise ValidationError({'detail': _('User %s has no status in this election.') % user}) + raise ValidationError( + {"detail": _("User %s has no status in this election.") % user} + ) assignment.delete_related_user(user) - return _('Candidate %s was withdrawn successfully.') % user + return _("Candidate %s was withdrawn successfully.") % user - @detail_route(methods=['post', 'delete']) + @detail_route(methods=["post", "delete"]) def mark_elected(self, request, pk=None): """ View to mark other users as elected (POST) or undo this (DELETE). @@ -168,35 +212,41 @@ class AssignmentViewSet(ModelViewSet): """ user = self.get_user_from_request_data(request) assignment = self.get_object() - if request.method == 'POST': + if request.method == "POST": if not assignment.is_candidate(user): - raise ValidationError({'detail': _('User %s is not a candidate of this election.') % user}) + raise ValidationError( + {"detail": _("User %s is not a candidate of this election.") % user} + ) assignment.set_elected(user) - message = _('User %s was successfully elected.') % user + message = _("User %s was successfully elected.") % user else: # request.method == 'DELETE' if not assignment.is_elected(user): - detail = _('User %s is not an elected candidate of this election.') % user - raise ValidationError({'detail': detail}) + detail = ( + _("User %s is not an elected candidate of this election.") % user + ) + raise ValidationError({"detail": detail}) assignment.set_candidate(user) - message = _('User %s was successfully unelected.') % user - return Response({'detail': message}) + message = _("User %s was successfully unelected.") % user + return Response({"detail": message}) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def create_poll(self, request, pk=None): """ View to create a poll. It is a POST request without any data. """ assignment = self.get_object() if not assignment.candidates.exists(): - raise ValidationError({'detail': _('Can not create ballot because there are no candidates.')}) + raise ValidationError( + {"detail": _("Can not create ballot because there are no candidates.")} + ) with transaction.atomic(): poll = assignment.create_poll() - return Response({ - 'detail': _('Ballot created successfully.'), - 'createdPollId': poll.pk}) + return Response( + {"detail": _("Ballot created successfully."), "createdPollId": poll.pk} + ) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def sort_related_users(self, request, pk=None): """ Special view endpoint to sort the assignment related users. @@ -206,22 +256,25 @@ class AssignmentViewSet(ModelViewSet): assignment = self.get_object() # Check data - related_user_ids = request.data.get('related_users') + related_user_ids = request.data.get("related_users") if not isinstance(related_user_ids, list): - raise ValidationError( - {'detail': _('users has to be a list of IDs.')}) + raise ValidationError({"detail": _("users has to be a list of IDs.")}) # Get all related users from AssignmentRelatedUser. related_users = {} - for related_user in AssignmentRelatedUser.objects.filter(assignment__id=assignment.id): + for related_user in AssignmentRelatedUser.objects.filter( + assignment__id=assignment.id + ): related_users[related_user.pk] = related_user # Check all given candidates from the request valid_related_users = [] for related_user_id in related_user_ids: - if not isinstance(related_user_id, int) or related_users.get(related_user_id) is None: - raise ValidationError( - {'detail': _('Invalid data.')}) + if ( + not isinstance(related_user_id, int) + or related_users.get(related_user_id) is None + ): + raise ValidationError({"detail": _("Invalid data.")}) valid_related_users.append(related_users[related_user_id]) # Sort the related users @@ -236,7 +289,7 @@ class AssignmentViewSet(ModelViewSet): inform_changed_data(assignment) # Initiate response. - return Response({'detail': _('Assignment related users successfully sorted.')}) + return Response({"detail": _("Assignment related users successfully sorted.")}) class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): @@ -245,6 +298,7 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet) There are the following views: update, partial_update and destroy. """ + queryset = AssignmentPoll.objects.all() serializer_class = AssignmentAllPollSerializer @@ -252,5 +306,6 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet) """ Returns True if the user has required permissions. """ - return (has_perm(self.request.user, 'assignments.can_see') and - has_perm(self.request.user, 'assignments.can_manage')) + return has_perm(self.request.user, "assignments.can_see") and has_perm( + self.request.user, "assignments.can_manage" + ) diff --git a/openslides/core/__init__.py b/openslides/core/__init__.py index 8c0054d41..3471a882c 100644 --- a/openslides/core/__init__.py +++ b/openslides/core/__init__.py @@ -1 +1 @@ -default_app_config = 'openslides.core.apps.CoreAppConfig' +default_app_config = "openslides.core.apps.CoreAppConfig" diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py index b004f7199..9627f29bb 100644 --- a/openslides/core/access_permissions.py +++ b/openslides/core/access_permissions.py @@ -6,7 +6,8 @@ class ProjectorAccessPermissions(BaseAccessPermissions): """ Access permissions container for Projector and ProjectorViewSet. """ - base_permission = 'core.can_see_projector' + + base_permission = "core.can_see_projector" class TagAccessPermissions(BaseAccessPermissions): @@ -19,21 +20,24 @@ class ChatMessageAccessPermissions(BaseAccessPermissions): """ Access permissions container for ChatMessage and ChatMessageViewSet. """ - base_permission = 'core.can_use_chat' + + base_permission = "core.can_use_chat" class ProjectorMessageAccessPermissions(BaseAccessPermissions): """ Access permissions for ProjectorMessage. """ - base_permission = 'core.can_see_projector' + + base_permission = "core.can_see_projector" class CountdownAccessPermissions(BaseAccessPermissions): """ Access permissions for Countdown. """ - base_permission = 'core.can_see_projector' + + base_permission = "core.can_see_projector" class ConfigAccessPermissions(BaseAccessPermissions): diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 86c265307..c06c660e5 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -11,8 +11,8 @@ from ..utils.projector import register_projector_elements class CoreAppConfig(AppConfig): - name = 'openslides.core' - verbose_name = 'OpenSlides Core' + name = "openslides.core" + verbose_name = "OpenSlides Core" angular_site_module = True angular_projector_module = True @@ -54,7 +54,7 @@ class CoreAppConfig(AppConfig): # Skip all database related accesses during migrations. is_normal_server_start = False for sys_part in sys.argv: - for entry in ('runserver', 'gunicorn', 'daphne', 'create-example-data'): + for entry in ("runserver", "gunicorn", "daphne", "create-example-data"): if sys_part.endswith(entry): is_normal_server_start = True break @@ -68,27 +68,46 @@ class CoreAppConfig(AppConfig): # Connect signals. post_permission_creation.connect( - delete_django_app_permissions, - dispatch_uid='delete_django_app_permissions') + delete_django_app_permissions, dispatch_uid="delete_django_app_permissions" + ) permission_change.connect( - get_permission_change_data, - dispatch_uid='core_get_permission_change_data') + get_permission_change_data, dispatch_uid="core_get_permission_change_data" + ) - post_migrate.connect(call_save_default_values, sender=self, dispatch_uid='core_save_config_default_values') + post_migrate.connect( + call_save_default_values, + sender=self, + dispatch_uid="core_save_config_default_values", + ) # Register viewsets. - router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) - router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet) - router.register(self.get_model('Tag').get_collection_string(), TagViewSet) - router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config') - router.register(self.get_model('ProjectorMessage').get_collection_string(), ProjectorMessageViewSet) - router.register(self.get_model('Countdown').get_collection_string(), CountdownViewSet) - router.register(self.get_model('History').get_collection_string(), HistoryViewSet) + router.register( + self.get_model("Projector").get_collection_string(), ProjectorViewSet + ) + router.register( + self.get_model("ChatMessage").get_collection_string(), ChatMessageViewSet + ) + router.register(self.get_model("Tag").get_collection_string(), TagViewSet) + router.register( + self.get_model("ConfigStore").get_collection_string(), + ConfigViewSet, + "config", + ) + router.register( + self.get_model("ProjectorMessage").get_collection_string(), + ProjectorMessageViewSet, + ) + router.register( + self.get_model("Countdown").get_collection_string(), CountdownViewSet + ) + router.register( + self.get_model("History").get_collection_string(), HistoryViewSet + ) # Sets the cache and builds the startup history if is_normal_server_start: element_cache.ensure_cache() - self.get_model('History').objects.build_history() + self.get_model("History").objects.build_history() # Register client messages register_client_message(NotifyWebsocketClientMessage()) @@ -97,10 +116,13 @@ class CoreAppConfig(AppConfig): register_client_message(AutoupdateWebsocketClientMessage()) # register required_users - required_user.add_collection_string(self.get_model('ChatMessage').get_collection_string(), required_users) + required_user.add_collection_string( + self.get_model("ChatMessage").get_collection_string(), required_users + ) def get_config_variables(self): from .config_variables import get_config_variables + return get_config_variables() def get_startup_elements(self): @@ -108,7 +130,15 @@ class CoreAppConfig(AppConfig): Yields all Cachables required on startup i. e. opening the websocket connection. """ - for model_name in ('Projector', 'ChatMessage', 'Tag', 'ProjectorMessage', 'Countdown', 'ConfigStore', 'History'): + for model_name in ( + "Projector", + "ChatMessage", + "Tag", + "ProjectorMessage", + "Countdown", + "ConfigStore", + "History", + ): yield self.get_model(model_name) def get_angular_constants(self): @@ -118,9 +148,9 @@ class CoreAppConfig(AppConfig): # Client settings client_settings_keys = [ - 'MOTION_IDENTIFIER_MIN_DIGITS', - 'MOTION_IDENTIFIER_WITHOUT_BLANKS', - 'MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS' + "MOTION_IDENTIFIER_MIN_DIGITS", + "MOTION_IDENTIFIER_WITHOUT_BLANKS", + "MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS", ] client_settings_dict = {} for key in client_settings_keys: @@ -130,33 +160,40 @@ class CoreAppConfig(AppConfig): # Settings key does not exist. Do nothing. The client will # treat this as undefined. pass - constants['OpenSlidesSettings'] = client_settings_dict + constants["OpenSlidesSettings"] = client_settings_dict # Config variables config_groups: List[Any] = [] - for config_variable in sorted(config.config_variables.values(), key=attrgetter('weight')): + for config_variable in sorted( + config.config_variables.values(), key=attrgetter("weight") + ): if config_variable.is_hidden(): # Skip hidden config variables. Do not even check groups and subgroups. continue - if not config_groups or config_groups[-1]['name'] != config_variable.group: + if not config_groups or config_groups[-1]["name"] != config_variable.group: # Add new group. - config_groups.append(OrderedDict( - name=config_variable.group, - subgroups=[])) - if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup: + config_groups.append( + OrderedDict(name=config_variable.group, subgroups=[]) + ) + if ( + not config_groups[-1]["subgroups"] + or config_groups[-1]["subgroups"][-1]["name"] + != config_variable.subgroup + ): # Add new subgroup. - config_groups[-1]['subgroups'].append(OrderedDict( - name=config_variable.subgroup, - items=[])) + config_groups[-1]["subgroups"].append( + OrderedDict(name=config_variable.subgroup, items=[]) + ) # Add the config variable to the current group and subgroup. - config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data) - constants['OpenSlidesConfigVariables'] = config_groups + config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data) + constants["OpenSlidesConfigVariables"] = config_groups return constants def call_save_default_values(**kwargs): from .config import config + config.save_default_values() @@ -164,4 +201,4 @@ def required_users(element: Dict[str, Any]) -> Set[int]: """ Returns all user ids that are displayed as chatters. """ - return set(element['user_id']) + return set(element["user_id"]) diff --git a/openslides/core/config.py b/openslides/core/config.py index de5946ff2..e7a1950b4 100644 --- a/openslides/core/config.py +++ b/openslides/core/config.py @@ -1,13 +1,4 @@ -from typing import ( - Any, - Callable, - Dict, - Iterable, - Optional, - TypeVar, - Union, - cast, -) +from typing import Any, Callable, Dict, Iterable, Optional, TypeVar, Union, cast from asgiref.sync import async_to_sync from django.apps import apps @@ -21,16 +12,16 @@ from .models import ConfigStore INPUT_TYPE_MAPPING = { - 'string': str, - 'text': str, - 'markupText': str, - 'integer': int, - 'boolean': bool, - 'choice': str, - 'colorpicker': str, - 'datetimepicker': int, - 'static': dict, - 'translations': list, + "string": str, + "text": str, + "markupText": str, + "integer": int, + "boolean": bool, + "choice": str, + "colorpicker": str, + "datetimepicker": int, + "static": dict, + "translations": list, } @@ -54,9 +45,11 @@ class ConfigHandler: Returns the value of the config variable. """ if not self.exists(key): - raise ConfigNotFound(_('The config variable {} was not found.').format(key)) + raise ConfigNotFound(_("The config variable {} was not found.").format(key)) - return async_to_sync(element_cache.get_element_full_data)(self.get_collection_string(), self.get_key_to_id()[key])['value'] + return async_to_sync(element_cache.get_element_full_data)( + self.get_collection_string(), self.get_key_to_id()[key] + )["value"] def get_key_to_id(self) -> Dict[str, int]: """ @@ -80,7 +73,7 @@ class ConfigHandler: all_data = await element_cache.get_all_full_data() elements = all_data[self.get_collection_string()] for element in elements: - self.key_to_id[element['key']] = element['id'] + self.key_to_id[element["key"]] = element["id"] def exists(self, key: str) -> bool: """ @@ -102,7 +95,7 @@ class ConfigHandler: try: config_variable = self.config_variables[key] except KeyError: - raise ConfigNotFound(_('The config variable {} was not found.').format(key)) + raise ConfigNotFound(_("The config variable {} was not found.").format(key)) # Validate datatype and run validators. expected_type = INPUT_TYPE_MAPPING[config_variable.input_type] @@ -111,17 +104,21 @@ class ConfigHandler: try: value = expected_type(value) except ValueError: - raise ConfigError(_('Wrong datatype. Expected %(expected_type)s, got %(got_type)s.') % { - 'expected_type': expected_type, 'got_type': type(value)}) + raise ConfigError( + _("Wrong datatype. Expected %(expected_type)s, got %(got_type)s.") + % {"expected_type": expected_type, "got_type": type(value)} + ) - if config_variable.input_type == 'choice': + if config_variable.input_type == "choice": # Choices can be a callable. In this case call it at this place if callable(config_variable.choices): choices = config_variable.choices() else: choices = config_variable.choices - if choices is None or value not in map(lambda choice: choice['value'], choices): - raise ConfigError(_('Invalid input. Choice does not match.')) + if choices is None or value not in map( + lambda choice: choice["value"], choices + ): + raise ConfigError(_("Invalid input. Choice does not match.")) for validator in config_variable.validators: try: @@ -129,34 +126,36 @@ class ConfigHandler: except DjangoValidationError as e: raise ConfigError(e.messages[0]) - if config_variable.input_type == 'static': + if config_variable.input_type == "static": if not isinstance(value, dict): - raise ConfigError(_('This has to be a dict.')) - whitelist = ( - 'path', - 'display_name', - ) + raise ConfigError(_("This has to be a dict.")) + whitelist = ("path", "display_name") for required_entry in whitelist: if required_entry not in value: - raise ConfigError(_('{} has to be given.'.format(required_entry))) + raise ConfigError(_("{} has to be given.".format(required_entry))) if not isinstance(value[required_entry], str): - raise ConfigError(_('{} has to be a string.'.format(required_entry))) + raise ConfigError( + _("{} has to be a string.".format(required_entry)) + ) - if config_variable.input_type == 'translations': + if config_variable.input_type == "translations": if not isinstance(value, list): - raise ConfigError(_('Translations has to be a list.')) + raise ConfigError(_("Translations has to be a list.")) for entry in value: if not isinstance(entry, dict): - raise ConfigError(_('Every value has to be a dict, not {}.'.format(type(entry)))) - whitelist = ( - 'original', - 'translation', - ) + raise ConfigError( + _("Every value has to be a dict, not {}.".format(type(entry))) + ) + whitelist = ("original", "translation") for required_entry in whitelist: if required_entry not in entry: - raise ConfigError(_('{} has to be given.'.format(required_entry))) + raise ConfigError( + _("{} has to be given.".format(required_entry)) + ) if not isinstance(entry[required_entry], str): - raise ConfigError(_('{} has to be a string.'.format(required_entry))) + raise ConfigError( + _("{} has to be a string.".format(required_entry)) + ) # Save the new value to the database. db_value = ConfigStore.objects.get(key=key) @@ -178,7 +177,7 @@ class ConfigHandler: continue self.update_config_variables(get_config_variables()) - def update_config_variables(self, items: Iterable['ConfigVariable']) -> None: + def update_config_variables(self, items: Iterable["ConfigVariable"]) -> None: """ Updates the config_variables dict. """ @@ -189,7 +188,9 @@ class ConfigHandler: # be in already in self.config_variables intersection = set(item_index.keys()).intersection(self.config_variables.keys()) if intersection: - raise ConfigError(_('Too many values for config variables {} found.').format(intersection)) + raise ConfigError( + _("Too many values for config variables {} found.").format(intersection) + ) self.config_variables.update(item_index) @@ -224,19 +225,22 @@ use x = config[...], to set it use config[...] = x. """ -T = TypeVar('T') +T = TypeVar("T") ChoiceType = Optional[Iterable[Dict[str, str]]] ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]] ValidatorsType = Iterable[Callable[[T], None]] OnChangeType = Callable[[], None] -ConfigVariableDict = TypedDict('ConfigVariableDict', { - 'key': str, - 'default_value': Any, - 'input_type': str, - 'label': str, - 'help_text': str, - 'choices': ChoiceType, -}) +ConfigVariableDict = TypedDict( + "ConfigVariableDict", + { + "key": str, + "default_value": Any, + "input_type": str, + "label": str, + "help_text": str, + "choices": ChoiceType, + }, +) class ConfigVariable: @@ -265,27 +269,47 @@ class ConfigVariable: the value during setup of the database if the admin uses the respective command line option. """ - def __init__(self, name: str, default_value: T, input_type: str = 'string', - label: str = None, help_text: str = None, choices: ChoiceCallableType = None, - hidden: bool = False, weight: int = 0, group: str = None, subgroup: str = None, - validators: ValidatorsType = None, on_change: OnChangeType = None) -> None: + + def __init__( + self, + name: str, + default_value: T, + input_type: str = "string", + label: str = None, + help_text: str = None, + choices: ChoiceCallableType = None, + hidden: bool = False, + weight: int = 0, + group: str = None, + subgroup: str = None, + validators: ValidatorsType = None, + on_change: OnChangeType = None, + ) -> None: if input_type not in INPUT_TYPE_MAPPING: - raise ValueError(_('Invalid value for config attribute input_type.')) - if input_type == 'choice' and choices is None: - raise ConfigError(_("Either config attribute 'choices' must not be None or " - "'input_type' must not be 'choice'.")) - elif input_type != 'choice' and choices is not None: - raise ConfigError(_("Either config attribute 'choices' must be None or " - "'input_type' must be 'choice'.")) + raise ValueError(_("Invalid value for config attribute input_type.")) + if input_type == "choice" and choices is None: + raise ConfigError( + _( + "Either config attribute 'choices' must not be None or " + "'input_type' must not be 'choice'." + ) + ) + elif input_type != "choice" and choices is not None: + raise ConfigError( + _( + "Either config attribute 'choices' must be None or " + "'input_type' must be 'choice'." + ) + ) self.name = name self.default_value = default_value self.input_type = input_type self.label = label or name - self.help_text = help_text or '' + self.help_text = help_text or "" self.choices = choices self.hidden = hidden self.weight = weight - self.group = group or _('General') + self.group = group or _("General") self.subgroup = subgroup self.validators = validators or () self.on_change = on_change @@ -301,7 +325,7 @@ class ConfigVariable: input_type=self.input_type, label=self.label, help_text=self.help_text, - choices=self.choices() if callable(self.choices) else self.choices + choices=self.choices() if callable(self.choices) else self.choices, ) def is_hidden(self) -> bool: diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 7d911939d..0bfb9f32f 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -12,401 +12,429 @@ def get_config_variables(): (see apps.py). """ yield ConfigVariable( - name='general_event_name', - default_value='OpenSlides', - label='Event name', + name="general_event_name", + default_value="OpenSlides", + label="Event name", weight=110, - group='General', - subgroup='Event', - validators=(MaxLengthValidator(100),)) + group="General", + subgroup="Event", + validators=(MaxLengthValidator(100),), + ) yield ConfigVariable( - name='general_event_description', - default_value='Presentation and assembly system', - label='Short description of event', + name="general_event_description", + default_value="Presentation and assembly system", + label="Short description of event", weight=115, - group='General', - subgroup='Event', - validators=(MaxLengthValidator(100),)) + group="General", + subgroup="Event", + validators=(MaxLengthValidator(100),), + ) yield ConfigVariable( - name='general_event_date', - default_value='', - label='Event date', + name="general_event_date", + default_value="", + label="Event date", weight=120, - group='General', - subgroup='Event') + group="General", + subgroup="Event", + ) yield ConfigVariable( - name='general_event_location', - default_value='', - label='Event location', + name="general_event_location", + default_value="", + label="Event location", weight=125, - group='General', - subgroup='Event') + group="General", + subgroup="Event", + ) yield ConfigVariable( - name='general_event_legal_notice', + name="general_event_legal_notice", default_value='OpenSlides is a ' - 'free web based presentation and assembly system for ' - 'visualizing and controlling agenda, motions and ' - 'elections of an assembly.', - input_type='markupText', - label='Legal notice', + "free web based presentation and assembly system for " + "visualizing and controlling agenda, motions and " + "elections of an assembly.", + input_type="markupText", + label="Legal notice", weight=132, - group='General', - subgroup='Event') + group="General", + subgroup="Event", + ) yield ConfigVariable( - name='general_event_privacy_policy', - default_value='', - input_type='markupText', - label='Privacy policy', + name="general_event_privacy_policy", + default_value="", + input_type="markupText", + label="Privacy policy", weight=132, - group='General', - subgroup='Event') + group="General", + subgroup="Event", + ) yield ConfigVariable( - name='general_event_welcome_title', - default_value='Welcome to OpenSlides', - label='Front page title', + name="general_event_welcome_title", + default_value="Welcome to OpenSlides", + label="Front page title", weight=134, - group='General', - subgroup='Event') + group="General", + subgroup="Event", + ) yield ConfigVariable( - name='general_event_welcome_text', - default_value='[Space for your welcome text.]', - input_type='markupText', - label='Front page text', + name="general_event_welcome_text", + default_value="[Space for your welcome text.]", + input_type="markupText", + label="Front page text", weight=136, - group='General', - subgroup='Event') + group="General", + subgroup="Event", + ) # General System yield ConfigVariable( - name='general_system_enable_anonymous', + name="general_system_enable_anonymous", default_value=False, - input_type='boolean', - label='Allow access for anonymous guest users', + input_type="boolean", + label="Allow access for anonymous guest users", weight=138, - group='General', - subgroup='System') + group="General", + subgroup="System", + ) yield ConfigVariable( - name='general_login_info_text', - default_value='', - label='Show this text on the login page', + name="general_login_info_text", + default_value="", + label="Show this text on the login page", weight=140, - group='General', - subgroup='System') + group="General", + subgroup="System", + ) # General export settings yield ConfigVariable( - name='general_csv_separator', - default_value=',', - label='Separator used for all csv exports and examples', + name="general_csv_separator", + default_value=",", + label="Separator used for all csv exports and examples", weight=142, - group='General', - subgroup='Export') + group="General", + subgroup="Export", + ) yield ConfigVariable( - name='general_export_pdf_pagenumber_alignment', - default_value='center', - input_type='choice', - label='Page number alignment in PDF', + name="general_export_pdf_pagenumber_alignment", + default_value="center", + input_type="choice", + label="Page number alignment in PDF", choices=( - {'value': 'left', 'display_name': 'Left'}, - {'value': 'center', 'display_name': 'Center'}, - {'value': 'right', 'display_name': 'Right'}), + {"value": "left", "display_name": "Left"}, + {"value": "center", "display_name": "Center"}, + {"value": "right", "display_name": "Right"}, + ), weight=144, - group='General', - subgroup='Export') + group="General", + subgroup="Export", + ) yield ConfigVariable( - name='general_export_pdf_fontsize', - default_value='10', - input_type='choice', - label='Standard font size in PDF', + name="general_export_pdf_fontsize", + default_value="10", + input_type="choice", + label="Standard font size in PDF", choices=( - {'value': '10', 'display_name': '10'}, - {'value': '11', 'display_name': '11'}, - {'value': '12', 'display_name': '12'}), + {"value": "10", "display_name": "10"}, + {"value": "11", "display_name": "11"}, + {"value": "12", "display_name": "12"}, + ), weight=146, - group='General', - subgroup='Export') + group="General", + subgroup="Export", + ) # Projector yield ConfigVariable( - name='projector_language', - default_value='browser', - input_type='choice', - label='Projector language', + name="projector_language", + default_value="browser", + input_type="choice", + label="Projector language", choices=( - {'value': 'browser', 'display_name': 'Current browser language'}, - {'value': 'en', 'display_name': 'English'}, - {'value': 'de', 'display_name': 'Deutsch'}, - {'value': 'fr', 'display_name': 'Français'}, - {'value': 'es', 'display_name': 'Español'}, - {'value': 'pt', 'display_name': 'Português'}, - {'value': 'cs', 'display_name': 'Čeština'}, - {'value': 'ru', 'display_name': 'русский'}), + {"value": "browser", "display_name": "Current browser language"}, + {"value": "en", "display_name": "English"}, + {"value": "de", "display_name": "Deutsch"}, + {"value": "fr", "display_name": "Français"}, + {"value": "es", "display_name": "Español"}, + {"value": "pt", "display_name": "Português"}, + {"value": "cs", "display_name": "Čeština"}, + {"value": "ru", "display_name": "русский"}, + ), weight=150, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_enable_logo', + name="projector_enable_logo", default_value=True, - input_type='boolean', - label='Show logo on projector', - help_text='You can replace the logo by uploading an image and set it as ' - 'the "Projector logo" in "files".', + input_type="boolean", + label="Show logo on projector", + help_text="You can replace the logo by uploading an image and set it as " + 'the "Projector logo" in "files".', weight=152, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_enable_clock', + name="projector_enable_clock", default_value=True, - input_type='boolean', - label='Show the clock on projector', + input_type="boolean", + label="Show the clock on projector", weight=154, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_enable_title', + name="projector_enable_title", default_value=True, - input_type='boolean', - label='Show title and description of event on projector', + input_type="boolean", + label="Show title and description of event on projector", weight=155, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_enable_header_footer', + name="projector_enable_header_footer", default_value=True, - input_type='boolean', - label='Display header and footer', + input_type="boolean", + label="Display header and footer", weight=157, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_header_backgroundcolor', - default_value='#317796', - input_type='colorpicker', - label='Background color of projector header and footer', + name="projector_header_backgroundcolor", + default_value="#317796", + input_type="colorpicker", + label="Background color of projector header and footer", weight=160, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_header_fontcolor', - default_value='#F5F5F5', - input_type='colorpicker', - label='Font color of projector header and footer', + name="projector_header_fontcolor", + default_value="#F5F5F5", + input_type="colorpicker", + label="Font color of projector header and footer", weight=165, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_h1_fontcolor', - default_value='#317796', - input_type='colorpicker', - label='Font color of projector headline', + name="projector_h1_fontcolor", + default_value="#317796", + input_type="colorpicker", + label="Font color of projector headline", weight=170, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_default_countdown', + name="projector_default_countdown", default_value=60, - input_type='integer', - label='Predefined seconds of new countdowns', + input_type="integer", + label="Predefined seconds of new countdowns", weight=185, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_blank_color', - default_value='#FFFFFF', - input_type='colorpicker', - label='Color for blanked projector', + name="projector_blank_color", + default_value="#FFFFFF", + input_type="colorpicker", + label="Color for blanked projector", weight=190, - group='Projector') + group="Projector", + ) yield ConfigVariable( - name='projector_broadcast', + name="projector_broadcast", default_value=0, - input_type='integer', - label='Projector which is broadcasted', + input_type="integer", + label="Projector which is broadcasted", weight=200, - group='Projector', - hidden=True) + group="Projector", + hidden=True, + ) yield ConfigVariable( - name='projector_currentListOfSpeakers_reference', + name="projector_currentListOfSpeakers_reference", default_value=1, - input_type='integer', - label='Projector reference for list of speakers', + input_type="integer", + label="Projector reference for list of speakers", weight=201, - group='Projector', - hidden=True) + group="Projector", + hidden=True, + ) # Logos. yield ConfigVariable( - name='logos_available', + name="logos_available", default_value=[ - 'logo_projector_main', - 'logo_projector_header', - 'logo_web_header', - 'logo_pdf_header_L', - 'logo_pdf_header_R', - 'logo_pdf_footer_L', - 'logo_pdf_footer_R', - 'logo_pdf_ballot_paper'], + "logo_projector_main", + "logo_projector_header", + "logo_web_header", + "logo_pdf_header_L", + "logo_pdf_header_R", + "logo_pdf_footer_L", + "logo_pdf_footer_R", + "logo_pdf_ballot_paper", + ], weight=300, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) yield ConfigVariable( - name='logo_projector_main', - default_value={ - 'display_name': 'Projector logo', - 'path': ''}, - input_type='static', + name="logo_projector_main", + default_value={"display_name": "Projector logo", "path": ""}, + input_type="static", weight=301, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) yield ConfigVariable( - name='logo_projector_header', - default_value={ - 'display_name': 'Projector header image', - 'path': ''}, - input_type='static', + name="logo_projector_header", + default_value={"display_name": "Projector header image", "path": ""}, + input_type="static", weight=302, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) yield ConfigVariable( - name='logo_web_header', - default_value={ - 'display_name': 'Web interface header logo', - 'path': ''}, - input_type='static', + name="logo_web_header", + default_value={"display_name": "Web interface header logo", "path": ""}, + input_type="static", weight=303, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) # PDF logos yield ConfigVariable( - name='logo_pdf_header_L', - default_value={ - 'display_name': 'PDF header logo (left)', - 'path': ''}, - input_type='static', + name="logo_pdf_header_L", + default_value={"display_name": "PDF header logo (left)", "path": ""}, + input_type="static", weight=310, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) yield ConfigVariable( - name='logo_pdf_header_R', - default_value={ - 'display_name': 'PDF header logo (right)', - 'path': ''}, - input_type='static', + name="logo_pdf_header_R", + default_value={"display_name": "PDF header logo (right)", "path": ""}, + input_type="static", weight=311, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) yield ConfigVariable( - name='logo_pdf_footer_L', - default_value={ - 'display_name': 'PDF footer logo (left)', - 'path': ''}, - input_type='static', + name="logo_pdf_footer_L", + default_value={"display_name": "PDF footer logo (left)", "path": ""}, + input_type="static", weight=312, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) yield ConfigVariable( - name='logo_pdf_footer_R', - default_value={ - 'display_name': 'PDF footer logo (right)', - 'path': ''}, - input_type='static', + name="logo_pdf_footer_R", + default_value={"display_name": "PDF footer logo (right)", "path": ""}, + input_type="static", weight=313, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) yield ConfigVariable( - name='logo_pdf_ballot_paper', - default_value={ - 'display_name': 'PDF ballot paper logo', - 'path': ''}, - input_type='static', + name="logo_pdf_ballot_paper", + default_value={"display_name": "PDF ballot paper logo", "path": ""}, + input_type="static", weight=314, - group='Logo', - hidden=True) + group="Logo", + hidden=True, + ) # Fonts yield ConfigVariable( - name='fonts_available', - default_value=[ - 'font_regular', - 'font_italic', - 'font_bold', - 'font_bold_italic'], + name="fonts_available", + default_value=["font_regular", "font_italic", "font_bold", "font_bold_italic"], weight=320, - group='Font', - hidden=True) + group="Font", + hidden=True, + ) yield ConfigVariable( - name='font_regular', + name="font_regular", default_value={ - 'display_name': 'Font regular', - 'default': 'static/fonts/Roboto-Regular.woff', - 'path': ''}, - input_type='static', + "display_name": "Font regular", + "default": "static/fonts/Roboto-Regular.woff", + "path": "", + }, + input_type="static", weight=321, - group='Font', - hidden=True) + group="Font", + hidden=True, + ) yield ConfigVariable( - name='font_italic', + name="font_italic", default_value={ - 'display_name': 'Font italic', - 'default': 'static/fonts/Roboto-Medium.woff', - 'path': ''}, - input_type='static', + "display_name": "Font italic", + "default": "static/fonts/Roboto-Medium.woff", + "path": "", + }, + input_type="static", weight=321, - group='Font', - hidden=True) + group="Font", + hidden=True, + ) yield ConfigVariable( - name='font_bold', + name="font_bold", default_value={ - 'display_name': 'Font bold', - 'default': 'static/fonts/Roboto-Condensed-Regular.woff', - 'path': ''}, - input_type='static', + "display_name": "Font bold", + "default": "static/fonts/Roboto-Condensed-Regular.woff", + "path": "", + }, + input_type="static", weight=321, - group='Font', - hidden=True) + group="Font", + hidden=True, + ) yield ConfigVariable( - name='font_bold_italic', + name="font_bold_italic", default_value={ - 'display_name': 'Font bold italic', - 'default': 'static/fonts/Roboto-Condensed-Light.woff', - 'path': ''}, - input_type='static', + "display_name": "Font bold italic", + "default": "static/fonts/Roboto-Condensed-Light.woff", + "path": "", + }, + input_type="static", weight=321, - group='Font', - hidden=True) + group="Font", + hidden=True, + ) # Custom translations yield ConfigVariable( - name='translations', - label='Custom translations', + name="translations", + label="Custom translations", default_value=[], - input_type='translations', + input_type="translations", weight=1000, - group='Custom translations') + group="Custom translations", + ) diff --git a/openslides/core/management/commands/backupdb.py b/openslides/core/management/commands/backupdb.py index 87703d81b..d3d821982 100644 --- a/openslides/core/management/commands/backupdb.py +++ b/openslides/core/management/commands/backupdb.py @@ -10,17 +10,18 @@ class Command(BaseCommand): """ Command to backup the SQLite3 database. """ - help = 'Backups the SQLite3 database.' + + help = "Backups the SQLite3 database." def add_arguments(self, parser): parser.add_argument( - '--path', - default='database_backup.sqlite', - help='Path for the backup file (Default: database_backup.sqlite).' + "--path", + default="database_backup.sqlite", + help="Path for the backup file (Default: database_backup.sqlite).", ) def handle(self, *args, **options): - path = options.get('path') + path = options.get("path") @transaction.atomic def do_backup(src_path, dest_path): @@ -39,8 +40,11 @@ class Command(BaseCommand): 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)) + 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.') + "Default database is not SQLite3. Only SQLite3 databases" + "can currently be backuped." + ) diff --git a/openslides/core/management/commands/changeconfig.py b/openslides/core/management/commands/changeconfig.py index aa93afdfa..9a8f1ce07 100644 --- a/openslides/core/management/commands/changeconfig.py +++ b/openslides/core/management/commands/changeconfig.py @@ -8,25 +8,28 @@ class Command(BaseCommand): """ Command to change OpenSlides config values. """ - help = 'Changes OpenSlides config values.' + + help = "Changes OpenSlides config values." def add_arguments(self, parser): parser.add_argument( - 'key', - help='Config key. See config_variables.py in every app.' + "key", help="Config key. See config_variables.py in every app." ) parser.add_argument( - 'value', - help='New config value. For a falsy boolean use "False".' + "value", help='New config value. For a falsy boolean use "False".' ) def handle(self, *args, **options): - if options['value'].lower() == 'false': - options['value'] = False + if options["value"].lower() == "false": + options["value"] = False try: - config[options['key']] = options['value'] + config[options["key"]] = options["value"] except (ConfigError, ConfigNotFound) as e: raise CommandError(str(e)) self.stdout.write( - self.style.SUCCESS('Config {key} successfully changed to {value}.'.format( - key=options['key'], value=config[options['key']]))) + self.style.SUCCESS( + "Config {key} successfully changed to {value}.".format( + key=options["key"], value=config[options["key"]] + ) + ) + ) diff --git a/openslides/core/management/commands/insecurechangepassword.py b/openslides/core/management/commands/insecurechangepassword.py index 83e18c74c..8068744d8 100644 --- a/openslides/core/management/commands/insecurechangepassword.py +++ b/openslides/core/management/commands/insecurechangepassword.py @@ -7,19 +7,16 @@ class Command(BaseCommand): """ Command to change a user's password. """ - help = 'Changes user password.' + + help = "Changes user password." def add_arguments(self, parser): parser.add_argument( - 'username', - help='The name of the user to set the password for' - ) - parser.add_argument( - 'password', - help='The new password of the user' + "username", help="The name of the user to set the password for" ) + parser.add_argument("password", help="The new password of the user") def handle(self, *args, **options): - user = User.objects.get(username=options['username']) - user.set_password(options['password']) + user = User.objects.get(username=options["username"]) + user.set_password(options["password"]) user.save() diff --git a/openslides/core/management/commands/migrate.py b/openslides/core/management/commands/migrate.py index d6766ab47..d63a091ff 100644 --- a/openslides/core/management/commands/migrate.py +++ b/openslides/core/management/commands/migrate.py @@ -10,10 +10,12 @@ class Command(_Command): Migration command that does nearly the same as Django's migration command but also calls the post_permission_creation signal. """ + def handle(self, *args, **options): from django.conf import settings + # Creates the folder for a SQLite3 database if necessary. - if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3': + if settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3": try: os.makedirs(settings.OPENSLIDES_USER_DATA_PATH) except (FileExistsError, AttributeError): diff --git a/openslides/core/migrations/0001_initial.py b/openslides/core/migrations/0001_initial.py index e8092db39..5e5d1edba 100644 --- a/openslides/core/migrations/0001_initial.py +++ b/openslides/core/migrations/0001_initial.py @@ -18,11 +18,9 @@ def add_default_projector(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - Projector = apps.get_model('core', 'Projector') + Projector = apps.get_model("core", "Projector") projector_config = {} - projector_config[uuid.uuid4().hex] = { - 'name': 'core/clock', - 'stable': True} + projector_config[uuid.uuid4().hex] = {"name": "core/clock", "stable": True} # We use bulk_create here because we do not want model's save() method # to be called because we do not want our autoupdate signals to be # triggered. @@ -34,79 +32,126 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('mediafiles', '0001_initial'), + ("mediafiles", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='ChatMessage', + name="ChatMessage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message', models.TextField()), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("message", models.TextField()), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'default_permissions': (), - 'permissions': (('can_use_chat', 'Can use the chat'),), + "default_permissions": (), + "permissions": (("can_use_chat", "Can use the chat"),), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='ConfigStore', + name="ConfigStore", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(db_index=True, max_length=255, unique=True)), - ('value', jsonfield.fields.JSONField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("key", models.CharField(db_index=True, max_length=255, unique=True)), + ("value", jsonfield.fields.JSONField()), ], options={ - 'default_permissions': (), - 'permissions': (('can_manage_config', 'Can manage configuration'),), + "default_permissions": (), + "permissions": (("can_manage_config", "Can manage configuration"),), }, ), migrations.CreateModel( - name='CustomSlide', + name="CustomSlide", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=256)), - ('text', models.TextField(blank=True)), - ('weight', models.IntegerField(default=0)), - ('attachments', models.ManyToManyField(blank=True, to='mediafiles.Mediafile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=256)), + ("text", models.TextField(blank=True)), + ("weight", models.IntegerField(default=0)), + ( + "attachments", + models.ManyToManyField(blank=True, to="mediafiles.Mediafile"), + ), + ], + options={"default_permissions": (), "ordering": ("weight", "title")}, + bases=(openslides.utils.models.RESTModelMixin, models.Model), + ), + migrations.CreateModel( + name="Projector", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("config", jsonfield.fields.JSONField()), + ("scale", models.IntegerField(default=0)), + ("scroll", models.IntegerField(default=0)), ], options={ - 'default_permissions': (), - 'ordering': ('weight', 'title'), + "default_permissions": (), + "permissions": ( + ("can_see_projector", "Can see the projector"), + ("can_manage_projector", "Can manage the projector"), + ("can_see_frontpage", "Can see the front page"), + ), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='Projector', + name="Tag", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('config', jsonfield.fields.JSONField()), - ('scale', models.IntegerField(default=0)), - ('scroll', models.IntegerField(default=0)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), ], options={ - 'default_permissions': (), - 'permissions': ( - ('can_see_projector', 'Can see the projector'), - ('can_manage_projector', 'Can manage the projector'), - ('can_see_frontpage', 'Can see the front page')), - }, - bases=(openslides.utils.models.RESTModelMixin, models.Model), - ), - migrations.CreateModel( - name='Tag', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True)), - ], - options={ - 'default_permissions': (), - 'permissions': (('can_manage_tags', 'Can manage tags'),), - 'ordering': ('name',), + "default_permissions": (), + "permissions": (("can_manage_tags", "Can manage tags"),), + "ordering": ("name",), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), diff --git a/openslides/core/migrations/0002_misc_features.py b/openslides/core/migrations/0002_misc_features.py index 9a18f5c41..322238c54 100644 --- a/openslides/core/migrations/0002_misc_features.py +++ b/openslides/core/migrations/0002_misc_features.py @@ -15,10 +15,10 @@ def move_custom_slides_to_topics(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - ContentType = apps.get_model('contenttypes', 'ContentType') - CustomSlide = apps.get_model('core', 'CustomSlide') - Item = apps.get_model('agenda', 'Item') - Topic = apps.get_model('topics', 'Topic') + ContentType = apps.get_model("contenttypes", "ContentType") + CustomSlide = apps.get_model("core", "CustomSlide") + Item = apps.get_model("agenda", "Item") + Topic = apps.get_model("topics", "Topic") # Copy data. content_type_custom_slide = ContentType.objects.get_for_model(CustomSlide) @@ -28,7 +28,9 @@ def move_custom_slides_to_topics(apps, schema_editor): # no method 'get_agenda_title()'. See agenda/signals.py. topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text) topic.attachments.add(*custom_slide.attachments.all()) - item = Item.objects.get(object_id=custom_slide.pk, content_type=content_type_custom_slide) + item = Item.objects.get( + object_id=custom_slide.pk, content_type=content_type_custom_slide + ) item.object_id = topic.pk item.content_type = content_type_topic item.save(skip_autoupdate=True) @@ -42,20 +44,20 @@ def name_default_projector(apps, schema_editor): """ Set the name of the default projector to 'Defaultprojector' """ - Projector = apps.get_model('core', 'Projector') - Projector.objects.filter(pk=1).update(name='Default projector') + Projector = apps.get_model("core", "Projector") + Projector.objects.filter(pk=1).update(name="Default projector") def remove_old_countdowns_messages(apps, schema_editor): """ Remove old countdowns and messages created by 2.0 from projector elements which are unusable in 2.1. """ - Projector = apps.get_model('core', 'Projector') + Projector = apps.get_model("core", "Projector") projector = Projector.objects.get(pk=1) projector_config = projector.config for key, value in list(projector.config.items()): - if value.get('name') in ('core/countdown', 'core/message'): + if value.get("name") in ("core/countdown", "core/message"): del projector_config[key] projector.config = projector_config projector.save(skip_autoupdate=True) @@ -65,57 +67,74 @@ def add_projection_defaults(apps, schema_editor): """ Adds projectiondefaults for messages and countdowns. """ - Projector = apps.get_model('core', 'Projector') - ProjectionDefault = apps.get_model('core', 'ProjectionDefault') + Projector = apps.get_model("core", "Projector") + ProjectionDefault = apps.get_model("core", "ProjectionDefault") # The default projector (pk=1) is always available. default_projector = Projector.objects.get(pk=1) projectiondefaults = [] - projectiondefaults.append(ProjectionDefault( - name='agenda_all_items', - display_name='Agenda', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='topics', - display_name='Topics', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='agenda_list_of_speakers', - display_name='List of speakers', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='agenda_current_list_of_speakers', - display_name='Current list of speakers', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='motions', - display_name='Motions', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='motionBlocks', - display_name='Motion blocks', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='assignments', - display_name='Elections', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='users', - display_name='Participants', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='mediafiles', - display_name='Files', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='messages', - display_name='Messages', - projector=default_projector)) - projectiondefaults.append(ProjectionDefault( - name='countdowns', - display_name='Countdowns', - projector=default_projector)) + projectiondefaults.append( + ProjectionDefault( + name="agenda_all_items", display_name="Agenda", projector=default_projector + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="topics", display_name="Topics", projector=default_projector + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="agenda_list_of_speakers", + display_name="List of speakers", + projector=default_projector, + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="agenda_current_list_of_speakers", + display_name="Current list of speakers", + projector=default_projector, + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="motions", display_name="Motions", projector=default_projector + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="motionBlocks", + display_name="Motion blocks", + projector=default_projector, + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="assignments", display_name="Elections", projector=default_projector + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="users", display_name="Participants", projector=default_projector + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="mediafiles", display_name="Files", projector=default_projector + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="messages", display_name="Messages", projector=default_projector + ) + ) + projectiondefaults.append( + ProjectionDefault( + name="countdowns", display_name="Countdowns", projector=default_projector + ) + ) # Create all new projectiondefaults ProjectionDefault.objects.bulk_create(projectiondefaults) @@ -125,114 +144,141 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('sessions', '0001_initial'), - ('contenttypes', '0002_remove_content_type_name'), - ('core', '0001_initial'), - ('agenda', '0001_initial'), # ('agenda', '0002_item_duration') is not required but would be also ok. - ('topics', '0001_initial'), + ("sessions", "0001_initial"), + ("contenttypes", "0002_remove_content_type_name"), + ("core", "0001_initial"), + ( + "agenda", + "0001_initial", + ), # ('agenda', '0002_item_duration') is not required but would be also ok. + ("topics", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Countdown', + name="Countdown", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(blank=True, max_length=256)), - ('running', models.BooleanField(default=False)), - ('default_time', models.PositiveIntegerField(default=60)), - ('countdown_time', models.FloatField(default=60)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(blank=True, max_length=256)), + ("running", models.BooleanField(default=False)), + ("default_time", models.PositiveIntegerField(default=60)), + ("countdown_time", models.FloatField(default=60)), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='ProjectionDefault', + name="ProjectionDefault", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=256)), - ('display_name', models.CharField(max_length=256)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=256)), + ("display_name", models.CharField(max_length=256)), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='ProjectorMessage', + name="ProjectorMessage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message', models.TextField(blank=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("message", models.TextField(blank=True)), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='Session', + name="Session", fields=[ - ('session_ptr', models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to='sessions.Session')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "session_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="sessions.Session", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], - options={ - 'default_permissions': (), - }, - bases=('sessions.session',), - ), - migrations.RunPython( - move_custom_slides_to_topics - ), - migrations.RemoveField( - model_name='customslide', - name='attachments', - ), - migrations.DeleteModel( - name='CustomSlide', + options={"default_permissions": ()}, + bases=("sessions.session",), ), + migrations.RunPython(move_custom_slides_to_topics), + migrations.RemoveField(model_name="customslide", name="attachments"), + migrations.DeleteModel(name="CustomSlide"), migrations.AlterModelOptions( - name='chatmessage', - options={'default_permissions': (), 'permissions': (('can_use_chat', 'Can use the chat'), ('can_manage_chat', 'Can manage the chat'))}, + name="chatmessage", + options={ + "default_permissions": (), + "permissions": ( + ("can_use_chat", "Can use the chat"), + ("can_manage_chat", "Can manage the chat"), + ), + }, ), migrations.AddField( - model_name='projector', - name='blank', + model_name="projector", + name="blank", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='projector', - name='height', + model_name="projector", + name="height", field=models.PositiveIntegerField(default=915), ), migrations.AddField( - model_name='projector', - name='name', + model_name="projector", + name="name", field=models.CharField(blank=True, max_length=255, unique=True), ), migrations.AddField( - model_name='projector', - name='width', + model_name="projector", + name="width", field=models.PositiveIntegerField(default=1220), ), migrations.AddField( - model_name='projectiondefault', - name='projector', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projectiondefaults', to='core.Projector'), - ), - migrations.RunPython( - name_default_projector - ), - migrations.RunPython( - remove_old_countdowns_messages - ), - migrations.RunPython( - add_projection_defaults + model_name="projectiondefault", + name="projector", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="projectiondefaults", + to="core.Projector", + ), ), + migrations.RunPython(name_default_projector), + migrations.RunPython(remove_old_countdowns_messages), + migrations.RunPython(add_projection_defaults), ] diff --git a/openslides/core/migrations/0003_auto_20161217_1158.py b/openslides/core/migrations/0003_auto_20161217_1158.py index e32151bb2..28f03a5c3 100644 --- a/openslides/core/migrations/0003_auto_20161217_1158.py +++ b/openslides/core/migrations/0003_auto_20161217_1158.py @@ -11,22 +11,16 @@ def remove_session_content_type(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - ContentType = apps.get_model('contenttypes', 'ContentType') - Session = apps.get_model('core', 'Session') + ContentType = apps.get_model("contenttypes", "ContentType") + Session = apps.get_model("core", "Session") ContentType.objects.get_for_model(Session).delete() class Migration(migrations.Migration): - dependencies = [ - ('core', '0002_misc_features'), - ] + dependencies = [("core", "0002_misc_features")] operations = [ - migrations.RunPython( - remove_session_content_type - ), - migrations.DeleteModel( - name='Session', - ), + migrations.RunPython(remove_session_content_type), + migrations.DeleteModel(name="Session"), ] diff --git a/openslides/core/migrations/0004_auto_20170215_1624.py b/openslides/core/migrations/0004_auto_20170215_1624.py index 36d0bcee3..9b1d93575 100644 --- a/openslides/core/migrations/0004_auto_20170215_1624.py +++ b/openslides/core/migrations/0004_auto_20170215_1624.py @@ -13,25 +13,19 @@ def rename_projector_message_slides(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - Projector = apps.get_model('core', 'Projector') + Projector = apps.get_model("core", "Projector") for projector in Projector.objects.all(): new_config = {} for key, value in projector.config.items(): new_config[key] = value - if value['name'] == 'core/projectormessage': - new_config[key]['name'] = 'core/projector-message' + if value["name"] == "core/projectormessage": + new_config[key]["name"] = "core/projector-message" projector.config = new_config projector.save(skip_autoupdate=True) class Migration(migrations.Migration): - dependencies = [ - ('core', '0003_auto_20161217_1158'), - ] + dependencies = [("core", "0003_auto_20161217_1158")] - operations = [ - migrations.RunPython( - rename_projector_message_slides - ), - ] + operations = [migrations.RunPython(rename_projector_message_slides)] diff --git a/openslides/core/migrations/0005_auto_20170412_1258.py b/openslides/core/migrations/0005_auto_20170412_1258.py index fc7e615eb..7b71d08ee 100644 --- a/openslides/core/migrations/0005_auto_20170412_1258.py +++ b/openslides/core/migrations/0005_auto_20170412_1258.py @@ -11,22 +11,26 @@ from openslides.utils.migrations import ( class Migration(migrations.Migration): - dependencies = [ - ('core', '0004_auto_20170215_1624'), - ] + dependencies = [("core", "0004_auto_20170215_1624")] operations = [ migrations.AlterModelOptions( - name='configstore', + name="configstore", options={ - 'default_permissions': (), - 'permissions': ( - ('can_manage_config', 'Can manage configuration'), - ('can_manage_logos', 'Can manage logos') - ) + "default_permissions": (), + "permissions": ( + ("can_manage_config", "Can manage configuration"), + ("can_manage_logos", "Can manage logos"), + ), }, ), - migrations.RunPython(add_permission_to_groups_based_on_existing_permission( - 'can_manage_config', 'configstore', 'core', 'can_manage_logos', 'Can manage logos' - )), + migrations.RunPython( + add_permission_to_groups_based_on_existing_permission( + "can_manage_config", + "configstore", + "core", + "can_manage_logos", + "Can manage logos", + ) + ), ] diff --git a/openslides/core/migrations/0006_auto_20180123_0903.py b/openslides/core/migrations/0006_auto_20180123_0903.py index d9b935269..b1d98733f 100644 --- a/openslides/core/migrations/0006_auto_20180123_0903.py +++ b/openslides/core/migrations/0006_auto_20180123_0903.py @@ -7,19 +7,17 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('core', '0005_auto_20170412_1258'), - ] + dependencies = [("core", "0005_auto_20170412_1258")] operations = [ migrations.AlterField( - model_name='projector', - name='height', + model_name="projector", + name="height", field=models.PositiveIntegerField(default=768), ), migrations.AlterField( - model_name='projector', - name='width', + model_name="projector", + name="width", field=models.PositiveIntegerField(default=1024), ), ] diff --git a/openslides/core/migrations/0007_auto_20180130_1400.py b/openslides/core/migrations/0007_auto_20180130_1400.py index f685beed4..cffcee30d 100644 --- a/openslides/core/migrations/0007_auto_20180130_1400.py +++ b/openslides/core/migrations/0007_auto_20180130_1400.py @@ -15,7 +15,7 @@ def delete_old_logo_permission(apps, schema_editor): If this is an old database, the new permission will be created and the old one deleted. Also it will be assigned to the groups, which had the old permission. """ - perm = Permission.objects.filter(codename='can_manage_logos') + perm = Permission.objects.filter(codename="can_manage_logos") if len(perm): perm = perm.get() @@ -30,9 +30,10 @@ def delete_old_logo_permission(apps, schema_editor): # Create new permission perm = Permission.objects.create( - codename='can_manage_logos_and_fonts', - name='Can manage logos and fonts', - content_type=content_type) + codename="can_manage_logos_and_fonts", + name="Can manage logos and fonts", + content_type=content_type, + ) for group in groups: group.permissions.add(perm) @@ -41,22 +42,18 @@ def delete_old_logo_permission(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('core', '0006_auto_20180123_0903'), - ] + dependencies = [("core", "0006_auto_20180123_0903")] operations = [ migrations.AlterModelOptions( - name='configstore', + name="configstore", options={ - 'default_permissions': (), - 'permissions': ( - ('can_manage_config', 'Can manage configuration'), - ('can_manage_logos_and_fonts', 'Can manage logos and fonts') - ) + "default_permissions": (), + "permissions": ( + ("can_manage_config", "Can manage configuration"), + ("can_manage_logos_and_fonts", "Can manage logos and fonts"), + ), }, ), - migrations.RunPython( - delete_old_logo_permission - ), + migrations.RunPython(delete_old_logo_permission), ] diff --git a/openslides/core/migrations/0008_changed_logo_fields.py b/openslides/core/migrations/0008_changed_logo_fields.py index 50a4a16d5..82b5db719 100644 --- a/openslides/core/migrations/0008_changed_logo_fields.py +++ b/openslides/core/migrations/0008_changed_logo_fields.py @@ -11,14 +11,14 @@ def logos_available_default_to_database(apps, schema_editor): """ Writes the new default value of the 'logos_available' into the database. """ - ConfigStore = apps.get_model('core', 'ConfigStore') + ConfigStore = apps.get_model("core", "ConfigStore") try: - logos_available = ConfigStore.objects.get(key='logos_available') + logos_available = ConfigStore.objects.get(key="logos_available") except ConfigStore.DoesNotExist: return # The key is not in the database, nothing to change here - default_value = config.config_variables['logos_available'].default_value + default_value = config.config_variables["logos_available"].default_value logos_available.value = default_value logos_available.save() @@ -28,16 +28,16 @@ def move_old_logo_settings(apps, schema_editor): moves the value of 'logo_pdf_header' to 'logo_pdf_header_L' and the same for the footer. The old ones are deleted. """ - ConfigStore = apps.get_model('core', 'ConfigStore') + ConfigStore = apps.get_model("core", "ConfigStore") - for place in ('header', 'footer'): + for place in ("header", "footer"): try: - logo_pdf = ConfigStore.objects.get(key='logo_pdf_{}'.format(place)) + logo_pdf = ConfigStore.objects.get(key="logo_pdf_{}".format(place)) except ConfigStore.DoesNotExist: continue # The old entry is not in the database, nothing to change here # The key of the new entry - new_value_key = 'logo_pdf_{}_L'.format(place) + new_value_key = "logo_pdf_{}_L".format(place) try: logo_pdf_L = ConfigStore.objects.get(key=new_value_key) except ConfigStore.DoesNotExist: @@ -45,7 +45,7 @@ def move_old_logo_settings(apps, schema_editor): logo_pdf_L.value = {} # Move the path to the new configentry - logo_pdf_L.value['path'] = logo_pdf.value.get('path', '') + logo_pdf_L.value["path"] = logo_pdf.value.get("path", "") # Save the new one, delete the old logo_pdf_L.save() logo_pdf.delete() @@ -53,15 +53,9 @@ def move_old_logo_settings(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('core', '0007_auto_20180130_1400'), - ] + dependencies = [("core", "0007_auto_20180130_1400")] operations = [ - migrations.RunPython( - logos_available_default_to_database - ), - migrations.RunPython( - move_old_logo_settings - ), + migrations.RunPython(logos_available_default_to_database), + migrations.RunPython(move_old_logo_settings), ] diff --git a/openslides/core/migrations/0009_auto_20181118_2126.py b/openslides/core/migrations/0009_auto_20181118_2126.py index 3c7f2ff3f..f35ee525a 100644 --- a/openslides/core/migrations/0009_auto_20181118_2126.py +++ b/openslides/core/migrations/0009_auto_20181118_2126.py @@ -13,42 +13,68 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('core', '0008_changed_logo_fields'), + ("core", "0008_changed_logo_fields"), ] operations = [ migrations.CreateModel( - name='History', + name="History", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('element_id', models.CharField(max_length=255)), - ('now', models.DateTimeField(auto_now_add=True)), - ('information', models.CharField(max_length=255)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("element_id", models.CharField(max_length=255)), + ("now", models.DateTimeField(auto_now_add=True)), + ("information", models.CharField(max_length=255)), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='HistoryData', + name="HistoryData", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('full_data', jsonfield.fields.JSONField( - dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "full_data", + jsonfield.fields.JSONField( + dump_kwargs={ + "cls": jsonfield.encoder.JSONEncoder, + "separators": (",", ":"), + }, + load_kwargs={}, + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, ), migrations.AddField( - model_name='history', - name='full_data', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='core.HistoryData'), + model_name="history", + name="full_data", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="core.HistoryData" + ), ), migrations.AddField( - model_name='history', - name='user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + model_name="history", + name="user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/openslides/core/models.py b/openslides/core/models.py index 394dc241d..6ecd853c0 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -24,13 +24,13 @@ class ProjectorManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all projectors. In the background projector defaults are prefetched from the database. """ - return self.get_queryset().prefetch_related( - 'projectiondefaults') + return self.get_queryset().prefetch_related("projectiondefaults") class Projector(RESTModelMixin, models.Model): @@ -72,6 +72,7 @@ class Projector(RESTModelMixin, models.Model): The projector can be controlled using the REST API with POST requests on e. g. the URL /rest/core/projector/1/activate_elements/. """ + access_permissions = ProjectorAccessPermissions() objects = ProjectorManager() @@ -86,24 +87,21 @@ class Projector(RESTModelMixin, models.Model): height = models.PositiveIntegerField(default=768) - name = models.CharField( - max_length=255, - unique=True, - blank=True) + name = models.CharField(max_length=255, unique=True, blank=True) - blank = models.BooleanField( - blank=False, - default=False) + blank = models.BooleanField(blank=False, default=False) class Meta: """ Contains general permissions that can not be placed in a specific app. """ + default_permissions = () permissions = ( - ('can_see_projector', 'Can see the projector'), - ('can_manage_projector', 'Can manage the projector'), - ('can_see_frontpage', 'Can see the front page'),) + ("can_see_projector", "Can see the projector"), + ("can_manage_projector", "Can manage the projector"), + ("can_see_frontpage", "Can see the front page"), + ) @property def elements(self): @@ -120,17 +118,19 @@ class Projector(RESTModelMixin, models.Model): for key, value in self.config.items(): # Use a copy here not to change the origin value in the config field. result[key] = value.copy() - result[key]['uuid'] = key - element = elements.get(value['name']) + result[key]["uuid"] = key + element = elements.get(value["name"]) if element is None: - result[key]['error'] = 'Projector element does not exist.' + result[key]["error"] = "Projector element does not exist." else: try: - result[key].update(element.check_and_update_data( - projector_object=self, - config_entry=value)) + result[key].update( + element.check_and_update_data( + projector_object=self, config_entry=value + ) + ) except ProjectorException as e: - result[key]['error'] = str(e) + result[key]["error"] = str(e) return result @classmethod @@ -173,14 +173,14 @@ class ProjectionDefault(RESTModelMixin, models.Model): special name like 'list_of_speakers'. The display_name is the shown name on the front end for the user. """ + name = models.CharField(max_length=256) display_name = models.CharField(max_length=256) projector = models.ForeignKey( - Projector, - on_delete=models.CASCADE, - related_name='projectiondefaults') + Projector, on_delete=models.CASCADE, related_name="projectiondefaults" + ) def get_root_rest_element(self): return self.projector @@ -197,17 +197,15 @@ class Tag(RESTModelMixin, models.Model): Model for tags. This tags can be used for other models like agenda items, motions or assignments. """ + access_permissions = TagAccessPermissions() - name = models.CharField( - max_length=255, - unique=True) + name = models.CharField(max_length=255, unique=True) class Meta: - ordering = ('name',) + ordering = ("name",) default_permissions = () - permissions = ( - ('can_manage_tags', 'Can manage tags'),) + permissions = (("can_manage_tags", "Can manage tags"),) def __str__(self): return self.name @@ -217,6 +215,7 @@ class ConfigStore(RESTModelMixin, models.Model): """ A model class to store all config variables in the database. """ + access_permissions = ConfigAccessPermissions() key = models.CharField(max_length=255, unique=True, db_index=True) @@ -228,12 +227,13 @@ class ConfigStore(RESTModelMixin, models.Model): class Meta: default_permissions = () permissions = ( - ('can_manage_config', 'Can manage configuration'), - ('can_manage_logos_and_fonts', 'Can manage logos and fonts')) + ("can_manage_config", "Can manage configuration"), + ("can_manage_logos_and_fonts", "Can manage logos and fonts"), + ) @classmethod def get_collection_string(cls): - return 'core/config' + return "core/config" class ChatMessage(RESTModelMixin, models.Model): @@ -242,31 +242,32 @@ class ChatMessage(RESTModelMixin, models.Model): At the moment we only have one global chat room for managers. """ + access_permissions = ChatMessageAccessPermissions() - can_see_permission = 'core.can_use_chat' + can_see_permission = "core.can_use_chat" message = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) class Meta: default_permissions = () permissions = ( - ('can_use_chat', 'Can use the chat'), - ('can_manage_chat', 'Can manage the chat'),) + ("can_use_chat", "Can use the chat"), + ("can_manage_chat", "Can manage the chat"), + ) def __str__(self): - return 'Message {}'.format(self.timestamp) + return "Message {}".format(self.timestamp) class ProjectorMessage(RESTModelMixin, models.Model): """ Model for ProjectorMessages. """ + access_permissions = ProjectorMessageAccessPermissions() message = models.TextField(blank=True) @@ -280,16 +281,18 @@ class ProjectorMessage(RESTModelMixin, models.Model): projector message projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='core/projector-message', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="core/projector-message", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) class Countdown(RESTModelMixin, models.Model): """ Model for countdowns. """ + access_permissions = CountdownAccessPermissions() description = models.CharField(max_length=256, blank=True) @@ -309,19 +312,22 @@ class Countdown(RESTModelMixin, models.Model): countdown projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='core/countdown', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="core/countdown", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) def control(self, action, skip_autoupdate=False): - if action not in ('start', 'stop', 'reset'): - raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action)) + if action not in ("start", "stop", "reset"): + raise ValueError( + "Action must be 'start', 'stop' or 'reset', not {}.".format(action) + ) - if action == 'start': + if action == "start": self.running = True self.countdown_time = now().timestamp() + self.default_time - elif action == 'stop' and self.running: + elif action == "stop" and self.running: self.running = False self.countdown_time = self.countdown_time - now().timestamp() else: # reset @@ -337,6 +343,7 @@ class HistoryData(models.Model): This is not a RESTModel. It is not cachable and can only be reached by a special viewset. """ + full_data = JSONField() class Meta: @@ -347,6 +354,7 @@ class HistoryManager(models.Manager): """ Customized model manager for the history model. """ + def add_elements(self, elements): """ Method to add elements to the history. This does not trigger autoupdate. @@ -354,18 +362,26 @@ class HistoryManager(models.Manager): with transaction.atomic(): instances = [] for element in elements: - if element['disable_history'] or element['collection_string'] == self.model.get_collection_string(): + if ( + element["disable_history"] + or element["collection_string"] + == self.model.get_collection_string() + ): # Do not update history for history elements itself or if history is disabled. continue # HistoryData is not a root rest element so there is no autoupdate and not history saving here. - data = HistoryData.objects.create(full_data=element['full_data']) + data = HistoryData.objects.create(full_data=element["full_data"]) instance = self.model( - element_id=get_element_id(element['collection_string'], element['id']), - information=element['information'], - user_id=element['user_id'], + element_id=get_element_id( + element["collection_string"], element["id"] + ), + information=element["information"], + user_id=element["user_id"], full_data=data, ) - instance.save(skip_autoupdate=True) # Skip autoupdate and of course history saving. + instance.save( + skip_autoupdate=True + ) # Skip autoupdate and of course history saving. instances.append(instance) return instances @@ -380,14 +396,16 @@ class HistoryManager(models.Manager): all_full_data = async_to_sync(element_cache.get_all_full_data)() for collection_string, data in all_full_data.items(): for full_data in data: - elements.append(Element( - id=full_data['id'], - collection_string=collection_string, - full_data=full_data, - information='', - user_id=None, - disable_history=False, - )) + elements.append( + Element( + id=full_data["id"], + collection_string=collection_string, + full_data=full_data, + information="", + user_id=None, + disable_history=False, + ) + ) instances = self.add_elements(elements) return instances @@ -399,29 +417,22 @@ class History(RESTModelMixin, models.Model): This model itself is not part of the history. This means that if you delete a user you may lose the information of the user field here. """ + access_permissions = HistoryAccessPermissions() objects = HistoryManager() - element_id = models.CharField( - max_length=255, - ) + element_id = models.CharField(max_length=255) now = models.DateTimeField(auto_now_add=True) - information = models.CharField( - max_length=255, - ) + information = models.CharField(max_length=255) user = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, - on_delete=models.SET_NULL) - - full_data = models.OneToOneField( - HistoryData, - on_delete=models.CASCADE, + settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL ) + full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE) + class Meta: default_permissions = () diff --git a/openslides/core/projector.py b/openslides/core/projector.py index a1c39551c..3ae29f8ba 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -9,29 +9,32 @@ class Clock(ProjectorElement): """ Clock on the projector. """ - name = 'core/clock' + + name = "core/clock" class CountdownElement(ProjectorElement): """ Countdown slide for the projector. """ - name = 'core/countdown' + + name = "core/countdown" def check_data(self): - if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('Countdown does not exists.') + if not Countdown.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("Countdown does not exists.") class ProjectorMessageElement(ProjectorElement): """ Short message on the projector. Rendered as overlay. """ - name = 'core/projector-message' + + name = "core/projector-message" def check_data(self): - if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('Message does not exists.') + if not ProjectorMessage.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("Message does not exists.") def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index 555056fcf..75675a41a 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -17,18 +17,21 @@ class JSONSerializerField(Field): """ Serializer for projector's and config JSONField. """ + def to_internal_value(self, data): """ Checks that data is a dictionary. The key is a hex UUID and the value is a dictionary with must have a key 'name'. """ if type(data) is not dict: - raise ValidationError({'detail': 'Data must be a dictionary.'}) + raise ValidationError({"detail": "Data must be a dictionary."}) for element in data.values(): if type(element) is not dict: - raise ValidationError({'detail': 'Data must be a dictionary.'}) - elif element.get('name') is None: - raise ValidationError({'detail': "Every dictionary must have a key 'name'."}) + raise ValidationError({"detail": "Data must be a dictionary."}) + elif element.get("name") is None: + raise ValidationError( + {"detail": "Every dictionary must have a key 'name'."} + ) return data def to_representation(self, value): @@ -42,65 +45,82 @@ class ProjectionDefaultSerializer(ModelSerializer): """ Serializer for core.models.ProjectionDefault objects. """ + class Meta: model = ProjectionDefault - fields = ('id', 'name', 'display_name', 'projector', ) + fields = ("id", "name", "display_name", "projector") class ProjectorSerializer(ModelSerializer): """ Serializer for core.models.Projector objects. """ + config = JSONSerializerField(write_only=True) projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True) class Meta: model = Projector - fields = ('id', 'config', 'elements', 'scale', 'scroll', 'name', 'blank', 'width', 'height', 'projectiondefaults', ) - read_only_fields = ('scale', 'scroll', 'blank', 'width', 'height', ) + fields = ( + "id", + "config", + "elements", + "scale", + "scroll", + "name", + "blank", + "width", + "height", + "projectiondefaults", + ) + read_only_fields = ("scale", "scroll", "blank", "width", "height") class TagSerializer(ModelSerializer): """ Serializer for core.models.Tag objects. """ + class Meta: model = Tag - fields = ('id', 'name', ) + fields = ("id", "name") class ConfigSerializer(ModelSerializer): """ Serializer for core.models.Tag objects. """ + value = JSONSerializerField() class Meta: model = ConfigStore - fields = ('id', 'key', 'value') + fields = ("id", "key", "value") class ChatMessageSerializer(ModelSerializer): """ Serializer for core.models.ChatMessage objects. """ + class Meta: model = ChatMessage - fields = ('id', 'message', 'timestamp', 'user', ) - read_only_fields = ('user', ) + fields = ("id", "message", "timestamp", "user") + read_only_fields = ("user",) class ProjectorMessageSerializer(ModelSerializer): """ Serializer for core.models.ProjectorMessage objects. """ + class Meta: model = ProjectorMessage - fields = ('id', 'message', ) + fields = ("id", "message") def validate(self, data): - if 'message' in data: - data['message'] = validate_html(data['message']) + if "message" in data: + data["message"] = validate_html(data["message"]) return data @@ -108,9 +128,10 @@ class CountdownSerializer(ModelSerializer): """ Serializer for core.models.Countdown objects. """ + class Meta: model = Countdown - fields = ('id', 'description', 'default_time', 'countdown_time', 'running', ) + fields = ("id", "description", "default_time", "countdown_time", "running") class HistorySerializer(ModelSerializer): @@ -119,6 +140,7 @@ class HistorySerializer(ModelSerializer): Does not contain full data of history object. """ + class Meta: model = History - fields = ('id', 'element_id', 'now', 'information', 'user', ) + fields = ("id", "element_id", "now", "information", "user") diff --git a/openslides/core/signals.py b/openslides/core/signals.py index f2a77e2f7..d3884b05e 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -21,9 +21,8 @@ def delete_django_app_permissions(sender, **kwargs): for auth, contenttypes and sessions. """ contenttypes = ContentType.objects.filter( - Q(app_label='auth') | - Q(app_label='contenttypes') | - Q(app_label='sessions')) + Q(app_label="auth") | Q(app_label="contenttypes") | Q(app_label="sessions") + ) Permission.objects.filter(content_type__in=contenttypes).delete() @@ -31,13 +30,13 @@ def get_permission_change_data(sender, permissions, **kwargs): """ Yields all necessary Cachables if the respective permissions change. """ - core_app = apps.get_app_config(app_label='core') + core_app = apps.get_app_config(app_label="core") for permission in permissions: if permission.content_type.app_label == core_app.label: - if permission.codename == 'can_see_projector': - yield core_app.get_model('Projector') - elif permission.codename == 'can_manage_projector': - yield core_app.get_model('ProjectorMessage') - yield core_app.get_model('Countdown') - elif permission.codename == 'can_use_chat': - yield core_app.get_model('ChatMessage') + if permission.codename == "can_see_projector": + yield core_app.get_model("Projector") + elif permission.codename == "can_manage_projector": + yield core_app.get_model("ProjectorMessage") + yield core_app.get_model("Countdown") + elif permission.codename == "can_use_chat": + yield core_app.get_model("ChatMessage") diff --git a/openslides/core/urls.py b/openslides/core/urls.py index cc6d6e236..e5e887367 100644 --- a/openslides/core/urls.py +++ b/openslides/core/urls.py @@ -4,15 +4,7 @@ from . import views urlpatterns = [ - url(r'^servertime/$', - views.ServerTime.as_view(), - name='core_servertime'), - - url(r'^version/$', - views.VersionView.as_view(), - name='core_version'), - - url(r'^history/$', - views.HistoryView.as_view(), - name='core_history'), + url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"), + url(r"^version/$", views.VersionView.as_view(), name="core_version"), + url(r"^history/$", views.HistoryView.as_view(), name="core_history"), ] diff --git a/openslides/core/views.py b/openslides/core/views.py index f491ed230..29de363b1 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -17,12 +17,7 @@ from mypy_extensions import TypedDict from .. import __license__ as license, __url__ as url, __version__ as version from ..utils import views as utils_views from ..utils.arguments import arguments -from ..utils.auth import ( - GROUP_ADMIN_PK, - anonymous_is_enabled, - has_perm, - in_some_groups, -) +from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups from ..utils.autoupdate import inform_changed_data, inform_deleted_data from ..utils.plugins import ( get_plugin_description, @@ -67,6 +62,7 @@ from .models import ( # Special Django views + class IndexView(View): """ The primary view for the OpenSlides client. Serves static files. If a file @@ -83,11 +79,11 @@ class IndexView(View): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - no_caching = arguments.get('no_template_caching', False) - if 'index' not in self.cache or no_caching: - self.cache['index'] = finders.find('index.html') + no_caching = arguments.get("no_template_caching", False) + if "index" not in self.cache or no_caching: + self.cache["index"] = finders.find("index.html") - self.index_document_root, self.index_path = os.path.split(self.cache['index']) + self.index_document_root, self.index_path = os.path.split(self.cache["index"]) def get(self, request, path, **kwargs) -> HttpResponse: """ @@ -97,18 +93,25 @@ class IndexView(View): try: response = serve(request, path, **kwargs) except Http404: - response = static.serve(request, self.index_path, document_root=self.index_document_root, **kwargs) + response = static.serve( + request, + self.index_path, + document_root=self.index_document_root, + **kwargs, + ) return response # Viewsets for the REST API + class ProjectorViewSet(ModelViewSet): """ API endpoint for the projector slide info. There are the following views: See strings in check_view_permissions(). """ + access_permissions = ProjectorAccessPermissions() queryset = Projector.objects.all() @@ -116,18 +119,31 @@ class ProjectorViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': - result = has_perm(self.request.user, 'core.can_see_projector') + elif self.action == "metadata": + result = has_perm(self.request.user, "core.can_see_projector") elif self.action in ( - 'create', 'update', 'partial_update', 'destroy', - 'activate_elements', 'prune_elements', 'update_elements', 'deactivate_elements', 'clear_elements', - 'project', 'control_view', 'set_resolution', 'set_scroll', 'control_blank', - 'broadcast', 'set_projectiondefault', + "create", + "update", + "partial_update", + "destroy", + "activate_elements", + "prune_elements", + "update_elements", + "deactivate_elements", + "clear_elements", + "project", + "control_view", + "set_resolution", + "set_scroll", + "control_blank", + "broadcast", + "set_projectiondefault", ): - result = (has_perm(self.request.user, 'core.can_see_projector') and - has_perm(self.request.user, 'core.can_manage_projector')) + result = has_perm(self.request.user, "core.can_see_projector") and has_perm( + self.request.user, "core.can_manage_projector" + ) else: result = False return result @@ -144,11 +160,11 @@ class ProjectorViewSet(ModelViewSet): if projection_default.projector.id == projector_instance.id: projection_default.projector_id = 1 projection_default.save() - if config['projector_broadcast'] == projector_instance.pk: - config['projector_broadcast'] = 0 + if config["projector_broadcast"] == projector_instance.pk: + config["projector_broadcast"] = 0 return super(ProjectorViewSet, self).destroy(*args, **kwargs) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def activate_elements(self, request, pk): """ REST API operation to activate projector elements. It expects a POST @@ -156,21 +172,25 @@ class ProjectorViewSet(ModelViewSet): of dictionaries to be appended to the projector config entry. """ if not isinstance(request.data, list): - raise ValidationError({'detail': 'Data must be a list.'}) + raise ValidationError({"detail": "Data must be a list."}) projector_instance = self.get_object() projector_config = projector_instance.config for element in request.data: - if element.get('name') is None: - raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) + if element.get("name") is None: + raise ValidationError( + {"detail": "Invalid projector element. Name is missing."} + ) projector_config[uuid.uuid4().hex] = element - serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) + serializer = self.get_serializer( + projector_instance, data={"config": projector_config}, partial=False + ) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def prune_elements(self, request, pk): """ REST API operation to activate projector elements. It expects a POST @@ -179,17 +199,19 @@ class ProjectorViewSet(ModelViewSet): entries are deleted but not entries with stable == True. """ if not isinstance(request.data, list): - raise ValidationError({'detail': 'Data must be a list.'}) + raise ValidationError({"detail": "Data must be a list."}) projector = self.get_object() elements = request.data if not isinstance(elements, list): - raise ValidationError({'detail': _('The data has to be a list.')}) + raise ValidationError({"detail": _("The data has to be a list.")}) for element in elements: if not isinstance(element, dict): - raise ValidationError({'detail': _('All elements have to be dicts.')}) - if element.get('name') is None: - raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) + raise ValidationError({"detail": _("All elements have to be dicts.")}) + if element.get("name") is None: + raise ValidationError( + {"detail": "Invalid projector element. Name is missing."} + ) return Response(self.prune(projector, elements)) def prune(self, projector, elements): @@ -201,21 +223,23 @@ class ProjectorViewSet(ModelViewSet): """ projector_config = {} for key, value in projector.config.items(): - if value.get('stable'): + if value.get("stable"): projector_config[key] = value for element in elements: projector_config[uuid.uuid4().hex] = element - serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False) + serializer = self.get_serializer( + projector, data={"config": projector_config}, partial=False + ) serializer.is_valid(raise_exception=True) serializer.save() # reset scroll level - if (projector.scroll != 0): + if projector.scroll != 0: projector.scroll = 0 projector.save() return serializer.data - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def update_elements(self, request, pk): """ REST API operation to update projector elements. It expects a POST @@ -237,8 +261,10 @@ class ProjectorViewSet(ModelViewSet): } """ if not isinstance(request.data, dict): - raise ValidationError({'detail': 'Data must be a dictionary.'}) - error = {'detail': 'Data must be a dictionary with UUIDs as keys and dictionaries as values.'} + raise ValidationError({"detail": "Data must be a dictionary."}) + error = { + "detail": "Data must be a dictionary with UUIDs as keys and dictionaries as values." + } for key, value in request.data.items(): try: uuid.UUID(hex=str(key)) @@ -251,15 +277,19 @@ class ProjectorViewSet(ModelViewSet): projector_config = projector_instance.config for key, value in request.data.items(): if key not in projector_config: - raise ValidationError({'detail': 'Invalid projector element. Wrong UUID.'}) + raise ValidationError( + {"detail": "Invalid projector element. Wrong UUID."} + ) projector_config[key].update(request.data[key]) - serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) + serializer = self.get_serializer( + projector_instance, data={"config": projector_config}, partial=False + ) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def deactivate_elements(self, request, pk): """ REST API operation to deactivate projector elements. It expects a @@ -268,12 +298,12 @@ class ProjectorViewSet(ModelViewSet): that should be deleted. """ if not isinstance(request.data, list): - raise ValidationError({'detail': 'Data must be a list of hex UUIDs.'}) + raise ValidationError({"detail": "Data must be a list of hex UUIDs."}) for item in request.data: try: uuid.UUID(hex=str(item)) except ValueError: - raise ValidationError({'detail': 'Data must be a list of hex UUIDs.'}) + raise ValidationError({"detail": "Data must be a list of hex UUIDs."}) projector_instance = self.get_object() projector_config = projector_instance.config @@ -281,14 +311,16 @@ class ProjectorViewSet(ModelViewSet): try: del projector_config[key] except KeyError: - raise ValidationError({'detail': 'Invalid UUID.'}) + raise ValidationError({"detail": "Invalid UUID."}) - serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) + serializer = self.get_serializer( + projector_instance, data={"config": projector_config}, partial=False + ) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def clear_elements(self, request, pk): """ REST API operation to deactivate all projector elements but not @@ -301,15 +333,17 @@ class ProjectorViewSet(ModelViewSet): def clear(self, projector): projector_config = {} for key, value in projector.config.items(): - if value.get('stable'): + if value.get("stable"): projector_config[key] = value - serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False) + serializer = self.get_serializer( + projector, data={"config": projector_config}, partial=False + ) serializer.is_valid(raise_exception=True) serializer.save() return serializer.data - @list_route(methods=['post']) + @list_route(methods=["post"]) def project(self, request, *args, **kwargs): """ REST API operation. Does a combination of clear_elements and prune_elements: @@ -327,33 +361,46 @@ class ProjectorViewSet(ModelViewSet): """ # The data has to be a dict. if not isinstance(request.data, dict): - raise ValidationError({'detail': _('The data has to be a dict.')}) + raise ValidationError({"detail": _("The data has to be a dict.")}) # Get projector ids to clear - clear_projector_ids = request.data.get('clear_ids', []) + clear_projector_ids = request.data.get("clear_ids", []) for id in clear_projector_ids: if not isinstance(id, int): - raise ValidationError({'detail': _('The id "{}" has to be int.').format(id)}) + raise ValidationError( + {"detail": _('The id "{}" has to be int.').format(id)} + ) # Get the projector id and validate element to prune. This is optional. - prune = request.data.get('prune') + prune = request.data.get("prune") if prune is not None: if not isinstance(prune, dict): - raise ValidationError({'detail': _('Prune has to be an object.')}) - prune_projector_id = prune.get('id') + raise ValidationError({"detail": _("Prune has to be an object.")}) + prune_projector_id = prune.get("id") if not isinstance(prune_projector_id, int): - raise ValidationError({'detail': _('The prune projector id has to be int.')}) + raise ValidationError( + {"detail": _("The prune projector id has to be int.")} + ) # Get the projector after all clear operations, but check, if it exist. if not Projector.objects.filter(pk=prune_projector_id).exists(): - raise ValidationError({ - 'detail': _('The projector with id "{}" does not exist').format(prune_projector_id)}) + raise ValidationError( + { + "detail": _('The projector with id "{}" does not exist').format( + prune_projector_id + ) + } + ) - prune_element = prune.get('element', {}) + prune_element = prune.get("element", {}) if not isinstance(prune_element, dict): - raise ValidationError({'detail': _('Prune element has to be a dict or not given.')}) - if prune_element.get('name') is None: - raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) + raise ValidationError( + {"detail": _("Prune element has to be a dict or not given.")} + ) + if prune_element.get("name") is None: + raise ValidationError( + {"detail": "Invalid projector element. Name is missing."} + ) # First step: Clear all given projectors for projector in Projector.objects.filter(pk__in=clear_projector_ids): @@ -367,7 +414,7 @@ class ProjectorViewSet(ModelViewSet): return Response() - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def set_resolution(self, request, pk): """ REST API operation to set the resolution. @@ -388,26 +435,34 @@ class ProjectorViewSet(ModelViewSet): } """ if not isinstance(request.data, dict): - raise ValidationError({'detail': 'Data must be a dictionary.'}) - if request.data.get('width') is None or request.data.get('height') is None: - raise ValidationError({'detail': 'A width and a height have to be given.'}) - if not isinstance(request.data['width'], int) or not isinstance(request.data['height'], int): - raise ValidationError({'detail': 'Data has to be integers.'}) - if (request.data['width'] < 800 or request.data['width'] > 3840 or - request.data['height'] < 340 or request.data['height'] > 2880): - raise ValidationError({'detail': 'The Resolution have to be between 800x340 and 3840x2880.'}) + raise ValidationError({"detail": "Data must be a dictionary."}) + if request.data.get("width") is None or request.data.get("height") is None: + raise ValidationError({"detail": "A width and a height have to be given."}) + if not isinstance(request.data["width"], int) or not isinstance( + request.data["height"], int + ): + raise ValidationError({"detail": "Data has to be integers."}) + if ( + request.data["width"] < 800 + or request.data["width"] > 3840 + or request.data["height"] < 340 + or request.data["height"] > 2880 + ): + raise ValidationError( + {"detail": "The Resolution have to be between 800x340 and 3840x2880."} + ) projector_instance = self.get_object() - projector_instance.width = request.data['width'] - projector_instance.height = request.data['height'] + projector_instance.width = request.data["width"] + projector_instance.height = request.data["height"] projector_instance.save() - message = 'Changing resolution to {width}x{height} was successful.'.format( - width=request.data['width'], - height=request.data['height']) - return Response({'detail': message}) + message = "Changing resolution to {width}x{height} was successful.".format( + width=request.data["width"], height=request.data["height"] + ) + return Response({"detail": message}) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def control_view(self, request, pk): """ REST API operation to control the projector view, i. e. scale and @@ -426,27 +481,32 @@ class ProjectorViewSet(ModelViewSet): } """ if not isinstance(request.data, dict): - raise ValidationError({'detail': 'Data must be a dictionary.'}) - if (request.data.get('action') not in ('scale', 'scroll') or - request.data.get('direction') not in ('up', 'down', 'reset')): - raise ValidationError({'detail': "Data must be a dictionary with an action ('scale' or 'scroll') " - "and a direction ('up', 'down' or 'reset')."}) + raise ValidationError({"detail": "Data must be a dictionary."}) + if request.data.get("action") not in ("scale", "scroll") or request.data.get( + "direction" + ) not in ("up", "down", "reset"): + raise ValidationError( + { + "detail": "Data must be a dictionary with an action ('scale' or 'scroll') " + "and a direction ('up', 'down' or 'reset')." + } + ) projector_instance = self.get_object() - if request.data['action'] == 'scale': - if request.data['direction'] == 'up': - projector_instance.scale = F('scale') + 1 - elif request.data['direction'] == 'down': - projector_instance.scale = F('scale') - 1 + if request.data["action"] == "scale": + if request.data["direction"] == "up": + projector_instance.scale = F("scale") + 1 + elif request.data["direction"] == "down": + projector_instance.scale = F("scale") - 1 else: # request.data['direction'] == 'reset' projector_instance.scale = 0 else: # request.data['action'] == 'scroll' - if request.data['direction'] == 'up': - projector_instance.scroll = F('scroll') + 1 - elif request.data['direction'] == 'down': - projector_instance.scroll = F('scroll') - 1 + if request.data["direction"] == "up": + projector_instance.scroll = F("scroll") + 1 + elif request.data["direction"] == "down": + projector_instance.scroll = F("scroll") - 1 else: # request.data['direction'] == 'reset' projector_instance.scroll = 0 @@ -454,12 +514,13 @@ class ProjectorViewSet(ModelViewSet): projector_instance.save(skip_autoupdate=True) projector_instance.refresh_from_db() inform_changed_data(projector_instance) - message = '{action} {direction} was successful.'.format( - action=request.data['action'].capitalize(), - direction=request.data['direction']) - return Response({'detail': message}) + message = "{action} {direction} was successful.".format( + action=request.data["action"].capitalize(), + direction=request.data["direction"], + ) + return Response({"detail": message}) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def set_scroll(self, request, pk): """ REST API operation to scroll the projector. @@ -468,17 +529,18 @@ class ProjectorViewSet(ModelViewSet): /rest/core/projector//set_scroll/ with a new value for scroll. """ if not isinstance(request.data, int): - raise ValidationError({'detail': 'Data must be an int.'}) + raise ValidationError({"detail": "Data must be an int."}) projector_instance = self.get_object() projector_instance.scroll = request.data projector_instance.save() - message = 'Setting scroll to {scroll} was successful.'.format( - scroll=request.data) - return Response({'detail': message}) + message = "Setting scroll to {scroll} was successful.".format( + scroll=request.data + ) + return Response({"detail": message}) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def control_blank(self, request, pk): """ REST API operation to blank the projector. @@ -487,16 +549,17 @@ class ProjectorViewSet(ModelViewSet): /rest/core/projector//control_blank/ with a value for blank. """ if not isinstance(request.data, bool): - raise ValidationError({'detail': 'Data must be a bool.'}) + raise ValidationError({"detail": "Data must be a bool."}) projector_instance = self.get_object() projector_instance.blank = request.data projector_instance.save() message = "Setting 'blank' to {blank} was successful.".format( - blank=request.data) - return Response({'detail': message}) + blank=request.data + ) + return Response({"detail": message}) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def broadcast(self, request, pk): """ REST API operation to (un-)broadcast the given projector. @@ -505,16 +568,17 @@ class ProjectorViewSet(ModelViewSet): It expects a POST request to /rest/core/projector//broadcast/ without an argument """ - if config['projector_broadcast'] == 0: - config['projector_broadcast'] = pk + if config["projector_broadcast"] == 0: + config["projector_broadcast"] = pk message = "Setting projector {id} as broadcast projector was successful.".format( - id=pk) + id=pk + ) else: - config['projector_broadcast'] = 0 + config["projector_broadcast"] = 0 message = "Disabling broadcast was successful." - return Response({'detail': message}) + return Response({"detail": message}) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def set_projectiondefault(self, request, pk): """ REST API operation to set a projectiondefault to the requested projector. The argument @@ -524,21 +588,28 @@ class ProjectorViewSet(ModelViewSet): /rest/core/projector//set_projectiondefault/ with the projectiondefault id as the argument """ if not isinstance(request.data, int): - raise ValidationError({'detail': 'Data must be an int.'}) + raise ValidationError({"detail": "Data must be an int."}) try: projectiondefault = ProjectionDefault.objects.get(pk=request.data) except ProjectionDefault.DoesNotExist: - raise ValidationError({'detail': 'The projectiondefault with pk={pk} was not found.'.format( - pk=request.data)}) + raise ValidationError( + { + "detail": "The projectiondefault with pk={pk} was not found.".format( + pk=request.data + ) + } + ) else: projector_instance = self.get_object() projectiondefault.projector = projector_instance projectiondefault.save() - return Response('Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format( - name=projectiondefault.display_name, - projector_id=projector_instance.pk)) + return Response( + 'Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format( + name=projectiondefault.display_name, projector_id=projector_instance.pk + ) + ) class TagViewSet(ModelViewSet): @@ -548,6 +619,7 @@ class TagViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = TagAccessPermissions() queryset = Tag.objects.all() @@ -555,14 +627,14 @@ class TagViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': + elif self.action == "metadata": # Every authenticated user can see the metadata. # Anonymous users can do so if they are enabled. result = self.request.user.is_authenticated or anonymous_is_enabled() - elif self.action in ('create', 'partial_update', 'update', 'destroy'): - result = has_perm(self.request.user, 'core.can_manage_tags') + elif self.action in ("create", "partial_update", "update", "destroy"): + result = has_perm(self.request.user, "core.can_manage_tags") else: result = False return result @@ -575,6 +647,7 @@ class ConfigViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, update and partial_update. """ + access_permissions = ConfigAccessPermissions() queryset = ConfigStore.objects.all() @@ -582,22 +655,22 @@ class ConfigViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': + elif self.action == "metadata": # Every authenticated user can see the metadata and list or # retrieve the config. Anonymous users can do so if they are # enabled. result = self.request.user.is_authenticated or anonymous_is_enabled() - elif self.action in ('partial_update', 'update'): + elif self.action in ("partial_update", "update"): # The user needs 'core.can_manage_logos_and_fonts' for all config values # starting with 'logo' and 'font'. For all other config values th euser needs # the default permissions 'core.can_manage_config'. - pk = self.kwargs['pk'] - if pk.startswith('logo') or pk.startswith('font'): - result = has_perm(self.request.user, 'core.can_manage_logos_and_fonts') + pk = self.kwargs["pk"] + if pk.startswith("logo") or pk.startswith("font"): + result = has_perm(self.request.user, "core.can_manage_logos_and_fonts") else: - result = has_perm(self.request.user, 'core.can_manage_config') + result = has_perm(self.request.user, "core.can_manage_config") else: result = False return result @@ -608,10 +681,10 @@ class ConfigViewSet(ModelViewSet): Example: {"value": 42} """ - key = kwargs['pk'] - value = request.data.get('value') + key = kwargs["pk"] + value = request.data.get("value") if value is None: - raise ValidationError({'detail': 'Invalid input. Config value is missing.'}) + raise ValidationError({"detail": "Invalid input. Config value is missing."}) # Validate and change value. try: @@ -619,10 +692,10 @@ class ConfigViewSet(ModelViewSet): except ConfigNotFound: raise Http404 except ConfigError as e: - raise ValidationError({'detail': str(e)}) + raise ValidationError({"detail": str(e)}) # Return response. - return Response({'key': key, 'value': value}) + return Response({"key": key, "value": value}) class ChatMessageViewSet(ModelViewSet): @@ -632,6 +705,7 @@ class ChatMessageViewSet(ModelViewSet): There are the following views: metadata, list, retrieve and create. The views partial_update, update and destroy are disabled. """ + access_permissions = ChatMessageAccessPermissions() queryset = ChatMessage.objects.all() @@ -639,18 +713,18 @@ class ChatMessageViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('metadata', 'create'): + elif self.action in ("metadata", "create"): # We do not want anonymous users to use the chat even the anonymous # group has the permission core.can_use_chat. - result = ( - self.request.user.is_authenticated and - has_perm(self.request.user, 'core.can_use_chat')) - elif self.action == 'clear': - result = ( - has_perm(self.request.user, 'core.can_use_chat') and - has_perm(self.request.user, 'core.can_manage_chat')) + result = self.request.user.is_authenticated and has_perm( + self.request.user, "core.can_use_chat" + ) + elif self.action == "clear": + result = has_perm(self.request.user, "core.can_use_chat") and has_perm( + self.request.user, "core.can_manage_chat" + ) else: result = False return result @@ -665,7 +739,7 @@ class ChatMessageViewSet(ModelViewSet): # to see users may not have it but can get it now. inform_changed_data([self.request.user]) - @list_route(methods=['post']) + @list_route(methods=["post"]) def clear(self, request): """ Deletes all chat messages. @@ -679,7 +753,7 @@ class ChatMessageViewSet(ModelViewSet): # Trigger autoupdate and setup response. if len(args) > 0: inform_deleted_data(args) - return Response({'detail': _('All chat messages deleted successfully.')}) + return Response({"detail": _("All chat messages deleted successfully.")}) class ProjectorMessageViewSet(ModelViewSet): @@ -689,6 +763,7 @@ class ProjectorMessageViewSet(ModelViewSet): There are the following views: list, retrieve, create, update, partial_update and destroy. """ + access_permissions = ProjectorMessageAccessPermissions() queryset = ProjectorMessage.objects.all() @@ -696,10 +771,10 @@ class ProjectorMessageViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('create', 'partial_update', 'update', 'destroy'): - result = has_perm(self.request.user, 'core.can_manage_projector') + elif self.action in ("create", "partial_update", "update", "destroy"): + result = has_perm(self.request.user, "core.can_manage_projector") else: result = False return result @@ -712,6 +787,7 @@ class CountdownViewSet(ModelViewSet): There are the following views: list, retrieve, create, update, partial_update and destroy. """ + access_permissions = CountdownAccessPermissions() queryset = Countdown.objects.all() @@ -719,10 +795,10 @@ class CountdownViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('create', 'partial_update', 'update', 'destroy'): - result = has_perm(self.request.user, 'core.can_manage_projector') + elif self.action in ("create", "partial_update", "update", "destroy"): + result = has_perm(self.request.user, "core.can_manage_projector") else: result = False return result @@ -734,6 +810,7 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): There are the following views: list, retrieve, clear_history. """ + access_permissions = HistoryAccessPermissions() queryset = History.objects.all() @@ -741,13 +818,13 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve', 'clear_history'): + if self.action in ("list", "retrieve", "clear_history"): result = self.get_access_permissions().check_permissions(self.request.user) else: result = False return result - @list_route(methods=['post']) + @list_route(methods=["post"]) def clear_history(self, request): """ Deletes and rebuilds the history. @@ -769,16 +846,18 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): inform_changed_data(history_instances) # Setup response. - return Response({'detail': _('History was deleted successfully.')}) + return Response({"detail": _("History was deleted successfully.")}) # Special API views + class ServerTime(utils_views.APIView): """ Returns the server time as UNIX timestamp. """ - http_method_names = ['get'] + + http_method_names = ["get"] def get_context_data(self, **context): return now().timestamp() @@ -789,27 +868,36 @@ class VersionView(utils_views.APIView): Returns a dictionary with the OpenSlides version and the version of all plugins. """ - http_method_names = ['get'] + + http_method_names = ["get"] def get_context_data(self, **context): - Result = TypedDict('Result', { - 'openslides_version': str, - 'openslides_license': str, - 'openslides_url': str, - 'plugins': List[Dict[str, str]]}) + Result = TypedDict( + "Result", + { + "openslides_version": str, + "openslides_license": str, + "openslides_url": str, + "plugins": List[Dict[str, str]], + }, + ) result: Result = dict( openslides_version=version, openslides_license=license, openslides_url=url, - plugins=[]) + plugins=[], + ) # Versions of plugins. for plugin in settings.INSTALLED_PLUGINS: - result['plugins'].append({ - 'verbose_name': get_plugin_verbose_name(plugin), - 'description': get_plugin_description(plugin), - 'version': get_plugin_version(plugin), - 'license': get_plugin_license(plugin), - 'url': get_plugin_url(plugin)}) + result["plugins"].append( + { + "verbose_name": get_plugin_verbose_name(plugin), + "description": get_plugin_description(plugin), + "version": get_plugin_version(plugin), + "license": get_plugin_license(plugin), + "url": get_plugin_url(plugin), + } + ) return result @@ -820,7 +908,8 @@ class HistoryView(utils_views.APIView): Use query paramter timestamp (UNIX timestamp) to get all elements from begin until (including) this timestamp. """ - http_method_names = ['get'] + + http_method_names = ["get"] def get_context_data(self, **context): """ @@ -830,19 +919,25 @@ class HistoryView(utils_views.APIView): if not in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK]): self.permission_denied(self.request) try: - timestamp = int(self.request.query_params.get('timestamp', 0)) + timestamp = int(self.request.query_params.get("timestamp", 0)) except (ValueError): - raise ValidationError({'detail': 'Invalid input. Timestamp should be an integer.'}) + raise ValidationError( + {"detail": "Invalid input. Timestamp should be an integer."} + ) data = [] - queryset = History.objects.select_related('full_data') + queryset = History.objects.select_related("full_data") if timestamp: - queryset = queryset.filter(now__lte=datetime.datetime.fromtimestamp(timestamp)) + queryset = queryset.filter( + now__lte=datetime.datetime.fromtimestamp(timestamp) + ) for instance in queryset: - data.append({ - 'full_data': instance.full_data.full_data, - 'element_id': instance.element_id, - 'timestamp': instance.now.timestamp(), - 'information': instance.information, - 'user_id': instance.user.pk if instance.user else None, - }) + data.append( + { + "full_data": instance.full_data.full_data, + "element_id": instance.element_id, + "timestamp": instance.now.timestamp(), + "information": instance.information, + "user_id": instance.user.pk if instance.user else None, + } + ) return data diff --git a/openslides/core/websocket.py b/openslides/core/websocket.py index d316ff5c9..c8f8885ab 100644 --- a/openslides/core/websocket.py +++ b/openslides/core/websocket.py @@ -12,7 +12,8 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage): """ Websocket message from a client to send a message to other clients. """ - identifier = 'notify' + + identifier = "notify" schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Notify elements.", @@ -21,31 +22,24 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage): "items": { "type": "object", "properties": { - "projectors": { - "type": "array", - "items": {"type": "integer"}, - }, - "reply_channels": { - "type": "array", - "items": {"type": "string"}, - }, - "users": { - "type": "array", - "items": {"type": "integer"}, - } - } + "projectors": {"type": "array", "items": {"type": "integer"}}, + "reply_channels": {"type": "array", "items": {"type": "string"}}, + "users": {"type": "array", "items": {"type": "integer"}}, + }, }, "minItems": 1, } - async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None: + async def receive_content( + self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str + ) -> None: await consumer.channel_layer.group_send( "site", { "type": "send_notify", "incomming": content, "senderReplyChannelName": consumer.channel_name, - "senderUserId": consumer.scope['user']['id'], + "senderUserId": consumer.scope["user"]["id"], }, ) @@ -54,19 +48,25 @@ class ConstantsWebsocketClientMessage(BaseWebsocketClientMessage): """ The Client requests the constants. """ - identifier = 'constants' + + identifier = "constants" content_required = False - async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None: + async def receive_content( + self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str + ) -> None: # Return all constants to the client. - await consumer.send_json(type='constants', content=get_constants(), in_response=id) + await consumer.send_json( + type="constants", content=get_constants(), in_response=id + ) class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage): """ The Client request database elements. """ - identifier = 'getElements' + + identifier = "getElements" schema = { "$schema": "http://json-schema.org/draft-07/schema#", "titel": "getElement request", @@ -74,31 +74,40 @@ class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage): "type": "object", "properties": { # change_id is not required - "change_id": { - "type": "integer", - } + "change_id": {"type": "integer"} }, } - async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None: - requested_change_id = content.get('change_id', 0) + async def receive_content( + self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str + ) -> None: + requested_change_id = content.get("change_id", 0) try: - element_data = await get_element_data(consumer.scope['user']['id'], requested_change_id) + element_data = await get_element_data( + consumer.scope["user"]["id"], requested_change_id + ) except ValueError as error: - await consumer.send_json(type='error', content=str(error), in_response=id) + await consumer.send_json(type="error", content=str(error), in_response=id) else: - await consumer.send_json(type='autoupdate', content=element_data, in_response=id) + await consumer.send_json( + type="autoupdate", content=element_data, in_response=id + ) class AutoupdateWebsocketClientMessage(BaseWebsocketClientMessage): """ The Client turns autoupdate on or off. """ - identifier = 'autoupdate' - async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None: + identifier = "autoupdate" + + async def receive_content( + self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str + ) -> None: # Turn on or off the autoupdate for the client if content: # accept any value, that can be interpreted as bool - await consumer.channel_layer.group_add('autoupdate', consumer.channel_name) + await consumer.channel_layer.group_add("autoupdate", consumer.channel_name) else: - await consumer.channel_layer.group_discard('autoupdate', consumer.channel_name) + await consumer.channel_layer.group_discard( + "autoupdate", consumer.channel_name + ) diff --git a/openslides/global_settings.py b/openslides/global_settings.py index 4235ebba1..d565c022f 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -9,68 +9,70 @@ MODULE_DIR = os.path.realpath(os.path.dirname(os.path.abspath(__file__))) # Application definition INSTALLED_APPS = [ - 'openslides.core', - 'openslides.users', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.staticfiles', - 'rest_framework', - 'channels', - 'openslides.agenda', - 'openslides.topics', - 'openslides.motions', - 'openslides.assignments', - 'openslides.mediafiles', + "openslides.core", + "openslides.users", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.staticfiles", + "rest_framework", + "channels", + "openslides.agenda", + "openslides.topics", + "openslides.motions", + "openslides.assignments", + "openslides.mediafiles", ] INSTALLED_PLUGINS = collect_plugins() # Adds all automaticly collected plugins MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'openslides.utils.autoupdate.AutoupdateBundleMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "openslides.utils.autoupdate.AutoupdateBundleMiddleware", ] -ROOT_URLCONF = 'openslides.urls' +ROOT_URLCONF = "openslides.urls" -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - }, + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + } ] # Email # https://docs.djangoproject.com/en/1.10/topics/email/ -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_TIMEOUT = 5 # Timeout in seconds for blocking operations like the connection attempt +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_TIMEOUT = ( + 5 +) # Timeout in seconds for blocking operations like the connection attempt # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ -LANGUAGE_CODE = 'en' +LANGUAGE_CODE = "en" LANGUAGES = ( - ('en', 'English'), - ('de', 'Deutsch'), - ('fr', 'Français'), - ('es', 'Español'), - ('pt', 'Português'), - ('cs', 'Český'), - ('ru', 'русский'), + ("en", "English"), + ("de", "Deutsch"), + ("fr", "Français"), + ("es", "Español"), + ("pt", "Português"), + ("cs", "Český"), + ("ru", "русский"), ) -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -78,62 +80,54 @@ USE_L10N = True USE_TZ = True -LOCALE_PATHS = [ - os.path.join(MODULE_DIR, 'locale'), -] +LOCALE_PATHS = [os.path.join(MODULE_DIR, "locale")] # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" -STATICFILES_DIRS = [ - os.path.join(MODULE_DIR, 'static'), -] +STATICFILES_DIRS = [os.path.join(MODULE_DIR, "static")] # Sessions and user authentication # https://docs.djangoproject.com/en/1.10/topics/http/sessions/ # https://docs.djangoproject.com/en/1.10/topics/auth/ -AUTH_USER_MODEL = 'users.User' +AUTH_USER_MODEL = "users.User" -AUTH_GROUP_MODEL = 'users.Group' +AUTH_GROUP_MODEL = "users.Group" -SESSION_COOKIE_NAME = 'OpenSlidesSessionID' +SESSION_COOKIE_NAME = "OpenSlidesSessionID" SESSION_EXPIRE_AT_BROWSER_CLOSE = True -CSRF_COOKIE_NAME = 'OpenSlidesCsrfToken' +CSRF_COOKIE_NAME = "OpenSlidesCsrfToken" CSRF_COOKIE_AGE = None PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.Argon2PasswordHasher', - 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", + "django.contrib.auth.hashers.Argon2PasswordHasher", + "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", + "django.contrib.auth.hashers.BCryptPasswordHasher", ] # Files # https://docs.djangoproject.com/en/1.10/topics/files/ -MEDIA_URL = '/media/' +MEDIA_URL = "/media/" # Django Channels # http://channels.readthedocs.io/en/latest/ -ASGI_APPLICATION = 'openslides.routing.application' +ASGI_APPLICATION = "openslides.routing.application" -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels.layers.InMemoryChannelLayer', - }, -} +CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} # Enable updating the last_login field for users on every login. diff --git a/openslides/mediafiles/__init__.py b/openslides/mediafiles/__init__.py index c3142580b..bdd03d1b5 100644 --- a/openslides/mediafiles/__init__.py +++ b/openslides/mediafiles/__init__.py @@ -1 +1 @@ -default_app_config = 'openslides.mediafiles.apps.MediafilesAppConfig' +default_app_config = "openslides.mediafiles.apps.MediafilesAppConfig" diff --git a/openslides/mediafiles/access_permissions.py b/openslides/mediafiles/access_permissions.py index 345bc6a72..b74e7800e 100644 --- a/openslides/mediafiles/access_permissions.py +++ b/openslides/mediafiles/access_permissions.py @@ -8,22 +8,24 @@ class MediafileAccessPermissions(BaseAccessPermissions): """ Access permissions container for Mediafile and MediafileViewSet. """ - base_permission = 'mediafiles.can_see' + + base_permission = "mediafiles.can_see" async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Returns the restricted serialized data for the instance prepared for the user. Removes hidden mediafiles for some users. """ # Parse data. - if await async_has_perm(user_id, 'mediafiles.can_see') and await async_has_perm(user_id, 'mediafiles.can_see_hidden'): + if await async_has_perm(user_id, "mediafiles.can_see") and await async_has_perm( + user_id, "mediafiles.can_see_hidden" + ): data = full_data - elif await async_has_perm(user_id, 'mediafiles.can_see'): + elif await async_has_perm(user_id, "mediafiles.can_see"): # Exclude hidden mediafiles. - data = [full for full in full_data if not full['hidden']] + data = [full for full in full_data if not full["hidden"]] else: data = [] diff --git a/openslides/mediafiles/apps.py b/openslides/mediafiles/apps.py index 74c8dcfdc..6357bd8c4 100644 --- a/openslides/mediafiles/apps.py +++ b/openslides/mediafiles/apps.py @@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements class MediafilesAppConfig(AppConfig): - name = 'openslides.mediafiles' - verbose_name = 'OpenSlides Mediafiles' + name = "openslides.mediafiles" + verbose_name = "OpenSlides Mediafiles" angular_site_module = True angular_projector_module = True @@ -27,20 +27,25 @@ class MediafilesAppConfig(AppConfig): # Connect signals. permission_change.connect( get_permission_change_data, - dispatch_uid='mediafiles_get_permission_change_data') + dispatch_uid="mediafiles_get_permission_change_data", + ) # Register viewsets. - router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet) + router.register( + self.get_model("Mediafile").get_collection_string(), MediafileViewSet + ) # register required_users - required_user.add_collection_string(self.get_model('Mediafile').get_collection_string(), required_users) + required_user.add_collection_string( + self.get_model("Mediafile").get_collection_string(), required_users + ) def get_startup_elements(self): """ Yields all Cachables required on startup i. e. opening the websocket connection. """ - yield self.get_model('Mediafile') + yield self.get_model("Mediafile") def required_users(element: Dict[str, Any]) -> Set[int]: @@ -49,4 +54,4 @@ def required_users(element: Dict[str, Any]) -> Set[int]: if request_user can see mediafiles. This function may return an empty set. """ - return set(element['uploader_id']) + return set(element["uploader_id"]) diff --git a/openslides/mediafiles/migrations/0001_initial.py b/openslides/mediafiles/migrations/0001_initial.py index 52914d00c..71a2477ab 100644 --- a/openslides/mediafiles/migrations/0001_initial.py +++ b/openslides/mediafiles/migrations/0001_initial.py @@ -13,25 +13,43 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='Mediafile', + name="Mediafile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('mediafile', models.FileField(upload_to='file')), - ('title', models.CharField(max_length=255, unique=True)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('uploader', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("mediafile", models.FileField(upload_to="file")), + ("title", models.CharField(max_length=255, unique=True)), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ( + "uploader", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'permissions': (('can_see', 'Can see the list of files'), ('can_upload', 'Can upload files'), ('can_manage', 'Can manage files')), - 'default_permissions': (), - 'ordering': ['title'], + "permissions": ( + ("can_see", "Can see the list of files"), + ("can_upload", "Can upload files"), + ("can_manage", "Can manage files"), + ), + "default_permissions": (), + "ordering": ["title"], }, bases=(openslides.utils.models.RESTModelMixin, models.Model), - ), + ) ] diff --git a/openslides/mediafiles/migrations/0002_mediafile_private.py b/openslides/mediafiles/migrations/0002_mediafile_private.py index cad7e4ca4..c58c9078d 100644 --- a/openslides/mediafiles/migrations/0002_mediafile_private.py +++ b/openslides/mediafiles/migrations/0002_mediafile_private.py @@ -7,22 +7,25 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('mediafiles', '0001_initial'), - ] + dependencies = [("mediafiles", "0001_initial")] operations = [ migrations.AlterModelOptions( - name='mediafile', - options={'default_permissions': (), 'ordering': ['title'], 'permissions': ( - ('can_see', 'Can see the list of files'), - ('can_see_hidden', 'Can see hidden files'), - ('can_upload', 'Can upload files'), - ('can_manage', 'Can manage files'))}, + name="mediafile", + options={ + "default_permissions": (), + "ordering": ["title"], + "permissions": ( + ("can_see", "Can see the list of files"), + ("can_see_hidden", "Can see hidden files"), + ("can_upload", "Can upload files"), + ("can_manage", "Can manage files"), + ), + }, ), migrations.AddField( - model_name='mediafile', - name='hidden', + model_name="mediafile", + name="hidden", field=models.BooleanField(default=False), ), ] diff --git a/openslides/mediafiles/models.py b/openslides/mediafiles/models.py index c418ce6cf..9238af6b0 100644 --- a/openslides/mediafiles/models.py +++ b/openslides/mediafiles/models.py @@ -13,10 +13,11 @@ class Mediafile(RESTModelMixin, models.Model): """ Class for uploaded files which can be delivered under a certain url. """ - access_permissions = MediafileAccessPermissions() - can_see_permission = 'mediafiles.can_see' - mediafile = models.FileField(upload_to='file') + access_permissions = MediafileAccessPermissions() + can_see_permission = "mediafiles.can_see" + + mediafile = models.FileField(upload_to="file") """ See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield for more information. @@ -26,10 +27,8 @@ class Mediafile(RESTModelMixin, models.Model): """A string representing the title of the file.""" uploader = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True, - blank=True) + settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True + ) """A user – the uploader of a file.""" hidden = models.BooleanField(default=False) @@ -42,13 +41,15 @@ class Mediafile(RESTModelMixin, models.Model): """ Meta class for the mediafile model. """ - ordering = ['title'] + + ordering = ["title"] default_permissions = () permissions = ( - ('can_see', 'Can see the list of files'), - ('can_see_hidden', 'Can see hidden files'), - ('can_upload', 'Can upload files'), - ('can_manage', 'Can manage files')) + ("can_see", "Can see the list of files"), + ("can_see_hidden", "Can see hidden files"), + ("can_upload", "Can upload files"), + ("can_manage", "Can manage files"), + ) def __str__(self): """ @@ -72,10 +73,11 @@ class Mediafile(RESTModelMixin, models.Model): mediafile projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='mediafiles/mediafile', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="mediafiles/mediafile", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) def get_filesize(self): """ @@ -85,26 +87,26 @@ class Mediafile(RESTModelMixin, models.Model): try: size = self.mediafile.size except OSError: - size_string = _('unknown') + size_string = _("unknown") else: if size < 1024: - size_string = '< 1 kB' + size_string = "< 1 kB" elif size >= 1024 * 1024: mB = size / 1024 / 1024 - size_string = '%d MB' % mB + size_string = "%d MB" % mB else: kB = size / 1024 - size_string = '%d kB' % kB + size_string = "%d kB" % kB return size_string def is_logo(self): - for key in config['logos_available']: - if config[key]['path'] == self.mediafile.url: + for key in config["logos_available"]: + if config[key]["path"] == self.mediafile.url: return True return False def is_font(self): - for key in config['fonts_available']: - if config[key]['path'] == self.mediafile.url: + for key in config["fonts_available"]: + if config[key]["path"] == self.mediafile.url: return True return False diff --git a/openslides/mediafiles/projector.py b/openslides/mediafiles/projector.py index f939ce4f4..2a6b0dee3 100644 --- a/openslides/mediafiles/projector.py +++ b/openslides/mediafiles/projector.py @@ -9,11 +9,12 @@ class MediafileSlide(ProjectorElement): """ Slide definitions for Mediafile model. """ - name = 'mediafiles/mediafile' + + name = "mediafiles/mediafile" def check_data(self): - if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('File does not exist.') + if not Mediafile.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("File does not exist.") def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: diff --git a/openslides/mediafiles/serializers.py b/openslides/mediafiles/serializers.py index e2f12680f..6fbab7340 100644 --- a/openslides/mediafiles/serializers.py +++ b/openslides/mediafiles/serializers.py @@ -10,9 +10,8 @@ from .models import Mediafile class AngularCompatibleFileField(FileField): - def to_internal_value(self, data): - if data == '': + if data == "": return None return super(AngularCompatibleFileField, self).to_internal_value(data) @@ -20,20 +19,17 @@ class AngularCompatibleFileField(FileField): if value is None: return None filetype = mimetypes.guess_type(value.path)[0] - result = { - 'name': value.name, - 'type': filetype - } - if filetype == 'application/pdf': + result = {"name": value.name, "type": filetype} + if filetype == "application/pdf": try: - result['pages'] = PdfFileReader(open(value.path, 'rb')).getNumPages() + result["pages"] = PdfFileReader(open(value.path, "rb")).getNumPages() except FileNotFoundError: # File was deleted from server. Set 'pages' to 0. - result['pages'] = 0 + result["pages"] = 0 except PdfReadError: # File could be encrypted but not be detected by PyPDF. - result['pages'] = 0 - result['encrypted'] = True + result["pages"] = 0 + result["encrypted"] = True return result @@ -41,6 +37,7 @@ class MediafileSerializer(ModelSerializer): """ Serializer for mediafile.models.Mediafile objects. """ + media_url_prefix = SerializerMethodField() filesize = SerializerMethodField() @@ -52,19 +49,20 @@ class MediafileSerializer(ModelSerializer): super(MediafileSerializer, self).__init__(*args, **kwargs) self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField if self.instance is not None: - self.fields['mediafile'].read_only = True + self.fields["mediafile"].read_only = True class Meta: model = Mediafile fields = ( - 'id', - 'title', - 'mediafile', - 'media_url_prefix', - 'uploader', - 'filesize', - 'hidden', - 'timestamp',) + "id", + "title", + "mediafile", + "media_url_prefix", + "uploader", + "filesize", + "hidden", + "timestamp", + ) def get_filesize(self, mediafile): return mediafile.get_filesize() diff --git a/openslides/mediafiles/signals.py b/openslides/mediafiles/signals.py index ad297ac19..1fe665b3c 100644 --- a/openslides/mediafiles/signals.py +++ b/openslides/mediafiles/signals.py @@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs): """ Yields all necessary collections if 'mediafiles.can_see' permission changes. """ - mediafiles_app = apps.get_app_config(app_label='mediafiles') + mediafiles_app = apps.get_app_config(app_label="mediafiles") for permission in permissions: # There could be only one 'mediafiles.can_see' and then we want to return data. - if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see': + if ( + permission.content_type.app_label == mediafiles_app.label + and permission.codename == "can_see" + ): yield from mediafiles_app.get_startup_elements() diff --git a/openslides/mediafiles/views.py b/openslides/mediafiles/views.py index 1a17862d68..03e479beb 100644 --- a/openslides/mediafiles/views.py +++ b/openslides/mediafiles/views.py @@ -9,6 +9,7 @@ from .models import Mediafile # Viewsets for the REST API + class MediafileViewSet(ModelViewSet): """ API endpoint for mediafile objects. @@ -16,6 +17,7 @@ class MediafileViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = MediafileAccessPermissions() queryset = Mediafile.objects.all() @@ -23,20 +25,24 @@ class MediafileViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': - result = has_perm(self.request.user, 'mediafiles.can_see') - elif self.action == 'create': - result = (has_perm(self.request.user, 'mediafiles.can_see') and - has_perm(self.request.user, 'mediafiles.can_upload')) - elif self.action in ('partial_update', 'update'): - result = (has_perm(self.request.user, 'mediafiles.can_see') and - has_perm(self.request.user, 'mediafiles.can_upload') and - has_perm(self.request.user, 'mediafiles.can_manage')) - elif self.action == 'destroy': - result = (has_perm(self.request.user, 'mediafiles.can_see') and - has_perm(self.request.user, 'mediafiles.can_manage')) + elif self.action == "metadata": + result = has_perm(self.request.user, "mediafiles.can_see") + elif self.action == "create": + result = has_perm(self.request.user, "mediafiles.can_see") and has_perm( + self.request.user, "mediafiles.can_upload" + ) + elif self.action in ("partial_update", "update"): + result = ( + has_perm(self.request.user, "mediafiles.can_see") + and has_perm(self.request.user, "mediafiles.can_upload") + and has_perm(self.request.user, "mediafiles.can_manage") + ) + elif self.action == "destroy": + result = has_perm(self.request.user, "mediafiles.can_see") and has_perm( + self.request.user, "mediafiles.can_manage" + ) else: result = False return result @@ -46,13 +52,15 @@ class MediafileViewSet(ModelViewSet): Customized view endpoint to upload a new file. """ # Check permission to check if the uploader has to be changed. - uploader_id = self.request.data.get('uploader_id') - if (uploader_id and - not has_perm(request.user, 'mediafiles.can_manage') and - str(self.request.user.pk) != str(uploader_id)): + uploader_id = self.request.data.get("uploader_id") + if ( + uploader_id + and not has_perm(request.user, "mediafiles.can_manage") + and str(self.request.user.pk) != str(uploader_id) + ): self.permission_denied(request) - if not self.request.data.get('mediafile'): - raise ValidationError({'detail': 'You forgot to provide a file.'}) + if not self.request.data.get("mediafile"): + raise ValidationError({"detail": "You forgot to provide a file."}) return super().create(request, *args, **kwargs) def destroy(self, request, *args, **kwargs): @@ -77,9 +85,11 @@ def protected_serve(request, path, document_root=None, show_indexes=False): except Mediafile.DoesNotExist: return HttpResponseNotFound(content="Not found.") - can_see = has_perm(request.user, 'mediafiles.can_see') + can_see = has_perm(request.user, "mediafiles.can_see") is_special_file = mediafile.is_logo() or mediafile.is_font() - is_hidden_but_no_perms = mediafile.hidden and not has_perm(request.user, 'mediafiles.can_see_hidden') + is_hidden_but_no_perms = mediafile.hidden and not has_perm( + request.user, "mediafiles.can_see_hidden" + ) if not is_special_file and (not can_see or is_hidden_but_no_perms): return HttpResponseForbidden(content="Forbidden.") diff --git a/openslides/motions/__init__.py b/openslides/motions/__init__.py index 02cf57e7f..80af65f90 100644 --- a/openslides/motions/__init__.py +++ b/openslides/motions/__init__.py @@ -1 +1 @@ -default_app_config = 'openslides.motions.apps.MotionsAppConfig' +default_app_config = "openslides.motions.apps.MotionsAppConfig" diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index ce6f2cbb5..9c0e24e7d 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -9,12 +9,12 @@ class MotionAccessPermissions(BaseAccessPermissions): """ Access permissions container for Motion and MotionViewSet. """ - base_permission = 'motions.can_see' + + base_permission = "motions.can_see" async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Returns the restricted serialized data for the instance prepared for the user. Removes motion if the user has not the permission to see @@ -23,33 +23,37 @@ class MotionAccessPermissions(BaseAccessPermissions): personal notes. """ # Parse data. - if await async_has_perm(user_id, 'motions.can_see'): + if await async_has_perm(user_id, "motions.can_see"): # TODO: Refactor this after personal_notes system is refactored. data = [] for full in full_data: # Check if user is submitter of this motion. if user_id: is_submitter = user_id in [ - submitter['user_id'] for submitter in full.get('submitters', [])] + submitter["user_id"] for submitter in full.get("submitters", []) + ] else: # Anonymous users can not be submitters. is_submitter = False # Check see permission for this motion. - required_permission_to_see = full['state_required_permission_to_see'] + required_permission_to_see = full["state_required_permission_to_see"] permission = ( - not required_permission_to_see or - await async_has_perm(user_id, required_permission_to_see) or - await async_has_perm(user_id, 'motions.can_manage') or - is_submitter) + not required_permission_to_see + or await async_has_perm(user_id, required_permission_to_see) + or await async_has_perm(user_id, "motions.can_manage") + or is_submitter + ) # Parse single motion. if permission: full_copy = deepcopy(full) - full_copy['comments'] = [] - for comment in full['comments']: - if await async_in_some_groups(user_id, comment['read_groups_id']): - full_copy['comments'].append(comment) + full_copy["comments"] = [] + for comment in full["comments"]: + if await async_in_some_groups( + user_id, comment["read_groups_id"] + ): + full_copy["comments"].append(comment) data.append(full_copy) else: data = [] @@ -61,23 +65,23 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions): """ Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet. """ - base_permission = 'motions.can_see' + + base_permission = "motions.can_see" async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Removes change recommendations if they are internal and the user has not the can_manage permission. To see change recommendation the user needs the can_see permission. """ # Parse data. - if await async_has_perm(user_id, 'motions.can_see'): - has_manage_perms = await async_has_perm(user_id, 'motion.can_manage') + if await async_has_perm(user_id, "motions.can_see"): + has_manage_perms = await async_has_perm(user_id, "motion.can_manage") data = [] for full in full_data: - if not full['internal'] or has_manage_perms: + if not full["internal"] or has_manage_perms: data.append(full) else: data = [] @@ -89,22 +93,22 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions): """ Access permissions container for MotionCommentSection and MotionCommentSectionViewSet. """ - base_permission = 'motions.can_see' + + base_permission = "motions.can_see" async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ If the user has manage rights, he can see all sections. If not all sections will be removed, when the user is not in at least one of the read_groups. """ data: List[Dict[str, Any]] = [] - if await async_has_perm(user_id, 'motions.can_manage'): + if await async_has_perm(user_id, "motions.can_manage"): data = full_data else: for full in full_data: - read_groups = full.get('read_groups_id', []) + read_groups = full.get("read_groups_id", []) if await async_in_some_groups(user_id, read_groups): data.append(full) return data @@ -114,25 +118,29 @@ class StatuteParagraphAccessPermissions(BaseAccessPermissions): """ Access permissions container for StatuteParagraph and StatuteParagraphViewSet. """ - base_permission = 'motions.can_see' + + base_permission = "motions.can_see" class CategoryAccessPermissions(BaseAccessPermissions): """ Access permissions container for Category and CategoryViewSet. """ - base_permission = 'motions.can_see' + + base_permission = "motions.can_see" class MotionBlockAccessPermissions(BaseAccessPermissions): """ Access permissions container for Category and CategoryViewSet. """ - base_permission = 'motions.can_see' + + base_permission = "motions.can_see" class WorkflowAccessPermissions(BaseAccessPermissions): """ Access permissions container for Workflow and WorkflowViewSet. """ - base_permission = 'motions.can_see' + + base_permission = "motions.can_see" diff --git a/openslides/motions/apps.py b/openslides/motions/apps.py index 553cd7411..686c920dc 100644 --- a/openslides/motions/apps.py +++ b/openslides/motions/apps.py @@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements class MotionsAppConfig(AppConfig): - name = 'openslides.motions' - verbose_name = 'OpenSlides Motion' + name = "openslides.motions" + verbose_name = "OpenSlides Motion" angular_site_module = True angular_projector_module = True @@ -17,10 +17,7 @@ class MotionsAppConfig(AppConfig): from openslides.core.signals import permission_change from openslides.utils.rest_api import router from .projector import get_projector_elements - from .signals import ( - create_builtin_workflows, - get_permission_change_data, - ) + from .signals import create_builtin_workflows, get_permission_change_data from . import serializers # noqa from .views import ( CategoryViewSet, @@ -40,29 +37,49 @@ class MotionsAppConfig(AppConfig): # Connect signals. post_migrate.connect( - create_builtin_workflows, - dispatch_uid='motion_create_builtin_workflows') + create_builtin_workflows, dispatch_uid="motion_create_builtin_workflows" + ) permission_change.connect( get_permission_change_data, - dispatch_uid='motions_get_permission_change_data') + dispatch_uid="motions_get_permission_change_data", + ) # Register viewsets. - router.register(self.get_model('Category').get_collection_string(), CategoryViewSet) - router.register(self.get_model('StatuteParagraph').get_collection_string(), StatuteParagraphViewSet) - router.register(self.get_model('Motion').get_collection_string(), MotionViewSet) - router.register(self.get_model('MotionBlock').get_collection_string(), MotionBlockViewSet) - router.register(self.get_model('MotionCommentSection').get_collection_string(), MotionCommentSectionViewSet) - router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet) - router.register(self.get_model('MotionChangeRecommendation').get_collection_string(), - MotionChangeRecommendationViewSet) - router.register(self.get_model('MotionPoll').get_collection_string(), MotionPollViewSet) - router.register(self.get_model('State').get_collection_string(), StateViewSet) + router.register( + self.get_model("Category").get_collection_string(), CategoryViewSet + ) + router.register( + self.get_model("StatuteParagraph").get_collection_string(), + StatuteParagraphViewSet, + ) + router.register(self.get_model("Motion").get_collection_string(), MotionViewSet) + router.register( + self.get_model("MotionBlock").get_collection_string(), MotionBlockViewSet + ) + router.register( + self.get_model("MotionCommentSection").get_collection_string(), + MotionCommentSectionViewSet, + ) + router.register( + self.get_model("Workflow").get_collection_string(), WorkflowViewSet + ) + router.register( + self.get_model("MotionChangeRecommendation").get_collection_string(), + MotionChangeRecommendationViewSet, + ) + router.register( + self.get_model("MotionPoll").get_collection_string(), MotionPollViewSet + ) + router.register(self.get_model("State").get_collection_string(), StateViewSet) # Register required_users - required_user.add_collection_string(self.get_model('Motion').get_collection_string(), required_users) + required_user.add_collection_string( + self.get_model("Motion").get_collection_string(), required_users + ) def get_config_variables(self): from .config_variables import get_config_variables + return get_config_variables() def get_startup_elements(self): @@ -70,8 +87,15 @@ class MotionsAppConfig(AppConfig): Yields all Cachables required on startup i. e. opening the websocket connection. """ - for model_name in ('Category', 'StatuteParagraph', 'Motion', 'MotionBlock', - 'Workflow', 'MotionChangeRecommendation', 'MotionCommentSection'): + for model_name in ( + "Category", + "StatuteParagraph", + "Motion", + "MotionBlock", + "Workflow", + "MotionChangeRecommendation", + "MotionCommentSection", + ): yield self.get_model(model_name) @@ -81,6 +105,8 @@ def required_users(element: Dict[str, Any]) -> Set[int]: any motion if request_user can see motions. This function may return an empty set. """ - submitters_supporters = set([submitter['user_id'] for submitter in element['submitters']]) - submitters_supporters.update(element['supporters_id']) + submitters_supporters = set( + [submitter["user_id"] for submitter in element["submitters"]] + ) + submitters_supporters.update(element["supporters_id"]) return submitters_supporters diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py index 289e4add6..54b0bd8b7 100644 --- a/openslides/motions/config_variables.py +++ b/openslides/motions/config_variables.py @@ -11,8 +11,10 @@ def get_workflow_choices(): Returns a list of all workflows to be used as choices for the config variable 'motions_workflow'. Each list item contains the pk and the display name. """ - return [{'value': str(workflow.pk), 'display_name': workflow.name} - for workflow in Workflow.objects.all()] + return [ + {"value": str(workflow.pk), "display_name": workflow.name} + for workflow in Workflow.objects.all() + ] def get_config_variables(): @@ -26,300 +28,339 @@ def get_config_variables(): # General yield ConfigVariable( - name='motions_workflow', - default_value='1', - input_type='choice', - label='Workflow of new motions', + name="motions_workflow", + default_value="1", + input_type="choice", + label="Workflow of new motions", choices=get_workflow_choices, weight=310, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_statute_amendments_workflow', - default_value='1', - input_type='choice', - label='Workflow of new statute amendments', + name="motions_statute_amendments_workflow", + default_value="1", + input_type="choice", + label="Workflow of new statute amendments", choices=get_workflow_choices, weight=312, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_identifier', - default_value='per_category', - input_type='choice', - label='Identifier', + name="motions_identifier", + default_value="per_category", + input_type="choice", + label="Identifier", choices=( - {'value': 'per_category', 'display_name': 'Numbered per category'}, - {'value': 'serially_numbered', 'display_name': 'Serially numbered'}, - {'value': 'manually', 'display_name': 'Set it manually'}), + {"value": "per_category", "display_name": "Numbered per category"}, + {"value": "serially_numbered", "display_name": "Serially numbered"}, + {"value": "manually", "display_name": "Set it manually"}, + ), weight=315, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_preamble', - default_value='The assembly may decide:', - label='Motion preamble', + name="motions_preamble", + default_value="The assembly may decide:", + label="Motion preamble", weight=320, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_default_line_numbering', - default_value='none', - input_type='choice', - label='Default line numbering', + name="motions_default_line_numbering", + default_value="none", + input_type="choice", + label="Default line numbering", choices=( - {'value': 'outside', 'display_name': 'outside'}, - {'value': 'inline', 'display_name': 'inline'}, - {'value': 'none', 'display_name': 'Disabled'}), + {"value": "outside", "display_name": "outside"}, + {"value": "inline", "display_name": "inline"}, + {"value": "none", "display_name": "Disabled"}, + ), weight=322, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_line_length', + name="motions_line_length", default_value=90, - input_type='integer', - label='Line length', - help_text='The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40', + input_type="integer", + label="Line length", + help_text="The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40", weight=323, - group='Motions', - subgroup='General', - validators=(MinValueValidator(40),)) + group="Motions", + subgroup="General", + validators=(MinValueValidator(40),), + ) yield ConfigVariable( - name='motions_disable_reason_on_projector', + name="motions_disable_reason_on_projector", default_value=False, - input_type='boolean', - label='Hide reason on projector', + input_type="boolean", + label="Hide reason on projector", weight=325, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_disable_sidebox_on_projector', + name="motions_disable_sidebox_on_projector", default_value=False, - input_type='boolean', - label='Hide meta information box on projector', + input_type="boolean", + label="Hide meta information box on projector", weight=326, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_disable_recommendation_on_projector', + name="motions_disable_recommendation_on_projector", default_value=False, - input_type='boolean', - label='Hide recommendation on projector', + input_type="boolean", + label="Hide recommendation on projector", weight=327, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_stop_submitting', + name="motions_stop_submitting", default_value=False, - input_type='boolean', - label='Stop submitting new motions by non-staff users', + input_type="boolean", + label="Stop submitting new motions by non-staff users", weight=331, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_recommendations_by', - default_value='', - label='Name of recommender', - help_text='Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.', + name="motions_recommendations_by", + default_value="", + label="Name of recommender", + help_text="Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.", weight=332, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_statute_recommendations_by', - default_value='', - label='Name of recommender for statute amendments', - help_text='Will be displayed as label before selected recommendation in statute amendments. ' + - 'Use an empty value to disable the recommendation system for statute amendments.', + name="motions_statute_recommendations_by", + default_value="", + label="Name of recommender for statute amendments", + help_text="Will be displayed as label before selected recommendation in statute amendments. " + + "Use an empty value to disable the recommendation system for statute amendments.", weight=333, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) yield ConfigVariable( - name='motions_recommendation_text_mode', - default_value='original', - input_type='choice', - label='Default text version for change recommendations', + name="motions_recommendation_text_mode", + default_value="original", + input_type="choice", + label="Default text version for change recommendations", choices=( - {'value': 'original', 'display_name': 'Original version'}, - {'value': 'changed', 'display_name': 'Changed version'}, - {'value': 'diff', 'display_name': 'Diff version'}, - {'value': 'agreed', 'display_name': 'Final version'}), + {"value": "original", "display_name": "Original version"}, + {"value": "changed", "display_name": "Changed version"}, + {"value": "diff", "display_name": "Diff version"}, + {"value": "agreed", "display_name": "Final version"}, + ), weight=334, - group='Motions', - subgroup='General') + group="Motions", + subgroup="General", + ) # Amendments yield ConfigVariable( - name='motions_statutes_enabled', + name="motions_statutes_enabled", default_value=False, - input_type='boolean', - label='Activate statute amendments', + input_type="boolean", + label="Activate statute amendments", weight=335, - group='Motions', - subgroup='Amendments') + group="Motions", + subgroup="Amendments", + ) yield ConfigVariable( - name='motions_amendments_enabled', + name="motions_amendments_enabled", default_value=False, - input_type='boolean', - label='Activate amendments', + input_type="boolean", + label="Activate amendments", weight=336, - group='Motions', - subgroup='Amendments') + group="Motions", + subgroup="Amendments", + ) yield ConfigVariable( - name='motions_amendments_main_table', + name="motions_amendments_main_table", default_value=False, - input_type='boolean', - label='Show amendments together with motions', + input_type="boolean", + label="Show amendments together with motions", weight=337, - group='Motions', - subgroup='Amendments') + group="Motions", + subgroup="Amendments", + ) yield ConfigVariable( - name='motions_amendments_prefix', - default_value='-', - label='Prefix for the identifier for amendments', + name="motions_amendments_prefix", + default_value="-", + label="Prefix for the identifier for amendments", weight=340, - group='Motions', - subgroup='Amendments') + group="Motions", + subgroup="Amendments", + ) yield ConfigVariable( - name='motions_amendments_text_mode', - default_value='freestyle', - input_type='choice', - label='How to create new amendments', + name="motions_amendments_text_mode", + default_value="freestyle", + input_type="choice", + label="How to create new amendments", choices=( - {'value': 'freestyle', 'display_name': 'Empty text field'}, - {'value': 'fulltext', 'display_name': 'Edit the whole motion text'}, - {'value': 'paragraph', 'display_name': 'Paragraph-based, Diff-enabled'}, + {"value": "freestyle", "display_name": "Empty text field"}, + {"value": "fulltext", "display_name": "Edit the whole motion text"}, + {"value": "paragraph", "display_name": "Paragraph-based, Diff-enabled"}, ), weight=342, - group='Motions', - subgroup='Amendments') + group="Motions", + subgroup="Amendments", + ) # Supporters yield ConfigVariable( - name='motions_min_supporters', + name="motions_min_supporters", default_value=0, - input_type='integer', - label='Number of (minimum) required supporters for a motion', - help_text='Choose 0 to disable the supporting system.', + input_type="integer", + label="Number of (minimum) required supporters for a motion", + help_text="Choose 0 to disable the supporting system.", weight=345, - group='Motions', - subgroup='Supporters', - validators=(MinValueValidator(0),)) + group="Motions", + subgroup="Supporters", + validators=(MinValueValidator(0),), + ) yield ConfigVariable( - name='motions_remove_supporters', + name="motions_remove_supporters", default_value=False, - input_type='boolean', - label='Remove all supporters of a motion if a submitter edits his motion in early state', + input_type="boolean", + label="Remove all supporters of a motion if a submitter edits his motion in early state", weight=350, - group='Motions', - subgroup='Supporters') + group="Motions", + subgroup="Supporters", + ) # Voting and ballot papers yield ConfigVariable( - name='motions_poll_100_percent_base', - default_value='YES_NO_ABSTAIN', - input_type='choice', - label='The 100 % base of a voting result consists of', + name="motions_poll_100_percent_base", + default_value="YES_NO_ABSTAIN", + input_type="choice", + label="The 100 % base of a voting result consists of", choices=( - {'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain'}, - {'value': 'YES_NO', 'display_name': 'Yes/No'}, - {'value': 'VALID', 'display_name': 'All valid ballots'}, - {'value': 'CAST', 'display_name': 'All casted ballots'}, - {'value': 'DISABLED', 'display_name': 'Disabled (no percents)'} - ), + {"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain"}, + {"value": "YES_NO", "display_name": "Yes/No"}, + {"value": "VALID", "display_name": "All valid ballots"}, + {"value": "CAST", "display_name": "All casted ballots"}, + {"value": "DISABLED", "display_name": "Disabled (no percents)"}, + ), weight=355, - group='Motions', - subgroup='Voting and ballot papers') + group="Motions", + subgroup="Voting and ballot papers", + ) # TODO: Add server side validation of the choices. yield ConfigVariable( - name='motions_poll_default_majority_method', - default_value=majorityMethods[0]['value'], - input_type='choice', + name="motions_poll_default_majority_method", + default_value=majorityMethods[0]["value"], + input_type="choice", choices=majorityMethods, - label='Required majority', - help_text='Default method to check whether a motion has reached the required majority.', + label="Required majority", + help_text="Default method to check whether a motion has reached the required majority.", weight=357, - group='Motions', - subgroup='Voting and ballot papers') + group="Motions", + subgroup="Voting and ballot papers", + ) yield ConfigVariable( - name='motions_pdf_ballot_papers_selection', - default_value='CUSTOM_NUMBER', - input_type='choice', - label='Number of ballot papers (selection)', + name="motions_pdf_ballot_papers_selection", + default_value="CUSTOM_NUMBER", + input_type="choice", + label="Number of ballot papers (selection)", choices=( - {'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'}, - {'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': 'Number of all participants'}, - {'value': 'CUSTOM_NUMBER', 'display_name': 'Use the following custom number'}), + {"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"}, + { + "value": "NUMBER_OF_ALL_PARTICIPANTS", + "display_name": "Number of all participants", + }, + { + "value": "CUSTOM_NUMBER", + "display_name": "Use the following custom number", + }, + ), weight=360, - group='Motions', - subgroup='Voting and ballot papers') + group="Motions", + subgroup="Voting and ballot papers", + ) yield ConfigVariable( - name='motions_pdf_ballot_papers_number', + name="motions_pdf_ballot_papers_number", default_value=8, - input_type='integer', - label='Custom number of ballot papers', + input_type="integer", + label="Custom number of ballot papers", weight=365, - group='Motions', - subgroup='Voting and ballot papers', - validators=(MinValueValidator(1),)) + group="Motions", + subgroup="Voting and ballot papers", + validators=(MinValueValidator(1),), + ) # PDF and DOCX export yield ConfigVariable( - name='motions_export_title', - default_value='Motions', - label='Title for PDF and DOCX documents (all motions)', + name="motions_export_title", + default_value="Motions", + label="Title for PDF and DOCX documents (all motions)", weight=370, - group='Motions', - subgroup='Export') + group="Motions", + subgroup="Export", + ) yield ConfigVariable( - name='motions_export_preamble', - default_value='', - label='Preamble text for PDF and DOCX documents (all motions)', + name="motions_export_preamble", + default_value="", + label="Preamble text for PDF and DOCX documents (all motions)", weight=375, - group='Motions', - subgroup='Export') + group="Motions", + subgroup="Export", + ) yield ConfigVariable( - name='motions_export_category_sorting', - default_value='prefix', - input_type='choice', - label='Sort categories by', + name="motions_export_category_sorting", + default_value="prefix", + input_type="choice", + label="Sort categories by", choices=( - {'value': 'prefix', 'display_name': 'Prefix'}, - {'value': 'name', 'display_name': 'Name'}), + {"value": "prefix", "display_name": "Prefix"}, + {"value": "name", "display_name": "Name"}, + ), weight=380, - group='Motions', - subgroup='Export') + group="Motions", + subgroup="Export", + ) yield ConfigVariable( - name='motions_export_sequential_number', + name="motions_export_sequential_number", default_value=True, - input_type='boolean', - label='Include the sequential number in PDF and DOCX', + input_type="boolean", + label="Include the sequential number in PDF and DOCX", weight=385, - group='Motions', - subgroup='Export') + group="Motions", + subgroup="Export", + ) diff --git a/openslides/motions/exceptions.py b/openslides/motions/exceptions.py index eca36f427..a514ce908 100644 --- a/openslides/motions/exceptions.py +++ b/openslides/motions/exceptions.py @@ -3,4 +3,5 @@ from openslides.utils.exceptions import OpenSlidesError class WorkflowError(OpenSlidesError): """Exception raised when errors in a workflow or state accure.""" + pass diff --git a/openslides/motions/migrations/0001_initial.py b/openslides/motions/migrations/0001_initial.py index 4ec450000..869f5dea8 100644 --- a/openslides/motions/migrations/0001_initial.py +++ b/openslides/motions/migrations/0001_initial.py @@ -15,200 +15,344 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('mediafiles', '0001_initial'), + ("mediafiles", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('core', '0001_initial'), + ("core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('prefix', models.CharField(blank=True, max_length=32)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("prefix", models.CharField(blank=True, max_length=32)), + ], + options={"ordering": ["prefix"], "default_permissions": ()}, + bases=(openslides.utils.models.RESTModelMixin, models.Model), + ), + migrations.CreateModel( + name="Motion", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "identifier", + models.CharField( + blank=True, max_length=255, null=True, unique=True + ), + ), + ("identifier_number", models.IntegerField(null=True)), ], options={ - 'ordering': ['prefix'], - 'default_permissions': (), + "verbose_name": "Motion", + "permissions": ( + ("can_see", "Can see motions"), + ("can_create", "Can create motions"), + ("can_support", "Can support motions"), + ("can_manage", "Can manage motions"), + ), + "ordering": ("identifier",), + "default_permissions": (), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='Motion', + name="MotionLog", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('identifier', models.CharField(blank=True, max_length=255, null=True, unique=True)), - ('identifier_number', models.IntegerField(null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("message_list", jsonfield.fields.JSONField()), + ("time", models.DateTimeField(auto_now=True)), + ( + "motion", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="log_messages", + to="motions.Motion", + ), + ), + ( + "person", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), ], - options={ - 'verbose_name': 'Motion', - 'permissions': ( - ('can_see', 'Can see motions'), - ('can_create', 'Can create motions'), - ('can_support', 'Can support motions'), - ('can_manage', 'Can manage motions')), - 'ordering': ('identifier',), - 'default_permissions': (), - }, + options={"ordering": ["-time"], "default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='MotionLog', + name="MotionOption", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message_list', jsonfield.fields.JSONField()), - ('time', models.DateTimeField(auto_now=True)), - ('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='log_messages', to='motions.Motion')), - ('person', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ) ], - options={ - 'ordering': ['-time'], - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='MotionOption', + name="MotionPoll", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "votesvalid", + openslides.utils.models.MinMaxIntegerField(blank=True, null=True), + ), + ( + "votesinvalid", + openslides.utils.models.MinMaxIntegerField(blank=True, null=True), + ), + ( + "votescast", + openslides.utils.models.MinMaxIntegerField(blank=True, null=True), + ), + ( + "motion", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="polls", + to="motions.Motion", + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='MotionPoll', + name="MotionVersion", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), - ('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), - ('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), - ('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='motions.Motion')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version_number", models.PositiveIntegerField(default=1)), + ("title", models.CharField(max_length=255)), + ("text", models.TextField()), + ("reason", models.TextField(blank=True, null=True)), + ("creation_time", models.DateTimeField(auto_now=True)), + ( + "motion", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="versions", + to="motions.Motion", + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='MotionVersion', + name="MotionVote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version_number', models.PositiveIntegerField(default=1)), - ('title', models.CharField(max_length=255)), - ('text', models.TextField()), - ('reason', models.TextField(blank=True, null=True)), - ('creation_time', models.DateTimeField(auto_now=True)), - ('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='motions.Motion')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("weight", models.IntegerField(default=1, null=True)), + ("value", models.CharField(max_length=255, null=True)), + ( + "option", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="motions.MotionOption", + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='MotionVote', + name="State", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('weight', models.IntegerField(default=1, null=True)), - ('value', models.CharField(max_length=255, null=True)), - ('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='motions.MotionOption')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("action_word", models.CharField(max_length=255)), + ("css_class", models.CharField(default="primary", max_length=255)), + ( + "required_permission_to_see", + models.CharField(blank=True, max_length=255), + ), + ("allow_support", models.BooleanField(default=False)), + ("allow_create_poll", models.BooleanField(default=False)), + ("allow_submitter_edit", models.BooleanField(default=False)), + ("versioning", models.BooleanField(default=False)), + ("leave_old_version_active", models.BooleanField(default=False)), + ("dont_set_identifier", models.BooleanField(default=False)), + ("next_states", models.ManyToManyField(to="motions.State")), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='State', + name="Workflow", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('action_word', models.CharField(max_length=255)), - ('css_class', models.CharField(default='primary', max_length=255)), - ('required_permission_to_see', models.CharField(blank=True, max_length=255)), - ('allow_support', models.BooleanField(default=False)), - ('allow_create_poll', models.BooleanField(default=False)), - ('allow_submitter_edit', models.BooleanField(default=False)), - ('versioning', models.BooleanField(default=False)), - ('leave_old_version_active', models.BooleanField(default=False)), - ('dont_set_identifier', models.BooleanField(default=False)), - ('next_states', models.ManyToManyField(to='motions.State')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "first_state", + models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="motions.State", + ), + ), ], - options={ - 'default_permissions': (), - }, - bases=(openslides.utils.models.RESTModelMixin, models.Model), - ), - migrations.CreateModel( - name='Workflow', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('first_state', models.OneToOneField( - null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State')), - ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.AddField( - model_name='state', - name='workflow', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='states', to='motions.Workflow'), - ), - migrations.AddField( - model_name='motionoption', - name='poll', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='motions.MotionPoll'), - ), - migrations.AddField( - model_name='motion', - name='active_version', + model_name="state", + name="workflow", field=models.ForeignKey( - null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='active_version', to='motions.MotionVersion'), + on_delete=django.db.models.deletion.CASCADE, + related_name="states", + to="motions.Workflow", + ), ), migrations.AddField( - model_name='motion', - name='attachments', - field=models.ManyToManyField(blank=True, to='mediafiles.Mediafile'), - ), - migrations.AddField( - model_name='motion', - name='category', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.Category'), - ), - migrations.AddField( - model_name='motion', - name='parent', + model_name="motionoption", + name="poll", field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='amendments', to='motions.Motion'), + on_delete=django.db.models.deletion.CASCADE, to="motions.MotionPoll" + ), ), migrations.AddField( - model_name='motion', - name='state', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.State'), + model_name="motion", + name="active_version", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="active_version", + to="motions.MotionVersion", + ), ), migrations.AddField( - model_name='motion', - name='submitters', - field=models.ManyToManyField(blank=True, related_name='motion_submitters', to=settings.AUTH_USER_MODEL), + model_name="motion", + name="attachments", + field=models.ManyToManyField(blank=True, to="mediafiles.Mediafile"), ), migrations.AddField( - model_name='motion', - name='supporters', - field=models.ManyToManyField(blank=True, related_name='motion_supporters', to=settings.AUTH_USER_MODEL), + model_name="motion", + name="category", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="motions.Category", + ), ), migrations.AddField( - model_name='motion', - name='tags', - field=models.ManyToManyField(blank=True, to='core.Tag'), + model_name="motion", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="amendments", + to="motions.Motion", + ), + ), + migrations.AddField( + model_name="motion", + name="state", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="motions.State", + ), + ), + migrations.AddField( + model_name="motion", + name="submitters", + field=models.ManyToManyField( + blank=True, + related_name="motion_submitters", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="motion", + name="supporters", + field=models.ManyToManyField( + blank=True, + related_name="motion_supporters", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="motion", + name="tags", + field=models.ManyToManyField(blank=True, to="core.Tag"), ), migrations.AlterUniqueTogether( - name='motionversion', - unique_together=set([('motion', 'version_number')]), + name="motionversion", unique_together=set([("motion", "version_number")]) ), ] diff --git a/openslides/motions/migrations/0002_misc_features.py b/openslides/motions/migrations/0002_misc_features.py index f8c077ea2..7e0919eb1 100644 --- a/openslides/motions/migrations/0002_misc_features.py +++ b/openslides/motions/migrations/0002_misc_features.py @@ -16,16 +16,16 @@ def change_label_of_state(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - State = apps.get_model('motions', 'State') + State = apps.get_model("motions", "State") try: - state = State.objects.get(name='commited a bill') + state = State.objects.get(name="commited a bill") except State.DoesNotExist: # State does not exists, there is nothing to change. pass else: - state.name = 'refered to committee' - state.action_word = 'Refer to committee' + state.name = "refered to committee" + state.action_word = "Refer to committee" state.save(skip_autoupdate=True) @@ -35,17 +35,17 @@ def add_recommendation_labels(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - State = apps.get_model('motions', 'State') + State = apps.get_model("motions", "State") name_label_map = { - 'accepted': 'Acceptance', - 'rejected': 'Rejection', - 'not decided': 'No decision', - 'permitted': 'Permission', - 'adjourned': 'Adjournment', - 'not concerned': 'No concernment', - 'refered to committee': 'Referral to committee', - 'rejected (not authorized)': 'Rejection (not authorized)', + "accepted": "Acceptance", + "rejected": "Rejection", + "not decided": "No decision", + "permitted": "Permission", + "adjourned": "Adjournment", + "not concerned": "No concernment", + "refered to committee": "Referral to committee", + "rejected (not authorized)": "Rejection (not authorized)", } for state in State.objects.all(): if name_label_map.get(state.name): @@ -57,101 +57,135 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('motions', '0001_initial'), + ("motions", "0001_initial"), ] operations = [ migrations.CreateModel( - name='MotionBlock', + name="MotionBlock", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.CreateModel( - name='MotionChangeRecommendation', + name="MotionChangeRecommendation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('rejected', models.BooleanField(default=False)), - ('type', models.PositiveIntegerField(default=0)), - ('line_from', models.PositiveIntegerField()), - ('line_to', models.PositiveIntegerField()), - ('text', models.TextField(blank=True)), - ('creation_time', models.DateTimeField(auto_now=True)), - ('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ('motion_version', models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, related_name='change_recommendations', to='motions.MotionVersion')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("rejected", models.BooleanField(default=False)), + ("type", models.PositiveIntegerField(default=0)), + ("line_from", models.PositiveIntegerField()), + ("line_to", models.PositiveIntegerField()), + ("text", models.TextField(blank=True)), + ("creation_time", models.DateTimeField(auto_now=True)), + ( + "author", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "motion_version", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="change_recommendations", + to="motions.MotionVersion", + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.AlterModelOptions( - name='motion', + name="motion", options={ - 'default_permissions': (), - 'ordering': ( - 'identifier', + "default_permissions": (), + "ordering": ("identifier",), + "permissions": ( + ("can_see", "Can see motions"), + ("can_create", "Can create motions"), + ("can_support", "Can support motions"), + ("can_see_and_manage_comments", "Can see and manage comments"), + ("can_manage", "Can manage motions"), ), - 'permissions': ( - ('can_see', 'Can see motions'), - ('can_create', 'Can create motions'), - ('can_support', 'Can support motions'), - ('can_see_and_manage_comments', 'Can see and manage comments'), - ('can_manage', 'Can manage motions') - ), - 'verbose_name': 'Motion', + "verbose_name": "Motion", }, ), migrations.AddField( - model_name='motion', - name='comments', + model_name="motion", + name="comments", field=jsonfield.fields.JSONField(null=True), ), migrations.AddField( - model_name='motion', - name='origin', + model_name="motion", + name="origin", field=models.CharField(blank=True, max_length=255), ), migrations.AddField( - model_name='motion', - name='recommendation', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'), + model_name="motion", + name="recommendation", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="motions.State", + ), ), migrations.AddField( - model_name='state', - name='recommendation_label', + model_name="state", + name="recommendation_label", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='state', - name='show_recommendation_extension_field', + model_name="state", + name="show_recommendation_extension_field", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='state', - name='show_state_extension_field', + model_name="state", + name="show_state_extension_field", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='motion', - name='state', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'), + model_name="motion", + name="state", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="motions.State", + ), ), migrations.AddField( - model_name='motion', - name='motion_block', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.MotionBlock'), - ), - migrations.RunPython( - change_label_of_state - ), - migrations.RunPython( - add_recommendation_labels + model_name="motion", + name="motion_block", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="motions.MotionBlock", + ), ), + migrations.RunPython(change_label_of_state), + migrations.RunPython(add_recommendation_labels), ] diff --git a/openslides/motions/migrations/0003_motion_comments.py b/openslides/motions/migrations/0003_motion_comments.py index 2622eb8be..f9330cbbc 100644 --- a/openslides/motions/migrations/0003_motion_comments.py +++ b/openslides/motions/migrations/0003_motion_comments.py @@ -12,24 +12,24 @@ def change_motions_comments(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - ConfigStore = apps.get_model('core', 'ConfigStore') - Motion = apps.get_model('motions', 'Motion') + ConfigStore = apps.get_model("core", "ConfigStore") + Motion = apps.get_model("motions", "Motion") try: - config_comments_fields = ConfigStore.objects.get(key='motions_comments').value + config_comments_fields = ConfigStore.objects.get(key="motions_comments").value except ConfigStore.DoesNotExist: config_comments_fields = [] # The old default: An empty list. comments_fields = {} for index, field in enumerate(config_comments_fields): - comments_fields[index+1] = field + comments_fields[index + 1] = field - max_index = len(config_comments_fields)-1 + max_index = len(config_comments_fields) - 1 try: - db_value = ConfigStore.objects.get(key='motions_comments') + db_value = ConfigStore.objects.get(key="motions_comments") except ConfigStore.DoesNotExist: - db_value = ConfigStore(key='motions_comments') + db_value = ConfigStore(key="motions_comments") db_value.value = comments_fields # We cannot provide skip_autoupdate=True here, becuase this object is a fake object. It does *not* # inherit from the RESTModelMixin, so the save() methos from base_model.py (django's default) @@ -42,19 +42,13 @@ def change_motions_comments(apps, schema_editor): for index, comment in enumerate(motion.comments or []): if index > max_index: break - comments[index+1] = comment + comments[index + 1] = comment motion.comments = comments motion.save(skip_autoupdate=True) class Migration(migrations.Migration): - dependencies = [ - ('motions', '0002_misc_features'), - ] + dependencies = [("motions", "0002_misc_features")] - operations = [ - migrations.RunPython( - change_motions_comments - ), - ] + operations = [migrations.RunPython(change_motions_comments)] diff --git a/openslides/motions/migrations/0004_motionchangerecommendation_other_description.py b/openslides/motions/migrations/0004_motionchangerecommendation_other_description.py index 5fc3061ef..07ef69a66 100644 --- a/openslides/motions/migrations/0004_motionchangerecommendation_other_description.py +++ b/openslides/motions/migrations/0004_motionchangerecommendation_other_description.py @@ -7,14 +7,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('motions', '0003_motion_comments'), - ] + dependencies = [("motions", "0003_motion_comments")] operations = [ migrations.AddField( - model_name='motionchangerecommendation', - name='other_description', + model_name="motionchangerecommendation", + name="other_description", field=models.TextField(blank=True), - ), + ) ] diff --git a/openslides/motions/migrations/0005_auto_20180202_1318.py b/openslides/motions/migrations/0005_auto_20180202_1318.py index ae303b119..162494408 100644 --- a/openslides/motions/migrations/0005_auto_20180202_1318.py +++ b/openslides/motions/migrations/0005_auto_20180202_1318.py @@ -11,7 +11,7 @@ def delete_old_comment_permission(apps, schema_editor): Deletes the old 'can_see_and_manage_comments' permission which is split up into two seperate permissions. """ - perm = Permission.objects.filter(codename='can_see_and_manage_comments') + perm = Permission.objects.filter(codename="can_see_and_manage_comments") if len(perm): perm = perm.get() @@ -26,13 +26,15 @@ def delete_old_comment_permission(apps, schema_editor): # Create new permission perm_see = Permission.objects.create( - codename='can_see_comments', - name='Can see comments', - content_type=content_type) + codename="can_see_comments", + name="Can see comments", + content_type=content_type, + ) perm_manage = Permission.objects.create( - codename='can_manage_comments', - name='Can manage comments', - content_type=content_type) + codename="can_manage_comments", + name="Can manage comments", + content_type=content_type, + ) for group in groups: group.permissions.add(perm_see) @@ -42,28 +44,24 @@ def delete_old_comment_permission(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('motions', '0004_motionchangerecommendation_other_description'), - ] + dependencies = [("motions", "0004_motionchangerecommendation_other_description")] operations = [ migrations.AlterModelOptions( - name='motion', + name="motion", options={ - 'default_permissions': (), - 'ordering': ('identifier',), - 'permissions': ( - ('can_see', 'Can see motions'), - ('can_create', 'Can create motions'), - ('can_support', 'Can support motions'), - ('can_see_comments', 'Can see comments'), - ('can_manage_comments', 'Can manage comments'), - ('can_manage', 'Can manage motions') + "default_permissions": (), + "ordering": ("identifier",), + "permissions": ( + ("can_see", "Can see motions"), + ("can_create", "Can create motions"), + ("can_support", "Can support motions"), + ("can_see_comments", "Can see comments"), + ("can_manage_comments", "Can manage comments"), + ("can_manage", "Can manage motions"), ), - 'verbose_name': 'Motion' + "verbose_name": "Motion", }, ), - migrations.RunPython( - delete_old_comment_permission - ), + migrations.RunPython(delete_old_comment_permission), ] diff --git a/openslides/motions/migrations/0006_submitter_model.py b/openslides/motions/migrations/0006_submitter_model.py index dc890b5d2..813673916 100644 --- a/openslides/motions/migrations/0006_submitter_model.py +++ b/openslides/motions/migrations/0006_submitter_model.py @@ -11,8 +11,8 @@ import openslides.utils.models def move_submitters_to_own_model(apps, schema_editor): - Motion = apps.get_model('motions', 'Motion') - Submitter = apps.get_model('motions', 'Submitter') + Motion = apps.get_model("motions", "Motion") + Submitter = apps.get_model("motions", "Submitter") for motion in Motion.objects.all(): weight = 0 @@ -32,41 +32,46 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('motions', '0005_auto_20180202_1318'), + ("motions", "0005_auto_20180202_1318"), ] operations = [ migrations.RenameField( - model_name='motion', - old_name='submitters', - new_name='submittersOld', + model_name="motion", old_name="submitters", new_name="submittersOld" ), migrations.CreateModel( - name='Submitter', + name="Submitter", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('weight', models.IntegerField(null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("weight", models.IntegerField(null=True)), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.AddField( - model_name='submitter', - name='motion', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitters', to='motions.Motion'), + model_name="submitter", + name="motion", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submitters", + to="motions.Motion", + ), ), migrations.AddField( - model_name='submitter', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.RunPython( - move_submitters_to_own_model - ), - migrations.RemoveField( - model_name='motion', - name='submittersOld', + model_name="submitter", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), + migrations.RunPython(move_submitters_to_own_model), + migrations.RemoveField(model_name="motion", name="submittersOld"), ] diff --git a/openslides/motions/migrations/0007_motionversion_amendment_data.py b/openslides/motions/migrations/0007_motionversion_amendment_data.py index d552d89f7..319dc7099 100644 --- a/openslides/motions/migrations/0007_motionversion_amendment_data.py +++ b/openslides/motions/migrations/0007_motionversion_amendment_data.py @@ -8,14 +8,12 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('motions', '0006_submitter_model'), - ] + dependencies = [("motions", "0006_submitter_model")] operations = [ migrations.AddField( - model_name='motionversion', - name='amendment_paragraphs', + model_name="motionversion", + name="amendment_paragraphs", field=jsonfield.fields.JSONField(null=True), - ), + ) ] diff --git a/openslides/motions/migrations/0008_auto_20180702_1128.py b/openslides/motions/migrations/0008_auto_20180702_1128.py index 3d2f2f059..ca6b61c3e 100644 --- a/openslides/motions/migrations/0008_auto_20180702_1128.py +++ b/openslides/motions/migrations/0008_auto_20180702_1128.py @@ -8,38 +8,38 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('motions', '0007_motionversion_amendment_data'), - ] + dependencies = [("motions", "0007_motionversion_amendment_data")] operations = [ migrations.AlterField( - model_name='workflow', - name='first_state', + model_name="workflow", + name="first_state", field=models.OneToOneField( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='+', - to='motions.State'), + related_name="+", + to="motions.State", + ), ), migrations.AlterField( - model_name='motion', - name='state', + model_name="motion", + name="state", field=models.ForeignKey( null=True, on_delete=django.db.models.deletion.PROTECT, - related_name='+', - to='motions.State'), + related_name="+", + to="motions.State", + ), ), migrations.AlterField( - model_name='state', - name='next_states', - field=models.ManyToManyField(blank=True, to='motions.State'), + model_name="state", + name="next_states", + field=models.ManyToManyField(blank=True, to="motions.State"), ), migrations.AlterField( - model_name='state', - name='action_word', + model_name="state", + name="action_word", field=models.CharField(blank=True, max_length=255), ), ] diff --git a/openslides/motions/migrations/0009_motionversion_modified_final_version.py b/openslides/motions/migrations/0009_motionversion_modified_final_version.py index 5aaca261c..f474f5b8d 100644 --- a/openslides/motions/migrations/0009_motionversion_modified_final_version.py +++ b/openslides/motions/migrations/0009_motionversion_modified_final_version.py @@ -7,14 +7,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('motions', '0008_auto_20180702_1128'), - ] + dependencies = [("motions", "0008_auto_20180702_1128")] operations = [ migrations.AddField( - model_name='motionversion', - name='modified_final_version', + model_name="motionversion", + name="modified_final_version", field=models.TextField(blank=True, null=True), - ), + ) ] diff --git a/openslides/motions/migrations/0010_auto_20180822_1042.py b/openslides/motions/migrations/0010_auto_20180822_1042.py index fbbc7f1cd..cd428fe1e 100644 --- a/openslides/motions/migrations/0010_auto_20180822_1042.py +++ b/openslides/motions/migrations/0010_auto_20180822_1042.py @@ -8,49 +8,51 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('motions', '0009_motionversion_modified_final_version'), - ] + dependencies = [("motions", "0009_motionversion_modified_final_version")] operations = [ migrations.AlterField( - model_name='motionpoll', - name='votescast', + model_name="motionpoll", + name="votescast", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='motionpoll', - name='votesinvalid', + model_name="motionpoll", + name="votesinvalid", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='motionpoll', - name='votesvalid', + model_name="motionpoll", + name="votesvalid", field=models.DecimalField( blank=True, decimal_places=6, max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), migrations.AlterField( - model_name='motionvote', - name='weight', + model_name="motionvote", + name="weight", field=models.DecimalField( decimal_places=6, - default=Decimal('1'), + default=Decimal("1"), max_digits=15, null=True, - validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), ), ] diff --git a/openslides/motions/migrations/0011_motion_version.py b/openslides/motions/migrations/0011_motion_version.py index 01758ffb0..f07fd9404 100644 --- a/openslides/motions/migrations/0011_motion_version.py +++ b/openslides/motions/migrations/0011_motion_version.py @@ -10,7 +10,7 @@ def copy_motion_version_content_to_motion(apps, schema_editor): """ Move all motion version content of the active version to the motion. """ - Motion = apps.get_model('motions', 'Motion') + Motion = apps.get_model("motions", "Motion") for motion in Motion.objects.all(): motion.title = motion.active_version.title @@ -26,7 +26,7 @@ def migrate_active_change_recommendations(apps, schema_editor): Delete all change recommendation of motion versions, that are not active. For active change recommendations the motion id will be set. """ - MotionChangeRecommendation = apps.get_model('motions', 'MotionChangeRecommendation') + MotionChangeRecommendation = apps.get_model("motions", "MotionChangeRecommendation") to_delete = [] for cr in MotionChangeRecommendation.objects.all(): # chack if version id matches the active version of the motion @@ -43,89 +43,65 @@ def migrate_active_change_recommendations(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('motions', '0010_auto_20180822_1042'), - ] + dependencies = [("motions", "0010_auto_20180822_1042")] operations = [ # Create new fields. Title and Text have empty defaults, but the values # should be overwritten by copy_motion_version_content_to_motion. In the next # migration file these defaults are removed. migrations.AddField( - model_name='motion', - name='title', - field=models.CharField(max_length=255, default=''), + model_name="motion", + name="title", + field=models.CharField(max_length=255, default=""), ), migrations.AddField( - model_name='motion', - name='text', - field=models.TextField(default=''), + model_name="motion", name="text", field=models.TextField(default="") ), migrations.AddField( - model_name='motion', - name='reason', + model_name="motion", + name="reason", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='motion', - name='modified_final_version', + model_name="motion", + name="modified_final_version", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='motion', - name='amendment_paragraphs', + model_name="motion", + name="amendment_paragraphs", field=jsonfield.fields.JSONField( dump_kwargs={ - 'cls': jsonfield.encoder.JSONEncoder, - 'separators': (',', ':') + "cls": jsonfield.encoder.JSONEncoder, + "separators": (",", ":"), }, load_kwargs={}, - null=True), + null=True, + ), ), - # Copy old motion version data migrations.RunPython(copy_motion_version_content_to_motion), - # Change recommendations migrations.AddField( - model_name='motionchangerecommendation', - name='motion', + model_name="motionchangerecommendation", + name="motion", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, null=True, # This is reverted in the next migration - related_name='change_recommendations', - to='motions.Motion'), + related_name="change_recommendations", + to="motions.Motion", + ), ), migrations.RunPython(migrate_active_change_recommendations), migrations.RemoveField( - model_name='motionchangerecommendation', - name='motion_version', + model_name="motionchangerecommendation", name="motion_version" ), - # remove motion version references from motion and state. - migrations.RemoveField( - model_name='motion', - name='active_version', - ), - migrations.AlterUniqueTogether( - name='motionversion', - unique_together=set(), - ), - migrations.RemoveField( - model_name='motionversion', - name='motion', - ), - migrations.RemoveField( - model_name='state', - name='leave_old_version_active', - ), - migrations.RemoveField( - model_name='state', - name='versioning', - ), - + migrations.RemoveField(model_name="motion", name="active_version"), + migrations.AlterUniqueTogether(name="motionversion", unique_together=set()), + migrations.RemoveField(model_name="motionversion", name="motion"), + migrations.RemoveField(model_name="state", name="leave_old_version_active"), + migrations.RemoveField(model_name="state", name="versioning"), # Delete motion version. - migrations.DeleteModel( - name='MotionVersion', - ), + migrations.DeleteModel(name="MotionVersion"), ] diff --git a/openslides/motions/migrations/0012_motion_comments.py b/openslides/motions/migrations/0012_motion_comments.py index 3e1c5a5fc..761f3c8e4 100644 --- a/openslides/motions/migrations/0012_motion_comments.py +++ b/openslides/motions/migrations/0012_motion_comments.py @@ -8,16 +8,18 @@ from django.db import migrations, models import openslides -def create_comment_sections_from_config_and_move_comments_to_own_model(apps, schema_editor): - ConfigStore = apps.get_model('core', 'ConfigStore') - Motion = apps.get_model('motions', 'Motion') - MotionComment = apps.get_model('motions', 'MotionComment') - MotionCommentSection = apps.get_model('motions', 'MotionCommentSection') +def create_comment_sections_from_config_and_move_comments_to_own_model( + apps, schema_editor +): + ConfigStore = apps.get_model("core", "ConfigStore") + Motion = apps.get_model("motions", "Motion") + MotionComment = apps.get_model("motions", "MotionComment") + MotionCommentSection = apps.get_model("motions", "MotionCommentSection") Group = apps.get_model(settings.AUTH_GROUP_MODEL) # try to get old motions_comments config variable, where all comment fields are saved try: - motions_comments = ConfigStore.objects.get(key='motions_comments') + motions_comments = ConfigStore.objects.get(key="motions_comments") except ConfigStore.DoesNotExist: return comments_sections = motions_comments.value @@ -26,14 +28,14 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch motions_comments.delete() # Get can_see_comments and can_manage_comments permissions and the associated groups - can_see_comments = Permission.objects.filter(codename='can_see_comments') + can_see_comments = Permission.objects.filter(codename="can_see_comments") if len(can_see_comments) == 1: # Save groups. list() is necessary to evaluate the database query right now. can_see_groups = list(can_see_comments.get().group_set.all()) else: can_see_groups = Group.objects.all() - can_manage_comments = Permission.objects.filter(codename='can_manage_comments') + can_manage_comments = Permission.objects.filter(codename="can_manage_comments") if len(can_manage_comments) == 1: # Save groups. list() is necessary to evaluate the database query right now. can_manage_groups = list(can_manage_comments.get().group_set.all()) @@ -50,12 +52,12 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch for id, section in comments_sections.items(): if section is None: continue - if section.get('forState', False): + if section.get("forState", False): forStateId = id - elif section.get('forRecommendation', False): + elif section.get("forRecommendation", False): forRecommendationId = id else: - comment_section = MotionCommentSection(name=section['name']) + comment_section = MotionCommentSection(name=section["name"]) comment_section.save(skip_autoupdate=True) comment_section.read_groups.add(*[group.id for group in can_see_groups]) comment_section.write_groups.add(*[group.id for group in can_manage_groups]) @@ -70,7 +72,7 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch for section_id, comment_value in motion.comments.items(): # Skip empty sections. comment_value = comment_value.strip() - if comment_value == '': + if comment_value == "": continue # Special comments will be moved to separate fields. if section_id == forStateId: @@ -83,136 +85,147 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch comment = MotionComment( comment=comment_value, motion=motion, - section=old_id_mapping[section_id]) + section=old_id_mapping[section_id], + ) comments.append(comment) MotionComment.objects.bulk_create(comments) class Migration(migrations.Migration): - dependencies = [ - ('users', '0006_user_email'), - ('motions', '0011_motion_version'), - ] + dependencies = [("users", "0006_user_email"), ("motions", "0011_motion_version")] operations = [ # Cleanup from last migration. Somehow cannot be done there. migrations.AlterField( # remove default='' - model_name='motion', - name='text', - field=models.TextField(), + model_name="motion", name="text", field=models.TextField() ), migrations.AlterField( # remove default='' - model_name='motion', - name='title', - field=models.CharField(max_length=255), + model_name="motion", name="title", field=models.CharField(max_length=255) ), migrations.AlterField( # remove null=True - model_name='motionchangerecommendation', - name='motion', + model_name="motionchangerecommendation", + name="motion", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name='change_recommendations', - to='motions.Motion'), + related_name="change_recommendations", + to="motions.Motion", + ), ), - # Add extension fields for former "special comments". No hack anymore.. migrations.AddField( - model_name='motion', - name='recommendation_extension', + model_name="motion", + name="recommendation_extension", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='motion', - name='state_extension', + model_name="motion", + name="state_extension", field=models.TextField(blank=True, null=True), ), - migrations.AlterModelOptions( - name='motion', + name="motion", options={ - 'default_permissions': (), - 'ordering': ('identifier',), - 'permissions': ( - ('can_see', 'Can see motions'), - ('can_create', 'Can create motions'), - ('can_support', 'Can support motions'), - ('can_manage', 'Can manage motions')), - 'verbose_name': 'Motion'}, + "default_permissions": (), + "ordering": ("identifier",), + "permissions": ( + ("can_see", "Can see motions"), + ("can_create", "Can create motions"), + ("can_support", "Can support motions"), + ("can_manage", "Can manage motions"), + ), + "verbose_name": "Motion", + }, ), # Comments and CommentsSection models migrations.CreateModel( - name='MotionComment', + name="MotionComment", fields=[ - ('id', models.AutoField( + ( + "id", + models.AutoField( auto_created=True, primary_key=True, serialize=False, - verbose_name='ID')), - ('comment', models.TextField()), + verbose_name="ID", + ), + ), + ("comment", models.TextField()), ], - options={ - 'default_permissions': (), - }, - bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore + options={"default_permissions": ()}, + bases=( + openslides.utils.models.RESTModelMixin, # type: ignore + models.Model, + ), ), migrations.CreateModel( - name='MotionCommentSection', + name="MotionCommentSection", fields=[ - ('id', models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('read_groups', models.ManyToManyField( - blank=True, - related_name='read_comments', - to=settings.AUTH_GROUP_MODEL)), - ('write_groups', models.ManyToManyField( - blank=True, - related_name='write_comments', - to=settings.AUTH_GROUP_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "read_groups", + models.ManyToManyField( + blank=True, + related_name="read_comments", + to=settings.AUTH_GROUP_MODEL, + ), + ), + ( + "write_groups", + models.ManyToManyField( + blank=True, + related_name="write_comments", + to=settings.AUTH_GROUP_MODEL, + ), + ), ], - options={ - 'default_permissions': (), - }, - bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore + options={"default_permissions": ()}, + bases=( + openslides.utils.models.RESTModelMixin, # type: ignore + models.Model, + ), ), migrations.AddField( - model_name='motioncomment', - name='section', + model_name="motioncomment", + name="section", field=models.ForeignKey( on_delete=django.db.models.deletion.PROTECT, - related_name='comments', - to='motions.MotionCommentSection'), + related_name="comments", + to="motions.MotionCommentSection", + ), ), migrations.AddField( - model_name='motioncomment', - name='motion', + model_name="motioncomment", + name="motion", field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to='motions.Motion'), + on_delete=django.db.models.deletion.CASCADE, to="motions.Motion" + ), ), migrations.AlterUniqueTogether( - name='motioncomment', - unique_together={('motion', 'section')}, + name="motioncomment", unique_together={("motion", "section")} ), - # Move the comments and sections - migrations.RunPython(create_comment_sections_from_config_and_move_comments_to_own_model), - - # Remove old comment field from motion, use the new model instead - migrations.RemoveField( - model_name='motion', - name='comments', + migrations.RunPython( + create_comment_sections_from_config_and_move_comments_to_own_model ), + # Remove old comment field from motion, use the new model instead + migrations.RemoveField(model_name="motion", name="comments"), migrations.AlterField( - model_name='motioncomment', - name='motion', + model_name="motioncomment", + name="motion", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name='comments', - to='motions.Motion'), + related_name="comments", + to="motions.Motion", + ), ), ] diff --git a/openslides/motions/migrations/0013_motion_sorting_and_statute.py b/openslides/motions/migrations/0013_motion_sorting_and_statute.py index 92f4ac7af..d3d0fc212 100644 --- a/openslides/motions/migrations/0013_motion_sorting_and_statute.py +++ b/openslides/motions/migrations/0013_motion_sorting_and_statute.py @@ -8,58 +8,55 @@ import openslides.utils.models class Migration(migrations.Migration): - dependencies = [ - ('motions', '0012_motion_comments'), - ] + dependencies = [("motions", "0012_motion_comments")] operations = [ migrations.AlterModelOptions( - name='motionblock', - options={ - 'default_permissions': (), - 'verbose_name': 'Motion block'}, + name="motionblock", + options={"default_permissions": (), "verbose_name": "Motion block"}, ), migrations.AddField( - model_name='motion', - name='sort_parent', + model_name="motion", + name="sort_parent", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='children', - to='motions.Motion'), + related_name="children", + to="motions.Motion", + ), ), migrations.AddField( - model_name='motion', - name='weight', - field=models.IntegerField(default=10000), + model_name="motion", name="weight", field=models.IntegerField(default=10000) ), migrations.CreateModel( - name='StatuteParagraph', + name="StatuteParagraph", fields=[ - ('id', models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID')), - ('title', models.CharField(max_length=255)), - ('text', models.TextField()), - ('weight', models.IntegerField(default=10000)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("text", models.TextField()), + ("weight", models.IntegerField(default=10000)), ], - options={ - 'ordering': ['weight', 'title'], - 'default_permissions': (), - }, + options={"ordering": ["weight", "title"], "default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), migrations.AddField( - model_name='motion', - name='statute_paragraph', + model_name="motion", + name="statute_paragraph", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='motions', - to='motions.StatuteParagraph'), + related_name="motions", + to="motions.StatuteParagraph", + ), ), ] diff --git a/openslides/motions/migrations/0014_motionchangerecommendation_internal.py b/openslides/motions/migrations/0014_motionchangerecommendation_internal.py index 8e1abd4cb..1fb706270 100644 --- a/openslides/motions/migrations/0014_motionchangerecommendation_internal.py +++ b/openslides/motions/migrations/0014_motionchangerecommendation_internal.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('motions', '0013_motion_sorting_and_statute'), - ] + dependencies = [("motions", "0013_motion_sorting_and_statute")] operations = [ migrations.AddField( - model_name='motionchangerecommendation', - name='internal', + model_name="motionchangerecommendation", + name="internal", field=models.BooleanField(default=False), - ), + ) ] diff --git a/openslides/motions/migrations/0015_metadata_permission.py b/openslides/motions/migrations/0015_metadata_permission.py index fc583971f..43ae8642d 100644 --- a/openslides/motions/migrations/0015_metadata_permission.py +++ b/openslides/motions/migrations/0015_metadata_permission.py @@ -5,24 +5,22 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('motions', '0014_motionchangerecommendation_internal'), - ] + dependencies = [("motions", "0014_motionchangerecommendation_internal")] operations = [ migrations.AlterModelOptions( - name='motion', + name="motion", options={ - 'default_permissions': (), - 'ordering': ('identifier',), - 'permissions': ( - ('can_see', 'Can see motions'), - ('can_create', 'Can create motions'), - ('can_support', 'Can support motions'), - ('can_manage_metadata', 'Can manage motion metadata'), - ('can_manage', 'Can manage motions') + "default_permissions": (), + "ordering": ("identifier",), + "permissions": ( + ("can_see", "Can see motions"), + ("can_create", "Can create motions"), + ("can_support", "Can support motions"), + ("can_manage_metadata", "Can manage motion metadata"), + ("can_manage", "Can manage motions"), ), - 'verbose_name': 'Motion' + "verbose_name": "Motion", }, - ), + ) ] diff --git a/openslides/motions/migrations/0016_merge_amendment_into_final.py b/openslides/motions/migrations/0016_merge_amendment_into_final.py index 670a8bced..03011f447 100644 --- a/openslides/motions/migrations/0016_merge_amendment_into_final.py +++ b/openslides/motions/migrations/0016_merge_amendment_into_final.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('motions', '0015_metadata_permission'), - ] + dependencies = [("motions", "0015_metadata_permission")] operations = [ migrations.AddField( - model_name='state', - name='merge_amendment_into_final', + model_name="state", + name="merge_amendment_into_final", field=models.SmallIntegerField(default=0), - ), + ) ] diff --git a/openslides/motions/migrations/0017_remove_state_action_word.py b/openslides/motions/migrations/0017_remove_state_action_word.py index e0265cd2c..213fc0749 100644 --- a/openslides/motions/migrations/0017_remove_state_action_word.py +++ b/openslides/motions/migrations/0017_remove_state_action_word.py @@ -5,13 +5,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('motions', '0016_merge_amendment_into_final'), - ] + dependencies = [("motions", "0016_merge_amendment_into_final")] - operations = [ - migrations.RemoveField( - model_name='state', - name='action_word', - ), - ] + operations = [migrations.RemoveField(model_name="state", name="action_word")] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 73b1a16c1..3071ae61e 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -40,6 +40,7 @@ class StatuteParagraph(RESTModelMixin, models.Model): """ Model for parts of the statute """ + access_permissions = StatuteParagraphAccessPermissions() title = models.CharField(max_length=255) @@ -55,7 +56,7 @@ class StatuteParagraph(RESTModelMixin, models.Model): class Meta: default_permissions = () - ordering = ['weight', 'title'] + ordering = ["weight", "title"] def __str__(self): return self.title @@ -65,25 +66,29 @@ class MotionManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all motions. In the background we join and prefetch all related models. """ - return (self.get_queryset() - .select_related('state') - .prefetch_related( - 'state__workflow', - 'comments', - 'comments__section', - 'comments__section__read_groups', - 'agenda_items', - 'log_messages', - 'polls', - 'attachments', - 'tags', - 'submitters', - 'supporters')) + return ( + self.get_queryset() + .select_related("state") + .prefetch_related( + "state__workflow", + "comments", + "comments__section", + "comments__section__read_groups", + "agenda_items", + "log_messages", + "polls", + "attachments", + "tags", + "submitters", + "supporters", + ) + ) class Motion(RESTModelMixin, models.Model): @@ -92,8 +97,9 @@ class Motion(RESTModelMixin, models.Model): This class is the main entry point to all other classes related to a motion. """ + access_permissions = MotionAccessPermissions() - can_see_permission = 'motions.can_see' + can_see_permission = "motions.can_see" objects = MotionManager() @@ -119,10 +125,11 @@ class Motion(RESTModelMixin, models.Model): """The reason for a motion.""" state = models.ForeignKey( - 'State', - related_name='+', + "State", + related_name="+", on_delete=models.PROTECT, # Do not let the user delete states, that are used for motions - null=True) # TODO: Check whether null=True is necessary. + null=True, + ) # TODO: Check whether null=True is necessary. """ The related state object. @@ -135,10 +142,8 @@ class Motion(RESTModelMixin, models.Model): """ recommendation = models.ForeignKey( - 'State', - related_name='+', - on_delete=models.SET_NULL, - null=True) + "State", related_name="+", on_delete=models.SET_NULL, null=True + ) """ The recommendation of a person or committee for this motion. """ @@ -148,8 +153,7 @@ class Motion(RESTModelMixin, models.Model): A text field fo additional information about the recommendation. """ - identifier = models.CharField(max_length=255, null=True, blank=True, - unique=True) + identifier = models.CharField(max_length=255, null=True, blank=True, unique=True) """ A string as human readable identifier for the motion. """ @@ -167,29 +171,26 @@ class Motion(RESTModelMixin, models.Model): """ sort_parent = models.ForeignKey( - 'self', + "self", on_delete=models.SET_NULL, null=True, blank=True, - related_name='children') + related_name="children", + ) """ A parent field for multi-depth sorting of motions. """ category = models.ForeignKey( - 'Category', - on_delete=models.SET_NULL, - null=True, - blank=True) + "Category", on_delete=models.SET_NULL, null=True, blank=True + ) """ ForeignKey to one category of motions. """ motion_block = models.ForeignKey( - 'MotionBlock', - on_delete=models.SET_NULL, - null=True, - blank=True) + "MotionBlock", on_delete=models.SET_NULL, null=True, blank=True + ) """ ForeignKey to one block of motions. """ @@ -206,11 +207,12 @@ class Motion(RESTModelMixin, models.Model): """ parent = models.ForeignKey( - 'self', + "self", on_delete=models.SET_NULL, null=True, blank=True, - related_name='amendments') + related_name="amendments", + ) """ Field for amendments to reference to the motion that should be altered. @@ -222,7 +224,8 @@ class Motion(RESTModelMixin, models.Model): on_delete=models.SET_NULL, null=True, blank=True, - related_name='motions') + related_name="motions", + ) """ Field to reference to a statute paragraph if this motion is a statute-amendment. @@ -235,26 +238,28 @@ class Motion(RESTModelMixin, models.Model): Tags to categorise motions. """ - supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters', blank=True) + supporters = models.ManyToManyField( + settings.AUTH_USER_MODEL, related_name="motion_supporters", blank=True + ) """ Users who support this motion. """ # In theory there could be one then more agenda_item. But we support only # one. See the property agenda_item. - agenda_items = GenericRelation(Item, related_name='motions') + agenda_items = GenericRelation(Item, related_name="motions") class Meta: default_permissions = () permissions = ( - ('can_see', 'Can see motions'), - ('can_create', 'Can create motions'), - ('can_support', 'Can support motions'), - ('can_manage_metadata', 'Can manage motion metadata'), - ('can_manage', 'Can manage motions'), + ("can_see", "Can see motions"), + ("can_create", "Can create motions"), + ("can_support", "Can support motions"), + ("can_manage_metadata", "Can manage motion metadata"), + ("can_manage", "Can manage motions"), ) - ordering = ('identifier', ) - verbose_name = ugettext_noop('Motion') + ordering = ("identifier",) + verbose_name = ugettext_noop("Motion") def __str__(self): """ @@ -284,14 +289,15 @@ class Motion(RESTModelMixin, models.Model): try: # Always skip autoupdate. Maybe we run it later in this method. with transaction.atomic(): - super(Motion, self).save(skip_autoupdate=True, *args, **kwargs) # type: ignore + super(Motion, self).save( # type: ignore + skip_autoupdate=True, *args, **kwargs + ) except IntegrityError: # Identifier is already used. - if hasattr(self, '_identifier_prefix'): + if hasattr(self, "_identifier_prefix"): # Calculate a new one and try again. self.identifier_number, self.identifier = self.increment_identifier_number( - self.identifier_number, - self._identifier_prefix, + self.identifier_number, self._identifier_prefix ) else: # Do not calculate a new one but reraise the IntegrityError. @@ -310,10 +316,11 @@ class Motion(RESTModelMixin, models.Model): motion projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='motions/motion', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="motions/motion", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) def set_identifier(self): """ @@ -321,27 +328,36 @@ class Motion(RESTModelMixin, models.Model): it is not set yet. """ # The identifier is already set or should be set manually. - if config['motions_identifier'] == 'manually' or self.identifier: + if config["motions_identifier"] == "manually" or self.identifier: # Do not set an identifier. return # If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier. - without_blank = hasattr(settings, 'MOTION_IDENTIFIER_WITHOUT_BLANKS') and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS + without_blank = ( + hasattr(settings, "MOTION_IDENTIFIER_WITHOUT_BLANKS") + and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS + ) # Build prefix. if self.is_amendment(): - parent_identifier = self.parent.identifier or '' + parent_identifier = self.parent.identifier or "" if without_blank: - prefix = '%s%s' % (parent_identifier, config['motions_amendments_prefix']) + prefix = "%s%s" % ( + parent_identifier, + config["motions_amendments_prefix"], + ) else: - prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix']) + prefix = "%s %s " % ( + parent_identifier, + config["motions_amendments_prefix"], + ) elif self.category is None or not self.category.prefix: - prefix = '' + prefix = "" else: if without_blank: - prefix = '%s' % self.category.prefix + prefix = "%s" % self.category.prefix else: - prefix = '%s ' % self.category.prefix + prefix = "%s " % self.category.prefix self._identifier_prefix = prefix # Use the already assigned identifier_number, if the motion has one. @@ -354,20 +370,22 @@ class Motion(RESTModelMixin, models.Model): if self.is_amendment(): motions = self.parent.amendments.all() # The motions should be counted per category. - elif config['motions_identifier'] == 'per_category': + elif config["motions_identifier"] == "per_category": motions = Motion.objects.filter(category=self.category) # The motions should be counted over all. else: motions = Motion.objects.all() - number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0 + number = ( + motions.aggregate(Max("identifier_number"))["identifier_number__max"] + or 0 + ) initial_increment = True # Calculate new identifier. number, identifier = self.increment_identifier_number( - number, - prefix, - initial_increment=initial_increment) + number, prefix, initial_increment=initial_increment + ) # Set identifier and identifier_number. self.identifier = identifier @@ -380,10 +398,10 @@ class Motion(RESTModelMixin, models.Model): """ if initial_increment: number += 1 - identifier = '%s%s' % (prefix, self.extend_identifier_number(number)) + identifier = "%s%s" % (prefix, self.extend_identifier_number(number)) while Motion.objects.filter(identifier=identifier).exists(): number += 1 - identifier = '%s%s' % (prefix, self.extend_identifier_number(number)) + identifier = "%s%s" % (prefix, self.extend_identifier_number(number)) return number, identifier def extend_identifier_number(self, number): @@ -393,10 +411,18 @@ class Motion(RESTModelMixin, models.Model): MOTION_IDENTIFIER_MIN_DIGITS. """ result = str(number) - if hasattr(settings, 'MOTION_IDENTIFIER_MIN_DIGITS') and settings.MOTION_IDENTIFIER_MIN_DIGITS: + if ( + hasattr(settings, "MOTION_IDENTIFIER_MIN_DIGITS") + and settings.MOTION_IDENTIFIER_MIN_DIGITS + ): if not isinstance(settings.MOTION_IDENTIFIER_MIN_DIGITS, int): - raise ImproperlyConfigured('Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer.') - result = '0' * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number))) + result + raise ImproperlyConfigured( + "Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer." + ) + result = ( + "0" * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number))) + + result + ) return result def is_submitter(self, user): @@ -423,7 +449,9 @@ class Motion(RESTModelMixin, models.Model): poll.set_options(skip_autoupdate=skip_autoupdate) return poll else: - raise WorkflowError('You can not create a poll in state %s.' % self.state.name) + raise WorkflowError( + "You can not create a poll in state %s." % self.state.name + ) @property def workflow_id(self): @@ -464,8 +492,10 @@ class Motion(RESTModelMixin, models.Model): elif self.state: new_state = self.state.workflow.first_state else: - new_state = (Workflow.objects.get(pk=config['motions_workflow']).first_state or - Workflow.objects.get(pk=config['motions_workflow']).states.all()[0]) + new_state = ( + Workflow.objects.get(pk=config["motions_workflow"]).first_state + or Workflow.objects.get(pk=config["motions_workflow"]).states.all()[0] + ) self.set_state(new_state) def set_recommendation(self, recommendation): @@ -499,7 +529,7 @@ class Motion(RESTModelMixin, models.Model): Note: It has to be the same return value like in JavaScript. """ if self.identifier: - title = '%s %s' % (_(self._meta.verbose_name), self.identifier) + title = "%s %s" % (_(self._meta.verbose_name), self.identifier) else: title = self.title return title @@ -512,9 +542,9 @@ class Motion(RESTModelMixin, models.Model): Note: It has to be the same return value like in JavaScript. """ if self.identifier: - title = '%s %s' % (_(self._meta.verbose_name), self.identifier) + title = "%s %s" % (_(self._meta.verbose_name), self.identifier) else: - title = '%s (%s)' % (self.title, _(self._meta.verbose_name)) + title = "%s (%s)" % (self.title, _(self._meta.verbose_name)) return title @property @@ -552,7 +582,7 @@ class Motion(RESTModelMixin, models.Model): A motion is a amendment if amendments are activated in the config and the motion has a parent. """ - return config['motions_amendments_enabled'] and self.parent is not None + return config["motions_amendments_enabled"] and self.parent is not None def is_paragraph_based_amendment(self): """ @@ -574,7 +604,12 @@ class Motion(RESTModelMixin, models.Model): """ Returns a list of all paragraph-based amendments to this motion """ - return list(filter(lambda amend: amend.is_paragraph_based_amendment(), self.amendments.all())) + return list( + filter( + lambda amend: amend.is_paragraph_based_amendment(), + self.amendments.all(), + ) + ) class MotionCommentSection(RESTModelMixin, models.Model): @@ -582,6 +617,7 @@ class MotionCommentSection(RESTModelMixin, models.Model): The model for comment sections for motions. Each comment is related to one section, so each motions has the ability to have comments from the same section. """ + access_permissions = MotionCommentSectionAccessPermissions() name = models.CharField(max_length=255) @@ -590,17 +626,15 @@ class MotionCommentSection(RESTModelMixin, models.Model): """ read_groups = models.ManyToManyField( - settings.AUTH_GROUP_MODEL, - blank=True, - related_name='read_comments') + settings.AUTH_GROUP_MODEL, blank=True, related_name="read_comments" + ) """ These groups have read-access to the section. """ write_groups = models.ManyToManyField( - settings.AUTH_GROUP_MODEL, - blank=True, - related_name='write_comments') + settings.AUTH_GROUP_MODEL, blank=True, related_name="write_comments" + ) """ These groups have write-access to the section. """ @@ -621,24 +655,22 @@ class MotionComment(RESTModelMixin, models.Model): """ motion = models.ForeignKey( - Motion, - on_delete=models.CASCADE, - related_name='comments') + Motion, on_delete=models.CASCADE, related_name="comments" + ) """ The motion where this comment belongs to. """ section = models.ForeignKey( - MotionCommentSection, - on_delete=models.PROTECT, - related_name='comments') + MotionCommentSection, on_delete=models.PROTECT, related_name="comments" + ) """ The section of the comment. """ class Meta: default_permissions = () - unique_together = ('motion', 'section') + unique_together = ("motion", "section") def get_root_rest_element(self): """ @@ -651,6 +683,7 @@ class SubmitterManager(models.Manager): """ Manager for Submitter model. Provides a customized add method. """ + def add(self, user, motion, skip_autoupdate=False): """ Customized manager method to prevent anonymous users to be a @@ -658,13 +691,13 @@ class SubmitterManager(models.Manager): for the initial sorting of the submitters. """ if self.filter(user=user, motion=motion).exists(): - raise OpenSlidesError( - _('{user} is already a submitter.').format(user=user)) + raise OpenSlidesError(_("{user} is already a submitter.").format(user=user)) if isinstance(user, AnonymousUser): - raise OpenSlidesError( - _('An anonymous user can not be a submitter.')) - weight = (self.filter(motion=motion).aggregate( - models.Max('weight'))['weight__max'] or 0) + raise OpenSlidesError(_("An anonymous user can not be a submitter.")) + weight = ( + self.filter(motion=motion).aggregate(models.Max("weight"))["weight__max"] + or 0 + ) submitter = self.model(user=user, motion=motion, weight=weight + 1) submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate) return submitter @@ -680,17 +713,14 @@ class Submitter(RESTModelMixin, models.Model): Use custom Manager. """ - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) """ ForeignKey to the user who is the submitter. """ motion = models.ForeignKey( - Motion, - on_delete=models.CASCADE, - related_name='submitters') + Motion, on_delete=models.CASCADE, related_name="submitters" + ) """ ForeignKey to the motion. """ @@ -714,6 +744,7 @@ class MotionChangeRecommendationManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all change recommendations. In the background we @@ -732,9 +763,8 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model): objects = MotionChangeRecommendationManager() motion = models.ForeignKey( - Motion, - on_delete=models.CASCADE, - related_name='change_recommendations') + Motion, on_delete=models.CASCADE, related_name="change_recommendations" + ) """The motion to which the change recommendation belongs.""" rejected = models.BooleanField(default=False) @@ -759,9 +789,8 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model): """The replacement for the section of the original text specified by motion, line_from and line_to""" author = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True) + settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True + ) """A user object, who created this change recommendation. Optional.""" creation_time = models.DateTimeField(auto_now=True) @@ -769,20 +798,27 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model): def collides_with_other_recommendation(self, recommendations): for recommendation in recommendations: - if (not (self.line_from < recommendation.line_from and self.line_to <= recommendation.line_from) and - not (self.line_from >= recommendation.line_to and self.line_to > recommendation.line_to)): + if not ( + self.line_from < recommendation.line_from + and self.line_to <= recommendation.line_from + ) and not ( + self.line_from >= recommendation.line_to + and self.line_to > recommendation.line_to + ): return True return False def save(self, *args, **kwargs): - recommendations = (MotionChangeRecommendation.objects - .filter(motion=self.motion) - .exclude(pk=self.pk)) + recommendations = MotionChangeRecommendation.objects.filter( + motion=self.motion + ).exclude(pk=self.pk) if self.collides_with_other_recommendation(recommendations): - raise ValidationError('The recommendation collides with an existing one (line %s - %s).' % - (self.line_from, self.line_to)) + raise ValidationError( + "The recommendation collides with an existing one (line %s - %s)." + % (self.line_from, self.line_to) + ) return super().save(*args, **kwargs) @@ -791,13 +827,18 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model): def __str__(self): """Return a string, representing this object.""" - return "Recommendation for Motion %s, line %s - %s" % (self.motion_id, self.line_from, self.line_to) + return "Recommendation for Motion %s, line %s - %s" % ( + self.motion_id, + self.line_from, + self.line_to, + ) class Category(RESTModelMixin, models.Model): """ Model for categories of motions. """ + access_permissions = CategoryAccessPermissions() name = models.CharField(max_length=255) @@ -811,7 +852,7 @@ class Category(RESTModelMixin, models.Model): class Meta: default_permissions = () - ordering = ['prefix'] + ordering = ["prefix"] def __str__(self): return self.name @@ -821,18 +862,20 @@ class MotionBlockManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all motion blocks. In the background the related agenda item is prefetched from the database. """ - return self.get_queryset().prefetch_related('agenda_items') + return self.get_queryset().prefetch_related("agenda_items") class MotionBlock(RESTModelMixin, models.Model): """ Model for blocks of motions. """ + access_permissions = MotionBlockAccessPermissions() objects = MotionBlockManager() @@ -841,10 +884,10 @@ class MotionBlock(RESTModelMixin, models.Model): # In theory there could be one then more agenda_item. But we support only # one. See the property agenda_item. - agenda_items = GenericRelation(Item, related_name='topics') + agenda_items = GenericRelation(Item, related_name="topics") class Meta: - verbose_name = ugettext_noop('Motion block') + verbose_name = ugettext_noop("Motion block") default_permissions = () def __str__(self): @@ -856,10 +899,11 @@ class MotionBlock(RESTModelMixin, models.Model): motion block projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='motions/motion-block', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="motions/motion-block", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) """ Container for runtime information for agenda app (on create or update of this instance). @@ -886,16 +930,15 @@ class MotionBlock(RESTModelMixin, models.Model): return self.title def get_agenda_title_with_type(self): - return '%s (%s)' % (self.get_agenda_title(), _(self._meta.verbose_name)) + return "%s (%s)" % (self.get_agenda_title(), _(self._meta.verbose_name)) class MotionLog(RESTModelMixin, models.Model): """Save a logmessage for a motion.""" motion = models.ForeignKey( - Motion, - on_delete=models.CASCADE, - related_name='log_messages') + Motion, on_delete=models.CASCADE, related_name="log_messages" + ) """The motion to witch the object belongs.""" message_list = JSONField() @@ -904,9 +947,8 @@ class MotionLog(RESTModelMixin, models.Model): """ person = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True) + settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True + ) """A user object, who created the log message. Optional.""" time = models.DateTimeField(auto_now=True) @@ -914,18 +956,20 @@ class MotionLog(RESTModelMixin, models.Model): class Meta: default_permissions = () - ordering = ['-time'] + ordering = ["-time"] def __str__(self): """ Return a string, representing the log message. """ localtime = timezone.localtime(self.time) - time = formats.date_format(localtime, 'DATETIME_FORMAT') - time_and_messages = '%s ' % time + ''.join(map(_, self.message_list)) + time = formats.date_format(localtime, "DATETIME_FORMAT") + time_and_messages = "%s " % time + "".join(map(_, self.message_list)) if self.person is not None: - return _('%(time_and_messages)s by %(person)s') % {'time_and_messages': time_and_messages, - 'person': self.person} + return _("%(time_and_messages)s by %(person)s") % { + "time_and_messages": time_and_messages, + "person": self.person, + } return time_and_messages def get_root_rest_element(self): @@ -941,9 +985,7 @@ class MotionVote(RESTModelMixin, BaseVote): There should allways be three MotionVote objects for each poll, one for 'yes', 'no', and 'abstain'.""" - option = models.ForeignKey( - 'MotionOption', - on_delete=models.CASCADE) + option = models.ForeignKey("MotionOption", on_delete=models.CASCADE) """The option object, to witch the vote belongs.""" class Meta: @@ -961,9 +1003,7 @@ class MotionOption(RESTModelMixin, BaseOption): There should be one MotionOption object for each poll.""" - poll = models.ForeignKey( - 'MotionPoll', - on_delete=models.CASCADE) + poll = models.ForeignKey("MotionPoll", on_delete=models.CASCADE) """The poll object, to witch the object belongs.""" vote_class = MotionVote @@ -984,16 +1024,13 @@ class MotionOption(RESTModelMixin, BaseOption): class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore """The Class to saves the vote result for a motion poll.""" - motion = models.ForeignKey( - Motion, - on_delete=models.CASCADE, - related_name='polls') + motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls") """The motion to witch the object belongs.""" option_class = MotionOption """The option class, witch links between this object the the votes.""" - vote_values = ['Yes', 'No', 'Abstain'] + vote_values = ["Yes", "No", "Abstain"] """The possible anwers for the poll. 'Yes, 'No' and 'Abstain'.""" class Meta: @@ -1003,7 +1040,7 @@ class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: i """ Representation method only for debugging purposes. """ - return 'MotionPoll for motion %s' % self.motion + return "MotionPoll for motion %s" % self.motion def set_options(self, skip_autoupdate=False): """Create the option class for this poll.""" @@ -1012,7 +1049,7 @@ class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: i self.get_option_class()(poll=self).save(skip_autoupdate=skip_autoupdate) def get_percent_base_choice(self): - return config['motions_poll_100_percent_base'] + return config["motions_poll_100_percent_base"] def get_slide_context(self, **context): return super(MotionPoll, self).get_slide_context(poll=self) @@ -1047,15 +1084,14 @@ class State(RESTModelMixin, models.Model): """A string for a recommendation to set the motion to this state.""" workflow = models.ForeignKey( - 'Workflow', - on_delete=models.CASCADE, - related_name='states') + "Workflow", on_delete=models.CASCADE, related_name="states" + ) """A many-to-one relation to a workflow.""" - next_states = models.ManyToManyField('self', symmetrical=False, blank=True) + next_states = models.ManyToManyField("self", symmetrical=False, blank=True) """A many-to-many relation to all states, that can be choosen from this state.""" - css_class = models.CharField(max_length=255, default='primary') + css_class = models.CharField(max_length=255, default="primary") """ A css class string for showing the state name in a coloured label based on bootstrap, e.g. 'danger' (red), 'success' (green), 'warning' (yellow), 'default' (grey). @@ -1131,9 +1167,11 @@ class State(RESTModelMixin, models.Model): recommendation_label is not an empty string. """ self.check_next_states() - if self.recommendation_label == '': - raise WorkflowError('The field recommendation_label of {} must not ' - 'be an empty string.'.format(self)) + if self.recommendation_label == "": + raise WorkflowError( + "The field recommendation_label of {} must not " + "be an empty string.".format(self) + ) super(State, self).save(**kwargs) def check_next_states(self): @@ -1143,7 +1181,10 @@ class State(RESTModelMixin, models.Model): return for state in self.next_states.all(): if not state.workflow == self.workflow: - raise WorkflowError('%s can not be next state of %s because it does not belong to the same workflow.' % (state, self)) + raise WorkflowError( + "%s can not be next state of %s because it does not belong to the same workflow." + % (state, self) + ) def get_root_rest_element(self): """ @@ -1156,21 +1197,25 @@ class WorkflowManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all workflows. In the background the first state is joined and all states and next states are prefetched from the database. """ - return (self.get_queryset() - .select_related('first_state') - .prefetch_related('states', 'states__next_states')) + return ( + self.get_queryset() + .select_related("first_state") + .prefetch_related("states", "states__next_states") + ) class Workflow(RESTModelMixin, models.Model): """ Defines a workflow for a motion. """ + access_permissions = WorkflowAccessPermissions() objects = WorkflowManager() @@ -1179,11 +1224,8 @@ class Workflow(RESTModelMixin, models.Model): """A string representing the workflow.""" first_state = models.OneToOneField( - State, - on_delete=models.SET_NULL, - related_name='+', - null=True, - blank=True) + State, on_delete=models.SET_NULL, related_name="+", null=True, blank=True + ) """A one-to-one relation to a state, the starting point for the workflow.""" class Meta: @@ -1205,5 +1247,6 @@ class Workflow(RESTModelMixin, models.Model): """Checks whether the first_state itself belongs to the workflow.""" if self.first_state and not self.first_state.workflow == self: raise WorkflowError( - '%s can not be first state of %s because it ' - 'does not belong to it.' % (self.first_state, self)) + "%s can not be first state of %s because it " + "does not belong to it." % (self.first_state, self) + ) diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index 7b56691b1..c01c68864 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -9,21 +9,22 @@ class MotionSlide(ProjectorElement): """ Slide definitions for Motion model. """ - name = 'motions/motion' + + name = "motions/motion" def check_data(self): - if not Motion.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('Motion does not exist.') + if not Motion.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("Motion does not exist.") def update_data(self): data = None try: - motion = Motion.objects.get(pk=self.config_entry.get('id')) + motion = Motion.objects.get(pk=self.config_entry.get("id")) except Motion.DoesNotExist: # Motion does not exist, so just do nothing. pass else: - data = {'agenda_item_id': motion.agenda_item_id} + data = {"agenda_item_id": motion.agenda_item_id} return data @@ -31,21 +32,22 @@ class MotionBlockSlide(ProjectorElement): """ Slide definitions for a block of motions (MotionBlock model). """ - name = 'motions/motion-block' + + name = "motions/motion-block" def check_data(self): - if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('MotionBlock does not exist.') + if not MotionBlock.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("MotionBlock does not exist.") def update_data(self): data = None try: - motion_block = MotionBlock.objects.get(pk=self.config_entry.get('id')) + motion_block = MotionBlock.objects.get(pk=self.config_entry.get("id")) except MotionBlock.DoesNotExist: # MotionBlock does not exist, so just do nothing. pass else: - data = {'agenda_item_id': motion_block.agenda_item_id} + data = {"agenda_item_id": motion_block.agenda_item_id} return data diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 2ed93be1b..df8ab3baf 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -39,48 +39,55 @@ def validate_workflow_field(value): Validator to ensure that the workflow with the given id exists. """ if not Workflow.objects.filter(pk=value).exists(): - raise ValidationError({'detail': _('Workflow %(pk)d does not exist.') % {'pk': value}}) + raise ValidationError( + {"detail": _("Workflow %(pk)d does not exist.") % {"pk": value}} + ) class StatuteParagraphSerializer(ModelSerializer): """ Serializer for motion.models.StatuteParagraph objects. """ + class Meta: model = StatuteParagraph - fields = ('id', 'title', 'text', 'weight') + fields = ("id", "title", "text", "weight") class CategorySerializer(ModelSerializer): """ Serializer for motion.models.Category objects. """ + class Meta: model = Category - fields = ('id', 'name', 'prefix',) + fields = ("id", "name", "prefix") class MotionBlockSerializer(ModelSerializer): """ Serializer for motion.models.Category objects. """ - agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3) + + agenda_type = IntegerField( + write_only=True, required=False, min_value=1, max_value=3 + ) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) class Meta: model = MotionBlock - fields = ('id', 'title', 'agenda_item_id', 'agenda_type', 'agenda_parent_id',) + fields = ("id", "title", "agenda_item_id", "agenda_type", "agenda_parent_id") def create(self, validated_data): """ Customized create method. Set information about related agenda item into agenda_item_update_information container. """ - agenda_type = validated_data.pop('agenda_type', None) - agenda_parent_id = validated_data.pop('agenda_parent_id', None) + agenda_type = validated_data.pop("agenda_type", None) + agenda_parent_id = validated_data.pop("agenda_parent_id", None) motion_block = MotionBlock(**validated_data) - motion_block.agenda_item_update_information['type'] = agenda_type - motion_block.agenda_item_update_information['parent_id'] = agenda_parent_id + motion_block.agenda_item_update_information["type"] = agenda_type + motion_block.agenda_item_update_information["parent_id"] = agenda_parent_id motion_block.save() return motion_block @@ -89,35 +96,38 @@ class StateSerializer(ModelSerializer): """ Serializer for motion.models.State objects. """ + class Meta: model = State fields = ( - 'id', - 'name', - 'recommendation_label', - 'css_class', - 'required_permission_to_see', - 'allow_support', - 'allow_create_poll', - 'allow_submitter_edit', - 'dont_set_identifier', - 'show_state_extension_field', - 'merge_amendment_into_final', - 'show_recommendation_extension_field', - 'next_states', - 'workflow') + "id", + "name", + "recommendation_label", + "css_class", + "required_permission_to_see", + "allow_support", + "allow_create_poll", + "allow_submitter_edit", + "dont_set_identifier", + "show_state_extension_field", + "merge_amendment_into_final", + "show_recommendation_extension_field", + "next_states", + "workflow", + ) class WorkflowSerializer(ModelSerializer): """ Serializer for motion.models.Workflow objects. """ + states = StateSerializer(many=True, read_only=True) class Meta: model = Workflow - fields = ('id', 'name', 'states', 'first_state',) - read_only_fields = ('first_state',) + fields = ("id", "name", "states", "first_state") + read_only_fields = ("first_state",) @transaction.atomic def create(self, validated_data): @@ -127,11 +137,11 @@ class WorkflowSerializer(ModelSerializer): """ workflow = super().create(validated_data) first_state = State.objects.create( - name='new', + name="new", workflow=workflow, allow_create_poll=True, allow_support=True, - allow_submitter_edit=True + allow_submitter_edit=True, ) workflow.first_state = first_state workflow.save() @@ -142,6 +152,7 @@ class AmendmentParagraphsJSONSerializerField(Field): """ Serializer for motions's amendment_paragraphs JSONField. """ + def to_representation(self, obj): """ Returns the value of the field. @@ -153,10 +164,12 @@ class AmendmentParagraphsJSONSerializerField(Field): Checks that data is a list of strings. """ if type(data) is not list: - raise ValidationError({'detail': 'Data must be a list.'}) + raise ValidationError({"detail": "Data must be a list."}) for paragraph in data: if type(paragraph) is not str and paragraph is not None: - raise ValidationError({'detail': 'Paragraph must be either a string or null/None.'}) + raise ValidationError( + {"detail": "Paragraph must be either a string or null/None."} + ) return data @@ -164,11 +177,12 @@ class MotionLogSerializer(ModelSerializer): """ Serializer for motion.models.MotionLog objects. """ + message = SerializerMethodField() class Meta: model = MotionLog - fields = ('message_list', 'person', 'time', 'message',) + fields = ("message_list", "person", "time", "message") def get_message(self, obj): """ @@ -181,27 +195,32 @@ class MotionPollSerializer(ModelSerializer): """ Serializer for motion.models.MotionPoll objects. """ + yes = SerializerMethodField() no = SerializerMethodField() abstain = SerializerMethodField() votes = DictField( - child=DecimalField(max_digits=15, decimal_places=6, min_value=-2, allow_null=True), - write_only=True) + child=DecimalField( + max_digits=15, decimal_places=6, min_value=-2, allow_null=True + ), + write_only=True, + ) has_votes = SerializerMethodField() class Meta: model = MotionPoll fields = ( - 'id', - 'motion', - 'yes', - 'no', - 'abstain', - 'votesvalid', - 'votesinvalid', - 'votescast', - 'votes', - 'has_votes') + "id", + "motion", + "yes", + "no", + "abstain", + "votesvalid", + "votesinvalid", + "votescast", + "votes", + "has_votes", + ) validators = (default_votes_validator,) def __init__(self, *args, **kwargs): @@ -211,21 +230,21 @@ class MotionPollSerializer(ModelSerializer): def get_yes(self, obj): try: - result: Optional[str] = str(self.get_votes_dict(obj)['Yes']) + result: Optional[str] = str(self.get_votes_dict(obj)["Yes"]) except KeyError: result = None return result def get_no(self, obj): try: - result: Optional[str] = str(self.get_votes_dict(obj)['No']) + result: Optional[str] = str(self.get_votes_dict(obj)["No"]) except KeyError: result = None return result def get_abstain(self, obj): try: - result: Optional[str] = str(self.get_votes_dict(obj)['Abstain']) + result: Optional[str] = str(self.get_votes_dict(obj)["Abstain"]) except KeyError: result = None return result @@ -256,21 +275,30 @@ class MotionPollSerializer(ModelSerializer): "votes": {"Yes": 10, "No": 4, "Abstain": -2} """ # Update votes. - votes = validated_data.get('votes') + votes = validated_data.get("votes") if votes: if len(votes) != len(instance.get_vote_values()): - raise ValidationError({ - 'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())}) + raise ValidationError( + { + "detail": _("You have to submit data for %d vote values.") + % len(instance.get_vote_values()) + } + ) for vote_value, vote_weight in votes.items(): if vote_value not in instance.get_vote_values(): - raise ValidationError({ - 'detail': _('Vote value %s is invalid.') % vote_value}) - instance.set_vote_objects_with_values(instance.get_options().get(), votes, skip_autoupdate=True) + raise ValidationError( + {"detail": _("Vote value %s is invalid.") % vote_value} + ) + instance.set_vote_objects_with_values( + instance.get_options().get(), votes, skip_autoupdate=True + ) # Update remaining writeable fields. - instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid) - instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid) - instance.votescast = validated_data.get('votescast', instance.votescast) + instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid) + instance.votesinvalid = validated_data.get( + "votesinvalid", instance.votesinvalid + ) + instance.votescast = validated_data.get("votescast", instance.votescast) instance.save() return instance @@ -279,27 +307,29 @@ class MotionChangeRecommendationSerializer(ModelSerializer): """ Serializer for motion.models.MotionChangeRecommendation objects. """ + class Meta: model = MotionChangeRecommendation fields = ( - 'id', - 'motion', - 'rejected', - 'internal', - 'type', - 'other_description', - 'line_from', - 'line_to', - 'text', - 'creation_time',) + "id", + "motion", + "rejected", + "internal", + "type", + "other_description", + "line_from", + "line_to", + "text", + "creation_time", + ) def is_title_cr(self, data): - return int(data['line_from']) == 0 and int(data['line_to']) == 0 + return int(data["line_from"]) == 0 and int(data["line_to"]) == 0 def validate(self, data): # Change recommendations for titles are stored as plain-text, thus they don't need to be html-escaped - if 'text' in data and not self.is_title_cr(data): - data['text'] = validate_html(data['text']) + if "text" in data and not self.is_title_cr(data): + data["text"] = validate_html(data["text"]) return data @@ -307,23 +337,18 @@ class MotionCommentSectionSerializer(ModelSerializer): """ Serializer for motion.models.MotionCommentSection objects. """ + read_groups = IdPrimaryKeyRelatedField( - many=True, - required=False, - queryset=get_group_model().objects.all()) + many=True, required=False, queryset=get_group_model().objects.all() + ) write_groups = IdPrimaryKeyRelatedField( - many=True, - required=False, - queryset=get_group_model().objects.all()) + many=True, required=False, queryset=get_group_model().objects.all() + ) class Meta: model = MotionCommentSection - fields = ( - 'id', - 'name', - 'read_groups', - 'write_groups',) + fields = ("id", "name", "read_groups", "write_groups") def create(self, validated_data): """ Call inform_changed_data on creation, so the cache includes the groups. """ @@ -336,15 +361,12 @@ class MotionCommentSerializer(ModelSerializer): """ Serializer for motion.models.MotionComment objects. """ + read_groups_id = SerializerMethodField() class Meta: model = MotionComment - fields = ( - 'id', - 'comment', - 'section', - 'read_groups_id',) + fields = ("id", "comment", "section", "read_groups_id") def get_read_groups_id(self, comment): return [group.id for group in comment.section.read_groups.all()] @@ -354,20 +376,17 @@ class SubmitterSerializer(ModelSerializer): """ Serializer for motion.models.Submitter objects. """ + class Meta: model = Submitter - fields = ( - 'id', - 'user', - 'motion', - 'weight', - ) + fields = ("id", "user", "motion", "weight") class MotionSerializer(ModelSerializer): """ Serializer for motion.models.Motion objects. """ + comments = MotionCommentSerializer(many=True, read_only=True) log_messages = MotionLogSerializer(many=True, read_only=True) polls = MotionPollSerializer(many=True, read_only=True) @@ -378,67 +397,76 @@ class MotionSerializer(ModelSerializer): title = CharField(max_length=255) amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False) workflow_id = IntegerField( - min_value=1, - required=False, - validators=[validate_workflow_field]) - agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3) + min_value=1, required=False, validators=[validate_workflow_field] + ) + agenda_type = IntegerField( + write_only=True, required=False, min_value=1, max_value=3 + ) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) submitters = SubmitterSerializer(many=True, read_only=True) class Meta: model = Motion fields = ( - 'id', - 'identifier', - 'title', - 'text', - 'amendment_paragraphs', - 'modified_final_version', - 'reason', - 'parent', - 'category', - 'comments', - 'motion_block', - 'origin', - 'submitters', - 'supporters', - 'state', - 'state_extension', - 'state_required_permission_to_see', - 'statute_paragraph', - 'workflow_id', - 'recommendation', - 'recommendation_extension', - 'tags', - 'attachments', - 'polls', - 'agenda_item_id', - 'agenda_type', - 'agenda_parent_id', - 'log_messages', - 'sort_parent', - 'weight',) - read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above. + "id", + "identifier", + "title", + "text", + "amendment_paragraphs", + "modified_final_version", + "reason", + "parent", + "category", + "comments", + "motion_block", + "origin", + "submitters", + "supporters", + "state", + "state_extension", + "state_required_permission_to_see", + "statute_paragraph", + "workflow_id", + "recommendation", + "recommendation_extension", + "tags", + "attachments", + "polls", + "agenda_item_id", + "agenda_type", + "agenda_parent_id", + "log_messages", + "sort_parent", + "weight", + ) + read_only_fields = ( + "state", + "recommendation", + ) # Some other fields are also read_only. See definitions above. def validate(self, data): - if 'text'in data: - data['text'] = validate_html(data['text']) + if "text" in data: + data["text"] = validate_html(data["text"]) - if 'modified_final_version' in data: - data['modified_final_version'] = validate_html(data['modified_final_version']) + if "modified_final_version" in data: + data["modified_final_version"] = validate_html( + data["modified_final_version"] + ) - if 'reason' in data: - data['reason'] = validate_html(data['reason']) + if "reason" in data: + data["reason"] = validate_html(data["reason"]) - if 'amendment_paragraphs' in data: - data['amendment_paragraphs'] = list(map(lambda entry: validate_html(entry) if type(entry) is str else None, - data['amendment_paragraphs'])) - data['text'] = '' + if "amendment_paragraphs" in data: + data["amendment_paragraphs"] = list( + map( + lambda entry: validate_html(entry) if type(entry) is str else None, + data["amendment_paragraphs"], + ) + ) + data["text"] = "" else: - if 'text' in data and len(data['text']) == 0: - raise ValidationError({ - 'detail': _('This field may not be blank.') - }) + if "text" in data and len(data["text"]) == 0: + raise ValidationError({"detail": _("This field may not be blank.")}) return data @@ -451,24 +479,28 @@ class MotionSerializer(ModelSerializer): agenda_item_update_information container. """ motion = Motion() - motion.title = validated_data['title'] - motion.text = validated_data['text'] - motion.amendment_paragraphs = validated_data.get('amendment_paragraphs') - motion.modified_final_version = validated_data.get('modified_final_version', '') - motion.reason = validated_data.get('reason', '') - motion.identifier = validated_data.get('identifier') - motion.category = validated_data.get('category') - motion.motion_block = validated_data.get('motion_block') - motion.origin = validated_data.get('origin', '') - motion.parent = validated_data.get('parent') - motion.statute_paragraph = validated_data.get('statute_paragraph') - motion.reset_state(validated_data.get('workflow_id')) - motion.agenda_item_update_information['type'] = validated_data.get('agenda_type') - motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id') + motion.title = validated_data["title"] + motion.text = validated_data["text"] + motion.amendment_paragraphs = validated_data.get("amendment_paragraphs") + motion.modified_final_version = validated_data.get("modified_final_version", "") + motion.reason = validated_data.get("reason", "") + motion.identifier = validated_data.get("identifier") + motion.category = validated_data.get("category") + motion.motion_block = validated_data.get("motion_block") + motion.origin = validated_data.get("origin", "") + motion.parent = validated_data.get("parent") + motion.statute_paragraph = validated_data.get("statute_paragraph") + motion.reset_state(validated_data.get("workflow_id")) + motion.agenda_item_update_information["type"] = validated_data.get( + "agenda_type" + ) + motion.agenda_item_update_information["parent_id"] = validated_data.get( + "agenda_parent_id" + ) motion.save() - motion.supporters.add(*validated_data.get('supporters', [])) - motion.attachments.add(*validated_data.get('attachments', [])) - motion.tags.add(*validated_data.get('tags', [])) + motion.supporters.add(*validated_data.get("supporters", [])) + motion.attachments.add(*validated_data.get("attachments", [])) + motion.tags.add(*validated_data.get("tags", [])) return motion @transaction.atomic @@ -477,8 +509,8 @@ class MotionSerializer(ModelSerializer): Customized method to update a motion. """ workflow_id = None - if 'workflow_id' in validated_data: - workflow_id = validated_data.pop('workflow_id') + if "workflow_id" in validated_data: + workflow_id = validated_data.pop("workflow_id") result = super().update(motion, validated_data) diff --git a/openslides/motions/signals.py b/openslides/motions/signals.py index eda1c549a..af36edeaa 100644 --- a/openslides/motions/signals.py +++ b/openslides/motions/signals.py @@ -14,98 +14,128 @@ def create_builtin_workflows(sender, **kwargs): # If there is at least one workflow, then do nothing. return - workflow_1 = Workflow(name='Simple Workflow') + workflow_1 = Workflow(name="Simple Workflow") workflow_1.save(skip_autoupdate=True) - state_1_1 = State(name=ugettext_noop('submitted'), - workflow=workflow_1, - allow_create_poll=True, - allow_support=True, - allow_submitter_edit=True) + state_1_1 = State( + name=ugettext_noop("submitted"), + workflow=workflow_1, + allow_create_poll=True, + allow_support=True, + allow_submitter_edit=True, + ) state_1_1.save(skip_autoupdate=True) - state_1_2 = State(name=ugettext_noop('accepted'), - workflow=workflow_1, - recommendation_label='Acceptance', - css_class='success', - merge_amendment_into_final=1) + state_1_2 = State( + name=ugettext_noop("accepted"), + workflow=workflow_1, + recommendation_label="Acceptance", + css_class="success", + merge_amendment_into_final=1, + ) state_1_2.save(skip_autoupdate=True) - state_1_3 = State(name=ugettext_noop('rejected'), - workflow=workflow_1, - recommendation_label='Rejection', - css_class='danger', - merge_amendment_into_final=-1) + state_1_3 = State( + name=ugettext_noop("rejected"), + workflow=workflow_1, + recommendation_label="Rejection", + css_class="danger", + merge_amendment_into_final=-1, + ) state_1_3.save(skip_autoupdate=True) - state_1_4 = State(name=ugettext_noop('not decided'), - workflow=workflow_1, - recommendation_label='No decision', - css_class='default', - merge_amendment_into_final=-1) + state_1_4 = State( + name=ugettext_noop("not decided"), + workflow=workflow_1, + recommendation_label="No decision", + css_class="default", + merge_amendment_into_final=-1, + ) state_1_4.save(skip_autoupdate=True) 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(skip_autoupdate=True) - workflow_2 = Workflow(name='Complex Workflow') + workflow_2 = Workflow(name="Complex Workflow") workflow_2.save(skip_autoupdate=True) - state_2_1 = State(name=ugettext_noop('published'), - workflow=workflow_2, - allow_support=True, - allow_submitter_edit=True, - dont_set_identifier=True) + state_2_1 = State( + name=ugettext_noop("published"), + workflow=workflow_2, + allow_support=True, + allow_submitter_edit=True, + dont_set_identifier=True, + ) state_2_1.save(skip_autoupdate=True) - state_2_2 = State(name=ugettext_noop('permitted'), - workflow=workflow_2, - recommendation_label='Permission', - allow_create_poll=True, - allow_submitter_edit=True) + state_2_2 = State( + name=ugettext_noop("permitted"), + workflow=workflow_2, + recommendation_label="Permission", + allow_create_poll=True, + allow_submitter_edit=True, + ) state_2_2.save(skip_autoupdate=True) - state_2_3 = State(name=ugettext_noop('accepted'), - workflow=workflow_2, - recommendation_label='Acceptance', - css_class='success', - merge_amendment_into_final=1) + state_2_3 = State( + name=ugettext_noop("accepted"), + workflow=workflow_2, + recommendation_label="Acceptance", + css_class="success", + merge_amendment_into_final=1, + ) state_2_3.save(skip_autoupdate=True) - state_2_4 = State(name=ugettext_noop('rejected'), - workflow=workflow_2, - recommendation_label='Rejection', - css_class='danger', - merge_amendment_into_final=-1) + state_2_4 = State( + name=ugettext_noop("rejected"), + workflow=workflow_2, + recommendation_label="Rejection", + css_class="danger", + merge_amendment_into_final=-1, + ) state_2_4.save(skip_autoupdate=True) - state_2_5 = State(name=ugettext_noop('withdrawed'), - workflow=workflow_2, - css_class='default', - merge_amendment_into_final=-1) + state_2_5 = State( + name=ugettext_noop("withdrawed"), + workflow=workflow_2, + css_class="default", + merge_amendment_into_final=-1, + ) state_2_5.save(skip_autoupdate=True) - state_2_6 = State(name=ugettext_noop('adjourned'), - workflow=workflow_2, - recommendation_label='Adjournment', - css_class='default', - merge_amendment_into_final=-1) + state_2_6 = State( + name=ugettext_noop("adjourned"), + workflow=workflow_2, + recommendation_label="Adjournment", + css_class="default", + merge_amendment_into_final=-1, + ) state_2_6.save(skip_autoupdate=True) - state_2_7 = State(name=ugettext_noop('not concerned'), - workflow=workflow_2, - recommendation_label='No concernment', - css_class='default', - merge_amendment_into_final=-1) + state_2_7 = State( + name=ugettext_noop("not concerned"), + workflow=workflow_2, + recommendation_label="No concernment", + css_class="default", + merge_amendment_into_final=-1, + ) state_2_7.save(skip_autoupdate=True) - state_2_8 = State(name=ugettext_noop('refered to committee'), - workflow=workflow_2, - recommendation_label='Referral to committee', - css_class='default', - merge_amendment_into_final=-1) + state_2_8 = State( + name=ugettext_noop("refered to committee"), + workflow=workflow_2, + recommendation_label="Referral to committee", + css_class="default", + merge_amendment_into_final=-1, + ) state_2_8.save(skip_autoupdate=True) - state_2_9 = State(name=ugettext_noop('needs review'), - workflow=workflow_2, - css_class='default', - merge_amendment_into_final=-1) + state_2_9 = State( + name=ugettext_noop("needs review"), + workflow=workflow_2, + css_class="default", + merge_amendment_into_final=-1, + ) state_2_9.save(skip_autoupdate=True) - state_2_10 = State(name=ugettext_noop('rejected (not authorized)'), - workflow=workflow_2, - recommendation_label='Rejection (not authorized)', - css_class='default', - merge_amendment_into_final=-1) + state_2_10 = State( + name=ugettext_noop("rejected (not authorized)"), + workflow=workflow_2, + recommendation_label="Rejection (not authorized)", + css_class="default", + merge_amendment_into_final=-1, + ) state_2_10.save(skip_autoupdate=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) + 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(skip_autoupdate=True) @@ -114,8 +144,11 @@ def get_permission_change_data(sender, permissions, **kwargs): """ Yields all necessary collections if 'motions.can_see' permission changes. """ - motions_app = apps.get_app_config(app_label='motions') + motions_app = apps.get_app_config(app_label="motions") for permission in permissions: # There could be only one 'motions.can_see' and then we want to return data. - if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see': + if ( + permission.content_type.app_label == motions_app.label + and permission.codename == "can_see" + ): yield from motions_app.get_startup_elements() diff --git a/openslides/motions/views.py b/openslides/motions/views.py index e452f57b4..0de3668f4 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -55,12 +55,14 @@ from .serializers import MotionPollSerializer, StateSerializer # Viewsets for the REST API + class MotionViewSet(ModelViewSet): """ API endpoint for motions. There are a lot of views. See check_view_permissions(). """ + access_permissions = MotionAccessPermissions() queryset = Motion.objects.all() @@ -68,28 +70,41 @@ class MotionViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('metadata', 'partial_update', 'update', 'destroy'): - result = has_perm(self.request.user, 'motions.can_see') + elif self.action in ("metadata", "partial_update", "update", "destroy"): + result = has_perm(self.request.user, "motions.can_see") # For partial_update, update and destroy requests the rest of the check is # done in the update method. See below. - elif self.action == 'create': - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_create') and - (not config['motions_stop_submitting'] or - has_perm(self.request.user, 'motions.can_manage'))) - elif self.action in ('set_state', 'set_recommendation', 'manage_multiple_recommendation', - 'follow_recommendation', 'manage_multiple_submitters', - 'manage_multiple_tags', 'create_poll'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage_metadata')) - elif self.action in ('sort', 'manage_comments'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) - elif self.action == 'support': - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_support')) + elif self.action == "create": + result = ( + has_perm(self.request.user, "motions.can_see") + and has_perm(self.request.user, "motions.can_create") + and ( + not config["motions_stop_submitting"] + or has_perm(self.request.user, "motions.can_manage") + ) + ) + elif self.action in ( + "set_state", + "set_recommendation", + "manage_multiple_recommendation", + "follow_recommendation", + "manage_multiple_submitters", + "manage_multiple_tags", + "create_poll", + ): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage_metadata" + ) + elif self.action in ("sort", "manage_comments"): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) + elif self.action == "support": + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_support" + ) else: result = False return result @@ -101,8 +116,13 @@ class MotionViewSet(ModelViewSet): """ motion = self.get_object() - if not ((has_perm(request.user, 'motions.can_manage') or - motion.is_submitter(request.user) and motion.state.allow_submitter_edit)): + if not ( + ( + has_perm(request.user, "motions.can_manage") + or motion.is_submitter(request.user) + and motion.state.allow_submitter_edit + ) + ): self.permission_denied(request) result = super().destroy(request, *args, **kwargs) @@ -110,8 +130,9 @@ class MotionViewSet(ModelViewSet): # Fire autoupdate again to save information to OpenSlides history. inform_deleted_data( [(motion.get_collection_string(), motion.pk)], - information='Motion deleted', - user_id=request.user.pk) + information="Motion deleted", + user_id=request.user.pk, + ) return result @@ -124,35 +145,34 @@ class MotionViewSet(ModelViewSet): request.data._mutable = True # Check if parent motion exists. - if request.data.get('parent_id') is not None: + if request.data.get("parent_id") is not None: try: - parent_motion = Motion.objects.get(pk=request.data['parent_id']) + parent_motion = Motion.objects.get(pk=request.data["parent_id"]) except Motion.DoesNotExist: - raise ValidationError({'detail': _('The parent motion does not exist.')}) + raise ValidationError( + {"detail": _("The parent motion does not exist.")} + ) else: parent_motion = None # Check permission to send some data. - if not has_perm(request.user, 'motions.can_manage'): + if not has_perm(request.user, "motions.can_manage"): # Remove fields that the user is not allowed to send. # The list() is required because we want to use del inside the loop. keys = list(request.data.keys()) - whitelist = [ - 'title', - 'text', - 'reason', - 'category_id', - ] + whitelist = ["title", "text", "reason", "category_id"] if parent_motion is not None: # For creating amendments. - whitelist.extend([ - 'parent_id', - 'amendment_paragraphs', - 'motion_block_id', # This and the category_id will be set to the matching - # values from parent_motion. - ]) - request.data['category_id'] = parent_motion.category_id - request.data['motion_block_id'] = parent_motion.motion_block_id + whitelist.extend( + [ + "parent_id", + "amendment_paragraphs", + "motion_block_id", # This and the category_id will be set to the matching + # values from parent_motion. + ] + ) + request.data["category_id"] = parent_motion.category_id + request.data["motion_block_id"] = parent_motion.motion_block_id for key in keys: if key not in whitelist: del request.data[key] @@ -166,13 +186,15 @@ class MotionViewSet(ModelViewSet): # Check for submitters and make ids unique if isinstance(request.data, QueryDict): - submitters_id = request.data.getlist('submitters_id') + submitters_id = request.data.getlist("submitters_id") else: - submitters_id = request.data.get('submitters_id') + submitters_id = request.data.get("submitters_id") if submitters_id is None: submitters_id = [] if not isinstance(submitters_id, list): - raise ValidationError({'detail': _('If submitters_id is given, it has to be a list.')}) + raise ValidationError( + {"detail": _("If submitters_id is given, it has to be a list.")} + ) submitters_id_unique = set() for id in submitters_id: @@ -197,7 +219,7 @@ class MotionViewSet(ModelViewSet): Submitter.objects.add(submitter, motion) # Write the log message and initiate response. - motion.write_log([ugettext_noop('Motion created')], request.user) + motion.write_log([ugettext_noop("Motion created")], request.user) # Send new submitters and supporters via autoupdate because users # without permission to see users may not have them but can get it now. @@ -207,10 +229,7 @@ class MotionViewSet(ModelViewSet): headers = self.get_success_headers(serializer.data) # Strip out response data so nobody gets unrestricted data. - data = ReturnDict( - id=serializer.data.get('id'), - serializer=serializer - ) + data = ReturnDict(id=serializer.data.get("id"), serializer=serializer) return Response(data, status=status.HTTP_201_CREATED, headers=headers) def update(self, request, *args, **kwargs): @@ -230,35 +249,32 @@ class MotionViewSet(ModelViewSet): motion = self.get_object() # Check permissions. - if (not has_perm(request.user, 'motions.can_manage') and - not has_perm(request.user, 'motions.can_manage_metadata') and - not (motion.is_submitter(request.user) and motion.state.allow_submitter_edit)): + if ( + not has_perm(request.user, "motions.can_manage") + and not has_perm(request.user, "motions.can_manage_metadata") + and not ( + motion.is_submitter(request.user) and motion.state.allow_submitter_edit + ) + ): self.permission_denied(request) # Check permission to send only some data. # Attention: Users with motions.can_manage permission can change all # fields even if they do not have motions.can_manage_metadata # permission. - if not has_perm(request.user, 'motions.can_manage'): + if not has_perm(request.user, "motions.can_manage"): # Remove fields that the user is not allowed to change. # The list() is required because we want to use del inside the loop. keys = list(request.data.keys()) whitelist: List[str] = [] # Add title, text and reason to the whitelist only, if the user is the submitter. if motion.is_submitter(request.user) and motion.state.allow_submitter_edit: - whitelist.extend(( - 'title', - 'text', - 'reason', - )) + whitelist.extend(("title", "text", "reason")) - if has_perm(request.user, 'motions.can_manage_metadata'): - whitelist.extend(( - 'category_id', - 'motion_block_id', - 'origin', - 'supporters_id', - )) + if has_perm(request.user, "motions.can_manage_metadata"): + whitelist.extend( + ("category_id", "motion_block_id", "origin", "supporters_id") + ) for key in keys: if key not in whitelist: @@ -266,19 +282,23 @@ class MotionViewSet(ModelViewSet): # Validate data and update motion. serializer = self.get_serializer( - motion, - data=request.data, - partial=kwargs.get('partial', False)) + motion, data=request.data, partial=kwargs.get("partial", False) + ) serializer.is_valid(raise_exception=True) updated_motion = serializer.save() # Write the log message, check removal of supporters and initiate response. # TODO: Log if a motion was updated. - updated_motion.write_log([ugettext_noop('Motion updated')], request.user) - if (config['motions_remove_supporters'] and updated_motion.state.allow_support and - not has_perm(request.user, 'motions.can_manage')): + updated_motion.write_log([ugettext_noop("Motion updated")], request.user) + if ( + config["motions_remove_supporters"] + and updated_motion.state.allow_support + and not has_perm(request.user, "motions.can_manage") + ): updated_motion.supporters.clear() - updated_motion.write_log([ugettext_noop('All supporters removed')], request.user) + updated_motion.write_log( + [ugettext_noop("All supporters removed")], request.user + ) # Send new supporters via autoupdate because users # without permission to see users may not have them but can get it now. @@ -287,14 +307,13 @@ class MotionViewSet(ModelViewSet): # Fire autoupdate again to save information to OpenSlides history. inform_changed_data( - updated_motion, - information='Motion updated', - user_id=request.user.pk) + updated_motion, information="Motion updated", user_id=request.user.pk + ) # We do not add serializer.data to response so nobody gets unrestricted data here. return Response() - @list_route(methods=['post']) + @list_route(methods=["post"]) def sort(self, request): """ Sort motions. Also checks sort_parent field to prevent hierarchical loops. @@ -302,12 +321,12 @@ class MotionViewSet(ModelViewSet): Note: This view is not tested! Maybe needs to be refactored. Add documentation abou the data to be send. """ - nodes = request.data.get('nodes', []) - sort_parent_id = request.data.get('parent_id') + nodes = request.data.get("nodes", []) + sort_parent_id = request.data.get("parent_id") motions = [] with transaction.atomic(): for index, node in enumerate(nodes): - id = node['id'] + id = node["id"] motion = Motion.objects.get(pk=id) motion.sort_parent_id = sort_parent_id motion.weight = index @@ -319,14 +338,15 @@ class MotionViewSet(ModelViewSet): ancestor = motion.sort_parent while ancestor is not None: if ancestor == motion: - raise ValidationError({'detail': _( - 'There must not be a hierarchical loop.')}) + raise ValidationError( + {"detail": _("There must not be a hierarchical loop.")} + ) ancestor = ancestor.sort_parent inform_changed_data(motions) - return Response({'detail': _('The motions has been sorted.')}) + return Response({"detail": _("The motions has been sorted.")}) - @detail_route(methods=['POST', 'DELETE']) + @detail_route(methods=["POST", "DELETE"]) def manage_comments(self, request, pk=None): """ Create, update and delete motion comments. @@ -340,40 +360,55 @@ class MotionViewSet(ModelViewSet): motion = self.get_object() # Get the comment section - section_id = request.data.get('section_id') + section_id = request.data.get("section_id") if not section_id or not isinstance(section_id, int): - raise ValidationError({'detail': _('You have to provide a section_id of type int.')}) + raise ValidationError( + {"detail": _("You have to provide a section_id of type int.")} + ) try: section = MotionCommentSection.objects.get(pk=section_id) except MotionCommentSection.DoesNotExist: - raise ValidationError({'detail': _('A comment section with id {} does not exist').format(section_id)}) + raise ValidationError( + { + "detail": _("A comment section with id {} does not exist").format( + section_id + ) + } + ) # the request user needs to see and write to the comment section - if (not in_some_groups(request.user, list(section.read_groups.values_list('pk', flat=True))) or - not in_some_groups(request.user, list(section.write_groups.values_list('pk', flat=True)))): - raise ValidationError({'detail': _('You are not allowed to see or write to the comment section.')}) + if not in_some_groups( + request.user, list(section.read_groups.values_list("pk", flat=True)) + ) or not in_some_groups( + request.user, list(section.write_groups.values_list("pk", flat=True)) + ): + raise ValidationError( + { + "detail": _( + "You are not allowed to see or write to the comment section." + ) + } + ) - if request.method == 'POST': # Create or update + if request.method == "POST": # Create or update # validate comment - comment_value = request.data.get('comment', '') + comment_value = request.data.get("comment", "") if not isinstance(comment_value, str): - raise ValidationError({'detail': _('The comment should be a string.')}) + raise ValidationError({"detail": _("The comment should be a string.")}) comment, created = MotionComment.objects.get_or_create( - motion=motion, - section=section, - defaults={ - 'comment': comment_value}) + motion=motion, section=section, defaults={"comment": comment_value} + ) if not created: comment.comment = comment_value comment.save() # write log motion.write_log( - [ugettext_noop('Comment {} updated').format(section.name)], - request.user) - message = _('Comment {} updated').format(section.name) + [ugettext_noop("Comment {} updated").format(section.name)], request.user + ) + message = _("Comment {} updated").format(section.name) else: # DELETE try: comment = MotionComment.objects.get(motion=motion, section=section) @@ -384,13 +419,14 @@ class MotionViewSet(ModelViewSet): comment.delete() motion.write_log( - [ugettext_noop('Comment {} deleted').format(section.name)], - request.user) - message = _('Comment {} deleted').format(section.name) + [ugettext_noop("Comment {} deleted").format(section.name)], + request.user, + ) + message = _("Comment {} deleted").format(section.name) - return Response({'detail': message}) + return Response({"detail": message}) - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.atomic def manage_multiple_submitters(self, request): """ @@ -398,7 +434,7 @@ class MotionViewSet(ModelViewSet): Send POST {"motions": [... see schema ...]} to changed the submitters. """ - motions = request.data.get('motions') + motions = request.data.get("motions") schema = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -408,16 +444,11 @@ class MotionViewSet(ModelViewSet): "items": { "type": "object", "properties": { - "id": { - "description": "The id of the motion.", - "type": "integer", - }, + "id": {"description": "The id of the motion.", "type": "integer"}, "submitters": { "description": "An array of user ids the should become submitters. Use an empty array to clear submitter field.", "type": "array", - "items": { - "type": "integer", - }, + "items": {"type": "integer"}, "uniqueItems": True, }, }, @@ -430,26 +461,30 @@ class MotionViewSet(ModelViewSet): try: jsonschema.validate(motions, schema) except jsonschema.ValidationError as err: - raise ValidationError({'detail': str(err)}) + raise ValidationError({"detail": str(err)}) motion_result = [] new_submitters = [] for item in motions: # Get motion. try: - motion = Motion.objects.get(pk=item['id']) + motion = Motion.objects.get(pk=item["id"]) except Motion.DoesNotExist: - raise ValidationError({'detail': 'Motion {} does not exist'.format(item['id'])}) + raise ValidationError( + {"detail": "Motion {} does not exist".format(item["id"])} + ) # Remove all submitters. Submitter.objects.filter(motion=motion).delete() # Set new submitters. - for submitter_id in item['submitters']: + for submitter_id in item["submitters"]: try: submitter = get_user_model().objects.get(pk=submitter_id) except get_user_model().DoesNotExist: - raise ValidationError({'detail': 'Submitter {} does not exist'.format(submitter_id)}) + raise ValidationError( + {"detail": "Submitter {} does not exist".format(submitter_id)} + ) Submitter.objects.add(submitter, motion) new_submitters.append(submitter) @@ -465,11 +500,15 @@ class MotionViewSet(ModelViewSet): inform_changed_data(new_submitters) # Send response. - return Response({ - 'detail': _('{number} motions successfully updated.').format(number=len(motion_result)), - }) + return Response( + { + "detail": _("{number} motions successfully updated.").format( + number=len(motion_result) + ) + } + ) - @detail_route(methods=['post', 'delete']) + @detail_route(methods=["post", "delete"]) def support(self, request, pk=None): """ Special view endpoint to support a motion or withdraw support @@ -481,32 +520,36 @@ class MotionViewSet(ModelViewSet): motion = self.get_object() # Support or unsupport motion. - if request.method == 'POST': + if request.method == "POST": # Support motion. - if not (motion.state.allow_support and - config['motions_min_supporters'] > 0 and - not motion.is_submitter(request.user) and - not motion.is_supporter(request.user)): - raise ValidationError({'detail': _('You can not support this motion.')}) + if not ( + motion.state.allow_support + and config["motions_min_supporters"] > 0 + and not motion.is_submitter(request.user) + and not motion.is_supporter(request.user) + ): + raise ValidationError({"detail": _("You can not support this motion.")}) motion.supporters.add(request.user) - motion.write_log([ugettext_noop('Motion supported')], request.user) + motion.write_log([ugettext_noop("Motion supported")], request.user) # Send new supporter via autoupdate because users without permission # to see users may not have it but can get it now. inform_changed_data([request.user]) - message = _('You have supported this motion successfully.') + message = _("You have supported this motion successfully.") else: # Unsupport motion. # request.method == 'DELETE' if not motion.state.allow_support or not motion.is_supporter(request.user): - raise ValidationError({'detail': _('You can not unsupport this motion.')}) + raise ValidationError( + {"detail": _("You can not unsupport this motion.")} + ) motion.supporters.remove(request.user) - motion.write_log([ugettext_noop('Motion unsupported')], request.user) - message = _('You have unsupported this motion successfully.') + motion.write_log([ugettext_noop("Motion unsupported")], request.user) + message = _("You have unsupported this motion successfully.") # Initiate response. - return Response({'detail': message}) + return Response({"detail": message}) - @detail_route(methods=['put']) + @detail_route(methods=["put"]) def set_state(self, request, pk=None): """ Special view endpoint to set and reset a state of a motion. @@ -516,7 +559,7 @@ class MotionViewSet(ModelViewSet): """ # Retrieve motion and state. motion = self.get_object() - state = request.data.get('state') + state = request.data.get("state") # Set or reset state. if state is not None: @@ -524,28 +567,40 @@ class MotionViewSet(ModelViewSet): try: state_id = int(state) except ValueError: - raise ValidationError({'detail': _('Invalid data. State must be an integer.')}) + raise ValidationError( + {"detail": _("Invalid data. State must be an integer.")} + ) if state_id not in [item.id for item in motion.state.next_states.all()]: raise ValidationError( - {'detail': _('You can not set the state to %(state_id)d.') % {'state_id': state_id}}) + { + "detail": _("You can not set the state to %(state_id)d.") + % {"state_id": state_id} + } + ) motion.set_state(state_id) else: # Reset state. motion.reset_state() # Save motion. - motion.save(update_fields=['state', 'identifier', 'identifier_number'], skip_autoupdate=True) - message = _('The state of the motion was set to %s.') % motion.state.name + motion.save( + update_fields=["state", "identifier", "identifier_number"], + skip_autoupdate=True, + ) + message = _("The state of the motion was set to %s.") % motion.state.name # Write the log message and initiate response. motion.write_log( - message_list=[ugettext_noop('State set to'), ' ', motion.state.name], + message_list=[ugettext_noop("State set to"), " ", motion.state.name], person=request.user, - skip_autoupdate=True) - inform_changed_data(motion, information='State set to {}.'.format(motion.state.name)) - return Response({'detail': message}) + skip_autoupdate=True, + ) + inform_changed_data( + motion, information="State set to {}.".format(motion.state.name) + ) + return Response({"detail": message}) - @detail_route(methods=['put']) + @detail_route(methods=["put"]) def set_recommendation(self, request, pk=None): """ Special view endpoint to set a recommendation of a motion. @@ -555,7 +610,7 @@ class MotionViewSet(ModelViewSet): """ # Retrieve motion and recommendation state. motion = self.get_object() - recommendation_state = request.data.get('recommendation') + recommendation_state = request.data.get("recommendation") # Set or reset recommendation. if recommendation_state is not None: @@ -563,31 +618,46 @@ class MotionViewSet(ModelViewSet): try: recommendation_state_id = int(recommendation_state) except ValueError: - raise ValidationError({'detail': _('Invalid data. Recommendation must be an integer.')}) - recommendable_states = State.objects.filter(workflow=motion.workflow_id, recommendation_label__isnull=False) - if recommendation_state_id not in [item.id for item in recommendable_states]: raise ValidationError( - {'detail': _('You can not set the recommendation to {recommendation_state_id}.').format( - recommendation_state_id=recommendation_state_id)}) + {"detail": _("Invalid data. Recommendation must be an integer.")} + ) + recommendable_states = State.objects.filter( + workflow=motion.workflow_id, recommendation_label__isnull=False + ) + if recommendation_state_id not in [ + item.id for item in recommendable_states + ]: + raise ValidationError( + { + "detail": _( + "You can not set the recommendation to {recommendation_state_id}." + ).format(recommendation_state_id=recommendation_state_id) + } + ) motion.set_recommendation(recommendation_state_id) else: # Reset recommendation. motion.recommendation = None # Save motion. - motion.save(update_fields=['recommendation'], skip_autoupdate=True) - label = motion.recommendation.recommendation_label if motion.recommendation else 'None' - message = _('The recommendation of the motion was set to %s.') % label + motion.save(update_fields=["recommendation"], skip_autoupdate=True) + label = ( + motion.recommendation.recommendation_label + if motion.recommendation + else "None" + ) + message = _("The recommendation of the motion was set to %s.") % label # Write the log message and initiate response. motion.write_log( - message_list=[ugettext_noop('Recommendation set to'), ' ', label], + message_list=[ugettext_noop("Recommendation set to"), " ", label], person=request.user, - skip_autoupdate=True) + skip_autoupdate=True, + ) inform_changed_data(motion) - return Response({'detail': message}) + return Response({"detail": message}) - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.atomic def manage_multiple_recommendation(self, request): """ @@ -595,7 +665,7 @@ class MotionViewSet(ModelViewSet): Send POST {"motions": [... see schema ...]} to changed the recommendations. """ - motions = request.data.get('motions') + motions = request.data.get("motions") schema = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -605,10 +675,7 @@ class MotionViewSet(ModelViewSet): "items": { "type": "object", "properties": { - "id": { - "description": "The id of the motion.", - "type": "integer", - }, + "id": {"description": "The id of the motion.", "type": "integer"}, "recommendation": { "description": "The state id the should become recommendation. Use 0 to clear recommendation field.", "type": "integer", @@ -623,39 +690,54 @@ class MotionViewSet(ModelViewSet): try: jsonschema.validate(motions, schema) except jsonschema.ValidationError as err: - raise ValidationError({'detail': str(err)}) + raise ValidationError({"detail": str(err)}) motion_result = [] for item in motions: # Get motion. try: - motion = Motion.objects.get(pk=item['id']) + motion = Motion.objects.get(pk=item["id"]) except Motion.DoesNotExist: - raise ValidationError({'detail': 'Motion {} does not exist'.format(item['id'])}) + raise ValidationError( + {"detail": "Motion {} does not exist".format(item["id"])} + ) # Set or reset recommendation. - recommendation_state_id = item['recommendation'] + recommendation_state_id = item["recommendation"] if recommendation_state_id == 0: # Reset recommendation. motion.recommendation = None else: # Check data and set recommendation. - recommendable_states = State.objects.filter(workflow=motion.workflow_id, recommendation_label__isnull=False) - if recommendation_state_id not in [item.id for item in recommendable_states]: + recommendable_states = State.objects.filter( + workflow=motion.workflow_id, recommendation_label__isnull=False + ) + if recommendation_state_id not in [ + item.id for item in recommendable_states + ]: raise ValidationError( - {'detail': _('You can not set the recommendation to {recommendation_state_id}.').format( - recommendation_state_id=recommendation_state_id)}) + { + "detail": _( + "You can not set the recommendation to {recommendation_state_id}." + ).format(recommendation_state_id=recommendation_state_id) + } + ) motion.set_recommendation(recommendation_state_id) # Save motion. - motion.save(update_fields=['recommendation'], skip_autoupdate=True) - label = motion.recommendation.recommendation_label if motion.recommendation else 'None' + motion.save(update_fields=["recommendation"], skip_autoupdate=True) + label = ( + motion.recommendation.recommendation_label + if motion.recommendation + else "None" + ) # Write the log message. motion.write_log( - message_list=[ugettext_noop('Recommendation set to'), ' ', label], + message_list=[ugettext_noop("Recommendation set to"), " ", label], person=request.user, - skip_autoupdate=True) + skip_autoupdate=True, + ) # Finish motion. motion_result.append(motion) @@ -664,58 +746,73 @@ class MotionViewSet(ModelViewSet): inform_changed_data(motion_result) # Send response. - return Response({ - 'detail': _('{number} motions successfully updated.').format(number=len(motion_result)), - }) + return Response( + { + "detail": _("{number} motions successfully updated.").format( + number=len(motion_result) + ) + } + ) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def follow_recommendation(self, request, pk=None): motion = self.get_object() if motion.recommendation is None: - raise ValidationError({'detail': _('Cannot set an empty recommendation.')}) + raise ValidationError({"detail": _("Cannot set an empty recommendation.")}) # Set state. motion.set_state(motion.recommendation) # Set the special state comment. - extension = request.data.get('state_extension') + extension = request.data.get("state_extension") if extension is not None: motion.state_extension = extension # Save and write log. motion.save( - update_fields=['state', 'identifier', 'identifier_number', 'state_extension'], - skip_autoupdate=True) + update_fields=[ + "state", + "identifier", + "identifier_number", + "state_extension", + ], + skip_autoupdate=True, + ) motion.write_log( - message_list=[ugettext_noop('State set to'), ' ', motion.state.name], + message_list=[ugettext_noop("State set to"), " ", motion.state.name], person=request.user, - skip_autoupdate=True) + skip_autoupdate=True, + ) # Now send all changes to the clients. inform_changed_data(motion) - return Response({'detail': 'Recommendation followed successfully.'}) + return Response({"detail": "Recommendation followed successfully."}) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def create_poll(self, request, pk=None): """ View to create a poll. It is a POST request without any data. """ motion = self.get_object() if not motion.state.allow_create_poll: - raise ValidationError({'detail': 'You can not create a poll in this motion state.'}) + raise ValidationError( + {"detail": "You can not create a poll in this motion state."} + ) try: with transaction.atomic(): poll = motion.create_poll(skip_autoupdate=True) except WorkflowError as e: - raise ValidationError({'detail': e}) - motion.write_log([ugettext_noop('Vote created')], request.user, skip_autoupdate=True) + raise ValidationError({"detail": e}) + motion.write_log( + [ugettext_noop("Vote created")], request.user, skip_autoupdate=True + ) inform_changed_data(motion) - return Response({ - 'detail': _('Vote created successfully.'), - 'createdPollId': poll.pk}) + return Response( + {"detail": _("Vote created successfully."), "createdPollId": poll.pk} + ) - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.atomic def manage_multiple_tags(self, request): """ @@ -723,7 +820,7 @@ class MotionViewSet(ModelViewSet): Send POST {"motions": [... see schema ...]} to changed the tags. """ - motions = request.data.get('motions') + motions = request.data.get("motions") schema = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -733,16 +830,11 @@ class MotionViewSet(ModelViewSet): "items": { "type": "object", "properties": { - "id": { - "description": "The id of the motion.", - "type": "integer", - }, + "id": {"description": "The id of the motion.", "type": "integer"}, "tags": { "description": "An array of tag ids the should become tags. Use an empty array to clear tag field.", "type": "array", - "items": { - "type": "integer", - }, + "items": {"type": "integer"}, "uniqueItems": True, }, }, @@ -755,21 +847,25 @@ class MotionViewSet(ModelViewSet): try: jsonschema.validate(motions, schema) except jsonschema.ValidationError as err: - raise ValidationError({'detail': str(err)}) + raise ValidationError({"detail": str(err)}) motion_result = [] for item in motions: # Get motion. try: - motion = Motion.objects.get(pk=item['id']) + motion = Motion.objects.get(pk=item["id"]) except Motion.DoesNotExist: - raise ValidationError({'detail': 'Motion {} does not exist'.format(item['id'])}) + raise ValidationError( + {"detail": "Motion {} does not exist".format(item["id"])} + ) # Set new tags - for tag_id in item['tags']: + for tag_id in item["tags"]: if not Tag.objects.filter(pk=tag_id).exists(): - raise ValidationError({'detail': 'Tag {} does not exist'.format(tag_id)}) - motion.tags.set(item['tags']) + raise ValidationError( + {"detail": "Tag {} does not exist".format(tag_id)} + ) + motion.tags.set(item["tags"]) # Finish motion. motion_result.append(motion) @@ -778,9 +874,13 @@ class MotionViewSet(ModelViewSet): inform_changed_data(motion_result) # Send response. - return Response({ - 'detail': _('{number} motions successfully updated.').format(number=len(motion_result)), - }) + return Response( + { + "detail": _("{number} motions successfully updated.").format( + number=len(motion_result) + ) + } + ) class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): @@ -789,6 +889,7 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): There are the following views: update, partial_update and destroy. """ + queryset = MotionPoll.objects.all() serializer_class = MotionPollSerializer @@ -796,8 +897,9 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): """ Returns True if the user has required permissions. """ - return (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage_metadata')) + return has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage_metadata" + ) def update(self, *args, **kwargs): """ @@ -805,7 +907,7 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): """ response = super().update(*args, **kwargs) poll = self.get_object() - poll.motion.write_log([ugettext_noop('Vote updated')], self.request.user) + poll.motion.write_log([ugettext_noop("Vote updated")], self.request.user) return response def destroy(self, *args, **kwargs): @@ -814,7 +916,7 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): """ poll = self.get_object() result = super().destroy(*args, **kwargs) - poll.motion.write_log([ugettext_noop('Vote deleted')], self.request.user) + poll.motion.write_log([ugettext_noop("Vote deleted")], self.request.user) return result @@ -825,6 +927,7 @@ class MotionChangeRecommendationViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = MotionChangeRecommendationAccessPermissions() queryset = MotionChangeRecommendation.objects.all() @@ -832,13 +935,14 @@ class MotionChangeRecommendationViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': - result = has_perm(self.request.user, 'motions.can_see') - elif self.action in ('create', 'destroy', 'partial_update', 'update'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) + elif self.action == "metadata": + result = has_perm(self.request.user, "motions.can_see") + elif self.action in ("create", "destroy", "partial_update", "update"): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) else: result = False return result @@ -850,13 +954,14 @@ class MotionChangeRecommendationViewSet(ModelViewSet): try: return super().create(request, *args, **kwargs) except DjangoValidationError as err: - return Response({'detail': err.message}, status=400) + return Response({"detail": err.message}, status=400) class MotionCommentSectionViewSet(ModelViewSet): """ API endpoint for motion comment fields. """ + access_permissions = MotionCommentSectionAccessPermissions() queryset = MotionCommentSection.objects.all() @@ -864,11 +969,12 @@ class MotionCommentSectionViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('create', 'destroy', 'update', 'partial_update'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) + elif self.action in ("create", "destroy", "update", "partial_update"): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) else: result = False return result @@ -882,19 +988,25 @@ class MotionCommentSectionViewSet(ModelViewSet): result = super().destroy(*args, **kwargs) except ProtectedError as e: # The protected objects can just be motion comments. - motions = ['"' + str(comment.motion) + '"' for comment in e.protected_objects.all()] + motions = [ + '"' + str(comment.motion) + '"' for comment in e.protected_objects.all() + ] count = len(motions) - motions_verbose = ', '.join(motions[:3]) + motions_verbose = ", ".join(motions[:3]) if count > 3: - motions_verbose += ', ...' + motions_verbose += ", ..." if count == 1: - msg = _('This section has still comments in motion {}.').format(motions_verbose) + msg = _("This section has still comments in motion {}.").format( + motions_verbose + ) else: - msg = _('This section has still comments in motions {}.').format(motions_verbose) + msg = _("This section has still comments in motions {}.").format( + motions_verbose + ) - msg += ' ' + _('Please remove all comments before deletion.') - raise ValidationError({'detail': msg}) + msg += " " + _("Please remove all comments before deletion.") + raise ValidationError({"detail": msg}) return result @@ -905,6 +1017,7 @@ class StatuteParagraphViewSet(ModelViewSet): There are the following views: list, retrieve, create, partial_update, update and destroy. """ + access_permissions = StatuteParagraphAccessPermissions() queryset = StatuteParagraph.objects.all() @@ -912,11 +1025,12 @@ class StatuteParagraphViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('create', 'partial_update', 'update', 'destroy'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) + elif self.action in ("create", "partial_update", "update", "destroy"): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) else: result = False return result @@ -929,6 +1043,7 @@ class CategoryViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update, destroy and numbering. """ + access_permissions = CategoryAccessPermissions() queryset = Category.objects.all() @@ -936,18 +1051,25 @@ class CategoryViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': - result = has_perm(self.request.user, 'motions.can_see') - elif self.action in ('create', 'partial_update', 'update', 'destroy', 'numbering'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) + elif self.action == "metadata": + result = has_perm(self.request.user, "motions.can_see") + elif self.action in ( + "create", + "partial_update", + "update", + "destroy", + "numbering", + ): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) else: result = False return result - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def numbering(self, request, pk=None): """ Special view endpoint to number all motions in this category. @@ -967,17 +1089,20 @@ class CategoryViewSet(ModelViewSet): instances = [] # If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier. - without_blank = hasattr(settings, 'MOTION_IDENTIFIER_WITHOUT_BLANKS') and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS + without_blank = ( + hasattr(settings, "MOTION_IDENTIFIER_WITHOUT_BLANKS") + and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS + ) # Prepare ordered list of motions. if not category.prefix: - prefix = '' + prefix = "" elif without_blank: - prefix = '%s' % category.prefix + prefix = "%s" % category.prefix else: - prefix = '%s ' % category.prefix + prefix = "%s " % category.prefix motions = category.motion_set.all() - motion_list = request.data.get('motions') + motion_list = request.data.get("motions") if motion_list: motion_dict = {} for motion in motions.filter(id__in=motion_list): @@ -992,62 +1117,86 @@ class CategoryViewSet(ModelViewSet): motions_to_be_sorted = [] for motion in motions: if motion.is_amendment(): - parent_identifier = motion.parent.identifier or '' + parent_identifier = motion.parent.identifier or "" if without_blank: - prefix = '%s%s' % (parent_identifier, config['motions_amendments_prefix']) + prefix = "%s%s" % ( + parent_identifier, + config["motions_amendments_prefix"], + ) else: - prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix']) + prefix = "%s %s " % ( + parent_identifier, + config["motions_amendments_prefix"], + ) number += 1 - new_identifier = '%s%s' % (prefix, motion.extend_identifier_number(number)) - motions_to_be_sorted.append({ - 'motion': motion, - 'old_identifier': motion.identifier, - 'new_identifier': new_identifier, - 'number': number - }) + new_identifier = "%s%s" % ( + prefix, + motion.extend_identifier_number(number), + ) + motions_to_be_sorted.append( + { + "motion": motion, + "old_identifier": motion.identifier, + "new_identifier": new_identifier, + "number": number, + } + ) # Remove old identifiers for motion in motions: motion.identifier = None # This line is to skip agenda item autoupdate. See agenda/signals.py. - motion.agenda_item_update_information['skip_autoupdate'] = True + motion.agenda_item_update_information["skip_autoupdate"] = True motion.save(skip_autoupdate=True) # Set new identifers and change identifiers of amendments. for obj in motions_to_be_sorted: - if Motion.objects.filter(identifier=obj['new_identifier']).exists(): + if Motion.objects.filter(identifier=obj["new_identifier"]).exists(): # Set the error message and let the code run into an IntegrityError - error_message = _('Numbering aborted because the motion identifier "%s" ' - 'already exists outside of this category.') % obj['new_identifier'] - motion = obj['motion'] - motion.identifier = obj['new_identifier'] - motion.identifier_number = obj['number'] + error_message = ( + _( + 'Numbering aborted because the motion identifier "%s" ' + "already exists outside of this category." + ) + % obj["new_identifier"] + ) + motion = obj["motion"] + motion.identifier = obj["new_identifier"] + motion.identifier_number = obj["number"] motion.save(skip_autoupdate=True) instances.append(motion) instances.append(motion.agenda_item) # Change identifiers of amendments. for child in motion.get_amendments_deep(): - if child.identifier and child.identifier.startswith(obj['old_identifier']): + if child.identifier and child.identifier.startswith( + obj["old_identifier"] + ): child.identifier = re.sub( - obj['old_identifier'], - obj['new_identifier'], + obj["old_identifier"], + obj["new_identifier"], child.identifier, - count=1) + count=1, + ) # This line is to skip agenda item autoupdate. See agenda/signals.py. - child.agenda_item_update_information['skip_autoupdate'] = True + child.agenda_item_update_information[ + "skip_autoupdate" + ] = True child.save(skip_autoupdate=True) instances.append(child) instances.append(child.agenda_item) except IntegrityError: if error_message is None: - error_message = _('Error: At least one identifier of this category does ' - 'already exist in another category.') - response = Response({'detail': error_message}, status=400) + error_message = _( + "Error: At least one identifier of this category does " + "already exist in another category." + ) + response = Response({"detail": error_message}, status=400) else: inform_changed_data(instances) - message = _('All motions in category {category} numbered ' - 'successfully.').format(category=category) - response = Response({'detail': message}) + message = _( + "All motions in category {category} numbered " "successfully." + ).format(category=category) + response = Response({"detail": message}) return response @@ -1058,6 +1207,7 @@ class MotionBlockViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = MotionBlockAccessPermissions() queryset = MotionBlock.objects.all() @@ -1065,18 +1215,25 @@ class MotionBlockViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': - result = has_perm(self.request.user, 'motions.can_see') - elif self.action in ('create', 'partial_update', 'update', 'destroy', 'follow_recommendations'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) + elif self.action == "metadata": + result = has_perm(self.request.user, "motions.can_see") + elif self.action in ( + "create", + "partial_update", + "update", + "destroy", + "follow_recommendations", + ): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) else: result = False return result - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def follow_recommendations(self, request, pk=None): """ View to set the states of all motions of this motion block each to @@ -1091,12 +1248,17 @@ class MotionBlockViewSet(ModelViewSet): motion.save(skip_autoupdate=True) # Write the log message. motion.write_log( - message_list=[ugettext_noop('State set to'), ' ', motion.state.name], + message_list=[ + ugettext_noop("State set to"), + " ", + motion.state.name, + ], person=request.user, - skip_autoupdate=True) + skip_autoupdate=True, + ) instances.append(motion) inform_changed_data(instances) - return Response({'detail': _('Followed recommendations successfully.')}) + return Response({"detail": _("Followed recommendations successfully.")}) class ProtectedErrorMessageMixin: @@ -1104,15 +1266,15 @@ class ProtectedErrorMessageMixin: # The protected objects can just be motions.. motions = ['"' + str(m) + '"' for m in error.protected_objects.all()] count = len(motions) - motions_verbose = ', '.join(motions[:3]) + motions_verbose = ", ".join(motions[:3]) if count > 3: - motions_verbose += ', ...' + motions_verbose += ", ..." if count == 1: - msg = _('This {} is assigned to motion {}.').format(name, motions_verbose) + msg = _("This {} is assigned to motion {}.").format(name, motions_verbose) else: - msg = _('This {} is assigned to motions {}.').format(name, motions_verbose) - return msg + ' ' + _('Please remove all assignments before deletion.') + msg = _("This {} is assigned to motions {}.").format(name, motions_verbose) + return msg + " " + _("Please remove all assignments before deletion.") class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin): @@ -1122,6 +1284,7 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = WorkflowAccessPermissions() queryset = Workflow.objects.all() @@ -1129,13 +1292,14 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': - result = has_perm(self.request.user, 'motions.can_see') - elif self.action in ('create', 'partial_update', 'update', 'destroy'): - result = (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) + elif self.action == "metadata": + result = has_perm(self.request.user, "motions.can_see") + elif self.action in ("create", "partial_update", "update", "destroy"): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) else: result = False return result @@ -1147,18 +1311,24 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin): try: result = super().destroy(*args, **kwargs) except ProtectedError as e: - msg = self.getProtectedErrorMessage('workflow', e) - raise ValidationError({'detail': msg}) + msg = self.getProtectedErrorMessage("workflow", e) + raise ValidationError({"detail": msg}) return result -class StateViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet, - ProtectedErrorMessageMixin): +class StateViewSet( + CreateModelMixin, + UpdateModelMixin, + DestroyModelMixin, + GenericViewSet, + ProtectedErrorMessageMixin, +): """ API endpoint for workflow states. There are the following views: create, update, partial_update and destroy. """ + queryset = State.objects.all() serializer_class = StateSerializer @@ -1166,19 +1336,24 @@ class StateViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, Generi """ Returns True if the user has required permissions. """ - return (has_perm(self.request.user, 'motions.can_see') and - has_perm(self.request.user, 'motions.can_manage')) + return has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) def destroy(self, *args, **kwargs): """ Customized view endpoint to delete a state. """ state = self.get_object() - if state.workflow.first_state.pk == state.pk: # is this the first state of the workflow? - raise ValidationError({'detail': _('You cannot delete the first state of the workflow.')}) + if ( + state.workflow.first_state.pk == state.pk + ): # is this the first state of the workflow? + raise ValidationError( + {"detail": _("You cannot delete the first state of the workflow.")} + ) try: result = super().destroy(*args, **kwargs) except ProtectedError as e: - msg = self.getProtectedErrorMessage('workflow', e) - raise ValidationError({'detail': msg}) + msg = self.getProtectedErrorMessage("workflow", e) + raise ValidationError({"detail": msg}) return result diff --git a/openslides/poll/majority.py b/openslides/poll/majority.py index 34643390d..7f498542b 100644 --- a/openslides/poll/majority.py +++ b/openslides/poll/majority.py @@ -1,7 +1,7 @@ # Common majority methods for all apps using polls. The first one should be the default. majorityMethods = ( - {'value': 'simple_majority', 'display_name': 'Simple majority'}, - {'value': 'two-thirds_majority', 'display_name': 'Two-thirds majority'}, - {'value': 'three-quarters_majority', 'display_name': 'Three-quarters majority'}, - {'value': 'disabled', 'display_name': 'Disabled'}, + {"value": "simple_majority", "display_name": "Simple majority"}, + {"value": "two-thirds_majority", "display_name": "Two-thirds majority"}, + {"value": "three-quarters_majority", "display_name": "Three-quarters majority"}, + {"value": "disabled", "display_name": "Disabled"}, ) diff --git a/openslides/poll/models.py b/openslides/poll/models.py index 643447692..502cd22fa 100644 --- a/openslides/poll/models.py +++ b/openslides/poll/models.py @@ -17,7 +17,8 @@ class BaseOption(models.Model): which has to be a subclass of BaseVote. Otherwise you have to override the get_vote_class method. """ - vote_class: Optional[Type['BaseVote']] = None + + vote_class: Optional[Type["BaseVote"]] = None class Meta: abstract = True @@ -27,7 +28,9 @@ class BaseOption(models.Model): def get_vote_class(self): if self.vote_class is None: - raise NotImplementedError('The option class %s has to have an attribute vote_class.' % self) + raise NotImplementedError( + "The option class %s has to have an attribute vote_class." % self + ) return self.vote_class def __getitem__(self, name): @@ -44,8 +47,14 @@ class BaseVote(models.Model): Subclasses have to define an option field. This must be a ForeignKeyField to a subclass of BasePoll. """ - weight = models.DecimalField(default=Decimal('1'), null=True, validators=[ - MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) + + weight = models.DecimalField( + default=Decimal("1"), + null=True, + validators=[MinValueValidator(Decimal("-2"))], + max_digits=15, + decimal_places=6, + ) value = models.CharField(max_length=255, null=True) class Meta: @@ -73,12 +82,28 @@ class CollectDefaultVotesMixin(models.Model): Mixin for a poll to collect the default vote values for valid votes, invalid votes and votes cast. """ - votesvalid = models.DecimalField(null=True, blank=True, validators=[ - MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) - votesinvalid = models.DecimalField(null=True, blank=True, validators=[ - MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) - votescast = models.DecimalField(null=True, blank=True, validators=[ - MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) + + votesvalid = models.DecimalField( + null=True, + blank=True, + validators=[MinValueValidator(Decimal("-2"))], + max_digits=15, + decimal_places=6, + ) + votesinvalid = models.DecimalField( + null=True, + blank=True, + validators=[MinValueValidator(Decimal("-2"))], + max_digits=15, + decimal_places=6, + ) + votescast = models.DecimalField( + null=True, + blank=True, + validators=[MinValueValidator(Decimal("-2"))], + max_digits=15, + decimal_places=6, + ) class Meta: abstract = True @@ -87,13 +112,16 @@ class CollectDefaultVotesMixin(models.Model): """ Returns one of the strings of the percent base. """ - raise NotImplementedError('You have to provide a get_percent_base_choice() method.') + raise NotImplementedError( + "You have to provide a get_percent_base_choice() method." + ) class PublishPollMixin(models.Model): """ Mixin for a poll to add a flag whether the poll is published or not. """ + published = models.BooleanField(default=False) class Meta: @@ -108,7 +136,8 @@ class BasePoll(models.Model): """ Base poll class. """ - vote_values = ['Votes'] + + vote_values = ["Votes"] class Meta: abstract = True @@ -183,7 +212,7 @@ class BasePoll(models.Model): try: vote = self.get_votes().filter(option=option_id).get(value=value) except ObjectDoesNotExist: - values.append(self.get_vote_class()(value=value, weight='')) + values.append(self.get_vote_class()(value=value, weight="")) else: values.append(vote) return values @@ -195,15 +224,18 @@ def print_value(value, percent_base=0): 'undocumented' or the vote value with percent value if so. """ if value == -1: - verbose_value = _('majority') + verbose_value = _("majority") elif value == -2: - verbose_value = _('undocumented') + verbose_value = _("undocumented") elif value is None: - verbose_value = _('undocumented') + verbose_value = _("undocumented") else: if percent_base: - locale.setlocale(locale.LC_ALL, '') - verbose_value = u'%d (%s %%)' % (value, locale.format('%.1f', value * percent_base)) + locale.setlocale(locale.LC_ALL, "") + verbose_value = "%d (%s %%)" % ( + value, + locale.format("%.1f", value * percent_base), + ) else: - verbose_value = u'%s' % value + verbose_value = "%s" % value return verbose_value diff --git a/openslides/poll/serializers.py b/openslides/poll/serializers.py index e9432537a..974fba17a 100644 --- a/openslides/poll/serializers.py +++ b/openslides/poll/serializers.py @@ -10,8 +10,12 @@ def default_votes_validator(data): than or equal to -2. """ for key in data: - if (key in ('votesvalid', 'votesinvalid', 'votescast') and - data[key] is not None and - data[key] < -2): - raise ValidationError({'detail': _('Value for {} must not be less than -2').format(key)}) + if ( + key in ("votesvalid", "votesinvalid", "votescast") + and data[key] is not None + and data[key] < -2 + ): + raise ValidationError( + {"detail": _("Value for {} must not be less than -2").format(key)} + ) return data diff --git a/openslides/routing.py b/openslides/routing.py index a6e41c131..64fd3bee3 100644 --- a/openslides/routing.py +++ b/openslides/routing.py @@ -5,11 +5,9 @@ from openslides.utils.consumers import SiteConsumer from openslides.utils.middleware import AuthMiddlewareStack -application = ProtocolTypeRouter({ - # WebSocket chat handler - "websocket": AuthMiddlewareStack( - URLRouter([ - url(r"^ws/$", SiteConsumer), - ]) - ) -}) +application = ProtocolTypeRouter( + { + # WebSocket chat handler + "websocket": AuthMiddlewareStack(URLRouter([url(r"^ws/$", SiteConsumer)])) + } +) diff --git a/openslides/topics/__init__.py b/openslides/topics/__init__.py index f12ac1e7f..f91195581 100644 --- a/openslides/topics/__init__.py +++ b/openslides/topics/__init__.py @@ -1 +1 @@ -default_app_config = 'openslides.topics.apps.TopicsAppConfig' +default_app_config = "openslides.topics.apps.TopicsAppConfig" diff --git a/openslides/topics/access_permissions.py b/openslides/topics/access_permissions.py index b0a89a198..bc6bd7165 100644 --- a/openslides/topics/access_permissions.py +++ b/openslides/topics/access_permissions.py @@ -5,4 +5,5 @@ class TopicAccessPermissions(BaseAccessPermissions): """ Access permissions container for Topic and TopicViewSet. """ - base_permission = 'agenda.can_see' + + base_permission = "agenda.can_see" diff --git a/openslides/topics/apps.py b/openslides/topics/apps.py index 5e89c4051..f18b59684 100644 --- a/openslides/topics/apps.py +++ b/openslides/topics/apps.py @@ -4,8 +4,8 @@ from ..utils.projector import register_projector_elements class TopicsAppConfig(AppConfig): - name = 'openslides.topics' - verbose_name = 'OpenSlides Topics' + name = "openslides.topics" + verbose_name = "OpenSlides Topics" angular_site_module = True angular_projector_module = True @@ -23,15 +23,15 @@ class TopicsAppConfig(AppConfig): # Connect signals. permission_change.connect( - get_permission_change_data, - dispatch_uid='topics_get_permission_change_data') + get_permission_change_data, dispatch_uid="topics_get_permission_change_data" + ) # Register viewsets. - router.register(self.get_model('Topic').get_collection_string(), TopicViewSet) + router.register(self.get_model("Topic").get_collection_string(), TopicViewSet) def get_startup_elements(self): """ Yields all Cachables required on startup i. e. opening the websocket connection. """ - yield self.get_model('Topic') + yield self.get_model("Topic") diff --git a/openslides/topics/migrations/0001_initial.py b/openslides/topics/migrations/0001_initial.py index 67a89154e..e472e963a 100644 --- a/openslides/topics/migrations/0001_initial.py +++ b/openslides/topics/migrations/0001_initial.py @@ -11,22 +11,29 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('mediafiles', '0001_initial'), - ] + dependencies = [("mediafiles", "0001_initial")] operations = [ migrations.CreateModel( - name='Topic', + name="Topic", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=256)), - ('text', models.TextField(blank=True)), - ('attachments', models.ManyToManyField(blank=True, to='mediafiles.Mediafile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=256)), + ("text", models.TextField(blank=True)), + ( + "attachments", + models.ManyToManyField(blank=True, to="mediafiles.Mediafile"), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), - ), + ) ] diff --git a/openslides/topics/models.py b/openslides/topics/models.py index ff9185bc3..9a977a537 100644 --- a/openslides/topics/models.py +++ b/openslides/topics/models.py @@ -14,19 +14,21 @@ class TopicManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all topics. In the background all attachments and the related agenda item are prefetched from the database. """ - return self.get_queryset().prefetch_related('attachments', 'agenda_items') + return self.get_queryset().prefetch_related("attachments", "agenda_items") class Topic(RESTModelMixin, models.Model): """ Model for slides with custom content. Used to be called custom slide. """ + access_permissions = TopicAccessPermissions() objects = TopicManager() @@ -37,7 +39,7 @@ class Topic(RESTModelMixin, models.Model): # In theory there could be one then more agenda_item. But we support only # one. See the property agenda_item. - agenda_items = GenericRelation(Item, related_name='topics') + agenda_items = GenericRelation(Item, related_name="topics") class Meta: default_permissions = () @@ -51,10 +53,11 @@ class Topic(RESTModelMixin, models.Model): topic projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='topics/topic', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="topics/topic", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) """ Container for runtime information for agenda app (on create or update of this instance). diff --git a/openslides/topics/projector.py b/openslides/topics/projector.py index dfeb43ef6..4e8c51f0e 100644 --- a/openslides/topics/projector.py +++ b/openslides/topics/projector.py @@ -9,21 +9,22 @@ class TopicSlide(ProjectorElement): """ Slide definitions for topic model. """ - name = 'topics/topic' + + name = "topics/topic" def check_data(self): - if not Topic.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('Topic does not exist.') + if not Topic.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("Topic does not exist.") def update_data(self): data = None try: - topic = Topic.objects.get(pk=self.config_entry.get('id')) + topic = Topic.objects.get(pk=self.config_entry.get("id")) except Topic.DoesNotExist: # Topic does not exist, so just do nothing. pass else: - data = {'agenda_item_id': topic.agenda_item_id} + data = {"agenda_item_id": topic.agenda_item_id} return data diff --git a/openslides/topics/serializers.py b/openslides/topics/serializers.py index 6845598c1..b5faa181b 100644 --- a/openslides/topics/serializers.py +++ b/openslides/topics/serializers.py @@ -8,7 +8,10 @@ class TopicSerializer(ModelSerializer): """ Serializer for core.models.Topic objects. """ - agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3) + + agenda_type = IntegerField( + write_only=True, required=False, min_value=1, max_value=3 + ) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_comment = CharField(write_only=True, required=False, allow_blank=True) agenda_duration = IntegerField(write_only=True, required=False, min_value=1) @@ -17,21 +20,21 @@ class TopicSerializer(ModelSerializer): class Meta: model = Topic fields = ( - 'id', - 'title', - 'text', - 'attachments', - 'agenda_item_id', - 'agenda_type', - 'agenda_parent_id', - 'agenda_comment', - 'agenda_duration', - 'agenda_weight', + "id", + "title", + "text", + "attachments", + "agenda_item_id", + "agenda_type", + "agenda_parent_id", + "agenda_comment", + "agenda_duration", + "agenda_weight", ) def validate(self, data): - if 'text' in data: - data['text'] = validate_html(data['text']) + if "text" in data: + data["text"] = validate_html(data["text"]) return data def create(self, validated_data): @@ -39,18 +42,18 @@ class TopicSerializer(ModelSerializer): Customized create method. Set information about related agenda item into agenda_item_update_information container. """ - agenda_type = validated_data.pop('agenda_type', None) - agenda_parent_id = validated_data.pop('agenda_parent_id', None) - agenda_comment = validated_data.pop('agenda_comment', None) - agenda_duration = validated_data.pop('agenda_duration', None) - agenda_weight = validated_data.pop('agenda_weight', None) - attachments = validated_data.pop('attachments', []) + agenda_type = validated_data.pop("agenda_type", None) + agenda_parent_id = validated_data.pop("agenda_parent_id", None) + agenda_comment = validated_data.pop("agenda_comment", None) + agenda_duration = validated_data.pop("agenda_duration", None) + agenda_weight = validated_data.pop("agenda_weight", None) + attachments = validated_data.pop("attachments", []) topic = Topic(**validated_data) - topic.agenda_item_update_information['type'] = agenda_type - topic.agenda_item_update_information['parent_id'] = agenda_parent_id - topic.agenda_item_update_information['comment'] = agenda_comment - topic.agenda_item_update_information['duration'] = agenda_duration - topic.agenda_item_update_information['weight'] = agenda_weight + topic.agenda_item_update_information["type"] = agenda_type + topic.agenda_item_update_information["parent_id"] = agenda_parent_id + topic.agenda_item_update_information["comment"] = agenda_comment + topic.agenda_item_update_information["duration"] = agenda_duration + topic.agenda_item_update_information["weight"] = agenda_weight topic.save() topic.attachments.add(*attachments) return topic diff --git a/openslides/topics/signals.py b/openslides/topics/signals.py index 61b313021..f0e1495f1 100644 --- a/openslides/topics/signals.py +++ b/openslides/topics/signals.py @@ -7,8 +7,11 @@ def get_permission_change_data(sender, permissions, **kwargs): 'agenda.can_see' permission changes, because topics are strongly connected to the agenda items. """ - topics_app = apps.get_app_config(app_label='topics') + topics_app = apps.get_app_config(app_label="topics") for permission in permissions: # There could be only one 'agenda.can_see' and then we want to return data. - if permission.content_type.app_label == 'agenda' and permission.codename == 'can_see': + if ( + permission.content_type.app_label == "agenda" + and permission.codename == "can_see" + ): yield from topics_app.get_startup_elements() diff --git a/openslides/topics/views.py b/openslides/topics/views.py index 858c14ee4..6b6c1ffc3 100644 --- a/openslides/topics/views.py +++ b/openslides/topics/views.py @@ -12,6 +12,7 @@ class TopicViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = TopicAccessPermissions() queryset = Topic.objects.all() @@ -19,8 +20,8 @@ class TopicViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) else: - result = has_perm(self.request.user, 'agenda.can_manage') + result = has_perm(self.request.user, "agenda.can_manage") return result diff --git a/openslides/urls.py b/openslides/urls.py index 2fbd665f1..b5f45b5fe 100644 --- a/openslides/urls.py +++ b/openslides/urls.py @@ -10,19 +10,19 @@ from .core import views as core_views urlpatterns = [ # URLs for /media/ - url(r'^%s(?P.*)$' % settings.MEDIA_URL.lstrip('/'), protected_serve, {'document_root': settings.MEDIA_ROOT}), - + url( + r"^%s(?P.*)$" % settings.MEDIA_URL.lstrip("/"), + protected_serve, + {"document_root": settings.MEDIA_ROOT}, + ), # When a url without a leading slash is requested, redirect to the url with # the slash. This line has to be after static and media files. - url(r'^(?P.*[^/])$', RedirectView.as_view(url='/%(url)s/', permanent=True)), - + url(r"^(?P.*[^/])$", RedirectView.as_view(url="/%(url)s/", permanent=True)), # URLs for the rest system - url(r'^rest/', include(router.urls)), - + url(r"^rest/", include(router.urls)), # Other urls defined by modules and plugins - url(r'^apps/', include('openslides.urls_apps')), - + url(r"^apps/", include("openslides.urls_apps")), # Main entry point for all angular pages. # Has to be the last entry in the urls.py - url(r'^(?P.*)$', core_views.IndexView.as_view(), name="index"), + url(r"^(?P.*)$", core_views.IndexView.as_view(), name="index"), ] diff --git a/openslides/urls_apps.py b/openslides/urls_apps.py index c5b719c24..94860e0da 100644 --- a/openslides/urls_apps.py +++ b/openslides/urls_apps.py @@ -6,6 +6,6 @@ from openslides.utils.plugins import get_all_plugin_urlpatterns urlpatterns = get_all_plugin_urlpatterns() urlpatterns += [ - url(r'^core/', include('openslides.core.urls')), - url(r'^users/', include('openslides.users.urls')), + url(r"^core/", include("openslides.core.urls")), + url(r"^users/", include("openslides.users.urls")), ] diff --git a/openslides/users/__init__.py b/openslides/users/__init__.py index a12602089..e8ee416bc 100644 --- a/openslides/users/__init__.py +++ b/openslides/users/__init__.py @@ -1 +1 @@ -default_app_config = 'openslides.users.apps.UsersAppConfig' +default_app_config = "openslides.users.apps.UsersAppConfig" diff --git a/openslides/users/access_permissions.py b/openslides/users/access_permissions.py index d85e065fd..40843611e 100644 --- a/openslides/users/access_permissions.py +++ b/openslides/users/access_permissions.py @@ -11,15 +11,17 @@ class UserAccessPermissions(BaseAccessPermissions): """ async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Returns the restricted serialized data for the instance prepared for the user. Removes several fields for non admins so that they do not get the fields they should not get. """ - from .serializers import USERCANSEESERIALIZER_FIELDS, USERCANSEEEXTRASERIALIZER_FIELDS + from .serializers import ( + USERCANSEESERIALIZER_FIELDS, + USERCANSEEEXTRASERIALIZER_FIELDS, + ) def filtered_data(full_data, whitelist): """ @@ -36,19 +38,19 @@ class UserAccessPermissions(BaseAccessPermissions): # Prepare field set for users with "all" data, "many" data and with "little" data. all_data_fields = set(USERCANSEEEXTRASERIALIZER_FIELDS) - all_data_fields.add('groups_id') - all_data_fields.discard('groups') - all_data_fields.add('default_password') + all_data_fields.add("groups_id") + all_data_fields.discard("groups") + all_data_fields.add("default_password") many_data_fields = all_data_fields.copy() - many_data_fields.discard('default_password') + many_data_fields.discard("default_password") litte_data_fields = set(USERCANSEESERIALIZER_FIELDS) - litte_data_fields.add('groups_id') - litte_data_fields.discard('groups') + litte_data_fields.add("groups_id") + litte_data_fields.discard("groups") # Check user permissions. - if await async_has_perm(user_id, 'users.can_see_name'): - if await async_has_perm(user_id, 'users.can_see_extra_data'): - if await async_has_perm(user_id, 'users.can_manage'): + if await async_has_perm(user_id, "users.can_see_name"): + if await async_has_perm(user_id, "users.can_see_extra_data"): + if await async_has_perm(user_id, "users.can_manage"): data = [filtered_data(full, all_data_fields) for full in full_data] else: data = [filtered_data(full, many_data_fields) for full in full_data] @@ -63,10 +65,17 @@ class UserAccessPermissions(BaseAccessPermissions): can_see_collection_strings: Set[str] = set() for collection_string in required_user.get_collection_strings(): - if await async_has_perm(user_id, get_model_from_collection_string(collection_string).can_see_permission): + if await async_has_perm( + user_id, + get_model_from_collection_string( + collection_string + ).can_see_permission, + ): can_see_collection_strings.add(collection_string) - user_ids = await required_user.get_required_users(can_see_collection_strings) + user_ids = await required_user.get_required_users( + can_see_collection_strings + ) # Add oneself. if user_id: @@ -75,9 +84,9 @@ class UserAccessPermissions(BaseAccessPermissions): # Parse data. data = [ filtered_data(full, litte_data_fields) - for full - in full_data - if full['id'] in user_ids] + for full in full_data + if full["id"] in user_ids + ] return data @@ -95,9 +104,8 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions): """ async def get_restricted_data( - self, - full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Returns the restricted serialized data for the instance prepared for the user. Everybody gets only his own personal notes. @@ -107,7 +115,7 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions): data: List[Dict[str, Any]] = [] else: for full in full_data: - if full['user_id'] == user_id: + if full["user_id"] == user_id: data = [full] break else: diff --git a/openslides/users/apps.py b/openslides/users/apps.py index c32b82f83..09b902acc 100644 --- a/openslides/users/apps.py +++ b/openslides/users/apps.py @@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements class UsersAppConfig(AppConfig): - name = 'openslides.users' - verbose_name = 'OpenSlides Users' + name = "openslides.users" + verbose_name = "OpenSlides Users" angular_site_module = True angular_projector_module = True @@ -26,22 +26,26 @@ class UsersAppConfig(AppConfig): # Connect signals. post_permission_creation.connect( create_builtin_groups_and_admin, - dispatch_uid='create_builtin_groups_and_admin') + dispatch_uid="create_builtin_groups_and_admin", + ) permission_change.connect( - get_permission_change_data, - dispatch_uid='users_get_permission_change_data') + get_permission_change_data, dispatch_uid="users_get_permission_change_data" + ) # Disconnect the last_login signal if not settings.ENABLE_LAST_LOGIN_FIELD: - user_logged_in.disconnect(dispatch_uid='update_last_login') + user_logged_in.disconnect(dispatch_uid="update_last_login") # Register viewsets. - router.register(self.get_model('User').get_collection_string(), UserViewSet) - router.register(self.get_model('Group').get_collection_string(), GroupViewSet) - router.register(self.get_model('PersonalNote').get_collection_string(), PersonalNoteViewSet) + router.register(self.get_model("User").get_collection_string(), UserViewSet) + router.register(self.get_model("Group").get_collection_string(), GroupViewSet) + router.register( + self.get_model("PersonalNote").get_collection_string(), PersonalNoteViewSet + ) def get_config_variables(self): from .config_variables import get_config_variables + return get_config_variables() def get_startup_elements(self): @@ -49,7 +53,7 @@ class UsersAppConfig(AppConfig): Yields all Cachables required on startup i. e. opening the websocket connection. """ - for model_name in ('User', 'Group', 'PersonalNote'): + for model_name in ("User", "Group", "PersonalNote"): yield self.get_model(model_name) def get_angular_constants(self): @@ -57,7 +61,12 @@ class UsersAppConfig(AppConfig): permissions = [] for permission in Permission.objects.all(): - permissions.append({ - 'display_name': permission.name, - 'value': '.'.join((permission.content_type.app_label, permission.codename,))}) - return {'permissions': permissions} + permissions.append( + { + "display_name": permission.name, + "value": ".".join( + (permission.content_type.app_label, permission.codename) + ), + } + ) + return {"permissions": permissions} diff --git a/openslides/users/config_variables.py b/openslides/users/config_variables.py index 603dd9c82..3e3ecd053 100644 --- a/openslides/users/config_variables.py +++ b/openslides/users/config_variables.py @@ -12,111 +12,124 @@ def get_config_variables(): """ # Sorting yield ConfigVariable( - name='users_sort_by', - default_value='first_name', - input_type='choice', - label='Sort name of participants by', + name="users_sort_by", + default_value="first_name", + input_type="choice", + label="Sort name of participants by", choices=( - {'value': 'first_name', 'display_name': 'Given name'}, - {'value': 'last_name', 'display_name': 'Surname'}), + {"value": "first_name", "display_name": "Given name"}, + {"value": "last_name", "display_name": "Surname"}, + ), weight=510, - group='Participants', - subgroup='General') + group="Participants", + subgroup="General", + ) yield ConfigVariable( - name='users_enable_presence_view', + name="users_enable_presence_view", default_value=False, - input_type='boolean', - label='Enable participant presence view', + input_type="boolean", + label="Enable participant presence view", weight=511, - group='Participants', - subgroup='General') + group="Participants", + subgroup="General", + ) # PDF yield ConfigVariable( - name='users_pdf_welcometitle', - default_value='Welcome to OpenSlides', - label='Title for access data and welcome PDF', + name="users_pdf_welcometitle", + default_value="Welcome to OpenSlides", + label="Title for access data and welcome PDF", weight=520, - group='Participants', - subgroup='PDF') + group="Participants", + subgroup="PDF", + ) yield ConfigVariable( - name='users_pdf_welcometext', - default_value='[Place for your welcome and help text.]', - label='Help text for access data and welcome PDF', + name="users_pdf_welcometext", + default_value="[Place for your welcome and help text.]", + label="Help text for access data and welcome PDF", weight=530, - group='Participants', - subgroup='PDF') + group="Participants", + subgroup="PDF", + ) # TODO: Use Django's URLValidator here. yield ConfigVariable( - name='users_pdf_url', - default_value='http://example.com:8000', - label='System URL', - help_text='Used for QRCode in PDF of access data.', + name="users_pdf_url", + default_value="http://example.com:8000", + label="System URL", + help_text="Used for QRCode in PDF of access data.", weight=540, - group='Participants', - subgroup='PDF') + group="Participants", + subgroup="PDF", + ) yield ConfigVariable( - name='users_pdf_wlan_ssid', - default_value='', - label='WLAN name (SSID)', - help_text='Used for WLAN QRCode in PDF of access data.', + name="users_pdf_wlan_ssid", + default_value="", + label="WLAN name (SSID)", + help_text="Used for WLAN QRCode in PDF of access data.", weight=550, - group='Participants', - subgroup='PDF') + group="Participants", + subgroup="PDF", + ) yield ConfigVariable( - name='users_pdf_wlan_password', - default_value='', - label='WLAN password', - help_text='Used for WLAN QRCode in PDF of access data.', + name="users_pdf_wlan_password", + default_value="", + label="WLAN password", + help_text="Used for WLAN QRCode in PDF of access data.", weight=560, - group='Participants', - subgroup='PDF') + group="Participants", + subgroup="PDF", + ) yield ConfigVariable( - name='users_pdf_wlan_encryption', - default_value='', - input_type='choice', - label='WLAN encryption', - help_text='Used for WLAN QRCode in PDF of access data.', + name="users_pdf_wlan_encryption", + default_value="", + input_type="choice", + label="WLAN encryption", + help_text="Used for WLAN QRCode in PDF of access data.", choices=( - {'value': '', 'display_name': '---------'}, - {'value': 'WEP', 'display_name': 'WEP'}, - {'value': 'WPA', 'display_name': 'WPA/WPA2'}, - {'value': 'nopass', 'display_name': 'No encryption'}), + {"value": "", "display_name": "---------"}, + {"value": "WEP", "display_name": "WEP"}, + {"value": "WPA", "display_name": "WPA/WPA2"}, + {"value": "nopass", "display_name": "No encryption"}, + ), weight=570, - group='Participants', - subgroup='PDF') + group="Participants", + subgroup="PDF", + ) # Email yield ConfigVariable( - name='users_email_sender', - default_value='noreply@yourdomain.com', - input_type='string', - label='Email sender', + name="users_email_sender", + default_value="noreply@yourdomain.com", + input_type="string", + label="Email sender", weight=600, - group='Participants', - subgroup='Email') + group="Participants", + subgroup="Email", + ) yield ConfigVariable( - name='users_email_subject', - default_value='Your login for {event_name}', - input_type='string', - label='Email subject', - help_text='You can use {event_name} as a placeholder.', + name="users_email_subject", + default_value="Your login for {event_name}", + input_type="string", + label="Email subject", + help_text="You can use {event_name} as a placeholder.", weight=605, - group='Participants', - subgroup='Email') + group="Participants", + subgroup="Email", + ) yield ConfigVariable( - name='users_email_body', - default_value=dedent('''\ + name="users_email_body", + default_value=dedent( + """\ Dear {name}, this is your OpenSlides login for the event {event_name}: @@ -125,10 +138,12 @@ def get_config_variables(): username: {username} password: {password} - This email was generated automatically.'''), - input_type='text', - label='Email body', - help_text='Use these placeholders: {name}, {event_name}, {url}, {username}, {password}. The url referrs to the system url.', + This email was generated automatically.""" + ), + input_type="text", + label="Email body", + help_text="Use these placeholders: {name}, {event_name}, {url}, {username}, {password}. The url referrs to the system url.", weight=610, - group='Participants', - subgroup='Email') + group="Participants", + subgroup="Email", + ) diff --git a/openslides/users/management/commands/createopenslidesuser.py b/openslides/users/management/commands/createopenslidesuser.py index da4b25b3a..e5bd14e83 100644 --- a/openslides/users/management/commands/createopenslidesuser.py +++ b/openslides/users/management/commands/createopenslidesuser.py @@ -7,36 +7,24 @@ class Command(BaseCommand): """ Command to create an OpenSlides user. """ - help = 'Creates an OpenSlides user.' + + help = "Creates an OpenSlides user." def add_arguments(self, parser): - parser.add_argument( - 'first_name', - help='The first name of the new user.' - ) - parser.add_argument( - 'last_name', - help='The last name of the new user.' - ) - parser.add_argument( - 'username', - help='The username of the new user.' - ) - parser.add_argument( - 'password', - help='The password of the new user.' - ) - parser.add_argument( - 'groups_id', - help='The group id of the new user.' - ) + parser.add_argument("first_name", help="The first name of the new user.") + parser.add_argument("last_name", help="The last name of the new user.") + parser.add_argument("username", help="The username of the new user.") + parser.add_argument("password", help="The password of the new user.") + parser.add_argument("groups_id", help="The group id of the new user.") def handle(self, *args, **options): user_data = { - 'first_name': options['first_name'], - 'last_name': options['last_name'], - 'default_password': options['password'], + "first_name": options["first_name"], + "last_name": options["last_name"], + "default_password": options["password"], } - user = User.objects.create_user(options['username'], options['password'], **user_data) - if options['groups_id'].isdigit(): - user.groups.add(int(options['groups_id'])) + user = User.objects.create_user( + options["username"], options["password"], **user_data + ) + if options["groups_id"].isdigit(): + user.groups.add(int(options["groups_id"])) diff --git a/openslides/users/management/commands/createsuperuser.py b/openslides/users/management/commands/createsuperuser.py index 3cd477edb..1bc852174 100644 --- a/openslides/users/management/commands/createsuperuser.py +++ b/openslides/users/management/commands/createsuperuser.py @@ -7,11 +7,12 @@ class Command(BaseCommand): """ Command to create or reset the admin user. """ - help = 'Creates or resets the admin user.' + + help = "Creates or resets the admin user." def handle(self, *args, **options): created = User.objects.create_or_reset_admin_user() if created: - self.stdout.write('Admin user successfully created.') + self.stdout.write("Admin user successfully created.") else: - self.stdout.write('Admin user successfully reset.') + self.stdout.write("Admin user successfully reset.") diff --git a/openslides/users/migrations/0001_initial.py b/openslides/users/migrations/0001_initial.py index 2df42c281..f7d398ce7 100644 --- a/openslides/users/migrations/0001_initial.py +++ b/openslides/users/migrations/0001_initial.py @@ -12,55 +12,90 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0006_require_contenttypes_0002'), + ("auth", "0006_require_contenttypes_0002"), # The next line is not a dependency because we also want to support Django 1.8. # ('auth', '0007_alter_validators_add_error_messages'), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField( - default=False, - help_text='Designates that this user has all permissions without explicitly assigning them.', - verbose_name='superuser status')), - ('username', models.CharField(blank=True, max_length=255, unique=True)), - ('first_name', models.CharField(blank=True, max_length=255)), - ('last_name', models.CharField(blank=True, max_length=255)), - ('structure_level', models.CharField(blank=True, default='', max_length=255)), - ('title', models.CharField(blank=True, default='', max_length=50)), - ('about_me', models.TextField(blank=True, default='')), - ('comment', models.TextField(blank=True, default='')), - ('default_password', models.CharField(blank=True, default='', max_length=100)), - ('is_active', models.BooleanField(default=True)), - ('is_present', models.BooleanField(default=False)), - ('groups', models.ManyToManyField( - blank=True, - help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', - related_name='user_set', - related_query_name='user', - to='auth.Group', - verbose_name='groups')), - ('user_permissions', models.ManyToManyField( - blank=True, - help_text='Specific permissions for this user.', - related_name='user_set', - related_query_name='user', - to='auth.Permission', - verbose_name='user permissions')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ("username", models.CharField(blank=True, max_length=255, unique=True)), + ("first_name", models.CharField(blank=True, max_length=255)), + ("last_name", models.CharField(blank=True, max_length=255)), + ( + "structure_level", + models.CharField(blank=True, default="", max_length=255), + ), + ("title", models.CharField(blank=True, default="", max_length=50)), + ("about_me", models.TextField(blank=True, default="")), + ("comment", models.TextField(blank=True, default="")), + ( + "default_password", + models.CharField(blank=True, default="", max_length=100), + ), + ("is_active", models.BooleanField(default=True)), + ("is_present", models.BooleanField(default=False)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), ], options={ - 'permissions': ( - ('can_see_name', 'Can see names of users'), - ('can_see_extra_data', 'Can see extra data of users (e.g. present and comment)'), - ('can_manage', 'Can manage users')), - 'default_permissions': (), - 'ordering': ('last_name', 'first_name', 'username'), + "permissions": ( + ("can_see_name", "Can see names of users"), + ( + "can_see_extra_data", + "Can see extra data of users (e.g. present and comment)", + ), + ("can_manage", "Can manage users"), + ), + "default_permissions": (), + "ordering": ("last_name", "first_name", "username"), }, bases=(openslides.utils.models.RESTModelMixin, models.Model), - ), + ) ] diff --git a/openslides/users/migrations/0002_user_misc_default_groups.py b/openslides/users/migrations/0002_user_misc_default_groups.py index 810af7445..e65121441 100644 --- a/openslides/users/migrations/0002_user_misc_default_groups.py +++ b/openslides/users/migrations/0002_user_misc_default_groups.py @@ -18,8 +18,8 @@ def migrate_groups_and_user_permissions(apps, schema_editor): - the name of the first group is 'Guests', - the name of the second group is 'Registered users'. """ - User = apps.get_model('users', 'User') - Group = apps.get_model('auth', 'Group') + User = apps.get_model("users", "User") + Group = apps.get_model("auth", "Group") try: group_default = Group.objects.get(pk=1) @@ -28,11 +28,14 @@ def migrate_groups_and_user_permissions(apps, schema_editor): # One of the groups does not exist. Just do nothing. pass else: - if group_default.name == 'Guests' and group_registered.name == 'Registered users': + if ( + group_default.name == "Guests" + and group_registered.name == "Registered users" + ): # Rename groups pk 1 and 2. - group_default.name = 'Default' + group_default.name = "Default" group_default.save() - group_registered.name = 'Previous group Registered' + group_registered.name = "Previous group Registered" group_registered.save() # Move users without groups to group pk 2. @@ -51,22 +54,20 @@ def migrate_groups_and_user_permissions(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('auth', '0008_alter_user_username_max_length'), - ('users', '0001_initial'), + ("auth", "0008_alter_user_username_max_length"), + ("users", "0001_initial"), ] operations = [ migrations.AddField( - model_name='user', - name='is_committee', + model_name="user", + name="is_committee", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='user', - name='number', - field=models.CharField(blank=True, default='', max_length=50), - ), - migrations.RunPython( - migrate_groups_and_user_permissions + model_name="user", + name="number", + field=models.CharField(blank=True, default="", max_length=50), ), + migrations.RunPython(migrate_groups_and_user_permissions), ] diff --git a/openslides/users/migrations/0003_group.py b/openslides/users/migrations/0003_group.py index 6f32d4214..13d9a7da6 100644 --- a/openslides/users/migrations/0003_group.py +++ b/openslides/users/migrations/0003_group.py @@ -15,8 +15,8 @@ def create_openslides_groups(apps, schema_editor): """ # We get the model from the versioned app registry; # if we directly import it, it will be the wrong version. - DjangoGroup = apps.get_model('auth', 'Group') - Group = apps.get_model('users', 'Group') + DjangoGroup = apps.get_model("auth", "Group") + Group = apps.get_model("users", "Group") for group in DjangoGroup.objects.all(): Group.objects.create(group_ptr_id=group.pk, name=group.name) @@ -24,31 +24,29 @@ def create_openslides_groups(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('auth', '0008_alter_user_username_max_length'), - ('users', '0002_user_misc_default_groups'), + ("auth", "0008_alter_user_username_max_length"), + ("users", "0002_user_misc_default_groups"), ] operations = [ migrations.CreateModel( - name='Group', - fields=[( - 'group_ptr', - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to='auth.Group'))], - options={ - 'default_permissions': (), - }, - bases=(openslides.utils.models.RESTModelMixin, 'auth.group'), - managers=[ - ('objects', openslides.users.models.GroupManager()), + name="Group", + fields=[ + ( + "group_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="auth.Group", + ), + ) ], + options={"default_permissions": ()}, + bases=(openslides.utils.models.RESTModelMixin, "auth.group"), + managers=[("objects", openslides.users.models.GroupManager())], ), - migrations.RunPython( - create_openslides_groups, - ), + migrations.RunPython(create_openslides_groups), ] diff --git a/openslides/users/migrations/0004_personalnote.py b/openslides/users/migrations/0004_personalnote.py index c718095cd..dda4bb822 100644 --- a/openslides/users/migrations/0004_personalnote.py +++ b/openslides/users/migrations/0004_personalnote.py @@ -10,23 +10,42 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('users', '0003_group'), + ("contenttypes", "0002_remove_content_type_name"), + ("users", "0003_group"), ] operations = [ migrations.CreateModel( - name='PersonalNote', + name="PersonalNote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('object_id', models.PositiveIntegerField()), - ('note', models.TextField(blank=True)), - ('star', models.BooleanField(default=False)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='personal_notes', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("object_id", models.PositiveIntegerField()), + ("note", models.TextField(blank=True)), + ("star", models.BooleanField(default=False)), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.ContentType", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="personal_notes", + to=settings.AUTH_USER_MODEL, + ), + ), ], - options={ - 'default_permissions': (), - }, - ), + options={"default_permissions": ()}, + ) ] diff --git a/openslides/users/migrations/0005_personalnote_rework.py b/openslides/users/migrations/0005_personalnote_rework.py index e1bbd66c1..63be1d0dd 100644 --- a/openslides/users/migrations/0005_personalnote_rework.py +++ b/openslides/users/migrations/0005_personalnote_rework.py @@ -12,32 +12,34 @@ import openslides.utils.models class Migration(migrations.Migration): - dependencies = [ - ('users', '0004_personalnote'), - ] + dependencies = [("users", "0004_personalnote")] operations = [ - migrations.RemoveField( - model_name='personalnote', - name='content_type', - ), - migrations.RemoveField( - model_name='personalnote', - name='user', - ), - migrations.DeleteModel( - name='PersonalNote', - ), + migrations.RemoveField(model_name="personalnote", name="content_type"), + migrations.RemoveField(model_name="personalnote", name="user"), + migrations.DeleteModel(name="PersonalNote"), migrations.CreateModel( - name='PersonalNote', + name="PersonalNote", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('notes', jsonfield.fields.JSONField()), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("notes", jsonfield.fields.JSONField()), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], - options={ - 'default_permissions': (), - }, + options={"default_permissions": ()}, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), ] diff --git a/openslides/users/migrations/0006_user_email.py b/openslides/users/migrations/0006_user_email.py index 4c5f800f5..0b9fccb25 100644 --- a/openslides/users/migrations/0006_user_email.py +++ b/openslides/users/migrations/0006_user_email.py @@ -7,19 +7,17 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0005_personalnote_rework'), - ] + dependencies = [("users", "0005_personalnote_rework")] operations = [ migrations.AddField( - model_name='user', - name='email', + model_name="user", + name="email", field=models.EmailField(blank=True, max_length=254), ), migrations.AddField( - model_name='user', - name='last_email_send', + model_name="user", + name="last_email_send", field=models.DateTimeField(blank=True, null=True), ), ] diff --git a/openslides/users/migrations/0007_superadmin.py b/openslides/users/migrations/0007_superadmin.py index ef2c69d5e..d9206f002 100644 --- a/openslides/users/migrations/0007_superadmin.py +++ b/openslides/users/migrations/0007_superadmin.py @@ -15,7 +15,7 @@ def create_superadmin_group(apps, schema_editor): users from it to the new superadmin group and delete it. If not, check for the staff group and assign all users to the superadmin group. """ - Group = apps.get_model('users', 'Group') + Group = apps.get_model("users", "Group") # If no groups exists at all, skip this migration if Group.objects.count() == 0: @@ -27,12 +27,12 @@ def create_superadmin_group(apps, schema_editor): superadmin = Group.objects.get(pk=2) created_superadmin_group = False except Group.DoesNotExist: - superadmin = Group(pk=2, name='__temp__') + superadmin = Group(pk=2, name="__temp__") superadmin.save(skip_autoupdate=True) created_superadmin_group = True if not created_superadmin_group: - new_delegate = Group(name='Delegates2') + new_delegate = Group(name="Delegates2") new_delegate.save(skip_autoupdate=True) new_delegate.permissions.set(superadmin.permissions.all()) superadmin.permissions.set([]) @@ -43,7 +43,7 @@ def create_superadmin_group(apps, schema_editor): finished_moving_users = False try: - admin = Group.objects.get(name='Admin') + admin = Group.objects.get(name="Admin") for user in admin.user_set.all(): user.groups.add(superadmin) user.groups.remove(admin) @@ -54,25 +54,21 @@ def create_superadmin_group(apps, schema_editor): if not finished_moving_users: try: - staff = Group.objects.get(name='Staff') + staff = Group.objects.get(name="Staff") for user in staff.user_set.all(): user.groups.add(superadmin) except Group.DoesNotExist: pass - superadmin.name = 'Admin' + superadmin.name = "Admin" superadmin.save(skip_autoupdate=True) if not created_superadmin_group: - new_delegate.name = 'Delegates' + new_delegate.name = "Delegates" new_delegate.save(skip_autoupdate=True) class Migration(migrations.Migration): - dependencies = [ - ('users', '0006_user_email'), - ] + dependencies = [("users", "0006_user_email")] - operations = [ - migrations.RunPython(create_superadmin_group), - ] + operations = [migrations.RunPython(create_superadmin_group)] diff --git a/openslides/users/models.py b/openslides/users/models.py index b6ca42b10..35a3215cf 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -33,19 +33,24 @@ class UserManager(BaseUserManager): Customized manager that creates new users only with a password and a username. It also supports our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all users. In the background all groups are prefetched from the database together with all permissions and content types. """ - return self.get_queryset().prefetch_related(Prefetch( - 'groups', - queryset=Group.objects - .select_related('group_ptr') - .prefetch_related(Prefetch( - 'permissions', - queryset=Permission.objects.select_related('content_type'))))) + return self.get_queryset().prefetch_related( + Prefetch( + "groups", + queryset=Group.objects.select_related("group_ptr").prefetch_related( + Prefetch( + "permissions", + queryset=Permission.objects.select_related("content_type"), + ) + ), + ) + ) def create_user(self, username, password, skip_autoupdate=False, **kwargs): """ @@ -64,14 +69,11 @@ class UserManager(BaseUserManager): """ created = False try: - admin = self.get(username='admin') + admin = self.get(username="admin") except ObjectDoesNotExist: - admin = self.model( - username='admin', - last_name='Administrator', - ) + admin = self.model(username="admin", last_name="Administrator") created = True - admin.default_password = 'admin' + admin.default_password = "admin" admin.password = make_password(admin.default_password) admin.save(skip_autoupdate=True) admin.groups.add(GROUP_ADMIN_PK) @@ -85,12 +87,13 @@ class UserManager(BaseUserManager): last_name = last_name.strip() if first_name and last_name: - base_name = ' '.join((first_name, last_name)) + base_name = " ".join((first_name, last_name)) else: base_name = first_name or last_name if not base_name: - raise ValueError("Either 'first_name' or 'last_name' must not be " - "empty.") + raise ValueError( + "Either 'first_name' or 'last_name' must not be " "empty." + ) if not self.filter(username=base_name).exists(): generated_username = base_name @@ -98,7 +101,7 @@ class UserManager(BaseUserManager): counter = 0 while True: counter += 1 - test_name = '%s %d' % (base_name, counter) + test_name = "%s %d" % (base_name, counter) if not self.filter(username=test_name).exists(): generated_username = test_name break @@ -109,9 +112,9 @@ class UserManager(BaseUserManager): """ Generates a random passwort. Do not use l, o, I, O, 1 or 0. """ - chars = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789' + chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" size = 8 - return ''.join([choice(chars) for i in range(size)]) + return "".join([choice(chars) for i in range(size)]) class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): @@ -121,78 +124,54 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): in other OpenSlides apps like motion submitter or (assignment) election candidates. """ + access_permissions = UserAccessPermissions() - USERNAME_FIELD = 'username' + USERNAME_FIELD = "username" - username = models.CharField( - max_length=255, - unique=True, - blank=True) + username = models.CharField(max_length=255, unique=True, blank=True) - first_name = models.CharField( - max_length=255, - blank=True) + first_name = models.CharField(max_length=255, blank=True) - last_name = models.CharField( - max_length=255, - blank=True) + last_name = models.CharField(max_length=255, blank=True) email = models.EmailField(blank=True) - last_email_send = models.DateTimeField( - blank=True, - null=True) + last_email_send = models.DateTimeField(blank=True, null=True) # TODO: Try to remove the default argument in the following fields. - structure_level = models.CharField( - max_length=255, - blank=True, - default='') + structure_level = models.CharField(max_length=255, blank=True, default="") - title = models.CharField( - max_length=50, - blank=True, - default='') + title = models.CharField(max_length=50, blank=True, default="") - number = models.CharField( - max_length=50, - blank=True, - default='') + number = models.CharField(max_length=50, blank=True, default="") - about_me = models.TextField( - blank=True, - default='') + about_me = models.TextField(blank=True, default="") - comment = models.TextField( - blank=True, - default='') + comment = models.TextField(blank=True, default="") - default_password = models.CharField( - max_length=100, - blank=True, - default='') + default_password = models.CharField(max_length=100, blank=True, default="") - is_active = models.BooleanField( - default=True) + is_active = models.BooleanField(default=True) - is_present = models.BooleanField( - default=False) + is_present = models.BooleanField(default=False) - is_committee = models.BooleanField( - default=False) + is_committee = models.BooleanField(default=False) objects = UserManager() class Meta: default_permissions = () permissions = ( - ('can_see_name', 'Can see names of users'), - ('can_see_extra_data', 'Can see extra data of users (e.g. present and comment)'), - ('can_manage', 'Can manage users'), + ("can_see_name", "Can see names of users"), + ( + "can_see_extra_data", + "Can see extra data of users (e.g. present and comment)", + ), + ("can_manage", "Can manage users"), ) - ordering = ('last_name', 'first_name', 'username', ) + ordering = ("last_name", "first_name", "username") def __str__(self): # Strip white spaces from the name parts @@ -201,7 +180,7 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): # The user has a last_name and a first_name if first_name and last_name: - name = ' '.join((self.first_name, self.last_name)) + name = " ".join((self.first_name, self.last_name)) # The user has only a first_name or a last_name or no name else: name = first_name or last_name or self.username @@ -214,8 +193,8 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): Overridden method to skip autoupdate if only last_login field was updated as it is done during login. """ - if kwargs.get('update_fields') == ['last_login']: - kwargs['skip_autoupdate'] = True + if kwargs.get("update_fields") == ["last_login"]: + kwargs["skip_autoupdate"] = True return super().save(*args, **kwargs) def delete(self, skip_autoupdate=False, *args, **kwargs): @@ -224,18 +203,23 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): user projector element is disabled. """ Projector.remove_any( - skip_autoupdate=skip_autoupdate, - name='users/user', - id=self.pk) - return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore + skip_autoupdate=skip_autoupdate, name="users/user", id=self.pk + ) + return super().delete( # type: ignore + skip_autoupdate=skip_autoupdate, *args, **kwargs + ) def has_perm(self, perm): """ This method is closed. Do not use it but use openslides.utils.auth.has_perm. """ - raise RuntimeError('Do not use user.has_perm() but use openslides.utils.auth.has_perm') + raise RuntimeError( + "Do not use user.has_perm() but use openslides.utils.auth.has_perm" + ) - def send_invitation_email(self, connection, subject, message, skip_autoupdate=False): + def send_invitation_email( + self, connection, subject, message, skip_autoupdate=False + ): """ Sends an invitation email to the users. Returns True on success, False on failiure. May raise an ValidationError, if something went wrong. @@ -247,30 +231,37 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): # no error is raised and this is replaced with ''. class format_dict(dict): def __missing__(self, key): - return '' + return "" - message_format = format_dict({ - 'name': str(self), - 'event_name': config['general_event_name'], - 'url': config['users_pdf_url'], - 'username': self.username, - 'password': self.default_password}) + message_format = format_dict( + { + "name": str(self), + "event_name": config["general_event_name"], + "url": config["users_pdf_url"], + "username": self.username, + "password": self.default_password, + } + ) message = message.format(**message_format) - subject_format = format_dict({'event_name': config['general_event_name']}) + subject_format = format_dict({"event_name": config["general_event_name"]}) subject = subject.format(**subject_format) # Create an email and send it. - email = mail.EmailMessage(subject, message, config['users_email_sender'], [self.email]) + email = mail.EmailMessage( + subject, message, config["users_email_sender"], [self.email] + ) try: count = connection.send_messages([email]) except smtplib.SMTPDataError as e: error = e.smtp_code - helptext = '' + helptext = "" if error == 554: - helptext = ' Is the email sender correct?' + helptext = " Is the email sender correct?" connection.close() - raise ValidationError({'detail': 'Error {}. Cannot send email.{}'.format(error, helptext)}) + raise ValidationError( + {"detail": "Error {}. Cannot send email.{}".format(error, helptext)} + ) except smtplib.SMTPRecipientsRefused: pass # Run into returning false later else: @@ -296,22 +287,29 @@ class GroupManager(_GroupManager): """ Customized manager that supports our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all groups. In the background all permissions with the content types are prefetched from the database. """ - return (self.get_queryset() - .select_related('group_ptr') - .prefetch_related(Prefetch( - 'permissions', - queryset=Permission.objects.select_related('content_type')))) + return ( + self.get_queryset() + .select_related("group_ptr") + .prefetch_related( + Prefetch( + "permissions", + queryset=Permission.objects.select_related("content_type"), + ) + ) + ) class Group(RESTModelMixin, DjangoGroup): """ Extend the django group with support of our REST and caching system. """ + access_permissions = GroupAccessPermissions() objects = GroupManager() @@ -323,12 +321,13 @@ class PersonalNoteManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ + def get_full_queryset(self): """ Returns the normal queryset with all personal notes. In the background all users are prefetched from the database. """ - return self.get_queryset().select_related('user') + return self.get_queryset().select_related("user") class PersonalNote(RESTModelMixin, models.Model): @@ -336,13 +335,12 @@ class PersonalNote(RESTModelMixin, models.Model): Model for personal notes (e. g. likes/stars) of a user concerning different openslides objects like motions. """ + access_permissions = PersonalNoteAccessPermissions() objects = PersonalNoteManager() - user = models.OneToOneField( - User, - on_delete=models.CASCADE) + user = models.OneToOneField(User, on_delete=models.CASCADE) notes = JSONField() class Meta: diff --git a/openslides/users/projector.py b/openslides/users/projector.py index dc548a562..6ae22f2b5 100644 --- a/openslides/users/projector.py +++ b/openslides/users/projector.py @@ -9,11 +9,12 @@ class UserSlide(ProjectorElement): """ Slide definitions for User model. """ - name = 'users/user' + + name = "users/user" def check_data(self): - if not User.objects.filter(pk=self.config_entry.get('id')).exists(): - raise ProjectorException('User does not exist.') + if not User.objects.filter(pk=self.config_entry.get("id")).exists(): + raise ProjectorException("User does not exist.") def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index d35fb5674..38a9914a8 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -14,25 +14,25 @@ from .models import Group, PersonalNote, User USERCANSEESERIALIZER_FIELDS = ( - 'id', - 'username', - 'title', - 'first_name', - 'last_name', - 'structure_level', - 'number', - 'about_me', - 'groups', - 'is_present', - 'is_committee', + "id", + "username", + "title", + "first_name", + "last_name", + "structure_level", + "number", + "about_me", + "groups", + "is_present", + "is_committee", ) USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + ( - 'email', - 'last_email_send', - 'comment', - 'is_active', + "email", + "last_email_send", + "comment", + "is_active", ) @@ -42,18 +42,25 @@ class UserFullSerializer(ModelSerializer): Serializes all relevant fields for manager. """ + groups = IdPrimaryKeyRelatedField( many=True, required=False, queryset=Group.objects.exclude(pk=1), - help_text=ugettext_lazy('The groups this user belongs to. A user will ' - 'get all permissions granted to each of ' - 'his/her groups.')) + help_text=ugettext_lazy( + "The groups this user belongs to. A user will " + "get all permissions granted to each of " + "his/her groups." + ), + ) class Meta: model = User - fields = USERCANSEEEXTRASERIALIZER_FIELDS + ('default_password', 'session_auth_hash') - read_only_fields = ('last_email_send',) + fields = USERCANSEEEXTRASERIALIZER_FIELDS + ( + "default_password", + "session_auth_hash", + ) + read_only_fields = ("last_email_send",) def validate(self, data): """ @@ -62,22 +69,30 @@ class UserFullSerializer(ModelSerializer): """ try: - action = self.context['view'].action + action = self.context["view"].action except (KeyError, AttributeError): action = None # Check if we are in Patch context, if not, check if we have the mandatory fields - if action != 'partial_update': - if not (data.get('username') or data.get('first_name') or data.get('last_name')): - raise ValidationError({'detail': _('Username, given name and surname can not all be empty.')}) + if action != "partial_update": + if not ( + data.get("username") or data.get("first_name") or data.get("last_name") + ): + raise ValidationError( + { + "detail": _( + "Username, given name and surname can not all be empty." + ) + } + ) # Generate username. But only if it is not set and the serializer is not # called in a PATCH context (partial_update). - if not data.get('username') and action != 'partial_update': - data['username'] = User.objects.generate_username( - data.get('first_name', ''), - data.get('last_name', '')) + if not data.get("username") and action != "partial_update": + data["username"] = User.objects.generate_username( + data.get("first_name", ""), data.get("last_name", "") + ) return data def prepare_password(self, validated_data): @@ -85,9 +100,9 @@ class UserFullSerializer(ModelSerializer): Sets the default password. """ # Prepare setup password. - if not validated_data.get('default_password'): - validated_data['default_password'] = User.objects.generate_password() - validated_data['password'] = make_password(validated_data['default_password']) + if not validated_data.get("default_password"): + validated_data["default_password"] = User.objects.generate_password() + validated_data["password"] = make_password(validated_data["default_password"]) return validated_data def create(self, validated_data): @@ -105,15 +120,21 @@ class PermissionRelatedField(RelatedField): """ A custom field to use for the permission relationship. """ + default_error_messages = { - 'incorrect_value': ugettext_lazy('Incorrect value "{value}". Expected app_label.codename string.'), - 'does_not_exist': ugettext_lazy('Invalid permission "{value}". Object does not exist.')} + "incorrect_value": ugettext_lazy( + 'Incorrect value "{value}". Expected app_label.codename string.' + ), + "does_not_exist": ugettext_lazy( + 'Invalid permission "{value}". Object does not exist.' + ), + } def to_representation(self, value): """ Returns the permission code string (app_label.codename). """ - return '.'.join((value.content_type.app_label, value.codename,)) + return ".".join((value.content_type.app_label, value.codename)) def to_internal_value(self, data): """ @@ -122,13 +143,15 @@ class PermissionRelatedField(RelatedField): (app_label.codename) like to_representation() returns. """ try: - app_label, codename = data.split('.') + app_label, codename = data.split(".") except ValueError: - self.fail('incorrect_value', value=data) + self.fail("incorrect_value", value=data) try: - permission = Permission.objects.get(content_type__app_label=app_label, codename=codename) + permission = Permission.objects.get( + content_type__app_label=app_label, codename=codename + ) except Permission.DoesNotExist: - self.fail('does_not_exist', value=data) + self.fail("does_not_exist", value=data) return permission @@ -136,17 +159,12 @@ class GroupSerializer(ModelSerializer): """ Serializer for django.contrib.auth.models.Group objects. """ - permissions = PermissionRelatedField( - many=True, - queryset=Permission.objects.all()) + + permissions = PermissionRelatedField(many=True, queryset=Permission.objects.all()) class Meta: model = Group - fields = ( - 'id', - 'name', - 'permissions', - ) + fields = ("id", "name", "permissions") def update(self, *args, **kwargs): """ @@ -161,9 +179,10 @@ class PersonalNoteSerializer(ModelSerializer): """ Serializer for users.models.PersonalNote objects. """ + notes = JSONField() class Meta: model = PersonalNote - fields = ('id', 'user', 'notes', ) - read_only_fields = ('user', ) + fields = ("id", "user", "notes") + read_only_fields = ("user",) diff --git a/openslides/users/signals.py b/openslides/users/signals.py index 6a79f9fe7..5b4ac0be4 100644 --- a/openslides/users/signals.py +++ b/openslides/users/signals.py @@ -10,10 +10,13 @@ def get_permission_change_data(sender, permissions=None, **kwargs): """ Yields all necessary collections if 'users.can_see_name' permission changes. """ - users_app = apps.get_app_config(app_label='users') + users_app = apps.get_app_config(app_label="users") for permission in permissions: # There could be only one 'users.can_see_name' and then we want to return data. - if permission.content_type.app_label == users_app.label and permission.codename == 'can_see_name': + if ( + permission.content_type.app_label == users_app.label + and permission.codename == "can_see_name" + ): yield from users_app.get_startup_elements() @@ -29,128 +32,137 @@ def create_builtin_groups_and_admin(**kwargs): return permission_strings = ( - 'agenda.can_be_speaker', - 'agenda.can_manage', - 'agenda.can_manage_list_of_speakers', - 'agenda.can_see', - 'agenda.can_see_internal_items', - 'assignments.can_manage', - 'assignments.can_nominate_other', - 'assignments.can_nominate_self', - 'assignments.can_see', - 'core.can_manage_config', - 'core.can_manage_logos_and_fonts', - 'core.can_manage_projector', - 'core.can_manage_tags', - 'core.can_manage_chat', - 'core.can_see_frontpage', - 'core.can_see_projector', - 'core.can_use_chat', - 'mediafiles.can_manage', - 'mediafiles.can_see', - 'mediafiles.can_see_hidden', - 'mediafiles.can_upload', - 'motions.can_create', - 'motions.can_manage', - 'motions.can_manage_metadata', - 'motions.can_see', - 'motions.can_support', - 'users.can_manage', - 'users.can_see_extra_data', - 'users.can_see_name', ) + "agenda.can_be_speaker", + "agenda.can_manage", + "agenda.can_manage_list_of_speakers", + "agenda.can_see", + "agenda.can_see_internal_items", + "assignments.can_manage", + "assignments.can_nominate_other", + "assignments.can_nominate_self", + "assignments.can_see", + "core.can_manage_config", + "core.can_manage_logos_and_fonts", + "core.can_manage_projector", + "core.can_manage_tags", + "core.can_manage_chat", + "core.can_see_frontpage", + "core.can_see_projector", + "core.can_use_chat", + "mediafiles.can_manage", + "mediafiles.can_see", + "mediafiles.can_see_hidden", + "mediafiles.can_upload", + "motions.can_create", + "motions.can_manage", + "motions.can_manage_metadata", + "motions.can_see", + "motions.can_support", + "users.can_manage", + "users.can_see_extra_data", + "users.can_see_name", + ) permission_query = Q() permission_dict = {} # Load all permissions for permission_string in permission_strings: - app_label, codename = permission_string.split('.') + app_label, codename = permission_string.split(".") query_part = Q(content_type__app_label=app_label) & Q(codename=codename) permission_query = permission_query | query_part - for permission in Permission.objects.select_related('content_type').filter(permission_query): - permission_string = '.'.join((permission.content_type.app_label, permission.codename)) + for permission in Permission.objects.select_related("content_type").filter( + permission_query + ): + permission_string = ".".join( + (permission.content_type.app_label, permission.codename) + ) permission_dict[permission_string] = permission # Default (pk 1 == GROUP_DEFAULT_PK) base_permissions = ( - permission_dict['agenda.can_see'], - permission_dict['agenda.can_see_internal_items'], - permission_dict['assignments.can_see'], - permission_dict['core.can_see_frontpage'], - permission_dict['core.can_see_projector'], - permission_dict['mediafiles.can_see'], - permission_dict['motions.can_see'], - permission_dict['users.can_see_name'], ) - group_default = Group(pk=GROUP_DEFAULT_PK, name='Default') + permission_dict["agenda.can_see"], + permission_dict["agenda.can_see_internal_items"], + permission_dict["assignments.can_see"], + permission_dict["core.can_see_frontpage"], + permission_dict["core.can_see_projector"], + permission_dict["mediafiles.can_see"], + permission_dict["motions.can_see"], + permission_dict["users.can_see_name"], + ) + group_default = Group(pk=GROUP_DEFAULT_PK, name="Default") group_default.save(skip_autoupdate=True) group_default.permissions.add(*base_permissions) # Admin (pk 2 == GROUP_ADMIN_PK) - group_admin = Group(pk=GROUP_ADMIN_PK, name='Admin') + group_admin = Group(pk=GROUP_ADMIN_PK, name="Admin") group_admin.save(skip_autoupdate=True) # Delegates (pk 3) delegates_permissions = ( - permission_dict['agenda.can_see'], - permission_dict['agenda.can_see_internal_items'], - permission_dict['agenda.can_be_speaker'], - permission_dict['assignments.can_see'], - permission_dict['assignments.can_nominate_other'], - permission_dict['assignments.can_nominate_self'], - permission_dict['core.can_see_frontpage'], - permission_dict['core.can_see_projector'], - permission_dict['mediafiles.can_see'], - permission_dict['motions.can_see'], - permission_dict['motions.can_create'], - permission_dict['motions.can_support'], - permission_dict['users.can_see_name'], ) - group_delegates = Group(pk=3, name='Delegates') + permission_dict["agenda.can_see"], + permission_dict["agenda.can_see_internal_items"], + permission_dict["agenda.can_be_speaker"], + permission_dict["assignments.can_see"], + permission_dict["assignments.can_nominate_other"], + permission_dict["assignments.can_nominate_self"], + permission_dict["core.can_see_frontpage"], + permission_dict["core.can_see_projector"], + permission_dict["mediafiles.can_see"], + permission_dict["motions.can_see"], + permission_dict["motions.can_create"], + permission_dict["motions.can_support"], + permission_dict["users.can_see_name"], + ) + group_delegates = Group(pk=3, name="Delegates") group_delegates.save(skip_autoupdate=True) group_delegates.permissions.add(*delegates_permissions) # Staff (pk 4) staff_permissions = ( - permission_dict['agenda.can_see'], - permission_dict['agenda.can_see_internal_items'], - permission_dict['agenda.can_be_speaker'], - permission_dict['agenda.can_manage'], - permission_dict['agenda.can_manage_list_of_speakers'], - permission_dict['assignments.can_see'], - permission_dict['assignments.can_manage'], - permission_dict['assignments.can_nominate_other'], - permission_dict['assignments.can_nominate_self'], - permission_dict['core.can_see_frontpage'], - permission_dict['core.can_see_projector'], - permission_dict['core.can_manage_projector'], - permission_dict['core.can_manage_tags'], - permission_dict['core.can_use_chat'], - permission_dict['mediafiles.can_see'], - permission_dict['mediafiles.can_manage'], - permission_dict['mediafiles.can_upload'], - permission_dict['motions.can_see'], - permission_dict['motions.can_create'], - permission_dict['motions.can_manage'], - permission_dict['motions.can_manage_metadata'], - permission_dict['users.can_see_name'], - permission_dict['users.can_manage'], - permission_dict['users.can_see_extra_data'], - permission_dict['mediafiles.can_see_hidden'],) - group_staff = Group(pk=4, name='Staff') + permission_dict["agenda.can_see"], + permission_dict["agenda.can_see_internal_items"], + permission_dict["agenda.can_be_speaker"], + permission_dict["agenda.can_manage"], + permission_dict["agenda.can_manage_list_of_speakers"], + permission_dict["assignments.can_see"], + permission_dict["assignments.can_manage"], + permission_dict["assignments.can_nominate_other"], + permission_dict["assignments.can_nominate_self"], + permission_dict["core.can_see_frontpage"], + permission_dict["core.can_see_projector"], + permission_dict["core.can_manage_projector"], + permission_dict["core.can_manage_tags"], + permission_dict["core.can_use_chat"], + permission_dict["mediafiles.can_see"], + permission_dict["mediafiles.can_manage"], + permission_dict["mediafiles.can_upload"], + permission_dict["motions.can_see"], + permission_dict["motions.can_create"], + permission_dict["motions.can_manage"], + permission_dict["motions.can_manage_metadata"], + permission_dict["users.can_see_name"], + permission_dict["users.can_manage"], + permission_dict["users.can_see_extra_data"], + permission_dict["mediafiles.can_see_hidden"], + ) + group_staff = Group(pk=4, name="Staff") group_staff.save(skip_autoupdate=True) group_staff.permissions.add(*staff_permissions) # Committees (pk 5) committees_permissions = ( - permission_dict['agenda.can_see'], - permission_dict['agenda.can_see_internal_items'], - permission_dict['assignments.can_see'], - permission_dict['core.can_see_frontpage'], - permission_dict['core.can_see_projector'], - permission_dict['mediafiles.can_see'], - permission_dict['motions.can_see'], - permission_dict['motions.can_create'], - permission_dict['motions.can_support'], - permission_dict['users.can_see_name'], ) - group_committee = Group(pk=5, name='Committees') + permission_dict["agenda.can_see"], + permission_dict["agenda.can_see_internal_items"], + permission_dict["assignments.can_see"], + permission_dict["core.can_see_frontpage"], + permission_dict["core.can_see_projector"], + permission_dict["mediafiles.can_see"], + permission_dict["motions.can_see"], + permission_dict["motions.can_create"], + permission_dict["motions.can_support"], + permission_dict["users.can_see_name"], + ) + group_committee = Group(pk=5, name="Committees") group_committee.save(skip_autoupdate=True) group_committee.permissions.add(*committees_permissions) diff --git a/openslides/users/urls.py b/openslides/users/urls.py index 3039be7d8..abe7c222c 100644 --- a/openslides/users/urls.py +++ b/openslides/users/urls.py @@ -5,27 +5,18 @@ from . import views urlpatterns = [ # Auth - url(r'^login/$', - views.UserLoginView.as_view(), - name='user_login'), - - url(r'^logout/$', - views.UserLogoutView.as_view(), - name='user_logout'), - - url(r'^whoami/$', - views.WhoAmIView.as_view(), - name='user_whoami'), - - url(r'^setpassword/$', - views.SetPasswordView.as_view(), - name='user_setpassword'), - - url(r'^reset-password/$', + url(r"^login/$", views.UserLoginView.as_view(), name="user_login"), + url(r"^logout/$", views.UserLogoutView.as_view(), name="user_logout"), + url(r"^whoami/$", views.WhoAmIView.as_view(), name="user_whoami"), + url(r"^setpassword/$", views.SetPasswordView.as_view(), name="user_setpassword"), + url( + r"^reset-password/$", views.PasswordResetView.as_view(), - name='user_reset_password'), - - url(r'^reset-password-confirm/$', + name="user_reset_password", + ), + url( + r"^reset-password-confirm/$", views.PasswordResetConfirmView.as_view(), - name='password_reset_confirm'), + name="password_reset_confirm", + ), ] diff --git a/openslides/users/views.py b/openslides/users/views.py index 36e1f7700..c41323cf4 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -28,11 +28,7 @@ from ..utils.auth import ( anonymous_is_enabled, has_perm, ) -from ..utils.autoupdate import ( - Element, - inform_changed_data, - inform_changed_elements, -) +from ..utils.autoupdate import Element, inform_changed_data, inform_changed_elements from ..utils.cache import element_cache from ..utils.rest_api import ( ModelViewSet, @@ -55,6 +51,7 @@ from .serializers import GroupSerializer, PermissionRelatedField # Viewsets for the REST API + class UserViewSet(ModelViewSet): """ API endpoint for users. @@ -62,6 +59,7 @@ class UserViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update, destroy and reset_password. """ + access_permissions = UserAccessPermissions() queryset = User.objects.all() @@ -69,16 +67,24 @@ class UserViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': - result = has_perm(self.request.user, 'users.can_see_name') - elif self.action in ('update', 'partial_update'): + elif self.action == "metadata": + result = has_perm(self.request.user, "users.can_see_name") + elif self.action in ("update", "partial_update"): result = self.request.user.is_authenticated - elif self.action in ('create', 'destroy', 'reset_password', 'mass_import', 'mass_invite_email'): - result = (has_perm(self.request.user, 'users.can_see_name') and - has_perm(self.request.user, 'users.can_see_extra_data') and - has_perm(self.request.user, 'users.can_manage')) + elif self.action in ( + "create", + "destroy", + "reset_password", + "mass_import", + "mass_invite_email", + ): + result = ( + has_perm(self.request.user, "users.can_see_name") + and has_perm(self.request.user, "users.can_see_extra_data") + and has_perm(self.request.user, "users.can_manage") + ) else: result = False return result @@ -94,16 +100,18 @@ class UserViewSet(ModelViewSet): """ user = self.get_object() # Check permissions. - if (has_perm(self.request.user, 'users.can_see_name') and - has_perm(request.user, 'users.can_see_extra_data') and - has_perm(request.user, 'users.can_manage')): + if ( + has_perm(self.request.user, "users.can_see_name") + and has_perm(request.user, "users.can_see_extra_data") + and has_perm(request.user, "users.can_manage") + ): # The user has all permissions so he may update every user. - if request.data.get('is_active') is False and user == request.user: + if request.data.get("is_active") is False and user == request.user: # But a user can not deactivate himself. - raise ValidationError({'detail': _('You can not deactivate yourself.')}) + raise ValidationError({"detail": _("You can not deactivate yourself.")}) else: # The user does not have all permissions so he may only update himself. - if str(request.user.pk) != self.kwargs['pk']: + if str(request.user.pk) != self.kwargs["pk"]: self.permission_denied(request) # This is a hack to make request.data mutable. Otherwise fields can not be deleted. @@ -111,7 +119,7 @@ class UserViewSet(ModelViewSet): # Remove fields that the user is not allowed to change. # The list() is required because we want to use del inside the loop. for key in list(request.data.keys()): - if key not in ('username', 'about_me'): + if key not in ("username", "about_me"): del request.data[key] response = super().update(request, *args, **kwargs) # Maybe some group assignments have changed. Better delete the restricted user cache @@ -126,28 +134,28 @@ class UserViewSet(ModelViewSet): """ instance = self.get_object() if instance == self.request.user: - raise ValidationError({'detail': _('You can not delete yourself.')}) + raise ValidationError({"detail": _("You can not delete yourself.")}) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def reset_password(self, request, pk=None): """ View to reset the password using the requested password. """ user = self.get_object() - if isinstance(request.data.get('password'), str): + if isinstance(request.data.get("password"), str): try: - validate_password(request.data.get('password'), user=request.user) + validate_password(request.data.get("password"), user=request.user) except DjangoValidationError as errors: - raise ValidationError({'detail': ' '.join(errors)}) - user.set_password(request.data.get('password')) + raise ValidationError({"detail": " ".join(errors)}) + user.set_password(request.data.get("password")) user.save() - return Response({'detail': _('Password successfully reset.')}) + return Response({"detail": _("Password successfully reset.")}) else: - raise ValidationError({'detail': 'Password has to be a string.'}) + raise ValidationError({"detail": "Password has to be a string."}) - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.atomic def mass_import(self, request): """ @@ -155,9 +163,9 @@ class UserViewSet(ModelViewSet): Example: {"users": [{"first_name": "Max"}, {"first_name": "Maxi"}]} """ - users = request.data.get('users') + users = request.data.get("users") if not isinstance(users, list): - raise ValidationError({'detail': 'Users has to be a list.'}) + raise ValidationError({"detail": "Users has to be a list."}) created_users = [] # List of all track ids of all imported users. The track ids are just used in the client. @@ -171,42 +179,47 @@ class UserViewSet(ModelViewSet): # Skip invalid users. continue data = serializer.prepare_password(serializer.data) - groups = data['groups_id'] - del data['groups_id'] + groups = data["groups_id"] + del data["groups_id"] db_user = User(**data) db_user.save(skip_autoupdate=True) db_user.groups.add(*groups) created_users.append(db_user) - if 'importTrackId' in user: - imported_track_ids.append(user['importTrackId']) + if "importTrackId" in user: + imported_track_ids.append(user["importTrackId"]) # Now infom all clients and send a response inform_changed_data(created_users) - return Response({ - 'detail': _('{number} users successfully imported.').format(number=len(created_users)), - 'importedTrackIds': imported_track_ids}) + return Response( + { + "detail": _("{number} users successfully imported.").format( + number=len(created_users) + ), + "importedTrackIds": imported_track_ids, + } + ) - @list_route(methods=['post']) + @list_route(methods=["post"]) def mass_invite_email(self, request): """ Endpoint to send invitation emails to all given users (by id). Returns the number of emails send. """ - user_ids = request.data.get('user_ids') + user_ids = request.data.get("user_ids") if not isinstance(user_ids, list): - raise ValidationError({'detail': 'User_ids has to be a list.'}) + raise ValidationError({"detail": "User_ids has to be a list."}) for user_id in user_ids: if not isinstance(user_id, int): - raise ValidationError({'detail': 'User_id has to be an int.'}) + raise ValidationError({"detail": "User_id has to be an int."}) # Get subject and body from the response. Do not use the config values # because they might not be translated. - subject = request.data.get('subject') - message = request.data.get('message') + subject = request.data.get("subject") + message = request.data.get("message") if not isinstance(subject, str): - raise ValidationError({'detail': 'Subject has to be a string.'}) + raise ValidationError({"detail": "Subject has to be a string."}) if not isinstance(message, str): - raise ValidationError({'detail': 'Message has to be a string.'}) + raise ValidationError({"detail": "Message has to be a string."}) users = User.objects.filter(pk__in=user_ids) # Sending Emails. Keep track, which users gets an email. @@ -215,18 +228,24 @@ class UserViewSet(ModelViewSet): try: connection.open() except ConnectionRefusedError: - raise ValidationError({'detail': 'Cannot connect to SMTP server on {}:{}'.format( - settings.EMAIL_HOST, - settings.EMAIL_PORT)}) + raise ValidationError( + { + "detail": "Cannot connect to SMTP server on {}:{}".format( + settings.EMAIL_HOST, settings.EMAIL_PORT + ) + } + ) except smtplib.SMTPException as e: - raise ValidationError({'detail': '{}: {}'.format(e.errno, e.strerror)}) + raise ValidationError({"detail": "{}: {}".format(e.errno, e.strerror)}) success_users = [] user_pks_without_email = [] try: for user in users: if user.email: - if user.send_invitation_email(connection, subject, message, skip_autoupdate=True): + if user.send_invitation_email( + connection, subject, message, skip_autoupdate=True + ): success_users.append(user) else: user_pks_without_email.append(user.pk) @@ -235,25 +254,28 @@ class UserViewSet(ModelViewSet): connection.close() inform_changed_data(success_users) - return Response({ - 'count': len(success_users), - 'no_email_ids': user_pks_without_email}) + return Response( + {"count": len(success_users), "no_email_ids": user_pks_without_email} + ) class GroupViewSetMetadata(SimpleMetadata): """ Customized metadata class for OPTIONS requests. """ + def get_field_info(self, field): """ Customized method to change the display name of permission choices. """ field_info = super().get_field_info(field) - if field.field_name == 'permissions': - field_info['choices'] = [ + if field.field_name == "permissions": + field_info["choices"] = [ { - 'value': choice_value, - 'display_name': force_text(choice_name, strings_only=True).split(' | ')[2] + "value": choice_value, + "display_name": force_text(choice_name, strings_only=True).split( + " | " + )[2], } for choice_value, choice_name in field.choices.items() ] @@ -267,6 +289,7 @@ class GroupViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + metadata_class = GroupViewSetMetadata queryset = Group.objects.all() serializer_class = GroupSerializer @@ -276,17 +299,19 @@ class GroupViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == 'metadata': + elif self.action == "metadata": # Every authenticated user can see the metadata. # Anonymous users can do so if they are enabled. result = self.request.user.is_authenticated or anonymous_is_enabled() - elif self.action in ('create', 'partial_update', 'update', 'destroy'): + elif self.action in ("create", "partial_update", "update", "destroy"): # Users with all app permissions can edit groups. - result = (has_perm(self.request.user, 'users.can_see_name') and - has_perm(self.request.user, 'users.can_see_extra_data') and - has_perm(self.request.user, 'users.can_manage')) + result = ( + has_perm(self.request.user, "users.can_see_name") + and has_perm(self.request.user, "users.can_see_extra_data") + and has_perm(self.request.user, "users.can_manage") + ) else: # Deny request in any other case. result = False @@ -300,12 +325,16 @@ class GroupViewSet(ModelViewSet): group = self.get_object() # Collect old and new (given) permissions to get the difference. - old_permissions = list(group.permissions.all()) # Force evaluation so the perms don't change anymore. - permission_names = request.data['permissions'] + old_permissions = list( + group.permissions.all() + ) # Force evaluation so the perms don't change anymore. + permission_names = request.data["permissions"] if isinstance(permission_names, str): permission_names = [permission_names] given_permissions = [ - PermissionRelatedField(read_only=True).to_internal_value(data=perm) for perm in permission_names] + PermissionRelatedField(read_only=True).to_internal_value(data=perm) + for perm in permission_names + ] # Run super to update the group. response = super().update(request, *args, **kwargs) @@ -331,18 +360,25 @@ class GroupViewSet(ModelViewSet): # Some permissions are added. if len(new_permissions) > 0: elements: List[Element] = [] - signal_results = permission_change.send(None, permissions=new_permissions, action='added') + signal_results = permission_change.send( + None, permissions=new_permissions, action="added" + ) all_full_data = async_to_sync(element_cache.get_all_full_data)() for receiver, signal_collections in signal_results: for cachable in signal_collections: - for full_data in all_full_data.get(cachable.get_collection_string(), {}): - elements.append(Element( - id=full_data['id'], - collection_string=cachable.get_collection_string(), - full_data=full_data, - information='', - user_id=None, - disable_history=True)) + for full_data in all_full_data.get( + cachable.get_collection_string(), {} + ): + elements.append( + Element( + id=full_data["id"], + collection_string=cachable.get_collection_string(), + full_data=full_data, + information="", + user_id=None, + disable_history=True, + ) + ) inform_changed_elements(elements) # TODO: Some permissions are deleted. @@ -357,7 +393,7 @@ class GroupViewSet(ModelViewSet): if instance.pk in (GROUP_DEFAULT_PK, GROUP_ADMIN_PK): self.permission_denied(request) # The list() is required to evaluate the query - affected_users_ids = list(instance.user_set.values_list('pk', flat=True)) + affected_users_ids = list(instance.user_set.values_list("pk", flat=True)) # Delete the group self.perform_destroy(instance) @@ -375,6 +411,7 @@ class PersonalNoteViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update, and destroy. """ + access_permissions = PersonalNoteAccessPermissions() queryset = PersonalNote.objects.all() @@ -382,9 +419,15 @@ class PersonalNoteViewSet(ModelViewSet): """ Returns True if the user has required permissions. """ - if self.action in ('list', 'retrieve'): + if self.action in ("list", "retrieve"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action in ('metadata', 'create', 'partial_update', 'update', 'destroy'): + elif self.action in ( + "metadata", + "create", + "partial_update", + "update", + "destroy", + ): # Every authenticated user can see metadata and create personal # notes for himself and can manipulate only his own personal notes. # See self.perform_create(), self.update() and self.destroy(). @@ -421,19 +464,23 @@ class PersonalNoteViewSet(ModelViewSet): # Special API views + class UserLoginView(APIView): """ Login the user. """ - http_method_names = ['get', 'post'] + + http_method_names = ["get", "post"] def post(self, *args, **kwargs): # If the client tells that cookies are disabled, do not continue as guest (if enabled) - if not self.request.data.get('cookies', True): - raise ValidationError({'detail': _('Cookies have to be enabled to use OpenSlides.')}) + if not self.request.data.get("cookies", True): + raise ValidationError( + {"detail": _("Cookies have to be enabled to use OpenSlides.")} + ) form = AuthenticationForm(self.request, data=self.request.data) if not form.is_valid(): - raise ValidationError({'detail': _('Username or password is not correct.')}) + raise ValidationError({"detail": _("Username or password is not correct.")}) self.user = form.get_user() auth_login(self.request, self.user) return super().post(*args, **kwargs) @@ -448,35 +495,36 @@ class UserLoginView(APIView): For POST requests adds the id of the current user to the context. """ - if self.request.method == 'GET': - if config['general_login_info_text']: - context['info_text'] = config['general_login_info_text'] + if self.request.method == "GET": + if config["general_login_info_text"]: + context["info_text"] = config["general_login_info_text"] else: try: - user = User.objects.get(username='admin') + user = User.objects.get(username="admin") except User.DoesNotExist: - context['info_text'] = '' + context["info_text"] = "" else: - if user.check_password('admin'): - context['info_text'] = _( - 'Installation was successfully. Use {username} and ' - '{password} for first login. Important: Please change ' - 'your password!').format( - username='admin', - password='admin') + if user.check_password("admin"): + context["info_text"] = _( + "Installation was successfully. Use {username} and " + "{password} for first login. Important: Please change " + "your password!" + ).format( + username="admin", + password="admin", + ) else: - context['info_text'] = '' + context["info_text"] = "" # Add the privacy policy and legal notice, so the client can display it # even, it is not logged in. - context['privacy_policy'] = config['general_event_privacy_policy'] - context['legal_notice'] = config['general_event_legal_notice'] + context["privacy_policy"] = config["general_event_privacy_policy"] + context["legal_notice"] = config["general_event_legal_notice"] else: # self.request.method == 'POST' - context['user_id'] = self.user.pk - context['user'] = async_to_sync(element_cache.get_element_restricted_data)( - self.user.pk or 0, - self.user.get_collection_string(), - self.user.pk) + context["user_id"] = self.user.pk + context["user"] = async_to_sync(element_cache.get_element_restricted_data)( + self.user.pk or 0, self.user.get_collection_string(), self.user.pk + ) return super().get_context_data(**context) @@ -484,11 +532,12 @@ class UserLogoutView(APIView): """ Logout the user. """ - http_method_names = ['post'] + + http_method_names = ["post"] def post(self, *args, **kwargs): if not self.request.user.is_authenticated: - raise ValidationError({'detail': _('You are not authenticated.')}) + raise ValidationError({"detail": _("You are not authenticated.")}) auth_logout(self.request) return super().post(*args, **kwargs) @@ -497,7 +546,8 @@ class WhoAmIView(APIView): """ Returns the id of the requesting user. """ - http_method_names = ['get'] + + http_method_names = ["get"] def get_context_data(self, **context): """ @@ -508,36 +558,37 @@ class WhoAmIView(APIView): user_id = self.request.user.pk or 0 if user_id: user_data = async_to_sync(element_cache.get_element_restricted_data)( - user_id, - self.request.user.get_collection_string(), - user_id) + user_id, self.request.user.get_collection_string(), user_id + ) else: user_data = None return super().get_context_data( user_id=user_id or None, guest_enabled=anonymous_is_enabled(), user=user_data, - **context) + **context, + ) class SetPasswordView(APIView): """ Users can set a new password for themselves. """ - http_method_names = ['post'] + + http_method_names = ["post"] def post(self, request, *args, **kwargs): user = request.user - if user.check_password(request.data['old_password']): + if user.check_password(request.data["old_password"]): try: - validate_password(request.data.get('new_password'), user=user) + validate_password(request.data.get("new_password"), user=user) except DjangoValidationError as errors: - raise ValidationError({'detail': ' '.join(errors)}) - user.set_password(request.data['new_password']) + raise ValidationError({"detail": " ".join(errors)}) + user.set_password(request.data["new_password"]) user.save() update_session_auth_hash(request, user) else: - raise ValidationError({'detail': _('Old password does not match.')}) + raise ValidationError({"detail": _("Old password does not match.")}) return super().post(request, *args, **kwargs) @@ -549,30 +600,31 @@ class PasswordResetView(APIView): address will receive an email (means Django sends one or more emails to this address) with a one-use only link. """ - http_method_names = ['post'] + + http_method_names = ["post"] use_https = False # TODO: Do we use https? def post(self, request, *args, **kwargs): """ Loop over all users and send emails. """ - to_email = request.data.get('email') + to_email = request.data.get("email") for user in self.get_users(to_email): current_site = get_current_site(request) site_name = current_site.name context = { - 'email': to_email, - 'site_name': site_name, - 'protocol': 'https' if self.use_https else 'http', - 'domain': current_site.domain, - 'path': '/login/reset-password-confirm/', - 'user_id': urlsafe_base64_encode(force_bytes(user.pk)).decode(), - 'token': default_token_generator.make_token(user), - 'username': user.get_username(), + "email": to_email, + "site_name": site_name, + "protocol": "https" if self.use_https else "http", + "domain": current_site.domain, + "path": "/login/reset-password-confirm/", + "user_id": urlsafe_base64_encode(force_bytes(user.pk)).decode(), + "token": default_token_generator.make_token(user), + "username": user.get_username(), } # Send a django.core.mail.EmailMessage to `to_email`. - subject = _('Password reset for {}').format(site_name) - subject = ''.join(subject.splitlines()) + subject = _("Password reset for {}").format(site_name) + subject = "".join(subject.splitlines()) body = self.get_email_body(**context) from_email = None # TODO: Add nice from_email here. email_message = mail.EmailMessage(subject, body, from_email, [to_email]) @@ -586,10 +638,9 @@ class PasswordResetView(APIView): that prevent inactive users and users with unusable passwords from resetting their password. """ - active_users = User.objects.filter(**{ - 'email__iexact': email, - 'is_active': True, - }) + active_users = User.objects.filter( + **{"email__iexact": email, "is_active": True} + ) return (u for u in active_users if u.has_usable_password()) def get_email_body(self, **context): @@ -620,23 +671,26 @@ class PasswordResetConfirmView(APIView): Send POST request with {'user_id': , 'token': , 'password' } to set password of this user to the new one. """ - http_method_names = ['post'] + + http_method_names = ["post"] def post(self, request, *args, **kwargs): - uidb64 = request.data.get('user_id') - token = request.data.get('token') - password = request.data.get('password') + uidb64 = request.data.get("user_id") + token = request.data.get("token") + password = request.data.get("password") if not (uidb64 and token and password): - raise ValidationError({'detail': _('You have to provide user_id, token and password.')}) + raise ValidationError( + {"detail": _("You have to provide user_id, token and password.")} + ) user = self.get_user(uidb64) if user is None: - raise ValidationError({'detail': _('User does not exist.')}) + raise ValidationError({"detail": _("User does not exist.")}) if not default_token_generator.check_token(user, token): - raise ValidationError({'detail': _('Invalid token.')}) + raise ValidationError({"detail": _("Invalid token.")}) try: validate_password(password, user=user) except DjangoValidationError as errors: - raise ValidationError({'detail': ' '.join(errors)}) + raise ValidationError({"detail": " ".join(errors)}) user.set_password(password) user.save() return super().post(request, *args, **kwargs) diff --git a/openslides/utils/access_permissions.py b/openslides/utils/access_permissions.py index eb49737f5..3908516df 100644 --- a/openslides/utils/access_permissions.py +++ b/openslides/utils/access_permissions.py @@ -14,7 +14,7 @@ class BaseAccessPermissions: from this base class for every autoupdate root model. """ - base_permission = '' + base_permission = "" """ Set to a permission the user needs to see the element. @@ -40,8 +40,8 @@ class BaseAccessPermissions: return bool(user_id) or await async_anonymous_is_enabled() async def get_restricted_data( - self, full_data: List[Dict[str, Any]], - user_id: int) -> List[Dict[str, Any]]: + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: """ Returns the restricted serialized data for the instance prepared for the user. @@ -67,7 +67,9 @@ class RequiredUsers: """ return set(self.callables.keys()) - def add_collection_string(self, collection_string: str, callable: Callable[[Dict[str, Any]], Set[int]]) -> None: + def add_collection_string( + self, collection_string: str, callable: Callable[[Dict[str, Any]], Set[int]] + ) -> None: """ Add a callable for a collection_string to get the required users of the elements. diff --git a/openslides/utils/arguments.py b/openslides/utils/arguments.py index 74321fa46..35a0ca426 100644 --- a/openslides/utils/arguments.py +++ b/openslides/utils/arguments.py @@ -2,7 +2,7 @@ from argparse import Namespace from typing import Any, Optional -class OpenSlidesArguments(): +class OpenSlidesArguments: args: Optional[Namespace] = None def __getitem__(self, key: str) -> Any: diff --git a/openslides/utils/auth.py b/openslides/utils/auth.py index 7cfc4cd0c..7de638f7c 100644 --- a/openslides/utils/auth.py +++ b/openslides/utils/auth.py @@ -15,8 +15,8 @@ GROUP_DEFAULT_PK = 1 # This is the hard coded pk for the default group. GROUP_ADMIN_PK = 2 # This is the hard coded pk for the admin group. # Hard coded collection string for users and groups -group_collection_string = 'users/group' -user_collection_string = 'users/user' +group_collection_string = "users/group" +user_collection_string = "users/user" def get_group_model() -> Model: @@ -26,10 +26,13 @@ def get_group_model() -> Model: try: return apps.get_model(settings.AUTH_GROUP_MODEL, require_ready=False) except ValueError: - raise ImproperlyConfigured("AUTH_GROUP_MODEL must be of the form 'app_label.model_name'") + raise ImproperlyConfigured( + "AUTH_GROUP_MODEL must be of the form 'app_label.model_name'" + ) except LookupError: raise ImproperlyConfigured( - "AUTH_GROUP_MODEL refers to model '%s' that has not been installed" % settings.AUTH_GROUP_MODEL + "AUTH_GROUP_MODEL refers to model '%s' that has not been installed" + % settings.AUTH_GROUP_MODEL ) @@ -55,27 +58,35 @@ async def async_has_perm(user_id: int, perm: str) -> bool: has_perm = False elif not user_id: # Use the permissions from the default group. - default_group = await element_cache.get_element_full_data(group_collection_string, GROUP_DEFAULT_PK) + default_group = await element_cache.get_element_full_data( + group_collection_string, GROUP_DEFAULT_PK + ) if default_group is None: - raise RuntimeError('Default Group does not exist.') - has_perm = perm in default_group['permissions'] + raise RuntimeError("Default Group does not exist.") + has_perm = perm in default_group["permissions"] else: - user_data = await element_cache.get_element_full_data(user_collection_string, user_id) + user_data = await element_cache.get_element_full_data( + user_collection_string, user_id + ) if user_data is None: - raise RuntimeError('User with id {} does not exist.'.format(user_id)) - if GROUP_ADMIN_PK in user_data['groups_id']: + raise RuntimeError("User with id {} does not exist.".format(user_id)) + if GROUP_ADMIN_PK in user_data["groups_id"]: # User in admin group (pk 2) grants all permissions. has_perm = True else: # Get all groups of the user and then see, if one group has the required # permission. If the user has no groups, then use the default group. - group_ids = user_data['groups_id'] or [GROUP_DEFAULT_PK] + group_ids = user_data["groups_id"] or [GROUP_DEFAULT_PK] for group_id in group_ids: - group = await element_cache.get_element_full_data(group_collection_string, group_id) + group = await element_cache.get_element_full_data( + group_collection_string, group_id + ) if group is None: - raise RuntimeError('User is in non existing group with id {}.'.format(group_id)) + raise RuntimeError( + "User is in non existing group with id {}.".format(group_id) + ) - if perm in group['permissions']: + if perm in group["permissions"]: has_perm = True break else: @@ -119,16 +130,18 @@ async def async_in_some_groups(user_id: int, groups: List[int]) -> bool: # Use the permissions from the default group. in_some_groups = GROUP_DEFAULT_PK in groups else: - user_data = await element_cache.get_element_full_data(user_collection_string, user_id) + user_data = await element_cache.get_element_full_data( + user_collection_string, user_id + ) if user_data is None: - raise RuntimeError('User with id {} does not exist.'.format(user_id)) - if GROUP_ADMIN_PK in user_data['groups_id']: + raise RuntimeError("User with id {} does not exist.".format(user_id)) + if GROUP_ADMIN_PK in user_data["groups_id"]: # User in admin group (pk 2) grants all permissions. in_some_groups = True else: # Get all groups of the user and then see, if one group has the required # permission. If the user has no groups, then use the default group. - group_ids = user_data['groups_id'] or [GROUP_DEFAULT_PK] + group_ids = user_data["groups_id"] or [GROUP_DEFAULT_PK] for group_id in group_ids: if group_id in groups: in_some_groups = True @@ -143,7 +156,8 @@ def anonymous_is_enabled() -> bool: Returns True if the anonymous user is enabled in the settings. """ from ..core.config import config - return config['general_system_enable_anonymous'] + + return config["general_system_enable_anonymous"] async def async_anonymous_is_enabled() -> bool: @@ -151,11 +165,15 @@ async def async_anonymous_is_enabled() -> bool: Like anonymous_is_enabled but async. """ from ..core.config import config + if config.key_to_id is None: await config.build_key_to_id() config.key_to_id = cast(Dict[str, int], config.key_to_id) - element = await element_cache.get_element_full_data(config.get_collection_string(), config.key_to_id['general_system_enable_anonymous']) - return False if element is None else element['value'] + element = await element_cache.get_element_full_data( + config.get_collection_string(), + config.key_to_id["general_system_enable_anonymous"], + ) + return False if element is None else element["value"] AnyUser = Union[Model, int, AnonymousUser, None] @@ -187,5 +205,6 @@ def user_to_user_id(user: AnyUser) -> int: user_id = user.pk else: raise TypeError( - "Unsupported type for user. User {} has type {}.".format(user, type(user))) + "Unsupported type for user. User {} has type {}.".format(user, type(user)) + ) return user_id diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 8514b7c1d..495f2e477 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -11,33 +11,34 @@ from .cache import element_cache, get_element_id Element = TypedDict( - 'Element', + "Element", { - 'id': int, - 'collection_string': str, - 'full_data': Optional[Dict[str, Any]], - 'information': str, - 'user_id': Optional[int], - 'disable_history': bool, - } + "id": int, + "collection_string": str, + "full_data": Optional[Dict[str, Any]], + "information": str, + "user_id": Optional[int], + "disable_history": bool, + }, ) AutoupdateFormat = TypedDict( - 'AutoupdateFormat', + "AutoupdateFormat", { - 'changed': Dict[str, List[Dict[str, Any]]], - 'deleted': Dict[str, List[int]], - 'from_change_id': int, - 'to_change_id': int, - 'all_data': bool, + "changed": Dict[str, List[Dict[str, Any]]], + "deleted": Dict[str, List[int]], + "from_change_id": int, + "to_change_id": int, + "all_data": bool, }, ) def inform_changed_data( - instances: Union[Iterable[Model], Model], - information: str = '', - user_id: Optional[int] = None) -> None: + instances: Union[Iterable[Model], Model], + information: str = "", + user_id: Optional[int] = None, +) -> None: """ Informs the autoupdate system and the caching system about the creation or update of an element. @@ -48,7 +49,7 @@ def inform_changed_data( """ root_instances = set() if not isinstance(instances, Iterable): - instances = (instances, ) + instances = (instances,) for instance in instances: try: @@ -79,9 +80,10 @@ def inform_changed_data( def inform_deleted_data( - deleted_elements: Iterable[Tuple[str, int]], - information: str = '', - user_id: Optional[int] = None) -> None: + deleted_elements: Iterable[Tuple[str, int]], + information: str = "", + user_id: Optional[int] = None, +) -> None: """ Informs the autoupdate system and the caching system about the deletion of elements. @@ -119,7 +121,7 @@ def inform_changed_elements(changed_elements: Iterable[Element]) -> None: """ elements = {} for changed_element in changed_elements: - key = changed_element['collection_string'] + str(changed_element['id']) + key = changed_element["collection_string"] + str(changed_element["id"]) elements[key] = changed_element bundle = autoupdate_bundle.get(threading.get_ident()) @@ -141,6 +143,7 @@ class AutoupdateBundleMiddleware: """ Middleware to handle autoupdate bundling. """ + def __init__(self, get_response: Any) -> None: self.get_response = get_response # One-time configuration and initialization. @@ -163,6 +166,7 @@ def handle_changed_elements(elements: Iterable[Element]) -> None: Does nothing if elements is empty. """ + async def update_cache(elements: Iterable[Element]) -> int: """ Async helper function to update the cache. @@ -171,8 +175,8 @@ def handle_changed_elements(elements: Iterable[Element]) -> None: """ cache_elements: Dict[str, Optional[Dict[str, Any]]] = {} for element in elements: - element_id = get_element_id(element['collection_string'], element['id']) - cache_elements[element_id] = element['full_data'] + element_id = get_element_id(element["collection_string"], element["id"]) + cache_elements[element_id] = element["full_data"] return await element_cache.change_elements(cache_elements) async def async_handle_collection_elements(elements: Iterable[Element]) -> None: @@ -185,11 +189,7 @@ def handle_changed_elements(elements: Iterable[Element]) -> None: # Send autoupdate channel_layer = get_channel_layer() await channel_layer.group_send( - "autoupdate", - { - "type": "send_data", - "change_id": change_id, - }, + "autoupdate", {"type": "send_data", "change_id": change_id} ) if elements: @@ -199,14 +199,16 @@ def handle_changed_elements(elements: Iterable[Element]) -> None: # Convert history instances to Elements. history_elements: List[Element] = [] for history_instance in history_instances: - history_elements.append(Element( - id=history_instance.get_rest_pk(), - collection_string=history_instance.get_collection_string(), - full_data=history_instance.get_full_data(), - information='', - user_id=None, - disable_history=True, # This does not matter because history elements can never be part of the history itself. - )) + history_elements.append( + Element( + id=history_instance.get_rest_pk(), + collection_string=history_instance.get_collection_string(), + full_data=history_instance.get_full_data(), + information="", + user_id=None, + disable_history=True, # This does not matter because history elements can never be part of the history itself. + ) + ) # Chain elements and history elements. itertools.chain(elements, history_elements) @@ -217,7 +219,9 @@ def handle_changed_elements(elements: Iterable[Element]) -> None: ) -def save_history(elements: Iterable[Element]) -> Iterable: # TODO: Try to write Iterable[History] here +def save_history( + elements: Iterable[Element] +) -> Iterable: # TODO: Try to write Iterable[History] here """ Thin wrapper around the call of history saving manager method. diff --git a/openslides/utils/cache.py b/openslides/utils/cache.py index ebc91ed57..f338656b1 100644 --- a/openslides/utils/cache.py +++ b/openslides/utils/cache.py @@ -45,11 +45,12 @@ class ElementCache: """ def __init__( - self, - use_restricted_data_cache: bool = False, - cache_provider_class: Type[ElementCacheProvider] = RedisCacheProvider, - cachable_provider: Callable[[], List[Cachable]] = get_all_cachables, - start_time: int = None) -> None: + self, + use_restricted_data_cache: bool = False, + cache_provider_class: Type[ElementCacheProvider] = RedisCacheProvider, + cachable_provider: Callable[[], List[Cachable]] = get_all_cachables, + start_time: int = None, + ) -> None: """ Initializes the cache. @@ -63,7 +64,9 @@ class ElementCache: # Start time is used as first change_id if there is non in redis if start_time is None: # Use the miliseconds (rounted) since the 2016-02-29. - start_time = int((datetime.utcnow() - datetime(2016, 2, 29)).total_seconds()) * 1000 + start_time = ( + int((datetime.utcnow() - datetime(2016, 2, 29)).total_seconds()) * 1000 + ) self.start_time = start_time # Contains Futures to controll, that only one client updates the restricted_data. @@ -79,7 +82,10 @@ class ElementCache: """ # This method is neccessary to lazy load the cachables if self._cachables is None: - self._cachables = {cachable.get_collection_string(): cachable for cachable in self.cachable_provider()} + self._cachables = { + cachable.get_collection_string(): cachable + for cachable in self.cachable_provider() + } return self._cachables def ensure_cache(self, reset: bool = False) -> None: @@ -93,7 +99,7 @@ class ElementCache: cache_exists = async_to_sync(self.cache_provider.data_exists)() if reset or not cache_exists: - lock_name = 'ensure_cache' + lock_name = "ensure_cache" # Set a lock so only one process builds the cache if async_to_sync(self.cache_provider.set_lock)(lock_name): try: @@ -101,8 +107,12 @@ class ElementCache: for collection_string, cachable in self.cachables.items(): for element in cachable.get_elements(): mapping.update( - {get_element_id(collection_string, element['id']): - json.dumps(element)}) + { + get_element_id( + collection_string, element["id"] + ): json.dumps(element) + } + ) async_to_sync(self.cache_provider.reset_full_cache)(mapping) finally: async_to_sync(self.cache_provider.del_lock)(lock_name) @@ -113,7 +123,8 @@ class ElementCache: self.ensured = True async def change_elements( - self, elements: Dict[str, Optional[Dict[str, Any]]]) -> int: + self, elements: Dict[str, Optional[Dict[str, Any]]] + ) -> int: """ Changes elements in the cache. @@ -137,7 +148,9 @@ class ElementCache: if deleted_elements: await self.cache_provider.del_elements(deleted_elements) - return await self.cache_provider.add_changed_elements(self.start_time + 1, elements.keys()) + return await self.cache_provider.add_changed_elements( + self.start_time + 1, elements.keys() + ) async def get_all_full_data(self) -> Dict[str, List[Dict[str, Any]]]: """ @@ -154,7 +167,8 @@ class ElementCache: return dict(out) async def get_full_data( - self, change_id: int = 0, max_change_id: int = -1) -> Tuple[Dict[str, List[Dict[str, Any]]], List[str]]: + self, change_id: int = 0, max_change_id: int = -1 + ) -> Tuple[Dict[str, List[Dict[str, Any]]], List[str]]: """ Returns all full_data since change_id until max_change_id (including). max_change_id -1 means the highest change_id. @@ -182,22 +196,33 @@ class ElementCache: # not inform the user about deleted elements. raise RuntimeError( "change_id {} is lower then the lowest change_id in redis {}. " - "Catch this exception and rerun the method with change_id=0." - .format(change_id, lowest_change_id)) + "Catch this exception and rerun the method with change_id=0.".format( + change_id, lowest_change_id + ) + ) - raw_changed_elements, deleted_elements = await self.cache_provider.get_data_since(change_id, max_change_id=max_change_id) + raw_changed_elements, deleted_elements = await self.cache_provider.get_data_since( + change_id, max_change_id=max_change_id + ) return ( - {collection_string: [json.loads(value.decode()) for value in value_list] - for collection_string, value_list in raw_changed_elements.items()}, - deleted_elements) + { + collection_string: [json.loads(value.decode()) for value in value_list] + for collection_string, value_list in raw_changed_elements.items() + }, + deleted_elements, + ) - async def get_element_full_data(self, collection_string: str, id: int) -> Optional[Dict[str, Any]]: + async def get_element_full_data( + self, collection_string: str, id: int + ) -> Optional[Dict[str, Any]]: """ Returns one element as full data. Returns None if the element does not exist. """ - element = await self.cache_provider.get_element(get_element_id(collection_string, id)) + element = await self.cache_provider.get_element( + get_element_id(collection_string, id) + ) if element is None: return None @@ -244,7 +269,9 @@ class ElementCache: change_id = await self.get_current_change_id() if change_id > user_change_id: try: - full_data_elements, deleted_elements = await self.get_full_data(user_change_id + 1) + full_data_elements, deleted_elements = await self.get_full_data( + user_change_id + 1 + ) except RuntimeError: # The user_change_id is lower then the lowest change_id in the cache. # The whole restricted_data for that user has to be recreated. @@ -253,7 +280,9 @@ class ElementCache: else: # Remove deleted elements if deleted_elements: - await self.cache_provider.del_elements(deleted_elements, user_id) + await self.cache_provider.del_elements( + deleted_elements, user_id + ) mapping = {} for collection_string, full_data in full_data_elements.items(): @@ -261,9 +290,13 @@ class ElementCache: elements = await restricter(user_id, full_data) for element in elements: mapping.update( - {get_element_id(collection_string, element['id']): - json.dumps(element)}) - mapping['_config:change_id'] = str(change_id) + { + get_element_id( + collection_string, element["id"] + ): json.dumps(element) + } + ) + mapping["_config:change_id"] = str(change_id) await self.cache_provider.update_restricted_data(user_id, mapping) # Unset the lock await self.cache_provider.del_lock(lock_name) @@ -277,13 +310,17 @@ class ElementCache: while await self.cache_provider.get_lock(lock_name): await asyncio.sleep(0.01) - async def get_all_restricted_data(self, user_id: int) -> Dict[str, List[Dict[str, Any]]]: + async def get_all_restricted_data( + self, user_id: int + ) -> Dict[str, List[Dict[str, Any]]]: """ Like get_all_full_data but with restricted_data for an user. """ if not self.use_restricted_data_cache: all_restricted_data = {} - for collection_string, full_data in (await self.get_all_full_data()).items(): + for collection_string, full_data in ( + await self.get_all_full_data() + ).items(): restricter = self.cachables[collection_string].restrict_elements elements = await restricter(user_id, full_data) all_restricted_data[collection_string] = elements @@ -294,17 +331,15 @@ class ElementCache: out: Dict[str, List[Dict[str, Any]]] = defaultdict(list) restricted_data = await self.cache_provider.get_all_data(user_id) for element_id, data in restricted_data.items(): - if element_id.decode().startswith('_config'): + if element_id.decode().startswith("_config"): continue collection_string, __ = split_element_id(element_id) out[collection_string].append(json.loads(data.decode())) return dict(out) async def get_restricted_data( - self, - user_id: int, - change_id: int = 0, - max_change_id: int = -1) -> Tuple[Dict[str, List[Dict[str, Any]]], List[str]]: + self, user_id: int, change_id: int = 0, max_change_id: int = -1 + ) -> Tuple[Dict[str, List[Dict[str, Any]]], List[str]]: """ Like get_full_data but with restricted_data for an user. """ @@ -313,7 +348,9 @@ class ElementCache: return (await self.get_all_restricted_data(user_id), []) if not self.use_restricted_data_cache: - changed_elements, deleted_elements = await self.get_full_data(change_id, max_change_id) + changed_elements, deleted_elements = await self.get_full_data( + change_id, max_change_id + ) restricted_data = {} for collection_string, full_data in changed_elements.items(): restricter = self.cachables[collection_string].restrict_elements @@ -327,20 +364,29 @@ class ElementCache: # not inform the user about deleted elements. raise RuntimeError( "change_id {} is lower then the lowest change_id in redis {}. " - "Catch this exception and rerun the method with change_id=0." - .format(change_id, lowest_change_id)) + "Catch this exception and rerun the method with change_id=0.".format( + change_id, lowest_change_id + ) + ) # If another coroutine or another daphne server also updates the restricted # data, this waits until it is done. await self.update_restricted_data(user_id) - raw_changed_elements, deleted_elements = await self.cache_provider.get_data_since(change_id, user_id, max_change_id) + raw_changed_elements, deleted_elements = await self.cache_provider.get_data_since( + change_id, user_id, max_change_id + ) return ( - {collection_string: [json.loads(value.decode()) for value in value_list] - for collection_string, value_list in raw_changed_elements.items()}, - deleted_elements) + { + collection_string: [json.loads(value.decode()) for value in value_list] + for collection_string, value_list in raw_changed_elements.items() + }, + deleted_elements, + ) - async def get_element_restricted_data(self, user_id: int, collection_string: str, id: int) -> Optional[Dict[str, Any]]: + async def get_element_restricted_data( + self, user_id: int, collection_string: str, id: int + ) -> Optional[Dict[str, Any]]: """ Returns the restricted_data of one element. @@ -356,7 +402,9 @@ class ElementCache: await self.update_restricted_data(user_id) - out = await self.cache_provider.get_element(get_element_id(collection_string, id), user_id) + out = await self.cache_provider.get_element( + get_element_id(collection_string, id), user_id + ) return json.loads(out.decode()) if out else None async def get_current_change_id(self) -> int: @@ -379,7 +427,7 @@ class ElementCache: """ value = await self.cache_provider.get_lowest_change_id() if not value: - raise RuntimeError('There is no known change_id.') + raise RuntimeError("There is no known change_id.") # Return the score (second element) of the first (and only) element return value @@ -393,9 +441,12 @@ def load_element_cache(restricted_data: bool = True) -> ElementCache: else: cache_provider_class = MemmoryCacheProvider - return ElementCache(cache_provider_class=cache_provider_class, use_restricted_data_cache=restricted_data) + return ElementCache( + cache_provider_class=cache_provider_class, + use_restricted_data_cache=restricted_data, + ) # Set the element_cache -use_restricted_data = getattr(settings, 'RESTRICTED_DATA_CACHE', True) +use_restricted_data = getattr(settings, "RESTRICTED_DATA_CACHE", True) element_cache = load_element_cache(restricted_data=use_restricted_data) diff --git a/openslides/utils/cache_providers.py b/openslides/utils/cache_providers.py index 791ec2ce5..854bebcc0 100644 --- a/openslides/utils/cache_providers.py +++ b/openslides/utils/cache_providers.py @@ -19,59 +19,83 @@ class ElementCacheProvider(Protocol): See RedisCacheProvider as reverence implementation. """ - async def clear_cache(self) -> None: ... + async def clear_cache(self) -> None: + ... - async def reset_full_cache(self, data: Dict[str, str]) -> None: ... + async def reset_full_cache(self, data: Dict[str, str]) -> None: + ... - async def data_exists(self, user_id: Optional[int] = None) -> bool: ... + async def data_exists(self, user_id: Optional[int] = None) -> bool: + ... - async def add_elements(self, elements: List[str]) -> None: ... + async def add_elements(self, elements: List[str]) -> None: + ... - async def del_elements(self, elements: List[str], user_id: Optional[int] = None) -> None: ... + async def del_elements( + self, elements: List[str], user_id: Optional[int] = None + ) -> None: + ... - async def add_changed_elements(self, default_change_id: int, element_ids: Iterable[str]) -> int: ... + async def add_changed_elements( + self, default_change_id: int, element_ids: Iterable[str] + ) -> int: + ... - async def get_all_data(self, user_id: Optional[int] = None) -> Dict[bytes, bytes]: ... + async def get_all_data(self, user_id: Optional[int] = None) -> Dict[bytes, bytes]: + ... async def get_data_since( - self, - change_id: int, - user_id: Optional[int] = None, - max_change_id: int = -1) -> Tuple[Dict[str, List[bytes]], List[str]]: ... + self, change_id: int, user_id: Optional[int] = None, max_change_id: int = -1 + ) -> Tuple[Dict[str, List[bytes]], List[str]]: + ... - async def get_element(self, element_id: str, user_id: Optional[int] = None) -> Optional[bytes]: ... + async def get_element( + self, element_id: str, user_id: Optional[int] = None + ) -> Optional[bytes]: + ... - async def del_restricted_data(self, user_id: int) -> None: ... + async def del_restricted_data(self, user_id: int) -> None: + ... - async def set_lock(self, lock_name: str) -> bool: ... + async def set_lock(self, lock_name: str) -> bool: + ... - async def get_lock(self, lock_name: str) -> bool: ... + async def get_lock(self, lock_name: str) -> bool: + ... - async def del_lock(self, lock_name: str) -> None: ... + async def del_lock(self, lock_name: str) -> None: + ... - async def get_change_id_user(self, user_id: int) -> Optional[int]: ... + async def get_change_id_user(self, user_id: int) -> Optional[int]: + ... - async def update_restricted_data(self, user_id: int, data: Dict[str, str]) -> None: ... + async def update_restricted_data(self, user_id: int, data: Dict[str, str]) -> None: + ... - async def get_current_change_id(self) -> List[Tuple[str, int]]: ... + async def get_current_change_id(self) -> List[Tuple[str, int]]: + ... - async def get_lowest_change_id(self) -> Optional[int]: ... + async def get_lowest_change_id(self) -> Optional[int]: + ... class RedisCacheProvider: """ Cache provider that loads and saves the data to redis. """ - full_data_cache_key: str = 'full_data' - restricted_user_cache_key: str = 'restricted_data:{user_id}' - change_id_cache_key: str = 'change_id' - prefix: str = 'element_cache_' + + full_data_cache_key: str = "full_data" + restricted_user_cache_key: str = "restricted_data:{user_id}" + change_id_cache_key: str = "change_id" + prefix: str = "element_cache_" def get_full_data_cache_key(self) -> str: return "".join((self.prefix, self.full_data_cache_key)) def get_restricted_data_cache_key(self, user_id: int) -> str: - return "".join((self.prefix, self.restricted_user_cache_key.format(user_id=user_id))) + return "".join( + (self.prefix, self.restricted_user_cache_key.format(user_id=user_id)) + ) def get_change_id_cache_key(self) -> str: return "".join((self.prefix, self.change_id_cache_key)) @@ -81,7 +105,11 @@ class RedisCacheProvider: Deleted all cache entries created with this element cache. """ async with get_connection() as redis: - await redis.eval("return redis.call('del', 'fake_key', unpack(redis.call('keys', ARGV[1])))", keys=[], args=["{}*".format(self.prefix)]) + await redis.eval( + "return redis.call('del', 'fake_key', unpack(redis.call('keys', ARGV[1])))", + keys=[], + args=["{}*".format(self.prefix)], + ) async def reset_full_cache(self, data: Dict[str, str]) -> None: """ @@ -95,7 +123,8 @@ class RedisCacheProvider: tr.eval( "return redis.call('del', 'fake_key', unpack(redis.call('keys', ARGV[1])))", keys=[], - args=["{}{}*".format(self.prefix, self.restricted_user_cache_key)]) + args=["{}{}*".format(self.prefix, self.restricted_user_cache_key)], + ) tr.delete(self.get_change_id_cache_key()) tr.delete(self.get_full_data_cache_key()) tr.hmset_dict(self.get_full_data_cache_key(), data) @@ -123,11 +152,11 @@ class RedisCacheProvider: values are the elements. The elements have to be encoded, for example with json. """ async with get_connection() as redis: - await redis.hmset( - self.get_full_data_cache_key(), - *elements) + await redis.hmset(self.get_full_data_cache_key(), *elements) - async def del_elements(self, elements: List[str], user_id: Optional[int] = None) -> None: + async def del_elements( + self, elements: List[str], user_id: Optional[int] = None + ) -> None: """ Deletes elements from the cache. @@ -141,22 +170,24 @@ class RedisCacheProvider: cache_key = self.get_full_data_cache_key() else: cache_key = self.get_restricted_data_cache_key(user_id) - await redis.hdel( - cache_key, - *elements) + await redis.hdel(cache_key, *elements) - async def add_changed_elements(self, default_change_id: int, element_ids: Iterable[str]) -> int: + async def add_changed_elements( + self, default_change_id: int, element_ids: Iterable[str] + ) -> int: """ Saves which elements are change with a change_id. Generates and returns the change_id. """ async with get_connection() as redis: - return int(await redis.eval( - lua_script_change_data, - keys=[self.get_change_id_cache_key()], - args=[default_change_id, *element_ids] - )) + return int( + await redis.eval( + lua_script_change_data, + keys=[self.get_change_id_cache_key()], + args=[default_change_id, *element_ids], + ) + ) async def get_all_data(self, user_id: Optional[int] = None) -> Dict[bytes, bytes]: """ @@ -173,7 +204,9 @@ class RedisCacheProvider: async with get_connection() as redis: return await redis.hgetall(cache_key) - async def get_element(self, element_id: str, user_id: Optional[int] = None) -> Optional[bytes]: + async def get_element( + self, element_id: str, user_id: Optional[int] = None + ) -> Optional[bytes]: """ Returns one element from the cache. @@ -185,15 +218,11 @@ class RedisCacheProvider: cache_key = self.get_restricted_data_cache_key(user_id) async with get_connection() as redis: - return await redis.hget( - cache_key, - element_id) + return await redis.hget(cache_key, element_id) async def get_data_since( - self, - change_id: int, - user_id: Optional[int] = None, - max_change_id: int = -1) -> Tuple[Dict[str, List[bytes]], List[str]]: + self, change_id: int, user_id: Optional[int] = None, max_change_id: int = -1 + ) -> Tuple[Dict[str, List[bytes]], List[str]]: """ Returns all elements since a change_id. @@ -219,8 +248,9 @@ class RedisCacheProvider: # It returns a list where the odd values are the change_id and the # even values the element as json. The function wait_make_dict creates # a python dict from the returned list. - elements: Dict[bytes, Optional[bytes]] = await aioredis.util.wait_make_dict(redis.eval( - """ + elements: Dict[bytes, Optional[bytes]] = await aioredis.util.wait_make_dict( + redis.eval( + """ -- Get change ids of changed elements local element_ids = redis.call('zrangebyscore', KEYS[1], ARGV[1], ARGV[2]) @@ -232,11 +262,13 @@ class RedisCacheProvider: end return elements """, - keys=[self.get_change_id_cache_key(), cache_key], - args=[change_id, redis_max_change_id])) + keys=[self.get_change_id_cache_key(), cache_key], + args=[change_id, redis_max_change_id], + ) + ) for element_id, element_json in elements.items(): - if element_id.startswith(b'_config'): + if element_id.startswith(b"_config"): # Ignore config values from the change_id cache key continue if element_json is None: @@ -288,7 +320,9 @@ class RedisCacheProvider: This is the change_id where the restricted_data was last calculated. """ async with get_connection() as redis: - return await redis.hget(self.get_restricted_data_cache_key(user_id), '_config:change_id') + return await redis.hget( + self.get_restricted_data_cache_key(user_id), "_config:change_id" + ) async def update_restricted_data(self, user_id: int, data: Dict[str, str]) -> None: """ @@ -306,10 +340,8 @@ class RedisCacheProvider: """ async with get_connection() as redis: return await redis.zrevrangebyscore( - self.get_change_id_cache_key(), - withscores=True, - count=1, - offset=0) + self.get_change_id_cache_key(), withscores=True, count=1, offset=0 + ) async def get_lowest_change_id(self) -> Optional[int]: """ @@ -319,8 +351,8 @@ class RedisCacheProvider: """ async with get_connection() as redis: return await redis.zscore( - self.get_change_id_cache_key(), - '_config:lowest_change_id') + self.get_change_id_cache_key(), "_config:lowest_change_id" + ) class MemmoryCacheProvider: @@ -358,12 +390,16 @@ class MemmoryCacheProvider: async def add_elements(self, elements: List[str]) -> None: if len(elements) % 2: - raise ValueError("The argument elements of add_elements has to be a list with an even number of elements.") + raise ValueError( + "The argument elements of add_elements has to be a list with an even number of elements." + ) for i in range(0, len(elements), 2): - self.full_data[elements[i]] = elements[i+1] + self.full_data[elements[i]] = elements[i + 1] - async def del_elements(self, elements: List[str], user_id: Optional[int] = None) -> None: + async def del_elements( + self, elements: List[str], user_id: Optional[int] = None + ) -> None: if user_id is None: cache_dict = self.full_data else: @@ -375,7 +411,9 @@ class MemmoryCacheProvider: except KeyError: pass - async def add_changed_elements(self, default_change_id: int, element_ids: Iterable[str]) -> int: + async def add_changed_elements( + self, default_change_id: int, element_ids: Iterable[str] + ) -> int: element_ids = list(element_ids) try: change_id = (await self.get_current_change_id())[0][1] + 1 @@ -397,7 +435,9 @@ class MemmoryCacheProvider: return str_dict_to_bytes(cache_dict) - async def get_element(self, element_id: str, user_id: Optional[int] = None) -> Optional[bytes]: + async def get_element( + self, element_id: str, user_id: Optional[int] = None + ) -> Optional[bytes]: if user_id is None: cache_dict = self.full_data else: @@ -407,10 +447,8 @@ class MemmoryCacheProvider: return value.encode() if value is not None else None async def get_data_since( - self, - change_id: int, - user_id: Optional[int] = None, - max_change_id: int = -1) -> Tuple[Dict[str, List[bytes]], List[str]]: + self, change_id: int, user_id: Optional[int] = None, max_change_id: int = -1 + ) -> Tuple[Dict[str, List[bytes]], List[str]]: changed_elements: Dict[str, List[bytes]] = defaultdict(list) deleted_elements: List[str] = [] if user_id is None: @@ -420,7 +458,9 @@ class MemmoryCacheProvider: all_element_ids: Set[str] = set() for data_change_id, element_ids in self.change_id_data.items(): - if data_change_id >= change_id and (max_change_id == -1 or data_change_id <= max_change_id): + if data_change_id >= change_id and ( + max_change_id == -1 or data_change_id <= max_change_id + ): all_element_ids.update(element_ids) for element_id in all_element_ids: @@ -455,7 +495,7 @@ class MemmoryCacheProvider: async def get_change_id_user(self, user_id: int) -> Optional[int]: data = self.restricted_data.get(user_id, {}) - change_id = data.get('_config:change_id', None) + change_id = data.get("_config:change_id", None) return int(change_id) if change_id is not None else None async def update_restricted_data(self, user_id: int, data: Dict[str, str]) -> None: @@ -465,7 +505,7 @@ class MemmoryCacheProvider: async def get_current_change_id(self) -> List[Tuple[str, int]]: change_data = self.change_id_data if change_data: - return [('no_usefull_value', max(change_data.keys()))] + return [("no_usefull_value", max(change_data.keys()))] return [] async def get_lowest_change_id(self) -> Optional[int]: @@ -493,9 +533,8 @@ class Cachable(Protocol): """ async def restrict_elements( - self, - user_id: int, - elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + self, user_id: int, elements: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: """ Converts full_data to restricted_data. diff --git a/openslides/utils/consumers.py b/openslides/utils/consumers.py index 6e532ca12..622986692 100644 --- a/openslides/utils/consumers.py +++ b/openslides/utils/consumers.py @@ -13,7 +13,7 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer): Websocket Consumer for the site. """ - groups = ['site'] + groups = ["site"] async def connect(self) -> None: """ @@ -26,76 +26,87 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer): # self.scope['user'] is the full_data dict of the user. For an # anonymous user is it the dict {'id': 0} change_id = None - if not await async_anonymous_is_enabled() and not self.scope['user']['id']: + if not await async_anonymous_is_enabled() and not self.scope["user"]["id"]: await self.close() return - query_string = parse_qs(self.scope['query_string']) - if b'change_id' in query_string: + query_string = parse_qs(self.scope["query_string"]) + if b"change_id" in query_string: try: - change_id = int(query_string[b'change_id'][0]) + change_id = int(query_string[b"change_id"][0]) except ValueError: await self.close() # TODO: Find a way to send an error code return - if b'autoupdate' in query_string and query_string[b'autoupdate'][0].lower() not in [b'0', b'off', b'false']: + if b"autoupdate" in query_string and query_string[b"autoupdate"][ + 0 + ].lower() not in [b"0", b"off", b"false"]: # a positive value in autoupdate. Start autoupdate - await self.channel_layer.group_add('autoupdate', self.channel_name) + await self.channel_layer.group_add("autoupdate", self.channel_name) await self.accept() if change_id is not None: try: - data = await get_element_data(self.scope['user']['id'], change_id) + data = await get_element_data(self.scope["user"]["id"], change_id) except ValueError: # When the change_id is to big, do nothing pass else: - await self.send_json(type='autoupdate', content=data) + await self.send_json(type="autoupdate", content=data) async def disconnect(self, close_code: int) -> None: """ A user disconnects. Remove it from autoupdate. """ - await self.channel_layer.group_discard('autoupdate', self.channel_name) + await self.channel_layer.group_discard("autoupdate", self.channel_name) async def send_notify(self, event: Dict[str, Any]) -> None: """ Send a notify message to the user. """ - user_id = self.scope['user']['id'] + user_id = self.scope["user"]["id"] out = [] - for item in event['incomming']: - users = item.get('users') - reply_channels = item.get('replyChannels') - if ((isinstance(users, list) and user_id in users) - or (isinstance(reply_channels, list) and self.channel_name in reply_channels) - or users is None and reply_channels is None): - item['senderReplyChannelName'] = event.get('senderReplyChannelName') - item['senderUserId'] = event.get('senderUserId') + for item in event["incomming"]: + users = item.get("users") + reply_channels = item.get("replyChannels") + if ( + (isinstance(users, list) and user_id in users) + or ( + isinstance(reply_channels, list) + and self.channel_name in reply_channels + ) + or users is None + and reply_channels is None + ): + item["senderReplyChannelName"] = event.get("senderReplyChannelName") + item["senderUserId"] = event.get("senderUserId") out.append(item) if out: - await self.send_json(type='notify', content=out) + await self.send_json(type="notify", content=out) async def send_data(self, event: Dict[str, Any]) -> None: """ Send changed or deleted elements to the user. """ - change_id = event['change_id'] + change_id = event["change_id"] changed_elements, deleted_elements_ids = await element_cache.get_restricted_data( - self.scope['user']['id'], - change_id, - max_change_id=change_id) + self.scope["user"]["id"], change_id, max_change_id=change_id + ) deleted_elements: Dict[str, List[int]] = defaultdict(list) for element_id in deleted_elements_ids: collection_string, id = split_element_id(element_id) deleted_elements[collection_string].append(id) - await self.send_json(type='autoupdate', content=AutoupdateFormat( - changed=changed_elements, - deleted=deleted_elements, - from_change_id=change_id, - to_change_id=change_id, - all_data=False)) + await self.send_json( + type="autoupdate", + content=AutoupdateFormat( + changed=changed_elements, + deleted=deleted_elements, + from_change_id=change_id, + to_change_id=change_id, + all_data=False, + ), + ) diff --git a/openslides/utils/main.py b/openslides/utils/main.py index 2f784233c..f9e3afa39 100644 --- a/openslides/utils/main.py +++ b/openslides/utils/main.py @@ -14,10 +14,10 @@ from django.utils.crypto import get_random_string from mypy_extensions import NoReturn -DEVELOPMENT_VERSION = 'Development Version' -UNIX_VERSION = 'Unix Version' -WINDOWS_VERSION = 'Windows Version' -WINDOWS_PORTABLE_VERSION = 'Windows Portable Version' +DEVELOPMENT_VERSION = "Development Version" +UNIX_VERSION = "Unix Version" +WINDOWS_VERSION = "Windows Version" +WINDOWS_PORTABLE_VERSION = "Windows Portable Version" class PortableDirNotWritable(Exception): @@ -45,8 +45,8 @@ def detect_openslides_type() -> str: """ Returns the type of this OpenSlides version. """ - if sys.platform == 'win32': - if os.path.basename(sys.executable).lower() == 'openslides.exe': + if sys.platform == "win32": + if os.path.basename(sys.executable).lower() == "openslides.exe": # Note: sys.executable is the path of the *interpreter* # the portable version embeds python so it *is* the interpreter. # The wrappers generated by pip and co. will spawn the usual @@ -73,14 +73,15 @@ def get_default_settings_dir(openslides_type: str = None) -> str: if openslides_type == UNIX_VERSION: parent_directory = os.environ.get( - 'XDG_CONFIG_HOME', os.path.expanduser('~/.config')) + "XDG_CONFIG_HOME", os.path.expanduser("~/.config") + ) elif openslides_type == WINDOWS_VERSION: parent_directory = get_win32_app_data_dir() elif openslides_type == WINDOWS_PORTABLE_VERSION: parent_directory = get_win32_portable_dir() else: - raise TypeError('%s is not a valid OpenSlides type.' % openslides_type) - return os.path.join(parent_directory, 'openslides') + raise TypeError("%s is not a valid OpenSlides type." % openslides_type) + return os.path.join(parent_directory, "openslides") def get_local_settings_dir() -> str: @@ -89,10 +90,12 @@ def get_local_settings_dir() -> str: On Unix systems: 'personal_data/var/' """ - return os.path.join('personal_data', 'var') + return os.path.join("personal_data", "var") -def setup_django_settings_module(settings_path: str = None, local_installation: bool = False) -> None: +def setup_django_settings_module( + settings_path: str = None, local_installation: bool = False +) -> None: """ Sets the environment variable ENVIRONMENT_VARIABLE, that means 'DJANGO_SETTINGS_MODULE', to the given settings. @@ -111,22 +114,26 @@ def setup_django_settings_module(settings_path: str = None, local_installation: settings_dir = get_local_settings_dir() else: settings_dir = get_default_settings_dir() - settings_path = os.path.join(settings_dir, 'settings.py') + settings_path = os.path.join(settings_dir, "settings.py") 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_name = ".".join(settings_file.split(".")[:-1]) + if "." in settings_module_name: + raise ImproperlyConfigured( + "'.' is not an allowed character in the settings-file" + ) # 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) try: - os.environ['PYTHONPATH'] = os.pathsep.join((settings_module_dir, os.environ['PYTHONPATH'])) + 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 + os.environ["PYTHONPATH"] = settings_module_dir # Set the environment variable to the settings module os.environ[ENVIRONMENT_VARIABLE] = settings_module_name @@ -143,18 +150,24 @@ def get_default_settings_context(user_data_dir: str = None) -> Dict[str, str]: # Take it either from command line or get default path default_context = {} if user_data_dir: - default_context['openslides_user_data_dir'] = repr(user_data_dir) - default_context['import_function'] = '' + default_context["openslides_user_data_dir"] = repr(user_data_dir) + default_context["import_function"] = "" else: openslides_type = detect_openslides_type() if openslides_type == WINDOWS_PORTABLE_VERSION: - default_context['openslides_user_data_dir'] = 'get_win32_portable_user_data_dir()' - default_context['import_function'] = 'from openslides.utils.main import get_win32_portable_user_data_dir' + default_context[ + "openslides_user_data_dir" + ] = "get_win32_portable_user_data_dir()" + default_context[ + "import_function" + ] = "from openslides.utils.main import get_win32_portable_user_data_dir" else: data_dir = get_default_user_data_dir(openslides_type) - default_context['openslides_user_data_dir'] = repr(os.path.join(data_dir, 'openslides')) - default_context['import_function'] = '' - default_context['debug'] = 'False' + default_context["openslides_user_data_dir"] = repr( + os.path.join(data_dir, "openslides") + ) + default_context["import_function"] = "" + default_context["debug"] = "False" return default_context @@ -168,13 +181,14 @@ def get_default_user_data_dir(openslides_type: str) -> str: """ if openslides_type == UNIX_VERSION: default_user_data_dir = os.environ.get( - 'XDG_DATA_HOME', os.path.expanduser('~/.local/share')) + "XDG_DATA_HOME", os.path.expanduser("~/.local/share") + ) elif openslides_type == WINDOWS_VERSION: default_user_data_dir = get_win32_app_data_dir() elif openslides_type == WINDOWS_PORTABLE_VERSION: default_user_data_dir = get_win32_portable_dir() else: - raise TypeError('%s is not a valid OpenSlides type.' % openslides_type) + raise TypeError("%s is not a valid OpenSlides type." % openslides_type) return default_user_data_dir @@ -182,14 +196,18 @@ def get_win32_app_data_dir() -> str: """ Returns the directory of Windows' AppData directory. """ - shell32 = ctypes.WinDLL('shell32.dll') # type: ignore + shell32 = ctypes.WinDLL("shell32.dll") # type: ignore SHGetFolderPath = shell32.SHGetFolderPathW SHGetFolderPath.argtypes = ( - ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32, - ctypes.c_wchar_p) + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_void_p, + ctypes.c_uint32, + ctypes.c_wchar_p, + ) SHGetFolderPath.restype = ctypes.c_uint32 - CSIDL_LOCAL_APPDATA = 0x001c + CSIDL_LOCAL_APPDATA = 0x001C MAX_PATH = 260 buf = ctypes.create_unicode_buffer(MAX_PATH) @@ -213,8 +231,9 @@ def get_win32_portable_dir() -> str: fd, test_file = tempfile.mkstemp(dir=portable_dir) except OSError: raise PortableDirNotWritable( - 'Portable directory is not writeable. ' - 'Please choose another directory for settings and data files.') + "Portable directory is not writeable. " + "Please choose another directory for settings and data files." + ) else: os.close(fd) os.unlink(test_file) @@ -225,10 +244,15 @@ def get_win32_portable_user_data_dir() -> str: """ Returns the user data directory to the Windows portable version. """ - return os.path.join(get_win32_portable_dir(), 'openslides') + return os.path.join(get_win32_portable_dir(), "openslides") -def write_settings(settings_dir: str = None, settings_filename: str = 'settings.py', template: str = None, **context: str) -> str: +def write_settings( + settings_dir: str = None, + settings_filename: str = "settings.py", + template: str = None, + **context: str, +) -> str: """ Creates the settings file at the given dir using the given values for the file template. @@ -240,13 +264,15 @@ def write_settings(settings_dir: str = None, settings_filename: str = 'settings. settings_path = os.path.join(settings_dir, settings_filename) 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() # Create a random SECRET_KEY to put it in the settings. # from django.core.management.commands.startproject - chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' - context.setdefault('secret_key', get_random_string(50, chars)) + chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)" + context.setdefault("secret_key", get_random_string(50, chars)) for key, value in get_default_settings_context().items(): context.setdefault(key, value) @@ -254,14 +280,14 @@ def write_settings(settings_dir: str = None, settings_filename: str = 'settings. settings_module = os.path.realpath(settings_dir) if not os.path.exists(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) - if context['openslides_user_data_dir'] == 'get_win32_portable_user_data_dir()': + if context["openslides_user_data_dir"] == "get_win32_portable_user_data_dir()": openslides_user_data_dir = get_win32_portable_user_data_dir() else: - openslides_user_data_dir = context['openslides_user_data_dir'].strip("'") - os.makedirs(os.path.join(openslides_user_data_dir, 'static'), exist_ok=True) + openslides_user_data_dir = context["openslides_user_data_dir"].strip("'") + os.makedirs(os.path.join(openslides_user_data_dir, "static"), exist_ok=True) return os.path.realpath(settings_path) @@ -270,11 +296,11 @@ def open_browser(host: str, port: int) -> None: Launches the default web browser at the given host and port and opens the webinterface. Uses start_browser internally. """ - if host == '0.0.0.0': + if host == "0.0.0.0": # Windows does not support 0.0.0.0, so use 'localhost' instead - start_browser('http://localhost:%s' % port) + start_browser("http://localhost:%s" % port) else: - start_browser('http://%s:%s' % (host, port)) + start_browser("http://%s:%s" % (host, port)) def start_browser(browser_url: str) -> None: @@ -285,7 +311,7 @@ def start_browser(browser_url: str) -> None: try: browser = webbrowser.get() except webbrowser.Error: - print('Could not locate runnable browser: Skipping start') + print("Could not locate runnable browser: Skipping start") else: def function() -> None: @@ -311,10 +337,10 @@ def get_database_path_from_settings() -> Optional[str]: default = db_settings.get(DEFAULT_DB_ALIAS) if not default: raise DatabaseInSettingsError("Default databases is not configured") - database_path = default.get('NAME') + database_path = default.get("NAME") if not database_path: - raise DatabaseInSettingsError('No path or name specified for default database.') - if default.get('ENGINE') != 'django.db.backends.sqlite3': + raise DatabaseInSettingsError("No path or name specified for default database.") + if default.get("ENGINE") != "django.db.backends.sqlite3": database_path = None return database_path @@ -325,11 +351,15 @@ def is_local_installation() -> bool: This is the case if manage.py is used, or when the --local-installation flag is set. """ - return True if '--local-installation' in sys.argv or 'manage.py' in sys.argv[0] else False + return ( + True + if "--local-installation" in sys.argv or "manage.py" in sys.argv[0] + else False + ) def is_windows() -> bool: """ Returns True if the current system is Windows. Returns False otherwise. """ - return sys.platform == 'win32' + return sys.platform == "win32" diff --git a/openslides/utils/middleware.py b/openslides/utils/middleware.py index 1642f103e..79ac4cdb5 100644 --- a/openslides/utils/middleware.py +++ b/openslides/utils/middleware.py @@ -18,6 +18,7 @@ class CollectionAuthMiddleware(AuthMiddleware): Like the channels AuthMiddleware but returns a user dict id instead of a django Model as user. """ + def populate_scope(self, scope: Dict[str, Any]) -> None: # Make sure we have a session if "session" not in scope: @@ -41,7 +42,9 @@ async def get_user(scope: Dict[str, Any]) -> Dict[str, Any]: # This code is basicly from channels.auth: # https://github.com/django/channels/blob/d5e81a78e96770127da79248349808b6ee6ec2a7/channels/auth.py#L16 if "session" not in scope: - raise ValueError("Cannot find session in scope. You should wrap your consumer in SessionMiddleware.") + raise ValueError( + "Cannot find session in scope. You should wrap your consumer in SessionMiddleware." + ) session = scope["session"] user: Optional[Dict[str, Any]] = None try: @@ -56,13 +59,15 @@ async def get_user(scope: Dict[str, Any]) -> Dict[str, Any]: # Verify the session session_hash = session.get(HASH_SESSION_KEY) session_hash_verified = session_hash and constant_time_compare( - session_hash, - user['session_auth_hash']) + session_hash, user["session_auth_hash"] + ) if not session_hash_verified: session.flush() user = None - return user or {'id': 0} + return user or {"id": 0} # Handy shortcut for applying all three layers at once -AuthMiddlewareStack = lambda inner: CookieMiddleware(SessionMiddleware(CollectionAuthMiddleware(inner))) # noqa +AuthMiddlewareStack = lambda inner: CookieMiddleware( # noqa + SessionMiddleware(CollectionAuthMiddleware(inner)) +) diff --git a/openslides/utils/migrations.py b/openslides/utils/migrations.py index 303e6baed..d4950e349 100644 --- a/openslides/utils/migrations.py +++ b/openslides/utils/migrations.py @@ -5,11 +5,8 @@ from django.contrib.contenttypes.models import ContentType def add_permission_to_groups_based_on_existing_permission( - codename: str, - model: str, - app_label: str, - new_codename: str, - new_name: str) -> Callable[[Any, Any], None]: + codename: str, model: str, app_label: str, new_codename: str, new_name: str +) -> Callable[[Any, Any], None]: """ Creates the new permission given by new_codename and new_name to all groups, that have the base permission. This base permission is given by codename, model @@ -20,7 +17,9 @@ def add_permission_to_groups_based_on_existing_permission( def function(apps: Any, schema_editor: Any) -> None: content_type = ContentType.objects.filter(model=model, app_label=app_label) - base_perm = Permission.objects.filter(codename=codename, content_type__in=content_type) + base_perm = Permission.objects.filter( + codename=codename, content_type__in=content_type + ) if len(base_perm) is 1 and len(content_type) is 1: # get the actual content type and base permission @@ -32,12 +31,12 @@ def add_permission_to_groups_based_on_existing_permission( # Create new permission perm = Permission.objects.create( - codename=new_codename, - name=new_name, - content_type=content_type) + codename=new_codename, name=new_name, content_type=content_type + ) # Add this permission to all groups for group in groups: group.permissions.add(perm) group.save() + return function diff --git a/openslides/utils/models.py b/openslides/utils/models.py index 6b185cb07..b0c931f8b 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -13,12 +13,14 @@ class MinMaxIntegerField(models.IntegerField): IntegerField with options to set a min- and a max-value. """ - def __init__(self, min_value: int = None, max_value: int = None, *args: Any, **kwargs: Any) -> None: + def __init__( + self, min_value: int = None, max_value: int = None, *args: Any, **kwargs: Any + ) -> None: self.min_value, self.max_value = min_value, max_value super(MinMaxIntegerField, self).__init__(*args, **kwargs) def formfield(self, **kwargs: Any) -> Any: - defaults = {'min_value': self.min_value, 'max_value': self.max_value} + defaults = {"min_value": self.min_value, "max_value": self.max_value} defaults.update(kwargs) return super(MinMaxIntegerField, self).formfield(**defaults) @@ -45,7 +47,9 @@ class RESTModelMixin: its corresponding viewset. """ if cls.access_permissions is None: - raise ImproperlyConfigured("A RESTModel needs to have an access_permission.") + raise ImproperlyConfigured( + "A RESTModel needs to have an access_permission." + ) return cls.access_permissions @classmethod @@ -57,9 +61,12 @@ class RESTModelMixin: # TODO Check if this is a root rest element class and return None if not. app_label = cls._meta.app_label # type: ignore object_name = cls._meta.object_name # type: ignore - return '/'.join( - (convert_camel_case_to_pseudo_snake_case(app_label), - convert_camel_case_to_pseudo_snake_case(object_name))) + return "/".join( + ( + convert_camel_case_to_pseudo_snake_case(app_label), + convert_camel_case_to_pseudo_snake_case(object_name), + ) + ) def get_rest_pk(self) -> int: """ @@ -79,6 +86,7 @@ class RESTModelMixin: """ # We don't know how to fix this circular import from .autoupdate import inform_changed_data + return_value = super().save(*args, **kwargs) # type: ignore if not skip_autoupdate: inform_changed_data(self.get_root_rest_element()) @@ -95,6 +103,7 @@ class RESTModelMixin: """ # We don't know how to fix this circular import from .autoupdate import inform_changed_data, inform_deleted_data + instance_pk = self.pk # type: ignore return_value = super().delete(*args, **kwargs) # type: ignore if not skip_autoupdate: @@ -123,9 +132,8 @@ class RESTModelMixin: @classmethod async def restrict_elements( - cls, - user_id: int, - elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + cls, user_id: int, elements: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: """ Converts a list of elements from full_data to restricted_data. """ diff --git a/openslides/utils/plugins.py b/openslides/utils/plugins.py index 6719415f4..5410165ee 100644 --- a/openslides/utils/plugins.py +++ b/openslides/utils/plugins.py @@ -16,13 +16,17 @@ from openslides.utils.main import ( # Methods to collect plugins. + def collect_plugins_from_entry_points() -> Tuple[str, ...]: """ Collects all entry points in the group openslides_plugins from all distributions in the default working set and returns their module names as tuple. """ - return tuple(entry_point.module_name for entry_point in iter_entry_points('openslides_plugins')) + return tuple( + entry_point.module_name + for entry_point in iter_entry_points("openslides_plugins") + ) def collect_plugins_from_dir(plugin_dir: str) -> Tuple[str, ...]: @@ -42,8 +46,7 @@ def collect_plugins() -> Tuple[str, ...]: # Collect plugins in plugins/ directory of portable. if detect_openslides_type() == WINDOWS_PORTABLE_VERSION: - plugins_dir = os.path.join( - get_win32_portable_user_data_dir(), 'plugins') + plugins_dir = os.path.join(get_win32_portable_user_data_dir(), "plugins") if plugins_dir not in sys.path: sys.path.append(plugins_dir) collected_plugins += collect_plugins_from_dir(plugins_dir) @@ -53,6 +56,7 @@ def collect_plugins() -> Tuple[str, ...]: # Methods to retrieve plugin metadata and urlpatterns. + def get_plugin_verbose_name(plugin: str) -> str: """ Returns the verbose name of a plugin. The plugin argument must be a python @@ -73,7 +77,7 @@ def get_plugin_description(plugin: str) -> str: try: description = plugin_app_config.description except AttributeError: - description = '' + description = "" return description @@ -89,7 +93,7 @@ def get_plugin_version(plugin: str) -> str: try: version = plugin_app_config.version except AttributeError: - version = 'unknown' + version = "unknown" return version @@ -105,7 +109,7 @@ def get_plugin_license(plugin: str) -> str: try: license = plugin_app_config.license except AttributeError: - license = '' + license = "" return license @@ -121,7 +125,7 @@ def get_plugin_url(plugin: str) -> str: try: url = plugin_app_config.url except AttributeError: - url = '' + url = "" return url diff --git a/openslides/utils/projector.py b/openslides/utils/projector.py index 1cbc10275..dfb77bf6b 100644 --- a/openslides/utils/projector.py +++ b/openslides/utils/projector.py @@ -9,6 +9,7 @@ class ProjectorElement: subclassing from this base class with different names. The name attribute has to be set. """ + name: Optional[str] = None def check_and_update_data(self, projector_object: Any, config_entry: Any) -> Any: @@ -19,8 +20,9 @@ class ProjectorElement: """ self.projector_object = projector_object self.config_entry = config_entry - assert self.config_entry.get('name') == self.name, ( - 'To get data of a projector element, the correct config entry has to be given.') + assert ( + self.config_entry.get("name") == self.name + ), "To get data of a projector element, the correct config entry has to be given." self.check_data() return self.update_data() or {} @@ -47,7 +49,9 @@ class ProjectorElement: projector_elements: Dict[str, ProjectorElement] = {} -def register_projector_elements(elements: Generator[Type[ProjectorElement], None, None]) -> None: +def register_projector_elements( + elements: Generator[Type[ProjectorElement], None, None] +) -> None: """ Registers projector elements for later use. diff --git a/openslides/utils/redis.py b/openslides/utils/redis.py index 86baaa86f..4c385d4eb 100644 --- a/openslides/utils/redis.py +++ b/openslides/utils/redis.py @@ -9,7 +9,7 @@ except ImportError: use_redis = False else: # set use_redis to true, if there is a value for REDIS_ADDRESS in the settings - redis_address = getattr(settings, 'REDIS_ADDRESS', '') + redis_address = getattr(settings, "REDIS_ADDRESS", "") use_redis = bool(redis_address) @@ -17,12 +17,13 @@ class RedisConnectionContextManager: """ Async context manager for connections """ + # TODO: contextlib.asynccontextmanager can be used in python 3.7 def __init__(self, redis_address: str) -> None: self.redis_address = redis_address - async def __aenter__(self) -> 'aioredis.RedisConnection': + async def __aenter__(self) -> "aioredis.RedisConnection": self.conn = await aioredis.create_redis(self.redis_address) return self.conn diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 7f2d368f4..6d5729538 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -47,10 +47,24 @@ from .access_permissions import BaseAccessPermissions from .cache import element_cache -__all__ = ['detail_route', 'DecimalField', 'list_route', 'SimpleMetadata', - 'DestroyModelMixin', 'CharField', 'DictField', 'FileField', - 'IntegerField', 'JSONField', 'ListField', 'ListSerializer', 'status', 'RelatedField', - 'SerializerMethodField', 'ValidationError'] +__all__ = [ + "detail_route", + "DecimalField", + "list_route", + "SimpleMetadata", + "DestroyModelMixin", + "CharField", + "DictField", + "FileField", + "IntegerField", + "JSONField", + "ListField", + "ListSerializer", + "status", + "RelatedField", + "SerializerMethodField", + "ValidationError", +] router = DefaultRouter() @@ -63,7 +77,8 @@ class IdManyRelatedField(ManyRelatedField): Only works together with the IdPrimaryKeyRelatedField and our ModelSerializer. """ - field_name_suffix = '_id' + + field_name_suffix = "_id" def bind(self, field_name: str, parent: Any) -> None: """ @@ -71,7 +86,7 @@ class IdManyRelatedField(ManyRelatedField): See IdPrimaryKeyRelatedField for more informations. """ - self.source = field_name[:-len(self.field_name_suffix)] + self.source = field_name[: -len(self.field_name_suffix)] super().bind(field_name, parent) @@ -81,7 +96,8 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField): Only works together the our ModelSerializer. """ - field_name_suffix = '_id' + + field_name_suffix = "_id" def bind(self, field_name: str, parent: Any) -> None: """ @@ -94,7 +110,7 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField): # field_name is an empty string when the field is created with the # attribute many=True. In this case the suffix is added with the # IdManyRelatedField class. - self.source = field_name[:-len(self.field_name_suffix)] + self.source = field_name[: -len(self.field_name_suffix)] super().bind(field_name, parent) @classmethod @@ -104,7 +120,7 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField): IdManyRelatedField class instead of rest_framework.relations.ManyRelatedField class. """ - list_kwargs = {'child_relation': cls(*args, **kwargs)} + list_kwargs = {"child_relation": cls(*args, **kwargs)} for key in kwargs.keys(): if key in MANY_RELATION_KWARGS: list_kwargs[key] = kwargs[key] @@ -122,6 +138,7 @@ class PermissionMixin: Also connects container to handle access permissions for model and viewset. """ + access_permissions: Optional[BaseAccessPermissions] = None def get_permissions(self) -> Iterable[str]: @@ -196,6 +213,7 @@ class ModelSerializer(_ModelSerializer, metaclass=ModelSerializerRegisterer): ModelSerializer that changes the field names of related fields to FIELD_NAME_id. """ + serializer_related_field = IdPrimaryKeyRelatedField def get_fields(self) -> Any: @@ -217,6 +235,7 @@ class ListModelMixin(_ListModelMixin): """ Mixin to add the caching system to list requests. """ + def list(self, request: Any, *args: Any, **kwargs: Any) -> Response: model = self.get_queryset().model try: @@ -225,7 +244,9 @@ class ListModelMixin(_ListModelMixin): # The corresponding queryset does not support caching. response = super().list(request, *args, **kwargs) else: - all_restricted_data = async_to_sync(element_cache.get_all_restricted_data)(request.user.pk or 0) + all_restricted_data = async_to_sync(element_cache.get_all_restricted_data)( + request.user.pk or 0 + ) response = Response(all_restricted_data.get(collection_string, [])) return response @@ -234,6 +255,7 @@ class RetrieveModelMixin(_RetrieveModelMixin): """ Mixin to add the caching system to retrieve requests. """ + def retrieve(self, request: Any, *args: Any, **kwargs: Any) -> Response: model = self.get_queryset().model try: @@ -244,7 +266,9 @@ class RetrieveModelMixin(_RetrieveModelMixin): else: lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field user_id = request.user.pk or 0 - content = async_to_sync(element_cache.get_element_restricted_data)(user_id, collection_string, self.kwargs[lookup_url_kwarg]) + content = async_to_sync(element_cache.get_element_restricted_data)( + user_id, collection_string, self.kwargs[lookup_url_kwarg] + ) if content is None: raise Http404 response = Response(content) @@ -255,6 +279,7 @@ class CreateModelMixin(_CreateModelMixin): """ Mixin to override create requests. """ + def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: """ Just remove all response data (except 'id') so nobody may get @@ -264,8 +289,8 @@ class CreateModelMixin(_CreateModelMixin): """ response = super().create(request, *args, **kwargs) response.data = ReturnDict( - id=response.data.get('id'), - serializer=response.data.serializer # This kwarg is not send to the client. + id=response.data.get("id"), + serializer=response.data.serializer, # This kwarg is not send to the client. ) return response @@ -274,6 +299,7 @@ class UpdateModelMixin(_UpdateModelMixin): """ Mixin to override update requests. """ + def update(self, request: Request, *args: Any, **kwargs: Any) -> Response: """ Just remove all response data so nobody may get unrestricted data. @@ -289,6 +315,12 @@ class GenericViewSet(PermissionMixin, _GenericViewSet): pass -class ModelViewSet(PermissionMixin, ListModelMixin, RetrieveModelMixin, - CreateModelMixin, UpdateModelMixin, _ModelViewSet): +class ModelViewSet( + PermissionMixin, + ListModelMixin, + RetrieveModelMixin, + CreateModelMixin, + UpdateModelMixin, + _ModelViewSet, +): pass diff --git a/openslides/utils/utils.py b/openslides/utils/utils.py index 0e12298a9..56895eaa1 100644 --- a/openslides/utils/utils.py +++ b/openslides/utils/utils.py @@ -6,8 +6,8 @@ from django.apps import apps from django.db.models import Model -CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_1 = re.compile('(.)([A-Z][a-z]+)') -CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2 = re.compile('([a-z0-9])([A-Z])') +CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_1 = re.compile("(.)([A-Z][a-z]+)") +CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2 = re.compile("([a-z0-9])([A-Z])") def convert_camel_case_to_pseudo_snake_case(text: str) -> str: @@ -19,8 +19,8 @@ def convert_camel_case_to_pseudo_snake_case(text: str) -> str: Credits: epost (http://stackoverflow.com/a/1176023) """ - s1 = CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_1.sub(r'\1-\2', text) - return CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2.sub(r'\1-\2', s1).lower() + s1 = CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_1.sub(r"\1-\2", text) + return CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2.sub(r"\1-\2", s1).lower() def to_roman(number: int) -> str: @@ -69,6 +69,7 @@ def get_model_from_collection_string(collection_string: str) -> Type[Model]: """ Returns a model class which belongs to the argument collection_string. """ + def model_generator() -> Generator[Type[Model], None, None]: """ Yields all models of all apps. @@ -90,5 +91,9 @@ def get_model_from_collection_string(collection_string: str) -> Type[Model]: try: model = _models_to_collection_string[collection_string] except KeyError: - raise ValueError('Invalid message. A valid collection_string is missing. Got {}'.format(collection_string)) + raise ValueError( + "Invalid message. A valid collection_string is missing. Got {}".format( + collection_string + ) + ) return model diff --git a/openslides/utils/validate.py b/openslides/utils/validate.py index 5d3620720..a942a9606 100644 --- a/openslides/utils/validate.py +++ b/openslides/utils/validate.py @@ -2,22 +2,51 @@ import bleach allowed_tags = [ - 'a', 'img', # links and images - 'br', 'p', 'span', 'blockquote', # text layout - 'strike', 'strong', 'u', 'em', 'sup', 'sub', 'pre', # text formatting - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', # headings - 'ol', 'ul', 'li', # lists - 'table', 'caption', 'thead', 'tbody', 'th', 'tr', 'td', # tables + "a", + "img", # links and images + "br", + "p", + "span", + "blockquote", # text layout + "strike", + "strong", + "u", + "em", + "sup", + "sub", + "pre", # text formatting + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", # headings + "ol", + "ul", + "li", # lists + "table", + "caption", + "thead", + "tbody", + "th", + "tr", + "td", # tables ] allowed_attributes = { - '*': ['class', 'style'], - 'img': ['alt', 'src', 'title'], - 'a': ['href', 'title'], - 'th': ['scope'], - 'ol': ['start'], + "*": ["class", "style"], + "img": ["alt", "src", "title"], + "a": ["href", "title"], + "th": ["scope"], + "ol": ["start"], } allowed_styles = [ - 'color', 'background-color', 'height', 'width', 'text-align', 'float', 'padding' + "color", + "background-color", + "height", + "width", + "text-align", + "float", + "padding", ] @@ -27,9 +56,7 @@ def validate_html(html: str) -> str: Every field of a model that is loaded trusted in the DOM should be validated. During copy and paste from Word maybe some tabs are spread over the html. Remove them. """ - html = html.replace('\t', '') + html = html.replace("\t", "") return bleach.clean( - html, - tags=allowed_tags, - attributes=allowed_attributes, - styles=allowed_styles) + html, tags=allowed_tags, attributes=allowed_attributes, styles=allowed_styles + ) diff --git a/openslides/utils/websocket.py b/openslides/utils/websocket.py index 19e24d1fe..53559f789 100644 --- a/openslides/utils/websocket.py +++ b/openslides/utils/websocket.py @@ -14,15 +14,21 @@ class ProtocollAsyncJsonWebsocketConsumer(AsyncJsonWebsocketConsumer): Mixin for JSONWebsocketConsumers, that speaks the a special protocol. """ - async def send_json(self, type: str, content: Any, id: Optional[str] = None, in_response: Optional[str] = None) -> None: + async def send_json( + self, + type: str, + content: Any, + id: Optional[str] = None, + in_response: Optional[str] = None, + ) -> None: """ Sends the data with the type. """ - out = {'type': type, 'content': content} + out = {"type": type, "content": content} if id: - out['id'] = id + out["id"] = id if in_response: - out['in_response'] = in_response + out["in_response"] = in_response await super().send_json(out) async def receive_json(self, content: Any) -> None: @@ -33,18 +39,19 @@ class ProtocollAsyncJsonWebsocketConsumer(AsyncJsonWebsocketConsumer): jsonschema.validate(content, schema) except jsonschema.ValidationError as err: try: - in_response = content['id'] + in_response = content["id"] except (TypeError, KeyError): # content is not a dict (TypeError) or has not the key id (KeyError) in_response = None await self.send_json( - type='error', - content=str(err), - in_response=in_response) + type="error", content=str(err), in_response=in_response + ) return - await websocket_client_messages[content['type']].receive_content(self, content['content'], id=content['id']) + await websocket_client_messages[content["type"]].receive_content( + self, content["content"], id=content["id"] + ) schema: Dict[str, Any] = { @@ -57,13 +64,8 @@ schema: Dict[str, Any] = { "description": "Defines what kind of packages is packed.", "type": "string", }, - "content": { - "description": "The content of the package.", - }, - "id": { - "description": "An identifier of the package.", - "type": "string", - }, + "content": {"description": "The content of the package."}, + "id": {"description": "An identifier of the package.", "type": "string"}, "in_response": { "description": "The id of another package that the other part sent before.", "type": "string", @@ -94,8 +96,12 @@ class BaseWebsocketClientMessage: Desiedes, if the content property is required. """ - async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", message: Any, id: str) -> None: - raise NotImplementedError("WebsocketClientMessage needs the method receive_content().") + async def receive_content( + self, consumer: "ProtocollAsyncJsonWebsocketConsumer", message: Any, id: str + ) -> None: + raise NotImplementedError( + "WebsocketClientMessage needs the method receive_content()." + ) websocket_client_messages: Dict[str, BaseWebsocketClientMessage] = {} @@ -104,26 +110,33 @@ Saves all websocket client message object ordered by there identifier. """ -def register_client_message(websocket_client_message: BaseWebsocketClientMessage) -> None: +def register_client_message( + websocket_client_message: BaseWebsocketClientMessage +) -> None: """ Registers one websocket client message class. """ - if not websocket_client_message.identifier or websocket_client_message.identifier in websocket_client_messages: + if ( + not websocket_client_message.identifier + or websocket_client_message.identifier in websocket_client_messages + ): raise NotImplementedError("WebsocketClientMessage needs a unique identifier.") - websocket_client_messages[websocket_client_message.identifier] = websocket_client_message + websocket_client_messages[ + websocket_client_message.identifier + ] = websocket_client_message # Add the message schema to the schema message_schema: Dict[str, Any] = { - 'properties': { - 'type': {'const': websocket_client_message.identifier}, - 'content': websocket_client_message.schema, + "properties": { + "type": {"const": websocket_client_message.identifier}, + "content": websocket_client_message.schema, } } if websocket_client_message.content_required: - message_schema['required'] = ['content'] + message_schema["required"] = ["content"] - schema['anyOf'].append(message_schema) + schema["anyOf"].append(message_schema) async def get_element_data(user_id: int, change_id: int = 0) -> AutoupdateFormat: @@ -134,7 +147,9 @@ async def get_element_data(user_id: int, change_id: int = 0) -> AutoupdateFormat if change_id > current_change_id: raise ValueError("Requested change_id is higher this highest change_id.") try: - changed_elements, deleted_element_ids = await element_cache.get_restricted_data(user_id, change_id, current_change_id) + changed_elements, deleted_element_ids = await element_cache.get_restricted_data( + user_id, change_id, current_change_id + ) except RuntimeError: # The change_id is lower the the lowerst change_id in redis. Return all data changed_elements = await element_cache.get_all_restricted_data(user_id) @@ -152,4 +167,5 @@ async def get_element_data(user_id: int, change_id: int = 0) -> AutoupdateFormat deleted=deleted_elements, from_change_id=change_id, to_change_id=current_change_id, - all_data=all_data) + all_data=all_data, + ) diff --git a/tests/conftest.py b/tests/conftest.py index b99c1735a..7da8b2965 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,16 +11,17 @@ def pytest_collection_modifyitems(items): """ Helper until https://github.com/pytest-dev/pytest-django/issues/214 is fixed. """ + def get_marker_transaction(test): - marker = test.get_closest_marker('django_db') + marker = test.get_closest_marker("django_db") if marker: validate_django_db(marker) - return marker.kwargs['transaction'] + return marker.kwargs["transaction"] return None def has_fixture(test, fixture): - funcargnames = getattr(test, 'funcargnames', None) + funcargnames = getattr(test, "funcargnames", None) return funcargnames and fixture in funcargnames def weight_test_case(test): @@ -28,16 +29,18 @@ def pytest_collection_modifyitems(items): Key function for ordering test cases like the Django test runner. """ is_test_case_subclass = test.cls and issubclass(test.cls, TestCase) - is_transaction_test_case_subclass = test.cls and issubclass(test.cls, TransactionTestCase) + is_transaction_test_case_subclass = test.cls and issubclass( + test.cls, TransactionTestCase + ) if is_test_case_subclass or get_marker_transaction(test) is False: return 0 - elif has_fixture(test, 'db'): + elif has_fixture(test, "db"): return 0 if is_transaction_test_case_subclass or get_marker_transaction(test) is True: return 1 - elif has_fixture(test, 'transactional_db'): + elif has_fixture(test, "transactional_db"): return 1 return 0 @@ -54,12 +57,12 @@ def constants(request): """ from openslides.utils.constants import set_constants, get_constants_from_apps - if 'django_db' in request.node.keywords or is_django_unittest(request): + if "django_db" in request.node.keywords or is_django_unittest(request): # When the db is created, use the original constants set_constants(get_constants_from_apps()) else: # Else: Use fake constants - set_constants({'constant1': 'value1', 'constant2': 'value2'}) + set_constants({"constant1": "value1", "constant2": "value2"}) @pytest.fixture(autouse=True) @@ -67,7 +70,7 @@ def reset_cache(request): """ Resetts the cache for every test """ - if 'django_db' in request.node.keywords or is_django_unittest(request): + if "django_db" in request.node.keywords or is_django_unittest(request): # When the db is created, use the original cachables async_to_sync(element_cache.cache_provider.clear_cache)() element_cache.ensure_cache(reset=True) diff --git a/tests/example_data_generator/__init__.py b/tests/example_data_generator/__init__.py index 7254229ae..5759b149b 100644 --- a/tests/example_data_generator/__init__.py +++ b/tests/example_data_generator/__init__.py @@ -1 +1 @@ -default_app_config = 'tests.example_data_generator.apps.ExampleDataGeneratorAppConfig' +default_app_config = "tests.example_data_generator.apps.ExampleDataGeneratorAppConfig" diff --git a/tests/example_data_generator/apps.py b/tests/example_data_generator/apps.py index 9c740bb77..d10bd3f81 100644 --- a/tests/example_data_generator/apps.py +++ b/tests/example_data_generator/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig class ExampleDataGeneratorAppConfig(AppConfig): - name = 'tests.example_data_generator' - label = 'tests.example_data_generator' - verbose_name = 'Example Data Generator' - version = 'no specific version' + name = "tests.example_data_generator" + label = "tests.example_data_generator" + verbose_name = "Example Data Generator" + version = "no specific version" diff --git a/tests/example_data_generator/management/commands/create-example-data.py b/tests/example_data_generator/management/commands/create-example-data.py index 837c316ba..a3b7f8711 100644 --- a/tests/example_data_generator/management/commands/create-example-data.py +++ b/tests/example_data_generator/management/commands/create-example-data.py @@ -24,8 +24,9 @@ LOREM_IPSUM = [ commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim - id est laborum.

""".replace('\n', ' '), - + id est laborum.

""".replace( + "\n", " " + ), """\

Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam eaque ipsa, quae @@ -40,8 +41,9 @@ LOREM_IPSUM = [ aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit, qui in ea voluptate velit esse, quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla - pariatur?

""".replace('\n', ' '), - + pariatur?

""".replace( + "\n", " " + ), """\

At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores @@ -55,22 +57,25 @@ LOREM_IPSUM = [ necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut - perferendis doloribus asperiores repellat…

""".replace('\n', ' '), + perferendis doloribus asperiores repellat…

""".replace( + "\n", " " + ), ] DEFAULT_NUMBER = 100 -STAFF_USER_USERNAME = 'admin{}' -DEFAULT_USER_USERNAME = 'user{}' -PASSWORD = 'password' +STAFF_USER_USERNAME = "admin{}" +DEFAULT_USER_USERNAME = "user{}" +PASSWORD = "password" class Command(BaseCommand): """ Command to create example data for OpenSlides. """ - help = 'Create example data for OpenSlides.' - chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + help = "Create example data for OpenSlides." + + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" def add_arguments(self, parser): """ @@ -78,37 +83,41 @@ class Command(BaseCommand): are set by DEFAULT_NUMBER. """ parser.add_argument( - '--only', - action='store_true', - help='Only the given objects are created i. e. all defaults are ' - 'set to 0.', + "--only", + action="store_true", + help="Only the given objects are created i. e. all defaults are " + "set to 0.", ) parser.add_argument( - '-t', '--topics', + "-t", + "--topics", type=int, - help='Number of topics to be created (default {}).'.format( - DEFAULT_NUMBER), + help="Number of topics to be created (default {}).".format(DEFAULT_NUMBER), ) parser.add_argument( - '-m', '--motions', + "-m", + "--motions", type=int, - help='Number of motions to be created (default {}).'.format( - DEFAULT_NUMBER), + help="Number of motions to be created (default {}).".format(DEFAULT_NUMBER), ) parser.add_argument( - '-a', '--assignments', + "-a", + "--assignments", type=int, - help='Number of assignments to be created (default {}).'.format( - DEFAULT_NUMBER), + help="Number of assignments to be created (default {}).".format( + DEFAULT_NUMBER + ), ) parser.add_argument( - '-u', '--users', + "-u", + "--users", nargs=2, type=int, - help='Number of users to be created. The first number of users is ' - 'added to the group "Staff" (default {}). The second number ' - 'of users is not added to any group (default {}).'.format( - DEFAULT_NUMBER, DEFAULT_NUMBER), + help="Number of users to be created. The first number of users is " + 'added to the group "Staff" (default {}). The second number ' + "of users is not added to any group (default {}).".format( + DEFAULT_NUMBER, DEFAULT_NUMBER + ), ) def handle(self, *args, **options): @@ -119,12 +128,12 @@ class Command(BaseCommand): @transaction.atomic def create_topics(self, options): - number_of_topics = options['topics'] - if number_of_topics is None and not options['only']: + number_of_topics = options["topics"] + if number_of_topics is None and not options["only"]: number_of_topics = DEFAULT_NUMBER if number_of_topics is not None and number_of_topics > 0: - self.stdout.write('Start creating {} topcis ...'.format(number_of_topics)) - current_topics = list(Topic.objects.values_list('id', flat=True)) + self.stdout.write("Start creating {} topcis ...".format(number_of_topics)) + current_topics = list(Topic.objects.values_list("id", flat=True)) new_topics = [] for i in range(number_of_topics): new_topics.append(Topic(title=get_random_string(20, self.chars))) @@ -133,49 +142,62 @@ class Command(BaseCommand): for topic in Topic.objects.exclude(pk__in=current_topics): items.append(Item(content_object=topic, type=Item.AGENDA_ITEM)) Item.objects.bulk_create(items) - self.stdout.write(self.style.SUCCESS('{} topcis successfully created.'.format(number_of_topics))) + self.stdout.write( + self.style.SUCCESS( + "{} topcis successfully created.".format(number_of_topics) + ) + ) elif number_of_topics is not None and number_of_topics < 0: - raise CommandError('Number for topics must not be negative.') + raise CommandError("Number for topics must not be negative.") @transaction.atomic def create_motions(self, options): - number_of_motions = options['motions'] - if number_of_motions is None and not options['only']: + number_of_motions = options["motions"] + if number_of_motions is None and not options["only"]: number_of_motions = DEFAULT_NUMBER if number_of_motions is not None and number_of_motions > 0: - self.stdout.write('Start creating {} motions ...'.format(number_of_motions)) - text = '' + self.stdout.write("Start creating {} motions ...".format(number_of_motions)) + text = "" for i in range(MOTION_NUMBER_OF_PARAGRAPHS): text += dedent(LOREM_IPSUM[i % 3]) for i in range(number_of_motions): - motion = Motion( - title=get_random_string(20, self.chars), - text=text, - ) + motion = Motion(title=get_random_string(20, self.chars), text=text) motion.save(skip_autoupdate=True) - self.stdout.write(self.style.SUCCESS('{} motions successfully created.'.format(number_of_motions))) + self.stdout.write( + self.style.SUCCESS( + "{} motions successfully created.".format(number_of_motions) + ) + ) elif number_of_motions is not None and number_of_motions < 0: - raise CommandError('Number for motions must not be negative.') + raise CommandError("Number for motions must not be negative.") @transaction.atomic def create_assignments(self, options): - number_of_assignments = options['assignments'] - if number_of_assignments is None and not options['only']: + number_of_assignments = options["assignments"] + if number_of_assignments is None and not options["only"]: number_of_assignments = DEFAULT_NUMBER if number_of_assignments is not None and number_of_assignments > 0: - self.stdout.write('Start creating {} assignments ...'.format(number_of_assignments)) - current_assignments = list(Assignment.objects.values_list('id', flat=True)) + self.stdout.write( + "Start creating {} assignments ...".format(number_of_assignments) + ) + current_assignments = list(Assignment.objects.values_list("id", flat=True)) new_assignments = [] for i in range(number_of_assignments): - new_assignments.append(Assignment(title=get_random_string(20, self.chars), open_posts=1)) + new_assignments.append( + Assignment(title=get_random_string(20, self.chars), open_posts=1) + ) Assignment.objects.bulk_create(new_assignments) items = [] for assignment in Assignment.objects.exclude(pk__in=current_assignments): items.append(Item(content_object=assignment)) Item.objects.bulk_create(items) - self.stdout.write(self.style.SUCCESS('{} assignments successfully created.'.format(number_of_assignments))) + self.stdout.write( + self.style.SUCCESS( + "{} assignments successfully created.".format(number_of_assignments) + ) + ) elif number_of_assignments is not None and number_of_assignments < 0: - raise CommandError('Number for assignments must not be negative.') + raise CommandError("Number for assignments must not be negative.") def create_users(self, options): self.create_staff_users(options) @@ -183,58 +205,76 @@ class Command(BaseCommand): @transaction.atomic def create_staff_users(self, options): - if options['users'] is None and not options['only']: + if options["users"] is None and not options["only"]: staff_users: Optional[int] = DEFAULT_NUMBER - elif options['users'] is None: + elif options["users"] is None: staff_users = None else: - staff_users = options['users'][0] + staff_users = options["users"][0] if staff_users is not None and staff_users > 0: - self.stdout.write('Start creating {} staff users ...'.format(staff_users)) - group_staff = Group.objects.get(name='Staff') + self.stdout.write("Start creating {} staff users ...".format(staff_users)) + group_staff = Group.objects.get(name="Staff") hashed_password = make_password(PASSWORD) - current_users = list(User.objects.values_list('id', flat=True)) + current_users = list(User.objects.values_list("id", flat=True)) new_users = [] for i in range(staff_users): - new_users.append(User( - username=STAFF_USER_USERNAME.format(i), - default_password=PASSWORD, - password=hashed_password - )) + new_users.append( + User( + username=STAFF_USER_USERNAME.format(i), + default_password=PASSWORD, + password=hashed_password, + ) + ) try: User.objects.bulk_create(new_users) except IntegrityError: - self.stdout.write('FAILED: The requested staff users to create are already existing...') + self.stdout.write( + "FAILED: The requested staff users to create are already existing..." + ) else: for user in User.objects.exclude(pk__in=current_users): user.groups.add(group_staff) - self.stdout.write(self.style.SUCCESS('{} staff users successfully created.'.format(staff_users))) + self.stdout.write( + self.style.SUCCESS( + "{} staff users successfully created.".format(staff_users) + ) + ) elif staff_users is not None and staff_users < 0: - raise CommandError('Number for staff users must not be negative.') + raise CommandError("Number for staff users must not be negative.") @transaction.atomic def create_default_users(self, options): - if options['users'] is None and not options['only']: + if options["users"] is None and not options["only"]: default_users: Optional[int] = DEFAULT_NUMBER - elif options['users'] is None: + elif options["users"] is None: default_users = None else: - default_users = options['users'][1] + default_users = options["users"][1] if default_users is not None and default_users > 0: - self.stdout.write('Start creating {} default users ...'.format(default_users)) + self.stdout.write( + "Start creating {} default users ...".format(default_users) + ) hashed_password = make_password(PASSWORD) new_users = [] for i in range(default_users): - new_users.append(User( - username=DEFAULT_USER_USERNAME.format(i), - default_password=PASSWORD, - password=hashed_password - )) + new_users.append( + User( + username=DEFAULT_USER_USERNAME.format(i), + default_password=PASSWORD, + password=hashed_password, + ) + ) try: User.objects.bulk_create(new_users) except IntegrityError: - self.stdout.write('FAILED: The requested staff users to create are already existing...') + self.stdout.write( + "FAILED: The requested staff users to create are already existing..." + ) else: - self.stdout.write(self.style.SUCCESS('{} default users successfully created.'.format(default_users))) + self.stdout.write( + self.style.SUCCESS( + "{} default users successfully created.".format(default_users) + ) + ) elif default_users is not None and default_users < 0: - raise CommandError('Number for default users must not be negative.') + raise CommandError("Number for default users must not be negative.") diff --git a/tests/integration/agenda/test_models.py b/tests/integration/agenda/test_models.py index b0df81bb4..aa0d01f4f 100644 --- a/tests/integration/agenda/test_models.py +++ b/tests/integration/agenda/test_models.py @@ -9,7 +9,7 @@ class TestItemManager(TestCase): Test that get_root_and_children needs only one db query. """ for i in range(10): - Topic.objects.create(title='item{}'.format(i)) + Topic.objects.create(title="item{}".format(i)) with self.assertNumQueries(1): Item.objects.get_root_and_children() diff --git a/tests/integration/agenda/test_viewset.py b/tests/integration/agenda/test_viewset.py index 89a615f1e..bcfc68c4c 100644 --- a/tests/integration/agenda/test_viewset.py +++ b/tests/integration/agenda/test_viewset.py @@ -23,74 +23,86 @@ class RetrieveItem(TestCase): """ Tests retrieving items. """ + def setUp(self): self.client = APIClient() - config['general_system_enable_anonymous'] = True - self.item = Topic.objects.create(title='test_title_Idais2pheepeiz5uph1c').agenda_item + config["general_system_enable_anonymous"] = True + self.item = Topic.objects.create( + title="test_title_Idais2pheepeiz5uph1c" + ).agenda_item def test_normal_by_anonymous_without_perm_to_see_internal_items(self): - group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users. - permission_string = 'agenda.can_see_internal_items' - app_label, codename = permission_string.split('.') - permission = group.permissions.get(content_type__app_label=app_label, codename=codename) + group = get_user_model().groups.field.related_model.objects.get( + pk=1 + ) # Group with pk 1 is for anonymous users. + permission_string = "agenda.can_see_internal_items" + app_label, codename = permission_string.split(".") + permission = group.permissions.get( + content_type__app_label=app_label, codename=codename + ) group.permissions.remove(permission) self.item.type = Item.AGENDA_ITEM self.item.save() - response = self.client.get(reverse('item-detail', args=[self.item.pk])) + response = self.client.get(reverse("item-detail", args=[self.item.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_hidden_by_anonymous_without_manage_perms(self): - response = self.client.get(reverse('item-detail', args=[self.item.pk])) + response = self.client.get(reverse("item-detail", args=[self.item.pk])) self.assertEqual(response.status_code, 404) def test_hidden_by_anonymous_with_manage_perms(self): group = Group.objects.get(pk=1) # Group with pk 1 is for anonymous users. - permission_string = 'agenda.can_manage' - app_label, codename = permission_string.split('.') - permission = Permission.objects.get(content_type__app_label=app_label, codename=codename) + permission_string = "agenda.can_manage" + app_label, codename = permission_string.split(".") + permission = Permission.objects.get( + content_type__app_label=app_label, codename=codename + ) group.permissions.add(permission) inform_changed_data(group) - response = self.client.get(reverse('item-detail', args=[self.item.pk])) + response = self.client.get(reverse("item-detail", args=[self.item.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_internal_by_anonymous_without_perm_to_see_internal_items(self): group = Group.objects.get(pk=1) # Group with pk 1 is for anonymous users. - permission_string = 'agenda.can_see_internal_items' - app_label, codename = permission_string.split('.') - permission = group.permissions.get(content_type__app_label=app_label, codename=codename) + permission_string = "agenda.can_see_internal_items" + app_label, codename = permission_string.split(".") + permission = group.permissions.get( + content_type__app_label=app_label, codename=codename + ) group.permissions.remove(permission) inform_changed_data(group) self.item.type = Item.INTERNAL_ITEM self.item.save() - response = self.client.get(reverse('item-detail', args=[self.item.pk])) + response = self.client.get(reverse("item-detail", args=[self.item.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(sorted(response.data.keys()), sorted(( - 'id', - 'title', - 'speakers', - 'speaker_list_closed', - 'content_object',))) + self.assertEqual( + sorted(response.data.keys()), + sorted( + ("id", "title", "speakers", "speaker_list_closed", "content_object") + ), + ) forbidden_keys = ( - 'item_number', - 'title_with_type', - 'comment', - 'closed', - 'type', - 'is_internal', - 'is_hidden', - 'duration', - 'weight', - 'parent',) + "item_number", + "title_with_type", + "comment", + "closed", + "type", + "is_internal", + "is_hidden", + "duration", + "weight", + "parent", + ) for key in forbidden_keys: self.assertFalse(key in response.data.keys()) def test_normal_by_anonymous_cant_see_agenda_comments(self): self.item.type = Item.AGENDA_ITEM - self.item.comment = 'comment_gbiejd67gkbmsogh8374jf$kd' + self.item.comment = "comment_gbiejd67gkbmsogh8374jf$kd" self.item.save() - response = self.client.get(reverse('item-detail', args=[self.item.pk])) + response = self.client.get(reverse("item-detail", args=[self.item.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data.get('comment') is None) + self.assertTrue(response.data.get("comment") is None) @pytest.mark.django_db(transaction=False) @@ -105,14 +117,14 @@ def test_agenda_item_db_queries(): TODO: The last three request are a bug. """ for index in range(10): - Topic.objects.create(title='topic{}'.format(index)) - parent = Topic.objects.create(title='parent').agenda_item - child = Topic.objects.create(title='child').agenda_item + Topic.objects.create(title="topic{}".format(index)) + parent = Topic.objects.create(title="parent").agenda_item + child = Topic.objects.create(title="child").agenda_item child.parent = parent child.save() - Motion.objects.create(title='motion1') - Motion.objects.create(title='motion2') - Assignment.objects.create(title='assignment', open_posts=5) + Motion.objects.create(title="motion1") + Motion.objects.create(title="motion2") + Assignment.objects.create(title="assignment", open_posts=5) assert count_queries(Item.get_elements) == 6 @@ -121,57 +133,61 @@ class ManageSpeaker(TestCase): """ Tests managing speakers. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") - self.item = Topic.objects.create(title='test_title_aZaedij4gohn5eeQu8fe').agenda_item + self.item = Topic.objects.create( + title="test_title_aZaedij4gohn5eeQu8fe" + ).agenda_item self.user = get_user_model().objects.create_user( - username='test_user_jooSaex1bo5ooPhuphae', - password='test_password_e6paev4zeeh9n') + username="test_user_jooSaex1bo5ooPhuphae", + password="test_password_e6paev4zeeh9n", + ) def test_add_oneself(self): - response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk])) + response = self.client.post(reverse("item-manage-speaker", args=[self.item.pk])) self.assertEqual(response.status_code, 200) self.assertTrue(Speaker.objects.all().exists()) def test_add_oneself_twice(self): - Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) - response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk])) + Speaker.objects.add(get_user_model().objects.get(username="admin"), self.item) + response = self.client.post(reverse("item-manage-speaker", args=[self.item.pk])) self.assertEqual(response.status_code, 400) def test_add_oneself_when_closed(self): self.item.speaker_list_closed = True self.item.save() - response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk])) + response = self.client.post(reverse("item-manage-speaker", args=[self.item.pk])) self.assertEqual(response.status_code, 400) def test_remove_oneself(self): - Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) + Speaker.objects.add(get_user_model().objects.get(username="admin"), self.item) response = self.client.delete( - reverse('item-manage-speaker', args=[self.item.pk])) + reverse("item-manage-speaker", args=[self.item.pk]) + ) self.assertEqual(response.status_code, 200) self.assertFalse(Speaker.objects.all().exists()) def test_remove_self_not_on_list(self): response = self.client.delete( - reverse('item-manage-speaker', args=[self.item.pk])) + reverse("item-manage-speaker", args=[self.item.pk]) + ) self.assertEqual(response.status_code, 400) def test_add_someone_else(self): response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk]), - {'user': self.user.pk}) + reverse("item-manage-speaker", args=[self.item.pk]), {"user": self.user.pk} + ) self.assertEqual(response.status_code, 200) self.assertTrue(Speaker.objects.filter(item=self.item, user=self.user).exists()) def test_invalid_data_string_instead_of_integer(self): response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk]), - {'user': 'string_instead_of_integer'}) + reverse("item-manage-speaker", args=[self.item.pk]), + {"user": "string_instead_of_integer"}, + ) self.assertEqual(response.status_code, 400) @@ -180,94 +196,98 @@ class ManageSpeaker(TestCase): # Be careful: Here we do not test that the user does not exist. inexistent_user_pk = self.user.pk + 1000 response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk]), - {'user': inexistent_user_pk}) + reverse("item-manage-speaker", args=[self.item.pk]), + {"user": inexistent_user_pk}, + ) self.assertEqual(response.status_code, 400) def test_add_someone_else_twice(self): Speaker.objects.add(self.user, self.item) response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk]), - {'user': self.user.pk}) + reverse("item-manage-speaker", args=[self.item.pk]), {"user": self.user.pk} + ) self.assertEqual(response.status_code, 400) def test_add_someone_else_non_admin(self): - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') - group_delegates = type(group_admin).objects.get(name='Delegates') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") + group_delegates = type(group_admin).objects.get(name="Delegates") admin.groups.add(group_delegates) admin.groups.remove(group_admin) inform_changed_data(admin) response = self.client.post( - reverse('item-manage-speaker', args=[self.item.pk]), - {'user': self.user.pk}) + reverse("item-manage-speaker", args=[self.item.pk]), {"user": self.user.pk} + ) self.assertEqual(response.status_code, 403) def test_remove_someone_else(self): speaker = Speaker.objects.add(self.user, self.item) response = self.client.delete( - reverse('item-manage-speaker', args=[self.item.pk]), - {'speaker': speaker.pk}) + reverse("item-manage-speaker", args=[self.item.pk]), {"speaker": speaker.pk} + ) self.assertEqual(response.status_code, 200) - self.assertFalse(Speaker.objects.filter(item=self.item, user=self.user).exists()) + self.assertFalse( + Speaker.objects.filter(item=self.item, user=self.user).exists() + ) def test_remove_someone_else_not_on_list(self): response = self.client.delete( - reverse('item-manage-speaker', args=[self.item.pk]), - {'speaker': '1'}) + reverse("item-manage-speaker", args=[self.item.pk]), {"speaker": "1"} + ) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data.get('detail'), - ugettext('No speakers have been removed from the list of speakers.')) + self.assertEqual( + response.data.get("detail"), + ugettext("No speakers have been removed from the list of speakers."), + ) def test_remove_someone_else_invalid_data(self): response = self.client.delete( - reverse('item-manage-speaker', args=[self.item.pk]), - {'speaker': 'invalid'}) + reverse("item-manage-speaker", args=[self.item.pk]), {"speaker": "invalid"} + ) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data.get('detail'), - ugettext('No speakers have been removed from the list of speakers.')) + self.assertEqual( + response.data.get("detail"), + ugettext("No speakers have been removed from the list of speakers."), + ) def test_remove_someone_else_non_admin(self): - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') - group_delegates = type(group_admin).objects.get(name='Delegates') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") + group_delegates = type(group_admin).objects.get(name="Delegates") admin.groups.add(group_delegates) admin.groups.remove(group_admin) inform_changed_data(admin) speaker = Speaker.objects.add(self.user, self.item) response = self.client.delete( - reverse('item-manage-speaker', args=[self.item.pk]), - {'speaker': speaker.pk}) + reverse("item-manage-speaker", args=[self.item.pk]), {"speaker": speaker.pk} + ) self.assertEqual(response.status_code, 403) def test_mark_speaker(self): Speaker.objects.add(self.user, self.item) response = self.client.patch( - reverse('item-manage-speaker', args=[self.item.pk]), - { - 'user': self.user.pk, - 'marked': True, - }, - format='json' + reverse("item-manage-speaker", args=[self.item.pk]), + {"user": self.user.pk, "marked": True}, + format="json", ) self.assertEqual(response.status_code, 200) self.assertTrue(Speaker.objects.get().marked) def test_mark_speaker_non_admin(self): - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') - group_delegates = type(group_admin).objects.get(name='Delegates') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") + group_delegates = type(group_admin).objects.get(name="Delegates") admin.groups.add(group_delegates) admin.groups.remove(group_admin) inform_changed_data(admin) Speaker.objects.add(self.user, self.item) response = self.client.patch( - reverse('item-manage-speaker', args=[self.item.pk]), - {'user': self.user.pk}) + reverse("item-manage-speaker", args=[self.item.pk]), {"user": self.user.pk} + ) self.assertEqual(response.status_code, 403) @@ -276,75 +296,87 @@ class Speak(TestCase): """ Tests view to begin or end speech. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') - self.item = Topic.objects.create(title='test_title_KooDueco3zaiGhiraiho').agenda_item + self.client.login(username="admin", password="admin") + self.item = Topic.objects.create( + title="test_title_KooDueco3zaiGhiraiho" + ).agenda_item self.user = get_user_model().objects.create_user( - username='test_user_Aigh4vohb3seecha4aa4', - password='test_password_eneupeeVo5deilixoo8j') + username="test_user_Aigh4vohb3seecha4aa4", + password="test_password_eneupeeVo5deilixoo8j", + ) def test_begin_speech(self): Speaker.objects.add(self.user, self.item) - speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) + speaker = Speaker.objects.add( + get_user_model().objects.get(username="admin"), self.item + ) self.assertTrue(Speaker.objects.get(pk=speaker.pk).begin_time is None) response = self.client.put( - reverse('item-speak', args=[self.item.pk]), - {'speaker': speaker.pk}) + reverse("item-speak", args=[self.item.pk]), {"speaker": speaker.pk} + ) self.assertEqual(response.status_code, 200) self.assertFalse(Speaker.objects.get(pk=speaker.pk).begin_time is None) def test_begin_speech_next_speaker(self): speaker = Speaker.objects.add(self.user, self.item) - Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) + Speaker.objects.add(get_user_model().objects.get(username="admin"), self.item) - response = self.client.put(reverse('item-speak', args=[self.item.pk])) + response = self.client.put(reverse("item-speak", args=[self.item.pk])) self.assertEqual(response.status_code, 200) self.assertFalse(Speaker.objects.get(pk=speaker.pk).begin_time is None) def test_begin_speech_invalid_speaker_id(self): response = self.client.put( - reverse('item-speak', args=[self.item.pk]), - {'speaker': '1'}) + reverse("item-speak", args=[self.item.pk]), {"speaker": "1"} + ) self.assertEqual(response.status_code, 400) def test_begin_speech_invalid_data(self): response = self.client.put( - reverse('item-speak', args=[self.item.pk]), - {'speaker': 'invalid'}) + reverse("item-speak", args=[self.item.pk]), {"speaker": "invalid"} + ) self.assertEqual(response.status_code, 400) def test_end_speech(self): - speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) + speaker = Speaker.objects.add( + get_user_model().objects.get(username="admin"), self.item + ) speaker.begin_speech() self.assertFalse(Speaker.objects.get(pk=speaker.pk).begin_time is None) self.assertTrue(Speaker.objects.get(pk=speaker.pk).end_time is None) - response = self.client.delete(reverse('item-speak', args=[self.item.pk])) + response = self.client.delete(reverse("item-speak", args=[self.item.pk])) self.assertEqual(response.status_code, 200) self.assertFalse(Speaker.objects.get(pk=speaker.pk).end_time is None) def test_end_speech_no_current_speaker(self): - response = self.client.delete(reverse('item-speak', args=[self.item.pk])) + response = self.client.delete(reverse("item-speak", args=[self.item.pk])) self.assertEqual(response.status_code, 400) def test_begin_speech_with_countdown(self): - config['agenda_couple_countdown_and_speakers'] = True + config["agenda_couple_countdown_and_speakers"] = True Speaker.objects.add(self.user, self.item) - speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) + speaker = Speaker.objects.add( + get_user_model().objects.get(username="admin"), self.item + ) self.client.put( - reverse('item-speak', args=[self.item.pk]), - {'speaker': speaker.pk}) + reverse("item-speak", args=[self.item.pk]), {"speaker": speaker.pk} + ) # Countdown should be created with pk=1 and running self.assertEqual(Countdown.objects.all().count(), 1) countdown = Countdown.objects.get(pk=1) self.assertTrue(countdown.running) def test_end_speech_with_countdown(self): - config['agenda_couple_countdown_and_speakers'] = True - speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item) + config["agenda_couple_countdown_and_speakers"] = True + speaker = Speaker.objects.add( + get_user_model().objects.get(username="admin"), self.item + ) speaker.begin_speech() - self.client.delete(reverse('item-speak', args=[self.item.pk])) + self.client.delete(reverse("item-speak", args=[self.item.pk])) # Countdown should be created with pk=1 and stopped self.assertEqual(Countdown.objects.all().count(), 1) countdown = Countdown.objects.get(pk=1) @@ -355,74 +387,83 @@ class Numbering(TestCase): """ Tests view to number the agenda """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') - self.item_1 = Topic.objects.create(title='test_title_thuha8eef7ohXar3eech').agenda_item + self.client.login(username="admin", password="admin") + self.item_1 = Topic.objects.create( + title="test_title_thuha8eef7ohXar3eech" + ).agenda_item self.item_1.type = Item.AGENDA_ITEM self.item_1.weight = 1 self.item_1.save() - self.item_2 = Topic.objects.create(title='test_title_eisah7thuxa1eingaeLo').agenda_item + self.item_2 = Topic.objects.create( + title="test_title_eisah7thuxa1eingaeLo" + ).agenda_item self.item_2.type = Item.AGENDA_ITEM self.item_2.weight = 2 self.item_2.save() - self.item_2_1 = Topic.objects.create(title='test_title_Qui0audoaz5gie1phish').agenda_item + self.item_2_1 = Topic.objects.create( + title="test_title_Qui0audoaz5gie1phish" + ).agenda_item self.item_2_1.type = Item.AGENDA_ITEM self.item_2_1.parent = self.item_2 self.item_2_1.save() - self.item_3 = Topic.objects.create(title='test_title_ah7tphisheineisgaeLo').agenda_item + self.item_3 = Topic.objects.create( + title="test_title_ah7tphisheineisgaeLo" + ).agenda_item self.item_3.type = Item.AGENDA_ITEM self.item_3.weight = 3 self.item_3.save() def test_numbering(self): - response = self.client.post(reverse('item-numbering')) + response = self.client.post(reverse("item-numbering")) self.assertEqual(response.status_code, 200) - self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, '1') - self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, '2') - self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, '2.1') - self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, '3') + self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, "1") + self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, "2") + self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, "2.1") + self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, "3") def test_deactivated_numbering(self): - config['agenda_enable_numbering'] = False + config["agenda_enable_numbering"] = False - response = self.client.post(reverse('item-numbering')) + response = self.client.post(reverse("item-numbering")) self.assertEqual(response.status_code, 400) def test_roman_numbering(self): - config['agenda_numeral_system'] = 'roman' + config["agenda_numeral_system"] = "roman" - response = self.client.post(reverse('item-numbering')) + response = self.client.post(reverse("item-numbering")) self.assertEqual(response.status_code, 200) - self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, 'I') - self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, 'II') - self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, 'II.1') - self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, 'III') + self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, "I") + self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, "II") + self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, "II.1") + self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, "III") def test_with_internal_item(self): self.item_2.type = Item.INTERNAL_ITEM self.item_2.save() - response = self.client.post(reverse('item-numbering')) + response = self.client.post(reverse("item-numbering")) self.assertEqual(response.status_code, 200) - self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, '1') - self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, '') - self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, '') - self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, '2') + self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, "1") + self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, "") + self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, "") + self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, "2") def test_reset_numbering_with_internal_item(self): - self.item_2.item_number = 'test_number_Cieghae6ied5ool4hiem' + self.item_2.item_number = "test_number_Cieghae6ied5ool4hiem" self.item_2.type = Item.INTERNAL_ITEM self.item_2.save() - self.item_2_1.item_number = 'test_number_roQueTohg7fe1Is7aemu' + self.item_2_1.item_number = "test_number_roQueTohg7fe1Is7aemu" self.item_2_1.save() - response = self.client.post(reverse('item-numbering')) + response = self.client.post(reverse("item-numbering")) self.assertEqual(response.status_code, 200) - self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, '1') - self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, '') - self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, '') - self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, '2') + self.assertEqual(Item.objects.get(pk=self.item_1.pk).item_number, "1") + self.assertEqual(Item.objects.get(pk=self.item_2.pk).item_number, "") + self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, "") + self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, "2") diff --git a/tests/integration/assignments/test_viewset.py b/tests/integration/assignments/test_viewset.py index 3dba1d558..4474606c6 100644 --- a/tests/integration/assignments/test_viewset.py +++ b/tests/integration/assignments/test_viewset.py @@ -26,7 +26,7 @@ def test_assignment_db_queries(): TODO: The last request are a bug. """ for index in range(10): - Assignment.objects.create(title='assignment{}'.format(index), open_posts=1) + Assignment.objects.create(title="assignment{}".format(index), open_posts=1) assert count_queries(Assignment.get_elements) == 15 @@ -35,29 +35,46 @@ class CanidatureSelf(TestCase): """ Tests self candidation view. """ + def setUp(self): - self.client.login(username='admin', password='admin') - self.assignment = Assignment.objects.create(title='test_assignment_oikaengeijieh3ughiX7', open_posts=1) + self.client.login(username="admin", password="admin") + self.assignment = Assignment.objects.create( + title="test_assignment_oikaengeijieh3ughiX7", open_posts=1 + ) def test_nominate_self(self): - response = self.client.post(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.post( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 200) - self.assertTrue(Assignment.objects.get(pk=self.assignment.pk).candidates.filter(username='admin').exists()) + self.assertTrue( + Assignment.objects.get(pk=self.assignment.pk) + .candidates.filter(username="admin") + .exists() + ) def test_nominate_self_twice(self): - self.assignment.set_candidate(get_user_model().objects.get(username='admin')) + self.assignment.set_candidate(get_user_model().objects.get(username="admin")) - response = self.client.post(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.post( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 200) - self.assertTrue(Assignment.objects.get(pk=self.assignment.pk).candidates.filter(username='admin').exists()) + self.assertTrue( + Assignment.objects.get(pk=self.assignment.pk) + .candidates.filter(username="admin") + .exists() + ) def test_nominate_self_when_finished(self): self.assignment.set_phase(Assignment.PHASE_FINISHED) self.assignment.save() - response = self.client.post(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.post( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 400) @@ -65,69 +82,91 @@ class CanidatureSelf(TestCase): self.assignment.set_phase(Assignment.PHASE_VOTING) self.assignment.save() - response = self.client.post(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.post( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 200) - self.assertTrue(Assignment.objects.get(pk=self.assignment.pk).candidates.exists()) + self.assertTrue( + Assignment.objects.get(pk=self.assignment.pk).candidates.exists() + ) def test_nominate_self_during_voting_non_admin(self): self.assignment.set_phase(Assignment.PHASE_VOTING) self.assignment.save() - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') - group_delegates = type(group_admin).objects.get(name='Delegates') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") + group_delegates = type(group_admin).objects.get(name="Delegates") admin.groups.add(group_delegates) admin.groups.remove(group_admin) inform_changed_data(admin) - response = self.client.post(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.post( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 403) def test_withdraw_self(self): - self.assignment.set_candidate(get_user_model().objects.get(username='admin')) + self.assignment.set_candidate(get_user_model().objects.get(username="admin")) - response = self.client.delete(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.delete( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 200) - self.assertFalse(Assignment.objects.get(pk=self.assignment.pk).candidates.filter(username='admin').exists()) + self.assertFalse( + Assignment.objects.get(pk=self.assignment.pk) + .candidates.filter(username="admin") + .exists() + ) def test_withdraw_self_twice(self): - response = self.client.delete(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.delete( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 400) def test_withdraw_self_when_finished(self): - self.assignment.set_candidate(get_user_model().objects.get(username='admin')) + self.assignment.set_candidate(get_user_model().objects.get(username="admin")) self.assignment.set_phase(Assignment.PHASE_FINISHED) self.assignment.save() - response = self.client.delete(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.delete( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 400) def test_withdraw_self_during_voting(self): - self.assignment.set_candidate(get_user_model().objects.get(username='admin')) + self.assignment.set_candidate(get_user_model().objects.get(username="admin")) self.assignment.set_phase(Assignment.PHASE_VOTING) self.assignment.save() - response = self.client.delete(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.delete( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 200) - self.assertFalse(Assignment.objects.get(pk=self.assignment.pk).candidates.exists()) + self.assertFalse( + Assignment.objects.get(pk=self.assignment.pk).candidates.exists() + ) def test_withdraw_self_during_voting_non_admin(self): - self.assignment.set_candidate(get_user_model().objects.get(username='admin')) + self.assignment.set_candidate(get_user_model().objects.get(username="admin")) self.assignment.set_phase(Assignment.PHASE_VOTING) self.assignment.save() - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') - group_delegates = type(group_admin).objects.get(name='Delegates') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") + group_delegates = type(group_admin).objects.get(name="Delegates") admin.groups.add(group_delegates) admin.groups.remove(group_admin) inform_changed_data(admin) - response = self.client.delete(reverse('assignment-candidature-self', args=[self.assignment.pk])) + response = self.client.delete( + reverse("assignment-candidature-self", args=[self.assignment.pk]) + ) self.assertEqual(response.status_code, 403) @@ -135,21 +174,27 @@ class CanidatureSelf(TestCase): class CandidatureOther(TestCase): def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') - self.assignment = Assignment.objects.create(title='test_assignment_leiD6tiojigh1vei1ait', open_posts=1) + self.client.login(username="admin", password="admin") + self.assignment = Assignment.objects.create( + title="test_assignment_leiD6tiojigh1vei1ait", open_posts=1 + ) self.user = get_user_model().objects.create_user( - username='test_user_eeheekai4Phue6cahtho', - password='test_password_ThahXazeiV8veipeePh6') + username="test_user_eeheekai4Phue6cahtho", + password="test_password_ThahXazeiV8veipeePh6", + ) def test_invalid_data_empty_dict(self): response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), {}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), {} + ) self.assertEqual(response.status_code, 400) def test_invalid_data_string_instead_of_integer(self): response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), {'user': 'string_instead_of_integer'}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": "string_instead_of_integer"}, + ) self.assertEqual(response.status_code, 400) @@ -158,23 +203,33 @@ class CandidatureOther(TestCase): # Be careful: Here we do not test that the user does not exist. inexistent_user_pk = self.user.pk + 1000 response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), {'user': inexistent_user_pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": inexistent_user_pk}, + ) self.assertEqual(response.status_code, 400) def test_nominate_other(self): response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 200) - self.assertTrue(Assignment.objects.get(pk=self.assignment.pk).candidates.filter(username='test_user_eeheekai4Phue6cahtho').exists()) + self.assertTrue( + Assignment.objects.get(pk=self.assignment.pk) + .candidates.filter(username="test_user_eeheekai4Phue6cahtho") + .exists() + ) def test_nominate_other_twice(self): - self.assignment.set_candidate(get_user_model().objects.get(username='test_user_eeheekai4Phue6cahtho')) + self.assignment.set_candidate( + get_user_model().objects.get(username="test_user_eeheekai4Phue6cahtho") + ) response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 400) @@ -183,8 +238,9 @@ class CandidatureOther(TestCase): self.assignment.save() response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 400) @@ -193,40 +249,52 @@ class CandidatureOther(TestCase): self.assignment.save() response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 200) - self.assertTrue(Assignment.objects.get(pk=self.assignment.pk).candidates.filter(username='test_user_eeheekai4Phue6cahtho').exists()) + self.assertTrue( + Assignment.objects.get(pk=self.assignment.pk) + .candidates.filter(username="test_user_eeheekai4Phue6cahtho") + .exists() + ) def test_nominate_other_during_voting_non_admin(self): self.assignment.set_phase(Assignment.PHASE_VOTING) self.assignment.save() - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') - group_delegates = type(group_admin).objects.get(name='Delegates') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") + group_delegates = type(group_admin).objects.get(name="Delegates") admin.groups.add(group_delegates) admin.groups.remove(group_admin) inform_changed_data(admin) response = self.client.post( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 403) def test_delete_other(self): self.assignment.set_candidate(self.user) response = self.client.delete( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 200) - self.assertFalse(Assignment.objects.get(pk=self.assignment.pk).candidates.filter(username='test_user_eeheekai4Phue6cahtho').exists()) + self.assertFalse( + Assignment.objects.get(pk=self.assignment.pk) + .candidates.filter(username="test_user_eeheekai4Phue6cahtho") + .exists() + ) def test_delete_other_twice(self): response = self.client.delete( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 400) @@ -236,8 +304,9 @@ class CandidatureOther(TestCase): self.assignment.save() response = self.client.delete( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 400) @@ -247,26 +316,32 @@ class CandidatureOther(TestCase): self.assignment.save() response = self.client.delete( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 200) - self.assertFalse(Assignment.objects.get(pk=self.assignment.pk).candidates.filter(username='test_user_eeheekai4Phue6cahtho').exists()) + self.assertFalse( + Assignment.objects.get(pk=self.assignment.pk) + .candidates.filter(username="test_user_eeheekai4Phue6cahtho") + .exists() + ) def test_delete_other_during_voting_non_admin(self): self.assignment.set_candidate(self.user) self.assignment.set_phase(Assignment.PHASE_VOTING) self.assignment.save() - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') - group_delegates = type(group_admin).objects.get(name='Delegates') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") + group_delegates = type(group_admin).objects.get(name="Delegates") admin.groups.add(group_delegates) admin.groups.remove(group_admin) inform_changed_data(admin) response = self.client.delete( - reverse('assignment-candidature-other', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-candidature-other", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 403) @@ -276,69 +351,88 @@ class MarkElectedOtherUser(TestCase): Tests marking an elected user. We use an extra user here to show that admin can not only mark himself but also other users. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') - self.assignment = Assignment.objects.create(title='test_assignment_Ierohsh8rahshofiejai', open_posts=1) + self.client.login(username="admin", password="admin") + self.assignment = Assignment.objects.create( + title="test_assignment_Ierohsh8rahshofiejai", open_posts=1 + ) self.user = get_user_model().objects.create_user( - username='test_user_Oonei3rahji5jugh1eev', - password='test_password_aiphahb5Nah0cie4iP7o') + username="test_user_Oonei3rahji5jugh1eev", + password="test_password_aiphahb5Nah0cie4iP7o", + ) def test_mark_elected(self): - self.assignment.set_candidate(get_user_model().objects.get(username='test_user_Oonei3rahji5jugh1eev')) + self.assignment.set_candidate( + get_user_model().objects.get(username="test_user_Oonei3rahji5jugh1eev") + ) response = self.client.post( - reverse('assignment-mark-elected', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-mark-elected", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 200) - self.assertTrue(Assignment.objects.get(pk=self.assignment.pk).elected.filter(username='test_user_Oonei3rahji5jugh1eev').exists()) + self.assertTrue( + Assignment.objects.get(pk=self.assignment.pk) + .elected.filter(username="test_user_Oonei3rahji5jugh1eev") + .exists() + ) def test_mark_unelected(self): - user = get_user_model().objects.get(username='test_user_Oonei3rahji5jugh1eev') + user = get_user_model().objects.get(username="test_user_Oonei3rahji5jugh1eev") self.assignment.set_elected(user) response = self.client.delete( - reverse('assignment-mark-elected', args=[self.assignment.pk]), - {'user': self.user.pk}) + reverse("assignment-mark-elected", args=[self.assignment.pk]), + {"user": self.user.pk}, + ) self.assertEqual(response.status_code, 200) - self.assertFalse(Assignment.objects.get(pk=self.assignment.pk).elected.filter(username='test_user_Oonei3rahji5jugh1eev').exists()) + self.assertFalse( + Assignment.objects.get(pk=self.assignment.pk) + .elected.filter(username="test_user_Oonei3rahji5jugh1eev") + .exists() + ) class UpdateAssignmentPoll(TestCase): """ Tests updating polls of assignments. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') - self.assignment = Assignment.objects.create(title='test_assignment_ohneivoh9caiB8Yiungo', open_posts=1) - self.assignment.set_candidate(get_user_model().objects.get(username='admin')) + self.client.login(username="admin", password="admin") + self.assignment = Assignment.objects.create( + title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1 + ) + self.assignment.set_candidate(get_user_model().objects.get(username="admin")) self.poll = self.assignment.create_poll() def test_invalid_votesvalid_value(self): response = self.client.put( - reverse('assignmentpoll-detail', args=[self.poll.pk]), - {'assignment_id': self.assignment.pk, - 'votesvalid': '-3'}) + reverse("assignmentpoll-detail", args=[self.poll.pk]), + {"assignment_id": self.assignment.pk, "votesvalid": "-3"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_invalid_votesinvalid_value(self): response = self.client.put( - reverse('assignmentpoll-detail', args=[self.poll.pk]), - {'assignment_id': self.assignment.pk, - 'votesinvalid': '-3'}) + reverse("assignmentpoll-detail", args=[self.poll.pk]), + {"assignment_id": self.assignment.pk, "votesinvalid": "-3"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_invalid_votescast_value(self): response = self.client.put( - reverse('assignmentpoll-detail', args=[self.poll.pk]), - {'assignment_id': self.assignment.pk, - 'votescast': '-3'}) + reverse("assignmentpoll-detail", args=[self.poll.pk]), + {"assignment_id": self.assignment.pk, "votescast": "-3"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_empty_value_for_votesvalid(self): response = self.client.put( - reverse('assignmentpoll-detail', args=[self.poll.pk]), - {'assignment_id': self.assignment.pk, - 'votesvalid': ''}) + reverse("assignmentpoll-detail", args=[self.poll.pk]), + {"assignment_id": self.assignment.pk, "votesvalid": ""}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index ebc6af9ec..9e296ce10 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -4,11 +4,7 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient -from openslides import ( - __license__ as license, - __url__ as url, - __version__ as version, -) +from openslides import __license__ as license, __url__ as url, __version__ as version from openslides.core.config import ConfigVariable, config from openslides.core.models import Projector from openslides.topics.models import Topic @@ -20,74 +16,99 @@ class ProjectorAPI(TestCase): """ Tests requests from the anonymous user. """ + def test_slide_on_default_projector(self): - self.client.login(username='admin', password='admin') - topic = Topic.objects.create(title='title_que1olaish5Wei7que6i', text='text_aishah8Eh7eQuie5ooji') + self.client.login(username="admin", password="admin") + topic = Topic.objects.create( + title="title_que1olaish5Wei7que6i", text="text_aishah8Eh7eQuie5ooji" + ) default_projector = Projector.objects.get(pk=1) default_projector.config = { - 'aae4a07b26534cfb9af4232f361dce73': {'name': 'topics/topic', 'id': topic.id}} + "aae4a07b26534cfb9af4232f361dce73": {"name": "topics/topic", "id": topic.id} + } default_projector.save() - response = self.client.get(reverse('projector-detail', args=['1'])) + response = self.client.get(reverse("projector-detail", args=["1"])) content = json.loads(response.content.decode()) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(content['elements'], { - 'aae4a07b26534cfb9af4232f361dce73': - {'id': topic.id, - 'uuid': 'aae4a07b26534cfb9af4232f361dce73', - 'name': 'topics/topic', - 'agenda_item_id': topic.agenda_item_id}}) + self.assertEqual( + content["elements"], + { + "aae4a07b26534cfb9af4232f361dce73": { + "id": topic.id, + "uuid": "aae4a07b26534cfb9af4232f361dce73", + "name": "topics/topic", + "agenda_item_id": topic.agenda_item_id, + } + }, + ) def test_invalid_slide_on_default_projector(self): - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") default_projector = Projector.objects.get(pk=1) default_projector.config = { - 'fc6ef43b624043068c8e6e7a86c5a1b0': {'name': 'invalid_slide'}} + "fc6ef43b624043068c8e6e7a86c5a1b0": {"name": "invalid_slide"} + } default_projector.save() - response = self.client.get(reverse('projector-detail', args=['1'])) + response = self.client.get(reverse("projector-detail", args=["1"])) content = json.loads(response.content.decode()) - del content['projectiondefaults'] + del content["projectiondefaults"] self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(content, { - 'id': 1, - 'elements': { - 'fc6ef43b624043068c8e6e7a86c5a1b0': - {'name': 'invalid_slide', - 'uuid': 'fc6ef43b624043068c8e6e7a86c5a1b0', - 'error': 'Projector element does not exist.'}}, - 'scale': 0, - 'scroll': 0, - 'name': 'Default projector', - 'blank': False, - 'width': 1220, - 'height': 915}) + self.assertEqual( + content, + { + "id": 1, + "elements": { + "fc6ef43b624043068c8e6e7a86c5a1b0": { + "name": "invalid_slide", + "uuid": "fc6ef43b624043068c8e6e7a86c5a1b0", + "error": "Projector element does not exist.", + } + }, + "scale": 0, + "scroll": 0, + "name": "Default projector", + "blank": False, + "width": 1220, + "height": 915, + }, + ) class VersionView(TestCase): """ Tests the version info view. """ + def test_get(self): - self.client.login(username='admin', password='admin') - response = self.client.get(reverse('core_version')) - self.assertEqual(json.loads(response.content.decode()), { - 'openslides_version': version, - 'openslides_license': license, - 'openslides_url': url, - 'plugins': [ - {'verbose_name': 'OpenSlides Test Plugin', - 'description': 'This is a test plugin for OpenSlides.', - 'version': 'unknown', - 'license': 'MIT', - 'url': ''}]}) + self.client.login(username="admin", password="admin") + response = self.client.get(reverse("core_version")) + self.assertEqual( + json.loads(response.content.decode()), + { + "openslides_version": version, + "openslides_license": license, + "openslides_url": url, + "plugins": [ + { + "verbose_name": "OpenSlides Test Plugin", + "description": "This is a test plugin for OpenSlides.", + "version": "unknown", + "license": "MIT", + "url": "", + } + ], + }, + ) class ConfigViewSet(TestCase): """ Tests requests to deal with config variables. """ + def setUp(self): # Save the old value of the config object and add the test values # TODO: Can be changed to setUpClass when Django 1.8 is no longer supported @@ -103,21 +124,28 @@ class ConfigViewSet(TestCase): def test_update(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_Xeiizi7ooH8Thuk5aida']), - {'value': 'test_value_Phohx3oopeichaiTheiw'}) + reverse("config-detail", args=["test_var_Xeiizi7ooH8Thuk5aida"]), + {"value": "test_value_Phohx3oopeichaiTheiw"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(config['test_var_Xeiizi7ooH8Thuk5aida'], 'test_value_Phohx3oopeichaiTheiw') + self.assertEqual( + config["test_var_Xeiizi7ooH8Thuk5aida"], "test_value_Phohx3oopeichaiTheiw" + ) def test_update_wrong_datatype(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_ohhii4iavoh5Phoh5ahg']), - {'value': 'test_value_string'}) + reverse("config-detail", args=["test_var_ohhii4iavoh5Phoh5ahg"]), + {"value": "test_value_string"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': "Wrong datatype. Expected , got ."}) + self.assertEqual( + response.data, + {"detail": "Wrong datatype. Expected , got ."}, + ) def test_update_wrong_datatype_that_can_be_converted(self): """ @@ -125,63 +153,75 @@ class ConfigViewSet(TestCase): field. """ self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_ohhii4iavoh5Phoh5ahg']), - {'value': '12345'}) + reverse("config-detail", args=["test_var_ohhii4iavoh5Phoh5ahg"]), + {"value": "12345"}, + ) self.assertEqual(response.status_code, 200) def test_update_good_choice(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_wei0Rei9ahzooSohK1ph']), - {'value': 'key_2_yahb2ain1aeZ1lea1Pei'}) + reverse("config-detail", args=["test_var_wei0Rei9ahzooSohK1ph"]), + {"value": "key_2_yahb2ain1aeZ1lea1Pei"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(config['test_var_wei0Rei9ahzooSohK1ph'], 'key_2_yahb2ain1aeZ1lea1Pei') + self.assertEqual( + config["test_var_wei0Rei9ahzooSohK1ph"], "key_2_yahb2ain1aeZ1lea1Pei" + ) def test_update_bad_choice(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_wei0Rei9ahzooSohK1ph']), - {'value': 'test_value_bad_string'}) + reverse("config-detail", args=["test_var_wei0Rei9ahzooSohK1ph"]), + {"value": "test_value_bad_string"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'Invalid input. Choice does not match.'}) + self.assertEqual( + response.data, {"detail": "Invalid input. Choice does not match."} + ) def test_update_validator_ok(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_Hi7Oje8Oith7goopeeng']), - {'value': 'valid_string'}) + reverse("config-detail", args=["test_var_Hi7Oje8Oith7goopeeng"]), + {"value": "valid_string"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(config['test_var_Hi7Oje8Oith7goopeeng'], 'valid_string') + self.assertEqual(config["test_var_Hi7Oje8Oith7goopeeng"], "valid_string") def test_update_validator_invalid(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_Hi7Oje8Oith7goopeeng']), - {'value': 'invalid_string'}) + reverse("config-detail", args=["test_var_Hi7Oje8Oith7goopeeng"]), + {"value": "invalid_string"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'Invalid input.'}) + self.assertEqual(response.data, {"detail": "Invalid input."}) def test_update_only_with_key(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.put( - reverse('config-detail', args=['test_var_Xeiizi7ooH8Thuk5aida'])) + reverse("config-detail", args=["test_var_Xeiizi7ooH8Thuk5aida"]) + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'Invalid input. Config value is missing.'}) + self.assertEqual( + response.data, {"detail": "Invalid input. Config value is missing."} + ) def validator_for_testing(value): """ Validator for testing. """ - if value == 'invalid_string': - raise ValidationError({'detail': 'Invalid input.'}) + if value == "invalid_string": + raise ValidationError({"detail": "Invalid input."}) def set_simple_config_view_integration_config_test(): @@ -190,34 +230,42 @@ def set_simple_config_view_integration_config_test(): grouping. """ yield ConfigVariable( - name='test_var_aeW3Quahkah1phahCheo', + name="test_var_aeW3Quahkah1phahCheo", default_value=None, - label='test_label_aeNahsheu8phahk8taYo') + label="test_label_aeNahsheu8phahk8taYo", + ) + + yield ConfigVariable(name="test_var_Xeiizi7ooH8Thuk5aida", default_value="") yield ConfigVariable( - name='test_var_Xeiizi7ooH8Thuk5aida', - default_value='') + name="test_var_ohhii4iavoh5Phoh5ahg", default_value=0, input_type="integer" + ) yield ConfigVariable( - name='test_var_ohhii4iavoh5Phoh5ahg', - default_value=0, - input_type='integer') - - yield ConfigVariable( - name='test_var_wei0Rei9ahzooSohK1ph', - default_value='key_1_Queit2juchoocos2Vugh', - input_type='choice', + name="test_var_wei0Rei9ahzooSohK1ph", + default_value="key_1_Queit2juchoocos2Vugh", + input_type="choice", choices=( - {'value': 'key_1_Queit2juchoocos2Vugh', 'display_name': 'label_1_Queit2juchoocos2Vugh'}, - {'value': 'key_2_yahb2ain1aeZ1lea1Pei', 'display_name': 'label_2_yahb2ain1aeZ1lea1Pei'})) + { + "value": "key_1_Queit2juchoocos2Vugh", + "display_name": "label_1_Queit2juchoocos2Vugh", + }, + { + "value": "key_2_yahb2ain1aeZ1lea1Pei", + "display_name": "label_2_yahb2ain1aeZ1lea1Pei", + }, + ), + ) yield ConfigVariable( - name='test_var_Hi7Oje8Oith7goopeeng', - default_value='', - validators=(validator_for_testing,)) + name="test_var_Hi7Oje8Oith7goopeeng", + default_value="", + validators=(validator_for_testing,), + ) yield ConfigVariable( - name='test_var_pud2zah2teeNaiP7IoNa', + name="test_var_pud2zah2teeNaiP7IoNa", default_value=None, - label='test_label_xaing7eefaePheePhei6', - hidden=True) + label="test_label_xaing7eefaePheePhei6", + hidden=True, + ) diff --git a/tests/integration/core/test_viewset.py b/tests/integration/core/test_viewset.py index 874b50746..792076d44 100644 --- a/tests/integration/core/test_viewset.py +++ b/tests/integration/core/test_viewset.py @@ -29,7 +29,7 @@ def test_chat_message_db_queries(): Tests that only the following db queries are done: * 1 requests to get the list of all chatmessages. """ - user = User.objects.get(username='admin') + user = User.objects.get(username="admin") for index in range(10): ChatMessage.objects.create(user=user) @@ -43,7 +43,7 @@ def test_tag_db_queries(): * 1 requests to get the list of all tags. """ for index in range(10): - Tag.objects.create(name='tag{}'.format(index)) + Tag.objects.create(name="tag{}".format(index)) assert count_queries(Tag.get_elements) == 1 @@ -63,12 +63,15 @@ class ChatMessageViewSet(TestCase): """ Tests requests to deal with chat messages. """ + def setUp(self): - admin = User.objects.get(username='admin') + admin = User.objects.get(username="admin") self.client.force_login(admin) - ChatMessage.objects.create(message='test_message_peechiel8IeZoohaem9e', user=admin) + ChatMessage.objects.create( + message="test_message_peechiel8IeZoohaem9e", user=admin + ) def test_clear_chat(self): - response = self.client.post(reverse('chatmessage-clear')) + response = self.client.post(reverse("chatmessage-clear")) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(ChatMessage.objects.all().count(), 0) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 70e5903de..d739d251f 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -19,14 +19,15 @@ class TConfig: elements = [] config.key_to_id = {} for id, item in enumerate(config.config_variables.values()): - elements.append({'id': id+1, 'key': item.name, 'value': item.default_value}) - config.key_to_id[item.name] = id+1 + elements.append( + {"id": id + 1, "key": item.name, "value": item.default_value} + ) + config.key_to_id[item.name] = id + 1 return elements async def restrict_elements( - self, - user_id: int, - elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + self, user_id: int, elements: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: return elements @@ -40,16 +41,30 @@ class TUser: def get_elements(self) -> List[Dict[str, Any]]: return [ - {'id': 1, 'username': 'admin', 'title': '', 'first_name': '', - 'last_name': 'Administrator', 'structure_level': '', 'number': '', 'about_me': '', - 'groups_id': [4], 'is_present': False, 'is_committee': False, 'email': '', - 'last_email_send': None, 'comment': '', 'is_active': True, 'default_password': 'admin', - 'session_auth_hash': '362d4f2de1463293cb3aaba7727c967c35de43ee'}] + { + "id": 1, + "username": "admin", + "title": "", + "first_name": "", + "last_name": "Administrator", + "structure_level": "", + "number": "", + "about_me": "", + "groups_id": [4], + "is_present": False, + "is_committee": False, + "email": "", + "last_email_send": None, + "comment": "", + "is_active": True, + "default_password": "admin", + "session_auth_hash": "362d4f2de1463293cb3aaba7727c967c35de43ee", + } + ] async def restrict_elements( - self, - user_id: int, - elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + self, user_id: int, elements: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: return elements @@ -58,8 +73,14 @@ def count_queries(func, *args, **kwargs) -> int: with context: func(*args, **kwargs) - print("%d queries executed\nCaptured queries were:\n%s" % ( - len(context), - '\n'.join( - '%d. %s' % (i, query['sql']) for i, query in enumerate(context.captured_queries, start=1)))) + print( + "%d queries executed\nCaptured queries were:\n%s" + % ( + len(context), + "\n".join( + "%d. %s" % (i, query["sql"]) + for i, query in enumerate(context.captured_queries, start=1) + ), + ) + ) return len(context) diff --git a/tests/integration/mediafiles/test_viewset.py b/tests/integration/mediafiles/test_viewset.py index 47b662c80..4abe7a4b9 100644 --- a/tests/integration/mediafiles/test_viewset.py +++ b/tests/integration/mediafiles/test_viewset.py @@ -14,9 +14,8 @@ def test_mediafiles_db_queries(): """ for index in range(10): Mediafile.objects.create( - title='some_file{}'.format(index), - mediafile=SimpleUploadedFile( - 'some_file{}'.format(index), - b'some content.')) + title="some_file{}".format(index), + mediafile=SimpleUploadedFile("some_file{}".format(index), b"some content."), + ) assert count_queries(Mediafile.get_elements) == 1 diff --git a/tests/integration/motions/test_views.py b/tests/integration/motions/test_views.py index d3a434279..9897b11e8 100644 --- a/tests/integration/motions/test_views.py +++ b/tests/integration/motions/test_views.py @@ -9,13 +9,14 @@ class AnonymousRequests(TestCase): """ Tests requests from the anonymous user. """ + def setUp(self): self.client = Client() - config['general_system_enable_anonymous'] = True + config["general_system_enable_anonymous"] = True def test_motion_detail(self): - Motion.objects.create(title='test_motion') + Motion.objects.create(title="test_motion") - response = self.client.get('/motions/1/') + response = self.client.get("/motions/1/") self.assertEqual(response.status_code, 200) diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py index 0462c775b..e1d190ffc 100644 --- a/tests/integration/motions/test_viewset.py +++ b/tests/integration/motions/test_viewset.py @@ -52,24 +52,22 @@ def test_motion_db_queries(): Two comment sections are created and for each motions two comments. """ - section1 = MotionCommentSection.objects.create(name='test_section') - section2 = MotionCommentSection.objects.create(name='test_section') + section1 = MotionCommentSection.objects.create(name="test_section") + section2 = MotionCommentSection.objects.create(name="test_section") for index in range(10): - motion = Motion.objects.create(title='motion{}'.format(index)) + motion = Motion.objects.create(title="motion{}".format(index)) MotionComment.objects.create( - comment='test_comment', - motion=motion, - section=section1) + comment="test_comment", motion=motion, section=section1 + ) MotionComment.objects.create( - comment='test_comment2', - motion=motion, - section=section2) + comment="test_comment2", motion=motion, section=section2 + ) get_user_model().objects.create_user( - username='user_{}'.format(index), - password='password') + username="user_{}".format(index), password="password" + ) # TODO: Create some polls etc. assert count_queries(Motion.get_elements) == 12 @@ -82,7 +80,7 @@ def test_category_db_queries(): * 1 requests to get the list of all categories. """ for index in range(10): - Category.objects.create(name='category{}'.format(index)) + Category.objects.create(name="category{}".format(index)) assert count_queries(Category.get_elements) == 1 @@ -95,8 +93,8 @@ def test_statute_paragraph_db_queries(): """ for index in range(10): StatuteParagraph.objects.create( - title='statute_paragraph{}'.format(index), - text='text{}'.format(index)) + title="statute_paragraph{}".format(index), text="text{}".format(index) + ) assert count_queries(StatuteParagraph.get_elements) == 1 @@ -117,93 +115,107 @@ class TestStatuteParagraphs(TestCase): """ Tests all CRUD operations of statute paragraphs. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") def create_statute_paragraph(self): - self.title = 'test_title_fiWs82D0D)2kje3KDm2s' - self.text = 'test_text_3jfjoDqm,S;cmor3DJwk' - self.cp = StatuteParagraph.objects.create( - title=self.title, - text=self.text) + self.title = "test_title_fiWs82D0D)2kje3KDm2s" + self.text = "test_text_3jfjoDqm,S;cmor3DJwk" + self.cp = StatuteParagraph.objects.create(title=self.title, text=self.text) def test_create_simple(self): response = self.client.post( - reverse('statuteparagraph-list'), - {'title': 'test_title_f3FM328cq)tzdU238df2', - 'text': 'test_text_2fb)BEjwdI38=kfemiRkcOW'}) + reverse("statuteparagraph-list"), + { + "title": "test_title_f3FM328cq)tzdU238df2", + "text": "test_text_2fb)BEjwdI38=kfemiRkcOW", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) cp = StatuteParagraph.objects.get() - self.assertEqual(cp.title, 'test_title_f3FM328cq)tzdU238df2') - self.assertEqual(cp.text, 'test_text_2fb)BEjwdI38=kfemiRkcOW') + self.assertEqual(cp.title, "test_title_f3FM328cq)tzdU238df2") + self.assertEqual(cp.text, "test_text_2fb)BEjwdI38=kfemiRkcOW") def test_create_without_data(self): - response = self.client.post(reverse('statuteparagraph-list'), {}) + response = self.client.post(reverse("statuteparagraph-list"), {}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'title': ['This field is required.'], 'text': ['This field is required.']}) + self.assertEqual( + response.data, + {"title": ["This field is required."], "text": ["This field is required."]}, + ) def test_create_non_admin(self): - self.admin = get_user_model().objects.get(username='admin') + self.admin = get_user_model().objects.get(username="admin") self.admin.groups.add(GROUP_DELEGATE_PK) self.admin.groups.remove(GROUP_ADMIN_PK) inform_changed_data(self.admin) response = self.client.post( - reverse('statuteparagraph-list'), - {'title': 'test_title_f3(Dj2jdP39fjW2kdcwe', - 'text': 'test_text_vlC)=fwWmcwcpWMvnuw('}) + reverse("statuteparagraph-list"), + { + "title": "test_title_f3(Dj2jdP39fjW2kdcwe", + "text": "test_text_vlC)=fwWmcwcpWMvnuw(", + }, + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_retrieve_simple(self): self.create_statute_paragraph() - response = self.client.get(reverse('statuteparagraph-detail', args=[self.cp.pk])) + response = self.client.get( + reverse("statuteparagraph-detail", args=[self.cp.pk]) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(sorted(response.data.keys()), sorted(( - 'id', - 'title', - 'text', - 'weight',))) + self.assertEqual( + sorted(response.data.keys()), sorted(("id", "title", "text", "weight")) + ) def test_update_simple(self): self.create_statute_paragraph() response = self.client.patch( - reverse('statuteparagraph-detail', args=[self.cp.pk]), - {'text': 'test_text_ke(czr/cwk1Sl2seeFwE'}) + reverse("statuteparagraph-detail", args=[self.cp.pk]), + {"text": "test_text_ke(czr/cwk1Sl2seeFwE"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) cp = StatuteParagraph.objects.get() self.assertEqual(cp.title, self.title) - self.assertEqual(cp.text, 'test_text_ke(czr/cwk1Sl2seeFwE') + self.assertEqual(cp.text, "test_text_ke(czr/cwk1Sl2seeFwE") def test_update_non_admin(self): - self.admin = get_user_model().objects.get(username='admin') + self.admin = get_user_model().objects.get(username="admin") self.admin.groups.add(GROUP_DELEGATE_PK) self.admin.groups.remove(GROUP_ADMIN_PK) inform_changed_data(self.admin) self.create_statute_paragraph() response = self.client.patch( - reverse('statuteparagraph-detail', args=[self.cp.pk]), - {'text': 'test_text_ke(czr/cwk1Sl2seeFwE'}) + reverse("statuteparagraph-detail", args=[self.cp.pk]), + {"text": "test_text_ke(czr/cwk1Sl2seeFwE"}, + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) cp = StatuteParagraph.objects.get() self.assertEqual(cp.text, self.text) def test_delete_simple(self): self.create_statute_paragraph() - response = self.client.delete(reverse('statuteparagraph-detail', args=[self.cp.pk])) + response = self.client.delete( + reverse("statuteparagraph-detail", args=[self.cp.pk]) + ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(StatuteParagraph.objects.count(), 0) def test_delete_non_admin(self): - self.admin = get_user_model().objects.get(username='admin') + self.admin = get_user_model().objects.get(username="admin") self.admin.groups.add(GROUP_DELEGATE_PK) self.admin.groups.remove(GROUP_ADMIN_PK) inform_changed_data(self.admin) self.create_statute_paragraph() - response = self.client.delete(reverse('statuteparagraph-detail', args=[self.cp.pk])) + response = self.client.delete( + reverse("statuteparagraph-detail", args=[self.cp.pk]) + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(StatuteParagraph.objects.count(), 1) @@ -212,9 +224,10 @@ class CreateMotion(TestCase): """ Tests motion creation. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") def test_simple(self): """ @@ -224,95 +237,125 @@ class CreateMotion(TestCase): be the submitter. """ response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_OoCoo3MeiT9li5Iengu9', - 'text': 'test_text_thuoz0iecheiheereiCi'}) + reverse("motion-list"), + { + "title": "test_title_OoCoo3MeiT9li5Iengu9", + "text": "test_text_thuoz0iecheiheereiCi", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) motion = Motion.objects.get() - self.assertEqual(motion.title, 'test_title_OoCoo3MeiT9li5Iengu9') - self.assertEqual(motion.identifier, '1') + self.assertEqual(motion.title, "test_title_OoCoo3MeiT9li5Iengu9") + self.assertEqual(motion.identifier, "1") self.assertTrue(motion.submitters.exists()) - self.assertEqual(motion.submitters.get().user.username, 'admin') + self.assertEqual(motion.submitters.get().user.username, "admin") def test_with_reason(self): response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_saib4hiHaifo9ohp9yie', - 'text': 'test_text_shahhie8Ej4mohvoorie', - 'reason': 'test_reason_Ou8GivahYivoh3phoh9c'}) + reverse("motion-list"), + { + "title": "test_title_saib4hiHaifo9ohp9yie", + "text": "test_text_shahhie8Ej4mohvoorie", + "reason": "test_reason_Ou8GivahYivoh3phoh9c", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Motion.objects.get().reason, 'test_reason_Ou8GivahYivoh3phoh9c') + self.assertEqual( + Motion.objects.get().reason, "test_reason_Ou8GivahYivoh3phoh9c" + ) def test_without_data(self): - response = self.client.post( - reverse('motion-list'), - {}) + response = self.client.post(reverse("motion-list"), {}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'title': ['This field is required.'], 'text': ['This field is required.']}) + self.assertEqual( + response.data, + {"title": ["This field is required."], "text": ["This field is required."]}, + ) def test_with_category(self): category = Category.objects.create( - name='test_category_name_CiengahzooH4ohxietha', - prefix='TEST_PREFIX_la0eadaewuec3seoxeiN') + name="test_category_name_CiengahzooH4ohxietha", + prefix="TEST_PREFIX_la0eadaewuec3seoxeiN", + ) response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_Air0bahchaiph1ietoo2', - 'text': 'test_text_chaeF9wosh8OowazaiVu', - 'category_id': category.pk}) + reverse("motion-list"), + { + "title": "test_title_Air0bahchaiph1ietoo2", + "text": "test_text_chaeF9wosh8OowazaiVu", + "category_id": category.pk, + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) motion = Motion.objects.get() self.assertEqual(motion.category, category) - self.assertEqual(motion.identifier, 'TEST_PREFIX_la0eadaewuec3seoxeiN 1') + self.assertEqual(motion.identifier, "TEST_PREFIX_la0eadaewuec3seoxeiN 1") def test_with_submitters(self): submitter_1 = get_user_model().objects.create_user( - username='test_username_ooFe6aebei9ieQui2poo', - password='test_password_vie9saiQu5Aengoo9ku0') + username="test_username_ooFe6aebei9ieQui2poo", + password="test_password_vie9saiQu5Aengoo9ku0", + ) submitter_2 = get_user_model().objects.create_user( - username='test_username_eeciengoc4aihie5eeSh', - password='test_password_peik2Eihu5oTh7siequi') + username="test_username_eeciengoc4aihie5eeSh", + password="test_password_peik2Eihu5oTh7siequi", + ) response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_pha7moPh7quoth4paina', - 'text': 'test_text_YooGhae6tiangung5Rie', - 'submitters_id': [submitter_1.pk, submitter_2.pk]}) + reverse("motion-list"), + { + "title": "test_title_pha7moPh7quoth4paina", + "text": "test_text_YooGhae6tiangung5Rie", + "submitters_id": [submitter_1.pk, submitter_2.pk], + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) motion = Motion.objects.get() self.assertEqual(motion.submitters.count(), 2) def test_with_one_supporter(self): supporter = get_user_model().objects.create_user( - username='test_username_ahGhi4Quohyee7ohngie', - password='test_password_Nei6aeh8OhY8Aegh1ohX') + username="test_username_ahGhi4Quohyee7ohngie", + password="test_password_Nei6aeh8OhY8Aegh1ohX", + ) response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_Oecee4Da2Mu9EY6Ui4mu', - 'text': 'test_text_FbhgnTFgkbjdmvcjbffg', - 'supporters_id': [supporter.pk]}) + reverse("motion-list"), + { + "title": "test_title_Oecee4Da2Mu9EY6Ui4mu", + "text": "test_text_FbhgnTFgkbjdmvcjbffg", + "supporters_id": [supporter.pk], + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) motion = Motion.objects.get() - self.assertEqual(motion.supporters.get().username, 'test_username_ahGhi4Quohyee7ohngie') + self.assertEqual( + motion.supporters.get().username, "test_username_ahGhi4Quohyee7ohngie" + ) def test_with_tag(self): - tag = Tag.objects.create(name='test_tag_iRee3kiecoos4rorohth') + tag = Tag.objects.create(name="test_tag_iRee3kiecoos4rorohth") response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_Hahke4loos4eiduNiid9', - 'text': 'test_text_johcho0Ucaibiehieghe', - 'tags_id': [tag.pk]}) + reverse("motion-list"), + { + "title": "test_title_Hahke4loos4eiduNiid9", + "text": "test_text_johcho0Ucaibiehieghe", + "tags_id": [tag.pk], + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) motion = Motion.objects.get() - self.assertEqual(motion.tags.get().name, 'test_tag_iRee3kiecoos4rorohth') + self.assertEqual(motion.tags.get().name, "test_tag_iRee3kiecoos4rorohth") def test_with_workflow(self): """ Test to create a motion with a specific workflow. """ response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_eemuR5hoo4ru2ahgh5EJ', - 'text': 'test_text_ohviePopahPhoili7yee', - 'workflow_id': '2'}) + reverse("motion-list"), + { + "title": "test_title_eemuR5hoo4ru2ahgh5EJ", + "text": "test_text_ohviePopahPhoili7yee", + "workflow_id": "2", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Motion.objects.get().state.workflow_id, 2) @@ -321,15 +364,18 @@ class CreateMotion(TestCase): """ Test to create a motion by a delegate, non staff user. """ - self.admin = get_user_model().objects.get(username='admin') + self.admin = get_user_model().objects.get(username="admin") self.admin.groups.add(GROUP_DELEGATE_PK) self.admin.groups.remove(GROUP_ADMIN_PK) inform_changed_data(self.admin) response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_peiJozae0luew9EeL8bo', - 'text': 'test_text_eHohS8ohr5ahshoah8Oh'}) + reverse("motion-list"), + { + "title": "test_title_peiJozae0luew9EeL8bo", + "text": "test_text_eHohS8ohr5ahshoah8Oh", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -339,11 +385,14 @@ class CreateMotion(TestCase): """ parent_motion = self.create_parent_motion() response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_doe93Jsjd2sW20dkSl20', - 'text': 'test_text_feS20SksD8D25skmwD25', - 'parent_id': parent_motion.id}) - created_motion = Motion.objects.get(pk=int(response.data['id'])) + reverse("motion-list"), + { + "title": "test_title_doe93Jsjd2sW20dkSl20", + "text": "test_text_feS20SksD8D25skmwD25", + "parent_id": parent_motion.id, + }, + ) + created_motion = Motion.objects.get(pk=int(response.data["id"])) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(created_motion.parent, parent_motion) @@ -353,13 +402,16 @@ class CreateMotion(TestCase): Test to create an amendment motion with a non existing parent. """ response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_gEjdkW93Wj23KS2s8dSe', - 'text': 'test_text_lfwLIC&AjfsaoijOEusa', - 'parent_id': 100}) + reverse("motion-list"), + { + "title": "test_title_gEjdkW93Wj23KS2s8dSe", + "text": "test_text_lfwLIC&AjfsaoijOEusa", + "parent_id": 100, + }, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'The parent motion does not exist.'}) + self.assertEqual(response.data, {"detail": "The parent motion does not exist."}) def test_amendment_motion_non_admin(self): """ @@ -368,22 +420,26 @@ class CreateMotion(TestCase): """ parent_motion = self.create_parent_motion() category = Category.objects.create( - name='test_category_name_Dslk3Fj8s8Ps36S3Kskw', - prefix='TEST_PREFIX_L23skfmlq3kslamslS39') + name="test_category_name_Dslk3Fj8s8Ps36S3Kskw", + prefix="TEST_PREFIX_L23skfmlq3kslamslS39", + ) parent_motion.category = category parent_motion.save() - self.admin = get_user_model().objects.get(username='admin') + self.admin = get_user_model().objects.get(username="admin") self.admin.groups.add(GROUP_DELEGATE_PK) self.admin.groups.remove(GROUP_ADMIN_PK) inform_changed_data(self.admin) response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_fk3a0slalms47KSewnWG', - 'text': 'test_text_al3FMwSCNM31WOmw9ezx', - 'parent_id': parent_motion.id}) - created_motion = Motion.objects.get(pk=int(response.data['id'])) + reverse("motion-list"), + { + "title": "test_title_fk3a0slalms47KSewnWG", + "text": "test_text_al3FMwSCNM31WOmw9ezx", + "parent_id": parent_motion.id, + }, + ) + created_motion = Motion.objects.get(pk=int(response.data["id"])) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(created_motion.parent, parent_motion) @@ -394,86 +450,105 @@ class CreateMotion(TestCase): Returns a new created motion used for testing amendments. """ response = self.client.post( - reverse('motion-list'), - {'title': 'test_title_3leoeo2qac7830c92j9s', - 'text': 'test_text_9dm3ks9gDuW20Al38L9w'}) - return Motion.objects.get(pk=int(response.data['id'])) + reverse("motion-list"), + { + "title": "test_title_3leoeo2qac7830c92j9s", + "text": "test_text_9dm3ks9gDuW20Al38L9w", + }, + ) + return Motion.objects.get(pk=int(response.data["id"])) class RetrieveMotion(TestCase): """ Tests retrieving a motion (with poll results). """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion = Motion( - title='test_title_uj5eeSiedohSh3ohyaaj', - text='test_text_ithohchaeThohmae5aug') + title="test_title_uj5eeSiedohSh3ohyaaj", + text="test_text_ithohchaeThohmae5aug", + ) self.motion.save() self.motion.create_poll() for index in range(10): get_user_model().objects.create_user( - username='user_{}'.format(index), - password='password') + username="user_{}".format(index), password="password" + ) def test_guest_state_with_required_permission_to_see(self): - config['general_system_enable_anonymous'] = True + config["general_system_enable_anonymous"] = True guest_client = APIClient() state = self.motion.state - state.required_permission_to_see = 'permission_that_the_user_does_not_have_leeceiz9hi7iuta4ahY2' + state.required_permission_to_see = ( + "permission_that_the_user_does_not_have_leeceiz9hi7iuta4ahY2" + ) state.save() # The cache has to be cleared, see: # https://github.com/OpenSlides/OpenSlides/issues/3396 inform_changed_data(self.motion) - response = guest_client.get(reverse('motion-detail', args=[self.motion.pk])) + response = guest_client.get(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, 404) def test_admin_state_with_required_permission_to_see(self): state = self.motion.state - state.required_permission_to_see = 'permission_that_the_user_does_not_have_coo1Iewu8Eing2xahfoo' + state.required_permission_to_see = ( + "permission_that_the_user_does_not_have_coo1Iewu8Eing2xahfoo" + ) state.save() - response = self.client.get(reverse('motion-detail', args=[self.motion.pk])) + response = self.client.get(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_submitter_state_with_required_permission_to_see(self): state = self.motion.state - state.required_permission_to_see = 'permission_that_the_user_does_not_have_eiW8af9caizoh1thaece' + state.required_permission_to_see = ( + "permission_that_the_user_does_not_have_eiW8af9caizoh1thaece" + ) state.save() user = get_user_model().objects.create_user( - username='username_ohS2opheikaSa5theijo', - password='password_kau4eequaisheeBateef') + username="username_ohS2opheikaSa5theijo", + password="password_kau4eequaisheeBateef", + ) Submitter.objects.add(user, self.motion) submitter_client = APIClient() submitter_client.force_login(user) - response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk])) + response = submitter_client.get(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(self): - admin = get_user_model().objects.get(username='admin') + def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data( + self + ): + admin = get_user_model().objects.get(username="admin") Submitter.objects.add(admin, self.motion) - group = get_group_model().objects.get(pk=GROUP_DEFAULT_PK) # Group with pk 1 is for anonymous and default users. - permission_string = 'users.can_see_name' - app_label, codename = permission_string.split('.') - permission = group.permissions.get(content_type__app_label=app_label, codename=codename) + group = get_group_model().objects.get( + pk=GROUP_DEFAULT_PK + ) # Group with pk 1 is for anonymous and default users. + permission_string = "users.can_see_name" + app_label, codename = permission_string.split(".") + permission = group.permissions.get( + content_type__app_label=app_label, codename=codename + ) group.permissions.remove(permission) - config['general_system_enable_anonymous'] = True + config["general_system_enable_anonymous"] = True guest_client = APIClient() inform_changed_data(group) inform_changed_data(self.motion) - response_1 = guest_client.get(reverse('motion-detail', args=[self.motion.pk])) + response_1 = guest_client.get(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response_1.status_code, status.HTTP_200_OK) - submitter_id = response_1.data['submitters'][0]['user_id'] - response_2 = guest_client.get(reverse('user-detail', args=[submitter_id])) + submitter_id = response_1.data["submitters"][0]["user_id"] + response_2 = guest_client.get(reverse("user-detail", args=[submitter_id])) self.assertEqual(response_2.status_code, status.HTTP_200_OK) extra_user = get_user_model().objects.create_user( - username='username_wequePhieFoom0hai3wa', - password='password_ooth7taechai5Oocieya') + username="username_wequePhieFoom0hai3wa", + password="password_ooth7taechai5Oocieya", + ) - response_3 = guest_client.get(reverse('user-detail', args=[extra_user.pk])) + response_3 = guest_client.get(reverse("user-detail", args=[extra_user.pk])) self.assertEqual(response_3.status_code, 404) @@ -481,87 +556,99 @@ class UpdateMotion(TestCase): """ Tests updating motions. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion = Motion( - title='test_title_aeng7ahChie3waiR8xoh', - text='test_text_xeigheeha7thopubeu4U') + title="test_title_aeng7ahChie3waiR8xoh", + text="test_text_xeigheeha7thopubeu4U", + ) self.motion.save() def test_simple_patch(self): response = self.client.patch( - reverse('motion-detail', args=[self.motion.pk]), - {'identifier': 'test_identifier_jieseghohj7OoSah1Ko9'}) + reverse("motion-detail", args=[self.motion.pk]), + {"identifier": "test_identifier_jieseghohj7OoSah1Ko9"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) motion = Motion.objects.get() - self.assertEqual(motion.title, 'test_title_aeng7ahChie3waiR8xoh') - self.assertEqual(motion.identifier, 'test_identifier_jieseghohj7OoSah1Ko9') + self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh") + self.assertEqual(motion.identifier, "test_identifier_jieseghohj7OoSah1Ko9") def test_patch_workflow(self): """ Tests to only update the workflow of a motion. """ response = self.client.patch( - reverse('motion-detail', args=[self.motion.pk]), - {'workflow_id': '2'}) + reverse("motion-detail", args=[self.motion.pk]), {"workflow_id": "2"} + ) motion = Motion.objects.get() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(motion.title, 'test_title_aeng7ahChie3waiR8xoh') + self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh") self.assertEqual(motion.workflow_id, 2) def test_patch_supporters(self): supporter = get_user_model().objects.create_user( - username='test_username_ieB9eicah0uqu6Phoovo', - password='test_password_XaeTe3aesh8ohg6Cohwo') + username="test_username_ieB9eicah0uqu6Phoovo", + password="test_password_XaeTe3aesh8ohg6Cohwo", + ) response = self.client.patch( - reverse('motion-detail', args=[self.motion.pk]), - json.dumps({'supporters_id': [supporter.pk]}), - content_type='application/json') + reverse("motion-detail", args=[self.motion.pk]), + json.dumps({"supporters_id": [supporter.pk]}), + content_type="application/json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) motion = Motion.objects.get() - self.assertEqual(motion.title, 'test_title_aeng7ahChie3waiR8xoh') - self.assertEqual(motion.supporters.get().username, 'test_username_ieB9eicah0uqu6Phoovo') + self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh") + self.assertEqual( + motion.supporters.get().username, "test_username_ieB9eicah0uqu6Phoovo" + ) def test_patch_supporters_non_manager(self): non_admin = get_user_model().objects.create_user( - username='test_username_uqu6PhoovieB9eicah0o', - password='test_password_Xaesh8ohg6CoheTe3awo') + username="test_username_uqu6PhoovieB9eicah0o", + password="test_password_Xaesh8ohg6CoheTe3awo", + ) self.client.login( - username='test_username_uqu6PhoovieB9eicah0o', - password='test_password_Xaesh8ohg6CoheTe3awo') + username="test_username_uqu6PhoovieB9eicah0o", + password="test_password_Xaesh8ohg6CoheTe3awo", + ) motion = Motion.objects.get() Submitter.objects.add(non_admin, self.motion) motion.supporters.clear() response = self.client.patch( - reverse('motion-detail', args=[self.motion.pk]), - json.dumps({'supporters_id': [1]}), - content_type='application/json') + reverse("motion-detail", args=[self.motion.pk]), + json.dumps({"supporters_id": [1]}), + content_type="application/json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertFalse(motion.supporters.exists()) def test_removal_of_supporters(self): # No cache used here. - admin = get_user_model().objects.get(username='admin') - group_admin = admin.groups.get(name='Admin') + admin = get_user_model().objects.get(username="admin") + group_admin = admin.groups.get(name="Admin") admin.groups.remove(group_admin) Submitter.objects.add(admin, self.motion) supporter = get_user_model().objects.create_user( - username='test_username_ahshi4oZin0OoSh9chee', - password='test_password_Sia8ahgeenixu5cei2Ib') + username="test_username_ahshi4oZin0OoSh9chee", + password="test_password_Sia8ahgeenixu5cei2Ib", + ) self.motion.supporters.add(supporter) - config['motions_remove_supporters'] = True + config["motions_remove_supporters"] = True self.assertEqual(self.motion.supporters.count(), 1) inform_changed_data((admin, self.motion)) response = self.client.patch( - reverse('motion-detail', args=[self.motion.pk]), - {'title': 'new_title_ohph1aedie5Du8sai2ye'}) + reverse("motion-detail", args=[self.motion.pk]), + {"title": "new_title_ohph1aedie5Du8sai2ye"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) motion = Motion.objects.get() - self.assertEqual(motion.title, 'new_title_ohph1aedie5Du8sai2ye') + self.assertEqual(motion.title, "new_title_ohph1aedie5Du8sai2ye") self.assertEqual(motion.supporters.count(), 0) @@ -569,18 +656,19 @@ class DeleteMotion(TestCase): """ Tests deleting motions. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') - self.admin = get_user_model().objects.get(username='admin') + self.client.login(username="admin", password="admin") + self.admin = get_user_model().objects.get(username="admin") self.motion = Motion( - title='test_title_acle3fa93l11lwlkcc31', - text='test_text_f390sjfyycj29ss56sro') + title="test_title_acle3fa93l11lwlkcc31", + text="test_text_f390sjfyycj29ss56sro", + ) self.motion.save() def test_simple_delete(self): - response = self.client.delete( - reverse('motion-detail', args=[self.motion.pk])) + response = self.client.delete(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) motions = Motion.objects.count() self.assertEqual(motions, 0) @@ -591,7 +679,7 @@ class DeleteMotion(TestCase): inform_changed_data(self.admin) def put_motion_in_complex_workflow(self): - workflow = Workflow.objects.get(name='Complex Workflow') + workflow = Workflow.objects.get(name="Complex Workflow") self.motion.reset_state(workflow=workflow) self.motion.save() @@ -599,8 +687,7 @@ class DeleteMotion(TestCase): self.make_admin_delegate() self.put_motion_in_complex_workflow() - response = self.client.delete( - reverse('motion-detail', args=[self.motion.pk])) + response = self.client.delete(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_delete_own_motion_as_delegate(self): @@ -608,8 +695,7 @@ class DeleteMotion(TestCase): self.put_motion_in_complex_workflow() Submitter.objects.add(self.admin, self.motion) - response = self.client.delete( - reverse('motion-detail', args=[self.motion.pk])) + response = self.client.delete(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) motions = Motion.objects.count() self.assertEqual(motions, 0) @@ -619,83 +705,75 @@ class ManageMultipleSubmitters(TestCase): """ Tests adding and removing of submitters. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.admin = get_user_model().objects.get() self.motion1 = Motion( - title='test_title_SlqfMw(waso0saWMPqcZ', - text='test_text_f30skclqS9wWF=xdfaSL') + title="test_title_SlqfMw(waso0saWMPqcZ", + text="test_text_f30skclqS9wWF=xdfaSL", + ) self.motion1.save() self.motion2 = Motion( - title='test_title_f>FLEim38MC2m9PFp2jG', - text='test_text_kg39KFGm,ao)22FK9lLu') + title="test_title_f>FLEim38MC2m9PFp2jG", + text="test_text_kg39KFGm,ao)22FK9lLu", + ) self.motion2.save() def test_set_submitters(self): response = self.client.post( - reverse('motion-manage-multiple-submitters'), - json.dumps({ - 'motions': [ - { - 'id': self.motion1.id, - 'submitters': [ - self.admin.pk - ] - }, - { - 'id': self.motion2.id, - 'submitters': [ - self.admin.pk - ] - } - ] - }), - content_type='application/json' + reverse("motion-manage-multiple-submitters"), + json.dumps( + { + "motions": [ + {"id": self.motion1.id, "submitters": [self.admin.pk]}, + {"id": self.motion2.id, "submitters": [self.admin.pk]}, + ] + } + ), + content_type="application/json", ) self.assertEqual(response.status_code, 200) self.assertEqual(self.motion1.submitters.count(), 1) self.assertEqual(self.motion2.submitters.count(), 1) self.assertEqual( - self.motion1.submitters.get().user.pk, - self.motion2.submitters.get().user.pk) + self.motion1.submitters.get().user.pk, self.motion2.submitters.get().user.pk + ) def test_non_existing_user(self): response = self.client.post( - reverse('motion-manage-multiple-submitters'), - {'motions': [ - {'id': self.motion1.id, - 'submitters': [1337]}]}) + reverse("motion-manage-multiple-submitters"), + {"motions": [{"id": self.motion1.id, "submitters": [1337]}]}, + ) self.assertEqual(response.status_code, 400) self.assertEqual(self.motion1.submitters.count(), 0) def test_add_user_no_data(self): - response = self.client.post( - reverse('motion-manage-multiple-submitters')) + response = self.client.post(reverse("motion-manage-multiple-submitters")) self.assertEqual(response.status_code, 400) self.assertEqual(self.motion1.submitters.count(), 0) self.assertEqual(self.motion2.submitters.count(), 0) def test_add_user_invalid_data(self): response = self.client.post( - reverse('motion-manage-multiple-submitters'), - {'motions': ['invalid_str']}) + reverse("motion-manage-multiple-submitters"), {"motions": ["invalid_str"]} + ) self.assertEqual(response.status_code, 400) self.assertEqual(self.motion1.submitters.count(), 0) self.assertEqual(self.motion2.submitters.count(), 0) def test_add_without_permission(self): - admin = get_user_model().objects.get(username='admin') + admin = get_user_model().objects.get(username="admin") admin.groups.add(GROUP_DELEGATE_PK) admin.groups.remove(GROUP_ADMIN_PK) inform_changed_data(admin) response = self.client.post( - reverse('motion-manage-multiple-submitters'), - {'motions': [ - {'id': self.motion1.id, - 'submitters': [self.admin.pk]}]}) + reverse("motion-manage-multiple-submitters"), + {"motions": [{"id": self.motion1.id, "submitters": [self.admin.pk]}]}, + ) self.assertEqual(response.status_code, 403) self.assertEqual(self.motion1.submitters.count(), 0) self.assertEqual(self.motion2.submitters.count(), 0) @@ -707,12 +785,15 @@ class ManageComments(TestCase): Tests creation/updating and deletion of motion comments. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.admin = get_user_model().objects.get() - self.group_out = get_group_model().objects.get(pk=GROUP_DELEGATE_PK) # The admin should not be in this group + self.group_out = get_group_model().objects.get( + pk=GROUP_DELEGATE_PK + ) # The admin should not be in this group # Put the admin into the staff group, becaust in the admin group, he has all permissions for # every single comment section. @@ -722,19 +803,26 @@ class ManageComments(TestCase): self.group_in = get_group_model().objects.get(pk=GROUP_STAFF_PK) self.motion = Motion( - title='test_title_SlqfMw(waso0saWMPqcZ', - text='test_text_f30skclqS9wWF=xdfaSL') + title="test_title_SlqfMw(waso0saWMPqcZ", + text="test_text_f30skclqS9wWF=xdfaSL", + ) self.motion.save() - self.section_no_groups = MotionCommentSection(name='test_name_gj4F§(fj"(edm"§F3f3fs') + self.section_no_groups = MotionCommentSection( + name='test_name_gj4F§(fj"(edm"§F3f3fs' + ) self.section_no_groups.save() - self.section_read = MotionCommentSection(name='test_name_2wv30(d2S&kvelkakl39') + self.section_read = MotionCommentSection(name="test_name_2wv30(d2S&kvelkakl39") self.section_read.save() - self.section_read.read_groups.add(self.group_in, self.group_out) # Group out for testing multiple groups + self.section_read.read_groups.add( + self.group_in, self.group_out + ) # Group out for testing multiple groups self.section_read.write_groups.add(self.group_out) - self.section_read_write = MotionCommentSection(name='test_name_a3m9sd0(Mw2%slkrv30,') + self.section_read_write = MotionCommentSection( + name="test_name_a3m9sd0(Mw2%slkrv30," + ) self.section_read_write.save() self.section_read_write.read_groups.add(self.group_in) self.section_read_write.write_groups.add(self.group_in) @@ -743,78 +831,80 @@ class ManageComments(TestCase): comment = MotionComment( motion=self.motion, section=self.section_read_write, - comment='test_comment_gwic37Csc&3lf3eo2') + comment="test_comment_gwic37Csc&3lf3eo2", + ) comment.save() - response = self.client.get(reverse('motion-detail', args=[self.motion.pk])) + response = self.client.get(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue('comments' in response.data) - comments = response.data['comments'] + self.assertTrue("comments" in response.data) + comments = response.data["comments"] self.assertTrue(isinstance(comments, list)) self.assertEqual(len(comments), 1) - self.assertEqual(comments[0]['comment'], 'test_comment_gwic37Csc&3lf3eo2') + self.assertEqual(comments[0]["comment"], "test_comment_gwic37Csc&3lf3eo2") def test_retrieve_comment_no_read_permission(self): comment = MotionComment( motion=self.motion, section=self.section_no_groups, - comment='test_comment_fgkj3C7veo3ijWE(j2DJ') + comment="test_comment_fgkj3C7veo3ijWE(j2DJ", + ) comment.save() - response = self.client.get(reverse('motion-detail', args=[self.motion.pk])) + response = self.client.get(reverse("motion-detail", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue('comments' in response.data) - comments = response.data['comments'] + self.assertTrue("comments" in response.data) + comments = response.data["comments"] self.assertTrue(isinstance(comments, list)) self.assertEqual(len(comments), 0) def test_wrong_data_type(self): response = self.client.post( - reverse('motion-manage-comments', args=[self.motion.pk]), + reverse("motion-manage-comments", args=[self.motion.pk]), None, - format='json') + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - response.data['detail'], - 'You have to provide a section_id of type int.') + response.data["detail"], "You have to provide a section_id of type int." + ) def test_wrong_comment_data_type(self): response = self.client.post( - reverse('motion-manage-comments', args=[self.motion.pk]), + reverse("motion-manage-comments", args=[self.motion.pk]), { - 'section_id': self.section_read_write.id, - 'comment': [32, 'no_correct_data'] + "section_id": self.section_read_write.id, + "comment": [32, "no_correct_data"], }, - format='json') + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data['detail'], - 'The comment should be a string.') + self.assertEqual(response.data["detail"], "The comment should be a string.") def test_non_existing_section(self): response = self.client.post( - reverse('motion-manage-comments', args=[self.motion.pk]), - { - 'section_id': 42, - }, - format='json') + reverse("motion-manage-comments", args=[self.motion.pk]), + {"section_id": 42}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - response.data['detail'], - 'A comment section with id 42 does not exist') + response.data["detail"], "A comment section with id 42 does not exist" + ) def test_create_comment(self): response = self.client.post( - reverse('motion-manage-comments', args=[self.motion.pk]), + reverse("motion-manage-comments", args=[self.motion.pk]), { - 'section_id': self.section_read_write.pk, - 'comment': 'test_comment_fk3jrnfwsdg%fj=feijf' + "section_id": self.section_read_write.pk, + "comment": "test_comment_fk3jrnfwsdg%fj=feijf", }, - format='json') + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(MotionComment.objects.count(), 1) comment = MotionComment.objects.get() - self.assertEqual(comment.comment, 'test_comment_fk3jrnfwsdg%fj=feijf') + self.assertEqual(comment.comment, "test_comment_fk3jrnfwsdg%fj=feijf") # Check for a log entry motion_logs = MotionLog.objects.filter(motion=self.motion) @@ -826,19 +916,21 @@ class ManageComments(TestCase): comment = MotionComment( motion=self.motion, section=self.section_read_write, - comment='test_comment_fji387fqwdf&ff=)Fe3j') + comment="test_comment_fji387fqwdf&ff=)Fe3j", + ) comment.save() response = self.client.post( - reverse('motion-manage-comments', args=[self.motion.pk]), + reverse("motion-manage-comments", args=[self.motion.pk]), { - 'section_id': self.section_read_write.pk, - 'comment': 'test_comment_fk3jrnfwsdg%fj=feijf' + "section_id": self.section_read_write.pk, + "comment": "test_comment_fk3jrnfwsdg%fj=feijf", }, - format='json') + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) comment = MotionComment.objects.get() - self.assertEqual(comment.comment, 'test_comment_fk3jrnfwsdg%fj=feijf') + self.assertEqual(comment.comment, "test_comment_fk3jrnfwsdg%fj=feijf") # Check for a log entry motion_logs = MotionLog.objects.filter(motion=self.motion) @@ -850,15 +942,15 @@ class ManageComments(TestCase): comment = MotionComment( motion=self.motion, section=self.section_read_write, - comment='test_comment_5CJ"8f23jd3j2,r93keZ') + comment='test_comment_5CJ"8f23jd3j2,r93keZ', + ) comment.save() response = self.client.delete( - reverse('motion-manage-comments', args=[self.motion.pk]), - { - 'section_id': self.section_read_write.pk - }, - format='json') + reverse("motion-manage-comments", args=[self.motion.pk]), + {"section_id": self.section_read_write.pk}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(MotionComment.objects.count(), 0) @@ -874,11 +966,10 @@ class ManageComments(TestCase): a not existing comment. """ response = self.client.delete( - reverse('motion-manage-comments', args=[self.motion.pk]), - { - 'section_id': self.section_read_write.pk - }, - format='json') + reverse("motion-manage-comments", args=[self.motion.pk]), + {"section_id": self.section_read_write.pk}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(MotionComment.objects.count(), 0) @@ -888,102 +979,113 @@ class ManageComments(TestCase): def test_create_comment_no_write_permission(self): response = self.client.post( - reverse('motion-manage-comments', args=[self.motion.pk]), + reverse("motion-manage-comments", args=[self.motion.pk]), { - 'section_id': self.section_read.pk, - 'comment': 'test_comment_f38jfwqfj830fj4j(FU3' + "section_id": self.section_read.pk, + "comment": "test_comment_f38jfwqfj830fj4j(FU3", }, - format='json') + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(MotionComment.objects.count(), 0) self.assertEqual( - response.data['detail'], - 'You are not allowed to see or write to the comment section.') + response.data["detail"], + "You are not allowed to see or write to the comment section.", + ) def test_update_comment_no_write_permission(self): comment = MotionComment( motion=self.motion, section=self.section_read, - comment='test_comment_jg38dwiej2D832(D§dk)') + comment="test_comment_jg38dwiej2D832(D§dk)", + ) comment.save() response = self.client.post( - reverse('motion-manage-comments', args=[self.motion.pk]), + reverse("motion-manage-comments", args=[self.motion.pk]), { - 'section_id': self.section_read.pk, - 'comment': 'test_comment_fk3jrnfwsdg%fj=feijf' + "section_id": self.section_read.pk, + "comment": "test_comment_fk3jrnfwsdg%fj=feijf", }, - format='json') + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) comment = MotionComment.objects.get() - self.assertEqual(comment.comment, 'test_comment_jg38dwiej2D832(D§dk)') + self.assertEqual(comment.comment, "test_comment_jg38dwiej2D832(D§dk)") def test_delete_comment_no_write_permission(self): comment = MotionComment( motion=self.motion, section=self.section_read, - comment='test_comment_fej(NF§kfePOF383o8DN') + comment="test_comment_fej(NF§kfePOF383o8DN", + ) comment.save() response = self.client.delete( - reverse('motion-manage-comments', args=[self.motion.pk]), - { - 'section_id': self.section_read.pk - }, - format='json') + reverse("motion-manage-comments", args=[self.motion.pk]), + {"section_id": self.section_read.pk}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(MotionComment.objects.count(), 1) comment = MotionComment.objects.get() - self.assertEqual(comment.comment, 'test_comment_fej(NF§kfePOF383o8DN') + self.assertEqual(comment.comment, "test_comment_fej(NF§kfePOF383o8DN") class TestMotionCommentSection(TestCase): """ Tests creating, updating and deletion of comment sections. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.admin = get_user_model().objects.get() - self.admin.groups.add(GROUP_STAFF_PK) # Put the admin in a groiup with limited permissions for testing. + self.admin.groups.add( + GROUP_STAFF_PK + ) # Put the admin in a groiup with limited permissions for testing. self.admin.groups.remove(GROUP_ADMIN_PK) inform_changed_data(self.admin) self.group_in = get_group_model().objects.get(pk=GROUP_STAFF_PK) - self.group_out = get_group_model().objects.get(pk=GROUP_DELEGATE_PK) # The admin should not be in this group + self.group_out = get_group_model().objects.get( + pk=GROUP_DELEGATE_PK + ) # The admin should not be in this group def test_retrieve(self): """ Checks, if the sections can be seen by a manager. """ - section = MotionCommentSection(name='test_name_f3jOF3m8fp.New test

', - 'type': '0'}) + reverse("motionchangerecommendation-list"), + { + "line_from": "5", + "line_to": "7", + "motion_id": "1", + "text": "

New test

", + "type": "0", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_collission(self): @@ -1298,49 +1401,69 @@ class CreateMotionChangeRecommendation(TestCase): Two change recommendations with overlapping lines should lead to a Bad Request """ response = self.client.post( - reverse('motionchangerecommendation-list'), - {'line_from': '5', - 'line_to': '7', - 'motion_id': '1', - 'text': '

New test

', - 'type': '0'}) + reverse("motionchangerecommendation-list"), + { + "line_from": "5", + "line_to": "7", + "motion_id": "1", + "text": "

New test

", + "type": "0", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) response = self.client.post( - reverse('motionchangerecommendation-list'), - {'line_from': '3', - 'line_to': '6', - 'motion_id': '1', - 'text': '

New test

', - 'type': '0'}) + reverse("motionchangerecommendation-list"), + { + "line_from": "3", + "line_to": "6", + "motion_id": "1", + "text": "

New test

", + "type": "0", + }, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'The recommendation collides with an existing one (line 3 - 6).'}) + self.assertEqual( + response.data, + { + "detail": "The recommendation collides with an existing one (line 3 - 6)." + }, + ) def test_no_collission_different_motions(self): """ Two change recommendations with overlapping lines, but affecting different motions, should not interfere """ self.client.post( - reverse('motion-list'), - {'title': 'test_title_OoCoo3MeiT9li5Iengu9', - 'text': 'test_text_thuoz0iecheiheereiCi'}) + reverse("motion-list"), + { + "title": "test_title_OoCoo3MeiT9li5Iengu9", + "text": "test_text_thuoz0iecheiheereiCi", + }, + ) response = self.client.post( - reverse('motionchangerecommendation-list'), - {'line_from': '5', - 'line_to': '7', - 'motion_id': '1', - 'text': '

New test

', - 'type': '0'}) + reverse("motionchangerecommendation-list"), + { + "line_from": "5", + "line_to": "7", + "motion_id": "1", + "text": "

New test

", + "type": "0", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) response = self.client.post( - reverse('motionchangerecommendation-list'), - {'line_from': '3', - 'line_to': '6', - 'motion_id': '2', - 'text': '

New test

', - 'type': '0'}) + reverse("motionchangerecommendation-list"), + { + "line_from": "3", + "line_to": "6", + "motion_id": "2", + "text": "

New test

", + "type": "0", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -1348,123 +1471,162 @@ class SupportMotion(TestCase): """ Tests supporting a motion. """ + def setUp(self): - self.admin = get_user_model().objects.get(username='admin') + self.admin = get_user_model().objects.get(username="admin") self.admin.groups.add(GROUP_DELEGATE_PK) inform_changed_data(self.admin) - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion = Motion( - title='test_title_chee7ahCha6bingaew4e', - text='test_text_birah1theL9ooseeFaip') + title="test_title_chee7ahCha6bingaew4e", + text="test_text_birah1theL9ooseeFaip", + ) self.motion.save() def test_support(self): - config['motions_min_supporters'] = 1 + config["motions_min_supporters"] = 1 - response = self.client.post(reverse('motion-support', args=[self.motion.pk])) + response = self.client.post(reverse("motion-support", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'You have supported this motion successfully.'}) + self.assertEqual( + response.data, {"detail": "You have supported this motion successfully."} + ) def test_unsupport(self): - config['motions_min_supporters'] = 1 + config["motions_min_supporters"] = 1 self.motion.supporters.add(self.admin) - response = self.client.delete(reverse('motion-support', args=[self.motion.pk])) + response = self.client.delete(reverse("motion-support", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'You have unsupported this motion successfully.'}) + self.assertEqual( + response.data, {"detail": "You have unsupported this motion successfully."} + ) class SetState(TestCase): """ Tests setting a state. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion = Motion( - title='test_title_iac4ohquie9Ku6othieC', - text='test_text_Xohphei6Oobee0Evooyu') + title="test_title_iac4ohquie9Ku6othieC", + text="test_text_Xohphei6Oobee0Evooyu", + ) self.motion.save() self.state_id_accepted = 2 # This should be the id of the state 'accepted'. def test_set_state(self): response = self.client.put( - reverse('motion-set-state', args=[self.motion.pk]), - {'state': self.state_id_accepted}) + reverse("motion-set-state", args=[self.motion.pk]), + {"state": self.state_id_accepted}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'The state of the motion was set to accepted.'}) - self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, 'accepted') + self.assertEqual( + response.data, {"detail": "The state of the motion was set to accepted."} + ) + self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, "accepted") def test_set_state_with_string(self): # Using a string is not allowed even if it is the correct name of the state. response = self.client.put( - reverse('motion-set-state', args=[self.motion.pk]), - {'state': 'accepted'}) + reverse("motion-set-state", args=[self.motion.pk]), {"state": "accepted"} + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'Invalid data. State must be an integer.'}) + self.assertEqual( + response.data, {"detail": "Invalid data. State must be an integer."} + ) def test_set_unknown_state(self): invalid_state_id = 0 response = self.client.put( - reverse('motion-set-state', args=[self.motion.pk]), - {'state': invalid_state_id}) + reverse("motion-set-state", args=[self.motion.pk]), + {"state": invalid_state_id}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'You can not set the state to %d.' % invalid_state_id}) + self.assertEqual( + response.data, + {"detail": "You can not set the state to %d." % invalid_state_id}, + ) def test_reset(self): self.motion.set_state(self.state_id_accepted) self.motion.save() - response = self.client.put(reverse('motion-set-state', args=[self.motion.pk])) + response = self.client.put(reverse("motion-set-state", args=[self.motion.pk])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'The state of the motion was set to submitted.'}) - self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, 'submitted') + self.assertEqual( + response.data, {"detail": "The state of the motion was set to submitted."} + ) + self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, "submitted") class SetRecommendation(TestCase): """ Tests setting a recommendation. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion = Motion( - title='test_title_ahfooT5leilahcohJ2uz', - text='test_text_enoogh7OhPoo6eohoCus') + title="test_title_ahfooT5leilahcohJ2uz", + text="test_text_enoogh7OhPoo6eohoCus", + ) self.motion.save() self.state_id_accepted = 2 # This should be the id of the state 'accepted'. def test_set_recommendation(self): response = self.client.put( - reverse('motion-set-recommendation', args=[self.motion.pk]), - {'recommendation': self.state_id_accepted}) + reverse("motion-set-recommendation", args=[self.motion.pk]), + {"recommendation": self.state_id_accepted}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'The recommendation of the motion was set to Acceptance.'}) - self.assertEqual(Motion.objects.get(pk=self.motion.pk).recommendation.name, 'accepted') + self.assertEqual( + response.data, + {"detail": "The recommendation of the motion was set to Acceptance."}, + ) + self.assertEqual( + Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted" + ) def test_set_state_with_string(self): # Using a string is not allowed even if it is the correct name of the state. response = self.client.put( - reverse('motion-set-recommendation', args=[self.motion.pk]), - {'recommendation': 'accepted'}) + reverse("motion-set-recommendation", args=[self.motion.pk]), + {"recommendation": "accepted"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'Invalid data. Recommendation must be an integer.'}) + self.assertEqual( + response.data, + {"detail": "Invalid data. Recommendation must be an integer."}, + ) def test_set_unknown_recommendation(self): invalid_state_id = 0 response = self.client.put( - reverse('motion-set-recommendation', args=[self.motion.pk]), - {'recommendation': invalid_state_id}) + reverse("motion-set-recommendation", args=[self.motion.pk]), + {"recommendation": invalid_state_id}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'You can not set the recommendation to %d.' % invalid_state_id}) + self.assertEqual( + response.data, + {"detail": "You can not set the recommendation to %d." % invalid_state_id}, + ) def test_set_invalid_recommendation(self): # This is a valid state id, but this state is not recommendable because it belongs to a different workflow. invalid_state_id = 6 # State 'permitted' response = self.client.put( - reverse('motion-set-recommendation', args=[self.motion.pk]), - {'recommendation': invalid_state_id}) + reverse("motion-set-recommendation", args=[self.motion.pk]), + {"recommendation": invalid_state_id}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'You can not set the recommendation to %d.' % invalid_state_id}) + self.assertEqual( + response.data, + {"detail": "You can not set the recommendation to %d." % invalid_state_id}, + ) def test_set_invalid_recommendation_2(self): # This is a valid state id, but this state is not recommendable because it has not recommendation label @@ -1472,92 +1634,117 @@ class SetRecommendation(TestCase): self.motion.set_state(self.state_id_accepted) self.motion.save() response = self.client.put( - reverse('motion-set-recommendation', args=[self.motion.pk]), - {'recommendation': invalid_state_id}) + reverse("motion-set-recommendation", args=[self.motion.pk]), + {"recommendation": invalid_state_id}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'detail': 'You can not set the recommendation to %d.' % invalid_state_id}) + self.assertEqual( + response.data, + {"detail": "You can not set the recommendation to %d." % invalid_state_id}, + ) def test_reset(self): self.motion.set_recommendation(self.state_id_accepted) self.motion.save() - response = self.client.put(reverse('motion-set-recommendation', args=[self.motion.pk])) + response = self.client.put( + reverse("motion-set-recommendation", args=[self.motion.pk]) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'The recommendation of the motion was set to None.'}) + self.assertEqual( + response.data, + {"detail": "The recommendation of the motion was set to None."}, + ) self.assertTrue(Motion.objects.get(pk=self.motion.pk).recommendation is None) def test_set_recommendation_to_current_state(self): self.motion.set_state(self.state_id_accepted) self.motion.save() response = self.client.put( - reverse('motion-set-recommendation', args=[self.motion.pk]), - {'recommendation': self.state_id_accepted}) + reverse("motion-set-recommendation", args=[self.motion.pk]), + {"recommendation": self.state_id_accepted}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'The recommendation of the motion was set to Acceptance.'}) - self.assertEqual(Motion.objects.get(pk=self.motion.pk).recommendation.name, 'accepted') + self.assertEqual( + response.data, + {"detail": "The recommendation of the motion was set to Acceptance."}, + ) + self.assertEqual( + Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted" + ) class CreateMotionPoll(TestCase): """ Tests creating polls of motions. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion = Motion( - title='test_title_Aiqueigh2dae9phabiqu', - text='test_text_Neekoh3zou6li5rue8iL') + title="test_title_Aiqueigh2dae9phabiqu", + text="test_text_Neekoh3zou6li5rue8iL", + ) self.motion.save() def test_create_first_poll_with_values_then_second_poll_without(self): self.poll = self.motion.create_poll() - self.poll.set_vote_objects_with_values(self.poll.get_options().get(), {'Yes': 42, 'No': 43, 'Abstain': 44}) + self.poll.set_vote_objects_with_values( + self.poll.get_options().get(), {"Yes": 42, "No": 43, "Abstain": 44} + ) response = self.client.post( - reverse('motion-create-poll', args=[self.motion.pk])) + reverse("motion-create-poll", args=[self.motion.pk]) + ) self.assertEqual(self.motion.polls.count(), 2) - response = self.client.get(reverse('motion-detail', args=[self.motion.pk])) - for key in ('yes', 'no', 'abstain'): - self.assertTrue(response.data['polls'][1][key] is None, 'Vote value "{}" should be None.'.format(key)) + response = self.client.get(reverse("motion-detail", args=[self.motion.pk])) + for key in ("yes", "no", "abstain"): + self.assertTrue( + response.data["polls"][1][key] is None, + 'Vote value "{}" should be None.'.format(key), + ) class UpdateMotionPoll(TestCase): """ Tests updating polls of motions. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion = Motion( - title='test_title_Aiqueigh2dae9phabiqu', - text='test_text_Neekoh3zou6li5rue8iL') + title="test_title_Aiqueigh2dae9phabiqu", + text="test_text_Neekoh3zou6li5rue8iL", + ) self.motion.save() self.poll = self.motion.create_poll() def test_invalid_votesvalid_value(self): response = self.client.put( - reverse('motionpoll-detail', args=[self.poll.pk]), - {'motion_id': self.motion.pk, - 'votesvalid': '-3'}) + reverse("motionpoll-detail", args=[self.poll.pk]), + {"motion_id": self.motion.pk, "votesvalid": "-3"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_invalid_votesinvalid_value(self): response = self.client.put( - reverse('motionpoll-detail', args=[self.poll.pk]), - {'motion_id': self.motion.pk, - 'votesinvalid': '-3'}) + reverse("motionpoll-detail", args=[self.poll.pk]), + {"motion_id": self.motion.pk, "votesinvalid": "-3"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_invalid_votescast_value(self): response = self.client.put( - reverse('motionpoll-detail', args=[self.poll.pk]), - {'motion_id': self.motion.pk, - 'votescast': '-3'}) + reverse("motionpoll-detail", args=[self.poll.pk]), + {"motion_id": self.motion.pk, "votescast": "-3"}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_empty_value_for_votesvalid(self): response = self.client.put( - reverse('motionpoll-detail', args=[self.poll.pk]), - {'motion_id': self.motion.pk, - 'votesvalid': ''}) + reverse("motionpoll-detail", args=[self.poll.pk]), + {"motion_id": self.motion.pk, "votesvalid": ""}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1565,117 +1752,169 @@ class NumberMotionsInCategory(TestCase): """ Tests numbering motions in a category. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.category = Category.objects.create( - name='test_cateogory_name_zah6Ahd4Ifofaeree6ai', - prefix='test_prefix_ahz6tho2mooH8') + name="test_cateogory_name_zah6Ahd4Ifofaeree6ai", + prefix="test_prefix_ahz6tho2mooH8", + ) self.motion = Motion( - title='test_title_Eeha8Haf6peulu8ooc0z', - text='test_text_faghaZoov9ooV4Acaquk', - category=self.category) + title="test_title_Eeha8Haf6peulu8ooc0z", + text="test_text_faghaZoov9ooV4Acaquk", + category=self.category, + ) self.motion.save() - self.motion.identifier = '' + self.motion.identifier = "" self.motion.save() self.motion_2 = Motion( - title='test_title_kuheih2eja2Saeshusha', - text='test_text_Ha5ShaeraeSuthooP2Bu', - category=self.category) + title="test_title_kuheih2eja2Saeshusha", + text="test_text_Ha5ShaeraeSuthooP2Bu", + category=self.category, + ) self.motion_2.save() - self.motion_2.identifier = '' + self.motion_2.identifier = "" self.motion_2.save() def test_numbering(self): response = self.client.post( - reverse('category-numbering', args=[self.category.pk])) + reverse("category-numbering", args=[self.category.pk]) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'All motions in category test_cateogory_name_zah6Ahd4Ifofaeree6ai numbered successfully.'}) - self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, 'test_prefix_ahz6tho2mooH8 1') - self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2') + self.assertEqual( + response.data, + { + "detail": "All motions in category test_cateogory_name_zah6Ahd4Ifofaeree6ai numbered successfully." + }, + ) + self.assertEqual( + Motion.objects.get(pk=self.motion.pk).identifier, + "test_prefix_ahz6tho2mooH8 1", + ) + self.assertEqual( + Motion.objects.get(pk=self.motion_2.pk).identifier, + "test_prefix_ahz6tho2mooH8 2", + ) def test_numbering_existing_identifier(self): - self.motion_2.identifier = 'test_prefix_ahz6tho2mooH8 1' + self.motion_2.identifier = "test_prefix_ahz6tho2mooH8 1" self.motion_2.save() response = self.client.post( - reverse('category-numbering', args=[self.category.pk])) + reverse("category-numbering", args=[self.category.pk]) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'All motions in category test_cateogory_name_zah6Ahd4Ifofaeree6ai numbered successfully.'}) - self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, 'test_prefix_ahz6tho2mooH8 1') - self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2') + self.assertEqual( + response.data, + { + "detail": "All motions in category test_cateogory_name_zah6Ahd4Ifofaeree6ai numbered successfully." + }, + ) + self.assertEqual( + Motion.objects.get(pk=self.motion.pk).identifier, + "test_prefix_ahz6tho2mooH8 1", + ) + self.assertEqual( + Motion.objects.get(pk=self.motion_2.pk).identifier, + "test_prefix_ahz6tho2mooH8 2", + ) def test_numbering_with_given_order(self): self.motion_3 = Motion( - title='test_title_eeb0kua5ciike4su2auJ', - text='test_text_ahshuGhaew3eim8yoht7', - category=self.category) + title="test_title_eeb0kua5ciike4su2auJ", + text="test_text_ahshuGhaew3eim8yoht7", + category=self.category, + ) self.motion_3.save() - self.motion_3.identifier = '' + self.motion_3.identifier = "" self.motion_3.save() response = self.client.post( - reverse('category-numbering', args=[self.category.pk]), - {'motions': [3, 2]}, - format='json') + reverse("category-numbering", args=[self.category.pk]), + {"motions": [3, 2]}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'detail': 'All motions in category test_cateogory_name_zah6Ahd4Ifofaeree6ai numbered successfully.'}) + self.assertEqual( + response.data, + { + "detail": "All motions in category test_cateogory_name_zah6Ahd4Ifofaeree6ai numbered successfully." + }, + ) self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, None) - self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2') - self.assertEqual(Motion.objects.get(pk=self.motion_3.pk).identifier, 'test_prefix_ahz6tho2mooH8 1') + self.assertEqual( + Motion.objects.get(pk=self.motion_2.pk).identifier, + "test_prefix_ahz6tho2mooH8 2", + ) + self.assertEqual( + Motion.objects.get(pk=self.motion_3.pk).identifier, + "test_prefix_ahz6tho2mooH8 1", + ) class FollowRecommendationsForMotionBlock(TestCase): """ Tests following the recommendations of motions in an motion block. """ + def setUp(self): self.state_id_accepted = 2 # This should be the id of the state 'accepted'. self.state_id_rejected = 3 # This should be the id of the state 'rejected'. self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.motion_block = MotionBlock.objects.create( - title='test_motion_block_name_Ufoopiub7quaezaepeic') + title="test_motion_block_name_Ufoopiub7quaezaepeic" + ) self.motion = Motion( - title='test_title_yo8ohy5eifeiyied2AeD', - text='test_text_chi1aeth5faPhueQu8oh', - motion_block=self.motion_block) + title="test_title_yo8ohy5eifeiyied2AeD", + text="test_text_chi1aeth5faPhueQu8oh", + motion_block=self.motion_block, + ) self.motion.save() self.motion.set_recommendation(self.state_id_accepted) self.motion.save() self.motion_2 = Motion( - title='test_title_eith0EemaW8ahZa9Piej', - text='test_text_haeho1ohk3ou7pau2Jee', - motion_block=self.motion_block) + title="test_title_eith0EemaW8ahZa9Piej", + text="test_text_haeho1ohk3ou7pau2Jee", + motion_block=self.motion_block, + ) self.motion_2.save() self.motion_2.set_recommendation(self.state_id_rejected) self.motion_2.save() def test_follow_recommendations_for_motion_block(self): - response = self.client.post(reverse('motionblock-follow-recommendations', args=[self.motion_block.pk])) + response = self.client.post( + reverse("motionblock-follow-recommendations", args=[self.motion_block.pk]) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.id, self.state_id_accepted) - self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).state.id, self.state_id_rejected) + self.assertEqual( + Motion.objects.get(pk=self.motion.pk).state.id, self.state_id_accepted + ) + self.assertEqual( + Motion.objects.get(pk=self.motion_2.pk).state.id, self.state_id_rejected + ) class CreateWorkflow(TestCase): """ Tests the creating of workflows. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") def test_creation(self): Workflow.objects.all().delete() response = self.client.post( - reverse('workflow-list'), - {'name': 'test_name_OoCoo3MeiT9li5Iengu9'}) + reverse("workflow-list"), {"name": "test_name_OoCoo3MeiT9li5Iengu9"} + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) workflow = Workflow.objects.get() - self.assertEqual(workflow.name, 'test_name_OoCoo3MeiT9li5Iengu9') + self.assertEqual(workflow.name, "test_name_OoCoo3MeiT9li5Iengu9") first_state = workflow.first_state self.assertEqual(type(first_state), State) @@ -1684,15 +1923,17 @@ class UpdateWorkflow(TestCase): """ Tests the updating of workflows. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.workflow = Workflow.objects.first() def test_rename_workflow(self): response = self.client.patch( - reverse('workflow-detail', args=[self.workflow.pk]), - {'name': 'test_name_wofi38DiWLT"8d3lwfo3'}) + reverse("workflow-detail", args=[self.workflow.pk]), + {"name": 'test_name_wofi38DiWLT"8d3lwfo3'}, + ) workflow = Workflow.objects.get(pk=self.workflow.id) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1703,25 +1944,29 @@ class DeleteWorkflow(TestCase): """ Tests the deletion of workflows. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.workflow = Workflow.objects.first() def test_simple_delete(self): response = self.client.delete( - reverse('workflow-detail', args=[self.workflow.pk])) + reverse("workflow-detail", args=[self.workflow.pk]) + ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Workflow.objects.count(), 1) # Just the other default one def test_delete_with_assigned_motions(self): self.motion = Motion( - title='test_title_chee7ahCha6bingaew4e', - text='test_text_birah1theL9ooseeFaip') + title="test_title_chee7ahCha6bingaew4e", + text="test_text_birah1theL9ooseeFaip", + ) self.motion.reset_state(self.workflow) self.motion.save() response = self.client.delete( - reverse('workflow-detail', args=[self.workflow.pk])) + reverse("workflow-detail", args=[self.workflow.pk]) + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Workflow.objects.count(), 2) diff --git a/tests/integration/test_plugin/__init__.py b/tests/integration/test_plugin/__init__.py index d7708b634..0e36b3827 100644 --- a/tests/integration/test_plugin/__init__.py +++ b/tests/integration/test_plugin/__init__.py @@ -1,5 +1,5 @@ -__verbose_name__ = 'OpenSlides Test Plugin' -__description__ = 'This is a test plugin for OpenSlides.' -__license__ = 'MIT' +__verbose_name__ = "OpenSlides Test Plugin" +__description__ = "This is a test plugin for OpenSlides." +__license__ = "MIT" -default_app_config = 'tests.integration.test_plugin.apps.TestPluginAppConfig' +default_app_config = "tests.integration.test_plugin.apps.TestPluginAppConfig" diff --git a/tests/integration/test_plugin/apps.py b/tests/integration/test_plugin/apps.py index 8e295918b..710c33a2c 100644 --- a/tests/integration/test_plugin/apps.py +++ b/tests/integration/test_plugin/apps.py @@ -7,8 +7,9 @@ class TestPluginAppConfig(AppConfig): """ Test Plugin for the test tests.integration.core.test_views.VersionView """ - name = 'tests.integration.test_plugin' - label = 'tests.integration.test_plugin' + + name = "tests.integration.test_plugin" + label = "tests.integration.test_plugin" verbose_name = __verbose_name__ description = __description__ license = __license__ diff --git a/tests/integration/topics/test_viewset.py b/tests/integration/topics/test_viewset.py index fc0bd1406..2c8121ce4 100644 --- a/tests/integration/topics/test_viewset.py +++ b/tests/integration/topics/test_viewset.py @@ -18,7 +18,7 @@ def test_topic_item_db_queries(): * 1 request to get the agenda item """ for index in range(10): - Topic.objects.create(title='topic-{}'.format(index)) + Topic.objects.create(title="topic-{}".format(index)) assert count_queries(Topic.get_elements) == 3 @@ -27,19 +27,20 @@ class TopicCreate(TestCase): """ Tests creation of new topics. """ + def setUp(self): - self.client.login( - username='admin', - password='admin', - ) + self.client.login(username="admin", password="admin") def test_simple_create(self): response = self.client.post( - reverse('topic-list'), - {'title': 'test_title_ahyo1uifoo9Aiph2av5a', - 'text': 'test_text_chu9Uevoo5choo0Xithe'}) + reverse("topic-list"), + { + "title": "test_title_ahyo1uifoo9Aiph2av5a", + "text": "test_text_chu9Uevoo5choo0Xithe", + }, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) topic = Topic.objects.get() - self.assertEqual(topic.title, 'test_title_ahyo1uifoo9Aiph2av5a') - self.assertEqual(topic.text, 'test_text_chu9Uevoo5choo0Xithe') + self.assertEqual(topic.title, "test_title_ahyo1uifoo9Aiph2av5a") + self.assertEqual(topic.text, "test_text_chu9Uevoo5choo0Xithe") self.assertEqual(Item.objects.get(), topic.agenda_item) diff --git a/tests/integration/users/test_views.py b/tests/integration/users/test_views.py index c70a62ae9..d7a502068 100644 --- a/tests/integration/users/test_views.py +++ b/tests/integration/users/test_views.py @@ -7,7 +7,7 @@ from openslides.utils.test import TestCase class TestWhoAmIView(TestCase): - url = reverse('user_whoami') + url = reverse("user_whoami") def test_get_anonymous(self): response = self.client.get(self.url) @@ -15,16 +15,19 @@ class TestWhoAmIView(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual( json.loads(response.content.decode()), - {'user_id': None, 'user': None, 'guest_enabled': False}) + {"user_id": None, "user": None, "guest_enabled": False}, + ) def test_get_authenticated_user(self): - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertEqual(json.loads(response.content.decode()).get('user_id'), 1) - self.assertEqual(json.loads(response.content.decode()).get('guest_enabled'), False) + self.assertEqual(json.loads(response.content.decode()).get("user_id"), 1) + self.assertEqual( + json.loads(response.content.decode()).get("guest_enabled"), False + ) def test_post(self): response = self.client.post(self.url) @@ -33,7 +36,7 @@ class TestWhoAmIView(TestCase): class TestUserLogoutView(TestCase): - url = reverse('user_logout') + url = reverse("user_logout") def test_get(self): response = self.client.get(self.url) @@ -46,17 +49,17 @@ class TestUserLogoutView(TestCase): self.assertEqual(response.status_code, 400) def test_post_authenticated_user(self): - self.client.login(username='admin', password='admin') - self.client.session['test_key'] = 'test_value' + self.client.login(username="admin", password="admin") + self.client.session["test_key"] = "test_value" response = self.client.post(self.url) self.assertEqual(response.status_code, 200) - self.assertFalse(hasattr(self.client.session, 'test_key')) + self.assertFalse(hasattr(self.client.session, "test_key")) class TestUserLoginView(TestCase): - url = reverse('user_login') + url = reverse("user_login") def setUp(self): self.client = APIClient() @@ -65,8 +68,7 @@ class TestUserLoginView(TestCase): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertTrue( - json.loads(response.content.decode()).get('info_text')) + self.assertTrue(json.loads(response.content.decode()).get("info_text")) def test_post_no_data(self): response = self.client.post(self.url) @@ -75,15 +77,15 @@ class TestUserLoginView(TestCase): def test_post_correct_data(self): response = self.client.post( - self.url, - {'username': 'admin', 'password': 'admin'}) + self.url, {"username": "admin", "password": "admin"} + ) self.assertEqual(response.status_code, 200) - self.assertEqual(json.loads(response.content.decode()).get('user_id'), 1) + self.assertEqual(json.loads(response.content.decode()).get("user_id"), 1) def test_post_incorrect_data(self): response = self.client.post( - self.url, - {'username': 'wrong', 'password': 'wrong'}) + self.url, {"username": "wrong", "password": "wrong"} + ) self.assertEqual(response.status_code, 400) diff --git a/tests/integration/users/test_viewset.py b/tests/integration/users/test_viewset.py index 994c0b34a..b72f478a0 100644 --- a/tests/integration/users/test_viewset.py +++ b/tests/integration/users/test_viewset.py @@ -21,7 +21,7 @@ def test_user_db_queries(): * 1 requests to get the list of all groups. """ for index in range(10): - User.objects.create(username='user{}'.format(index)) + User.objects.create(username="user{}".format(index)) assert count_queries(User.get_elements) == 3 @@ -34,7 +34,7 @@ def test_group_db_queries(): * 1 request to get the permissions """ for index in range(10): - Group.objects.create(name='group{}'.format(index)) + Group.objects.create(name="group{}".format(index)) assert count_queries(Group.get_elements) == 2 @@ -43,31 +43,34 @@ class UserGetTest(TestCase): """ Tests to receive a users via REST API. """ + def test_get_with_user_who_is_in_group_with_pk_1(self): """ It is invalid, that a user is in the group with the pk 1. But if the database is invalid, the user should nevertheless be received. """ - admin = User.objects.get(username='admin') + admin = User.objects.get(username="admin") group1 = Group.objects.get(pk=1) admin.groups.add(group1) - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") - response = self.client.get('/rest/users/user/1/') + response = self.client.get("/rest/users/user/1/") self.assertEqual(response.status_code, 200) def test_get_with_user_without_permissions(self): group = Group.objects.get(pk=1) - permission_string = 'users.can_see_name' - app_label, codename = permission_string.split('.') - permission = group.permissions.get(content_type__app_label=app_label, codename=codename) + permission_string = "users.can_see_name" + app_label, codename = permission_string.split(".") + permission = group.permissions.get( + content_type__app_label=app_label, codename=codename + ) group.permissions.remove(permission) inform_changed_data(group) - config['general_system_enable_anonymous'] = True + config["general_system_enable_anonymous"] = True guest_client = APIClient() - response = guest_client.get('/rest/users/user/1/') + response = guest_client.get("/rest/users/user/1/") self.assertEqual(response.status_code, 404) @@ -76,50 +79,55 @@ class UserCreate(TestCase): """ Tests creation of users via REST API. """ + def test_simple_creation(self): - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") response = self.client.post( - reverse('user-list'), - {'last_name': 'Test name keimeiShieX4Aekoe3do'}) + reverse("user-list"), {"last_name": "Test name keimeiShieX4Aekoe3do"} + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - new_user = User.objects.get(username='Test name keimeiShieX4Aekoe3do') - self.assertEqual(response.data['id'], new_user.id) + new_user = User.objects.get(username="Test name keimeiShieX4Aekoe3do") + self.assertEqual(response.data["id"], new_user.id) def test_creation_with_group(self): - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") # These are the builtin groups 'Delegates' and 'Staff'. The pks are valid. - group_pks = (2, 3,) + group_pks = (2, 3) self.client.post( - reverse('user-list'), - {'last_name': 'Test name aedah1iequoof0Ashed4', - 'groups_id': group_pks}) + reverse("user-list"), + {"last_name": "Test name aedah1iequoof0Ashed4", "groups_id": group_pks}, + ) - user = User.objects.get(username='Test name aedah1iequoof0Ashed4') + user = User.objects.get(username="Test name aedah1iequoof0Ashed4") self.assertTrue(user.groups.filter(pk=group_pks[0]).exists()) self.assertTrue(user.groups.filter(pk=group_pks[1]).exists()) def test_creation_with_default_group(self): - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") # This is the builtin groups 'default'. # The pk is valid. But this group can not be added to users. group_pk = (1,) response = self.client.post( - reverse('user-list'), - {'last_name': 'Test name aedah1iequoof0Ashed4', - 'groups_id': group_pk}) + reverse("user-list"), + {"last_name": "Test name aedah1iequoof0Ashed4", "groups_id": group_pk}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'groups_id': ["Invalid pk \"%d\" - object does not exist." % group_pk]}) + self.assertEqual( + response.data, + {"groups_id": ['Invalid pk "%d" - object does not exist.' % group_pk]}, + ) class UserUpdate(TestCase): """ Tests update of users via REST API. """ + def test_simple_update_via_patch(self): """ Test to only update the last_name with a patch request. @@ -127,18 +135,19 @@ class UserUpdate(TestCase): The field username *should not* be changed by the request. """ admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") # This is the builtin user 'Administrator' with username 'admin'. The pk is valid. - user_pk = User.objects.get(username='admin').pk + user_pk = User.objects.get(username="admin").pk response = admin_client.patch( - reverse('user-detail', args=[user_pk]), - {'last_name': 'New name tu3ooh5Iez5Aec2laefo'}) + reverse("user-detail", args=[user_pk]), + {"last_name": "New name tu3ooh5Iez5Aec2laefo"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) user = User.objects.get(pk=user_pk) - self.assertEqual(user.last_name, 'New name tu3ooh5Iez5Aec2laefo') - self.assertEqual(user.username, 'admin') + self.assertEqual(user.last_name, "New name tu3ooh5Iez5Aec2laefo") + self.assertEqual(user.username, "admin") def test_simple_update_via_put(self): """ @@ -147,31 +156,31 @@ class UserUpdate(TestCase): The field username *should* be changed by the request. """ admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") # This is the builtin user 'Administrator'. The pk is valid. - user_pk = User.objects.get(username='admin').pk + user_pk = User.objects.get(username="admin").pk response = admin_client.put( - reverse('user-detail', args=[user_pk]), - {'last_name': 'New name Ohy4eeyei5'}) + reverse("user-detail", args=[user_pk]), {"last_name": "New name Ohy4eeyei5"} + ) self.assertEqual(response.status_code, 200) - self.assertEqual(User.objects.get(pk=user_pk).username, 'New name Ohy4eeyei5') + self.assertEqual(User.objects.get(pk=user_pk).username, "New name Ohy4eeyei5") def test_update_deactivate_yourselfself(self): """ Tests that an user can not deactivate himself. """ admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") # This is the builtin user 'Administrator'. The pk is valid. - user_pk = User.objects.get(username='admin').pk + user_pk = User.objects.get(username="admin").pk response = admin_client.patch( - reverse('user-detail', args=[user_pk]), - {'username': 'admin', - 'is_active': False}, - format='json') + reverse("user-detail", args=[user_pk]), + {"username": "admin", "is_active": False}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -180,48 +189,56 @@ class UserUpdate(TestCase): Tests that an user can update himself even if he is not a manager. """ user = User.objects.create_user( - username='non-admin zeiyeGhaoXoh4awe3xai', - password='non-admin chah1hoshohN5Oh7zouj') + username="non-admin zeiyeGhaoXoh4awe3xai", + password="non-admin chah1hoshohN5Oh7zouj", + ) client = APIClient() client.login( - username='non-admin zeiyeGhaoXoh4awe3xai', - password='non-admin chah1hoshohN5Oh7zouj') + username="non-admin zeiyeGhaoXoh4awe3xai", + password="non-admin chah1hoshohN5Oh7zouj", + ) response = client.put( - reverse('user-detail', args=[user.pk]), - {'username': 'New username IeWeipee5mahpi4quupo', - 'last_name': 'New name fae1Bu1Eyeis9eRox4xu', - 'about_me': 'New profile text Faemahphi3Hilokangei'}) + reverse("user-detail", args=[user.pk]), + { + "username": "New username IeWeipee5mahpi4quupo", + "last_name": "New name fae1Bu1Eyeis9eRox4xu", + "about_me": "New profile text Faemahphi3Hilokangei", + }, + ) self.assertEqual(response.status_code, 200) user = User.objects.get(pk=user.pk) - self.assertEqual(user.username, 'New username IeWeipee5mahpi4quupo') - self.assertEqual(user.about_me, 'New profile text Faemahphi3Hilokangei') + self.assertEqual(user.username, "New username IeWeipee5mahpi4quupo") + self.assertEqual(user.about_me, "New profile text Faemahphi3Hilokangei") # The user is not allowed to change some other fields (like last_name). - self.assertNotEqual(user.last_name, 'New name fae1Bu1Eyeis9eRox4xu') + self.assertNotEqual(user.last_name, "New name fae1Bu1Eyeis9eRox4xu") class UserDelete(TestCase): """ Tests delete of users via REST API. """ + def test_delete(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') - User.objects.create(username='Test name bo3zieT3iefahng0ahqu') + admin_client.login(username="admin", password="admin") + User.objects.create(username="Test name bo3zieT3iefahng0ahqu") - response = admin_client.delete(reverse('user-detail', args=['2'])) + response = admin_client.delete(reverse("user-detail", args=["2"])) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse(User.objects.filter(username='Test name bo3zieT3iefahng0ahqu').exists()) + self.assertFalse( + User.objects.filter(username="Test name bo3zieT3iefahng0ahqu").exists() + ) def test_delete_yourself(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") # This is the builtin user 'Administrator'. The pk is valid. admin_user_pk = 1 - response = admin_client.delete(reverse('user-detail', args=[admin_user_pk])) + response = admin_client.delete(reverse("user-detail", args=[admin_user_pk])) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -230,29 +247,35 @@ class UserResetPassword(TestCase): """ Tests resetting users password via REST API by a manager. """ + def test_reset(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') - user = User.objects.create(username='Test name ooMoa4ou4mohn2eo1ree') - user.default_password = 'new_password_Yuuh8OoQueePahngohy3' + admin_client.login(username="admin", password="admin") + user = User.objects.create(username="Test name ooMoa4ou4mohn2eo1ree") + user.default_password = "new_password_Yuuh8OoQueePahngohy3" user.save() response = admin_client.post( - reverse('user-reset-password', args=[user.pk]), - {'password': 'new_password_Yuuh8OoQueePahngohy3_new'}) + reverse("user-reset-password", args=[user.pk]), + {"password": "new_password_Yuuh8OoQueePahngohy3_new"}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(User.objects.get(pk=user.pk).check_password( - 'new_password_Yuuh8OoQueePahngohy3_new')) + self.assertTrue( + User.objects.get(pk=user.pk).check_password( + "new_password_Yuuh8OoQueePahngohy3_new" + ) + ) """ Tests whether a random password is set as default and actual password if no default password is provided. """ + def test_set_random_initial_password(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") serializer = UserFullSerializer() - user = serializer.create({'username': 'Test name 9gt043qwvnj2d0cr'}) + user = serializer.create({"username": "Test name 9gt043qwvnj2d0cr"}) user.save() default_password = User.objects.get(pk=user.pk).default_password @@ -265,25 +288,25 @@ class UserMassImport(TestCase): """ Tests mass import of users. """ + def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") def test_mass_import(self): user_1 = { - 'first_name': 'first_name_kafaith3woh3thie7Ciy', - 'last_name': 'last_name_phah0jaeph9ThoongaeL', - 'groups_id': [] + "first_name": "first_name_kafaith3woh3thie7Ciy", + "last_name": "last_name_phah0jaeph9ThoongaeL", + "groups_id": [], } user_2 = { - 'first_name': 'first_name_kohdao7Eibouwee8ma2O', - 'last_name': 'last_name_kafaith3woh3thie7Ciy', - 'groups_id': [] + "first_name": "first_name_kohdao7Eibouwee8ma2O", + "last_name": "last_name_kafaith3woh3thie7Ciy", + "groups_id": [], } response = self.client.post( - reverse('user-mass-import'), - {'users': [user_1, user_2]}, - format='json') + reverse("user-mass-import"), {"users": [user_1, user_2]}, format="json" + ) self.assertEqual(response.status_code, 200) self.assertEqual(User.objects.count(), 3) @@ -292,45 +315,45 @@ class UserSendIntivationEmail(TestCase): """ Tests sending an email to the user. """ + email = "admin@test-domain.com" def setUp(self): self.client = APIClient() - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") self.admin = User.objects.get() self.admin.email = self.email self.admin.save() def test_email_sending(self): data = { - 'user_ids': [self.admin.pk], - 'subject': config['users_email_subject'], - 'message': config['users_email_body'] + "user_ids": [self.admin.pk], + "subject": config["users_email_subject"], + "message": config["users_email_body"], } response = self.client.post( - reverse('user-mass-invite-email'), - data, - format='json') + reverse("user-mass-invite-email"), data, format="json" + ) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data["count"], 1) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to[0], self.email) class GroupMetadata(TestCase): def test_options_request_as_anonymous_user_activated(self): - config['general_system_enable_anonymous'] = True + config["general_system_enable_anonymous"] = True - response = self.client.options('/rest/users/group/') + response = self.client.options("/rest/users/group/") self.assertEqual(response.status_code, 200) - self.assertEqual(response.data['name'], 'Group List') - perm_list = response.data['actions']['POST']['permissions']['choices'] + self.assertEqual(response.data["name"], "Group List") + perm_list = response.data["actions"]["POST"]["permissions"]["choices"] self.assertEqual(type(perm_list), list) for item in perm_list: self.assertEqual(type(item), dict) - self.assertTrue(item.get('display_name') is not None) - self.assertTrue(item.get('value') is not None) + self.assertTrue(item.get("display_name") is not None) + self.assertTrue(item.get("value") is not None) class GroupReceive(TestCase): @@ -338,7 +361,7 @@ class GroupReceive(TestCase): """ Test to get the groups with an anonymous user, when they are deactivated. """ - response = self.client.get('/rest/users/group/') + response = self.client.get("/rest/users/group/") self.assertEqual(response.status_code, 403) @@ -346,9 +369,9 @@ class GroupReceive(TestCase): """ Test to get the groups with an anonymous user, when they are activated. """ - config['general_system_enable_anonymous'] = True + config["general_system_enable_anonymous"] = True - response = self.client.get('/rest/users/group/') + response = self.client.get("/rest/users/group/") self.assertEqual(response.status_code, 200) @@ -356,14 +379,14 @@ class GroupReceive(TestCase): """ Test to get the groups with an logged in user with no permissions. """ - user = User(username='test') - user.set_password('test') + user = User(username="test") + user.set_password("test") user.save() default_group = Group.objects.get(pk=1) default_group.permissions.all().delete() - self.client.login(username='test', password='test') + self.client.login(username="test", password="test") - response = self.client.get('/rest/users/group/') + response = self.client.get("/rest/users/group/") self.assertEqual(response.status_code, 200) @@ -372,156 +395,183 @@ class GroupCreate(TestCase): """ Tests creation of groups via REST API. """ + def test_creation(self): - self.client.login(username='admin', password='admin') + self.client.login(username="admin", password="admin") # This contains two valid permissions of the users app. - permissions = ('users.can_see_name', 'users.can_see_extra_data') + permissions = ("users.can_see_name", "users.can_see_extra_data") response = self.client.post( - reverse('group-list'), - {'name': 'Test name la8eephu9vaecheiKeif', - 'permissions': permissions}) + reverse("group-list"), + {"name": "Test name la8eephu9vaecheiKeif", "permissions": permissions}, + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - group = Group.objects.get(name='Test name la8eephu9vaecheiKeif') + group = Group.objects.get(name="Test name la8eephu9vaecheiKeif") for permission in permissions: - app_label, codename = permission.split('.') - self.assertTrue(group.permissions.get(content_type__app_label=app_label, codename=codename)) + app_label, codename = permission.split(".") + self.assertTrue( + group.permissions.get( + content_type__app_label=app_label, codename=codename + ) + ) def test_failed_creation_invalid_value(self): - self.client.login(username='admin', password='admin') - permissions = ('invalid_permission',) + self.client.login(username="admin", password="admin") + permissions = ("invalid_permission",) response = self.client.post( - reverse('group-list'), - {'name': 'Test name ool5aeb6Rai2aiLaith1', - 'permissions': permissions}) + reverse("group-list"), + {"name": "Test name ool5aeb6Rai2aiLaith1", "permissions": permissions}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( response.data, - {'permissions': ['Incorrect value "invalid_permission". Expected app_label.codename string.']}) + { + "permissions": [ + 'Incorrect value "invalid_permission". Expected app_label.codename string.' + ] + }, + ) def test_failed_creation_invalid_permission(self): - self.client.login(username='admin', password='admin') - permissions = ('invalid_app.invalid_permission',) + self.client.login(username="admin", password="admin") + permissions = ("invalid_app.invalid_permission",) response = self.client.post( - reverse('group-list'), - {'name': 'Test name wei2go2aiV3eophi9Ohg', - 'permissions': permissions}) + reverse("group-list"), + {"name": "Test name wei2go2aiV3eophi9Ohg", "permissions": permissions}, + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( response.data, - {'permissions': ['Invalid permission "invalid_app.invalid_permission". Object does not exist.']}) + { + "permissions": [ + 'Invalid permission "invalid_app.invalid_permission". Object does not exist.' + ] + }, + ) class GroupUpdate(TestCase): """ Tests update of groups via REST API. """ + def test_simple_update_via_patch(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") # This is the builtin group 'Delegates'. The pk is valid. group_pk = 2 # This contains one valid permission of the users app. - permissions = ('users.can_see_name',) + permissions = ("users.can_see_name",) response = admin_client.patch( - reverse('group-detail', args=[group_pk]), - {'permissions': permissions}) + reverse("group-detail", args=[group_pk]), {"permissions": permissions} + ) self.assertEqual(response.status_code, status.HTTP_200_OK) group = Group.objects.get(pk=group_pk) for permission in permissions: - app_label, codename = permission.split('.') - self.assertTrue(group.permissions.get(content_type__app_label=app_label, codename=codename)) + app_label, codename = permission.split(".") + self.assertTrue( + group.permissions.get( + content_type__app_label=app_label, codename=codename + ) + ) def test_simple_update_via_put(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") # This is the builtin group 'Delegates'. The pk is valid. group_pk = 2 # This contains one valid permission of the users app. - permissions = ('users.can_see_name',) + permissions = ("users.can_see_name",) response = admin_client.put( - reverse('group-detail', args=[group_pk]), - {'permissions': permissions}) + reverse("group-detail", args=[group_pk]), {"permissions": permissions} + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'name': ['This field is required.']}) + self.assertEqual(response.data, {"name": ["This field is required."]}) def test_update_via_put_with_new_permissions(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') - group = Group.objects.create(name='group_name_inooThe3dii4mahWeeSe') + admin_client.login(username="admin", password="admin") + group = Group.objects.create(name="group_name_inooThe3dii4mahWeeSe") # This contains all permissions. permissions = [ - 'agenda.can_be_speaker', - 'agenda.can_manage', - 'agenda.can_see', - 'agenda.can_see_internal_items', - 'assignments.can_manage', - 'assignments.can_nominate_other', - 'assignments.can_nominate_self', - 'assignments.can_see', - 'core.can_manage_config', - 'core.can_manage_projector', - 'core.can_manage_tags', - 'core.can_manage_chat', - 'core.can_see_frontpage', - 'core.can_see_projector', - 'core.can_use_chat', - 'mediafiles.can_manage', - 'mediafiles.can_see', - 'mediafiles.can_see_hidden', - 'mediafiles.can_upload', - 'motions.can_create', - 'motions.can_manage', - 'motions.can_see', - 'motions.can_support', - 'users.can_manage', - 'users.can_see_extra_data', - 'users.can_see_name', + "agenda.can_be_speaker", + "agenda.can_manage", + "agenda.can_see", + "agenda.can_see_internal_items", + "assignments.can_manage", + "assignments.can_nominate_other", + "assignments.can_nominate_self", + "assignments.can_see", + "core.can_manage_config", + "core.can_manage_projector", + "core.can_manage_tags", + "core.can_manage_chat", + "core.can_see_frontpage", + "core.can_see_projector", + "core.can_use_chat", + "mediafiles.can_manage", + "mediafiles.can_see", + "mediafiles.can_see_hidden", + "mediafiles.can_upload", + "motions.can_create", + "motions.can_manage", + "motions.can_see", + "motions.can_support", + "users.can_manage", + "users.can_see_extra_data", + "users.can_see_name", ] response = admin_client.put( - reverse('group-detail', args=[group.pk]), - {'name': 'new_group_name_Chie6duwaepoo8aech7r', - 'permissions': permissions}, - format='json') + reverse("group-detail", args=[group.pk]), + {"name": "new_group_name_Chie6duwaepoo8aech7r", "permissions": permissions}, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) group = Group.objects.get(pk=group.pk) for permission in permissions: - app_label, codename = permission.split('.') - self.assertTrue(group.permissions.get(content_type__app_label=app_label, codename=codename)) + app_label, codename = permission.split(".") + self.assertTrue( + group.permissions.get( + content_type__app_label=app_label, codename=codename + ) + ) class GroupDelete(TestCase): """ Tests delete of groups via REST API. """ + def test_delete(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') - group = Group.objects.create(name='Test name Koh4lohlaewoog9Ahsh5') + admin_client.login(username="admin", password="admin") + group = Group.objects.create(name="Test name Koh4lohlaewoog9Ahsh5") - response = admin_client.delete(reverse('group-detail', args=[group.pk])) + response = admin_client.delete(reverse("group-detail", args=[group.pk])) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse(Group.objects.filter(name='Test name Koh4lohlaewoog9Ahsh5').exists()) + self.assertFalse( + Group.objects.filter(name="Test name Koh4lohlaewoog9Ahsh5").exists() + ) def test_delete_builtin_groups(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") # The pk of builtin group 'Default' group_pk = 1 - response = admin_client.delete(reverse('group-detail', args=[group_pk])) + response = admin_client.delete(reverse("group-detail", args=[group_pk])) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -529,29 +579,34 @@ class PersonalNoteTest(TestCase): """ Tests for PersonalNote model. """ + def test_anonymous_without_personal_notes(self): - admin = User.objects.get(username='admin') - personal_note = PersonalNote.objects.create(user=admin, notes='["admin_personal_note_OoGh8choro0oosh0roob"]') - config['general_system_enable_anonymous'] = True + admin = User.objects.get(username="admin") + personal_note = PersonalNote.objects.create( + user=admin, notes='["admin_personal_note_OoGh8choro0oosh0roob"]' + ) + config["general_system_enable_anonymous"] = True guest_client = APIClient() - response = guest_client.get(reverse('personalnote-detail', args=[personal_note.pk])) + response = guest_client.get( + reverse("personalnote-detail", args=[personal_note.pk]) + ) self.assertEqual(response.status_code, 404) def test_admin_send_JSON(self): admin_client = APIClient() - admin_client.login(username='admin', password='admin') + admin_client.login(username="admin", password="admin") response = admin_client.post( - reverse('personalnote-list'), + reverse("personalnote-list"), { "notes": { "example-model": { "1": { "note": "note for the example.model with id 1 Oohae1JeuSedooyeeviH", - "star": True + "star": True, } } } }, - format='json' + format="json", ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) diff --git a/tests/integration/utils/test_consumers.py b/tests/integration/utils/test_consumers.py index a1d41d51d..b7820be7b 100644 --- a/tests/integration/utils/test_consumers.py +++ b/tests/integration/utils/test_consumers.py @@ -6,11 +6,7 @@ import pytest from asgiref.sync import sync_to_async from channels.testing import WebsocketCommunicator from django.conf import settings -from django.contrib.auth import ( - BACKEND_SESSION_KEY, - HASH_SESSION_KEY, - SESSION_KEY, -) +from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY from openslides.asgi import application from openslides.core.config import config @@ -21,11 +17,7 @@ from openslides.utils.autoupdate import ( ) from openslides.utils.cache import element_cache -from ...unit.utils.cache_provider import ( - Collection1, - Collection2, - get_cachable_provider, -) +from ...unit.utils.cache_provider import Collection1, Collection2, get_cachable_provider from ..helpers import TConfig, TUser @@ -38,7 +30,9 @@ async def prepare_element_cache(settings): """ await element_cache.cache_provider.clear_cache() orig_cachable_provider = element_cache.cachable_provider - element_cache.cachable_provider = get_cachable_provider([Collection1(), Collection2(), TConfig(), TUser()]) + element_cache.cachable_provider = get_cachable_provider( + [Collection1(), Collection2(), TConfig(), TUser()] + ) element_cache._cachables = None await sync_to_async(element_cache.ensure_cache)() yield @@ -52,7 +46,7 @@ async def prepare_element_cache(settings): async def get_communicator(): communicator: WebsocketCommunicator = None - def get_communicator(query_string=''): + def get_communicator(query_string=""): nonlocal communicator # use the outer communicator variable if query_string: query_string = "?{}".format(query_string) @@ -74,47 +68,60 @@ async def set_config(): """ Set a config variable in the element_cache without hitting the database. """ + async def _set_config(key, value): - with patch('openslides.utils.autoupdate.save_history'): + with patch("openslides.utils.autoupdate.save_history"): collection_string = config.get_collection_string() config_id = config.key_to_id[key] # type: ignore - full_data = {'id': config_id, 'key': key, 'value': value} - await sync_to_async(inform_changed_elements)([ - Element(id=config_id, collection_string=collection_string, full_data=full_data, information='', user_id=None, disable_history=True)]) + full_data = {"id": config_id, "key": key, "value": value} + await sync_to_async(inform_changed_elements)( + [ + Element( + id=config_id, + collection_string=collection_string, + full_data=full_data, + information="", + user_id=None, + disable_history=True, + ) + ] + ) return _set_config @pytest.mark.asyncio async def test_normal_connection(get_communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) connected, __ = await get_communicator().connect() assert connected @pytest.mark.asyncio async def test_connection_with_change_id(get_communicator, set_config): - await set_config('general_system_enable_anonymous', True) - communicator = get_communicator('change_id=0') + await set_config("general_system_enable_anonymous", True) + communicator = get_communicator("change_id=0") await communicator.connect() response = await communicator.receive_json_from() - type = response.get('type') - content = response.get('content') - assert type == 'autoupdate' - assert 'changed' in content - assert 'deleted' in content - assert 'from_change_id' in content - assert 'to_change_id' in content - assert Collection1().get_collection_string() in content['changed'] - assert Collection2().get_collection_string() in content['changed'] - assert TConfig().get_collection_string() in content['changed'] - assert TUser().get_collection_string() in content['changed'] + type = response.get("type") + content = response.get("content") + assert type == "autoupdate" + assert "changed" in content + assert "deleted" in content + assert "from_change_id" in content + assert "to_change_id" in content + assert Collection1().get_collection_string() in content["changed"] + assert Collection2().get_collection_string() in content["changed"] + assert TConfig().get_collection_string() in content["changed"] + assert TUser().get_collection_string() in content["changed"] @pytest.mark.asyncio -async def test_connection_with_change_id_get_restricted_data_with_restricted_data_cache(get_communicator, set_config): +async def test_connection_with_change_id_get_restricted_data_with_restricted_data_cache( + get_communicator, set_config +): """ Test, that the returned data is the restricted_data when restricted_data_cache is activated """ @@ -123,14 +130,14 @@ async def test_connection_with_change_id_get_restricted_data_with_restricted_dat original_use_restricted_data = element_cache.use_restricted_data_cache element_cache.use_restricted_data_cache = True - await set_config('general_system_enable_anonymous', True) - communicator = get_communicator('change_id=0') + await set_config("general_system_enable_anonymous", True) + communicator = get_communicator("change_id=0") await communicator.connect() response = await communicator.receive_json_from() - content = response.get('content') - assert content['changed']['app/collection1'][0]['value'] == 'restricted_value1' + content = response.get("content") + assert content["changed"]["app/collection1"][0]["value"] == "restricted_value1" finally: # reset the value of use_restricted_data_cache element_cache.use_restricted_data_cache = original_use_restricted_data @@ -138,8 +145,8 @@ async def test_connection_with_change_id_get_restricted_data_with_restricted_dat @pytest.mark.asyncio async def test_connection_with_invalid_change_id(get_communicator, set_config): - await set_config('general_system_enable_anonymous', True) - communicator = get_communicator('change_id=invalid') + await set_config("general_system_enable_anonymous", True) + communicator = get_communicator("change_id=invalid") connected, __ = await communicator.connect() assert connected is False @@ -147,8 +154,8 @@ async def test_connection_with_invalid_change_id(get_communicator, set_config): @pytest.mark.asyncio async def test_connection_with_to_big_change_id(get_communicator, set_config): - await set_config('general_system_enable_anonymous', True) - communicator = get_communicator('change_id=100') + await set_config("general_system_enable_anonymous", True) + communicator = get_communicator("change_id=100") connected, __ = await communicator.connect() @@ -158,30 +165,31 @@ async def test_connection_with_to_big_change_id(get_communicator, set_config): @pytest.mark.asyncio async def test_changed_data_autoupdate_off(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() # Change a config value - await set_config('general_event_name', 'Test Event') + await set_config("general_event_name", "Test Event") assert await communicator.receive_nothing() @pytest.mark.asyncio async def test_changed_data_autoupdate_on(get_communicator, set_config): - await set_config('general_system_enable_anonymous', True) - communicator = get_communicator('autoupdate=on') + await set_config("general_system_enable_anonymous", True) + communicator = get_communicator("autoupdate=on") await communicator.connect() # Change a config value - await set_config('general_event_name', 'Test Event') + await set_config("general_event_name", "Test Event") response = await communicator.receive_json_from() - id = config.get_key_to_id()['general_event_name'] - type = response.get('type') - content = response.get('content') - assert type == 'autoupdate' - assert content['changed'] == { - 'core/config': [{'id': id, 'key': 'general_event_name', 'value': 'Test Event'}]} + id = config.get_key_to_id()["general_event_name"] + type = response.get("type") + content = response.get("content") + assert type == "autoupdate" + assert content["changed"] == { + "core/config": [{"id": id, "key": "general_event_name", "value": "Test Event"}] + } @pytest.mark.asyncio @@ -196,12 +204,14 @@ async def test_with_user(): # login user with id 1 engine = import_module(settings.SESSION_ENGINE) session = engine.SessionStore() # type: ignore - session[SESSION_KEY] = '1' - session[HASH_SESSION_KEY] = '362d4f2de1463293cb3aaba7727c967c35de43ee' # see helpers.TUser - session[BACKEND_SESSION_KEY] = 'django.contrib.auth.backends.ModelBackend' + session[SESSION_KEY] = "1" + session[ + HASH_SESSION_KEY + ] = "362d4f2de1463293cb3aaba7727c967c35de43ee" # see helpers.TUser + session[BACKEND_SESSION_KEY] = "django.contrib.auth.backends.ModelBackend" session.save() scn = settings.SESSION_COOKIE_NAME - cookies = (b'cookie', '{}={}'.format(scn, session.session_key).encode()) + cookies = (b"cookie", "{}={}".format(scn, session.session_key).encode()) communicator = WebsocketCommunicator(application, "/ws/", headers=[cookies]) connected, __ = await communicator.connect() @@ -213,130 +223,152 @@ async def test_with_user(): @pytest.mark.asyncio async def test_receive_deleted_data(get_communicator, set_config): - await set_config('general_system_enable_anonymous', True) - communicator = get_communicator('autoupdate=on') + await set_config("general_system_enable_anonymous", True) + communicator = get_communicator("autoupdate=on") await communicator.connect() # Delete test element - with patch('openslides.utils.autoupdate.save_history'): - await sync_to_async(inform_deleted_data)([(Collection1().get_collection_string(), 1)]) + with patch("openslides.utils.autoupdate.save_history"): + await sync_to_async(inform_deleted_data)( + [(Collection1().get_collection_string(), 1)] + ) response = await communicator.receive_json_from() - type = response.get('type') - content = response.get('content') - assert type == 'autoupdate' - assert content['deleted'] == {Collection1().get_collection_string(): [1]} + type = response.get("type") + content = response.get("content") + assert type == "autoupdate" + assert content["deleted"] == {Collection1().get_collection_string(): [1]} @pytest.mark.asyncio async def test_send_notify(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'notify', 'content': [{'testmessage': 'foobar, what else.'}], 'id': 'test'}) + await communicator.send_json_to( + { + "type": "notify", + "content": [{"testmessage": "foobar, what else."}], + "id": "test", + } + ) response = await communicator.receive_json_from() - content = response['content'] + content = response["content"] assert isinstance(content, list) assert len(content) == 1 - assert content[0]['testmessage'] == 'foobar, what else.' - assert 'senderReplyChannelName' in content[0] - assert content[0]['senderUserId'] == 0 + assert content[0]["testmessage"] == "foobar, what else." + assert "senderReplyChannelName" in content[0] + assert content[0]["senderUserId"] == 0 @pytest.mark.asyncio async def test_invalid_websocket_message_type(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() await communicator.send_json_to([]) response = await communicator.receive_json_from() - assert response['type'] == 'error' + assert response["type"] == "error" @pytest.mark.asyncio async def test_invalid_websocket_message_no_id(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'test', 'content': 'foobar'}) + await communicator.send_json_to({"type": "test", "content": "foobar"}) response = await communicator.receive_json_from() - assert response['type'] == 'error' + assert response["type"] == "error" @pytest.mark.asyncio async def test_send_unknown_type(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'if_you_add_this_type_to_openslides_I_will_be_sad', 'content': True, 'id': 'test_id'}) + await communicator.send_json_to( + { + "type": "if_you_add_this_type_to_openslides_I_will_be_sad", + "content": True, + "id": "test_id", + } + ) response = await communicator.receive_json_from() - assert response['type'] == 'error' - assert response['in_response'] == 'test_id' + assert response["type"] == "error" + assert response["in_response"] == "test_id" @pytest.mark.asyncio async def test_request_constants(communicator, settings, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'constants', 'content': '', 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "constants", "content": "", "id": "test_id"} + ) response = await communicator.receive_json_from() - assert response['type'] == 'constants' + assert response["type"] == "constants" # See conftest.py for the content of 'content' - assert response['content'] == {'constant1': 'value1', 'constant2': 'value2'} + assert response["content"] == {"constant1": "value1", "constant2": "value2"} @pytest.mark.asyncio async def test_send_get_elements(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'getElements', 'content': {}, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "getElements", "content": {}, "id": "test_id"} + ) response = await communicator.receive_json_from() - type = response.get('type') - content = response.get('content') - assert type == 'autoupdate' - assert 'changed' in content - assert 'deleted' in content - assert 'from_change_id' in content - assert 'to_change_id' in content - assert Collection1().get_collection_string() in content['changed'] - assert Collection2().get_collection_string() in content['changed'] - assert TConfig().get_collection_string() in content['changed'] - assert TUser().get_collection_string() in content['changed'] + type = response.get("type") + content = response.get("content") + assert type == "autoupdate" + assert "changed" in content + assert "deleted" in content + assert "from_change_id" in content + assert "to_change_id" in content + assert Collection1().get_collection_string() in content["changed"] + assert Collection2().get_collection_string() in content["changed"] + assert TConfig().get_collection_string() in content["changed"] + assert TUser().get_collection_string() in content["changed"] @pytest.mark.asyncio async def test_send_get_elements_to_big_change_id(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': 100}, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "getElements", "content": {"change_id": 100}, "id": "test_id"} + ) response = await communicator.receive_json_from() - type = response.get('type') - assert type == 'error' - assert response.get('in_response') == 'test_id' + type = response.get("type") + assert type == "error" + assert response.get("in_response") == "test_id" @pytest.mark.asyncio async def test_send_get_elements_to_small_change_id(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': 1}, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "getElements", "content": {"change_id": 1}, "id": "test_id"} + ) response = await communicator.receive_json_from() - type = response.get('type') - assert type == 'autoupdate' - assert response.get('in_response') == 'test_id' - assert response.get('content')['all_data'] + type = response.get("type") + assert type == "autoupdate" + assert response.get("in_response") == "test_id" + assert response.get("content")["all_data"] @pytest.mark.asyncio @@ -345,40 +377,61 @@ async def test_send_connect_twice_with_clear_change_id_cache(communicator, set_c Test, that a second request with change_id+1 from the first request, returns an error. """ - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) element_cache.cache_provider.change_id_data = {} # type: ignore await communicator.connect() - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': 0}, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "getElements", "content": {"change_id": 0}, "id": "test_id"} + ) response1 = await communicator.receive_json_from() - first_change_id = response1.get('content')['to_change_id'] + first_change_id = response1.get("content")["to_change_id"] - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': first_change_id+1}, 'id': 'test_id'}) + await communicator.send_json_to( + { + "type": "getElements", + "content": {"change_id": first_change_id + 1}, + "id": "test_id", + } + ) response2 = await communicator.receive_json_from() - assert response2['type'] == 'error' - assert response2.get('content') == 'Requested change_id is higher this highest change_id.' + assert response2["type"] == "error" + assert ( + response2.get("content") + == "Requested change_id is higher this highest change_id." + ) @pytest.mark.asyncio -async def test_send_connect_twice_with_clear_change_id_cache_same_change_id_then_first_request(communicator, set_config): +async def test_send_connect_twice_with_clear_change_id_cache_same_change_id_then_first_request( + communicator, set_config +): """ Test, that a second request with the change_id from the first request, returns all data. A client should not do this but request for change_id+1 """ - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await element_cache.cache_provider.clear_cache() await communicator.connect() - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': 0}, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "getElements", "content": {"change_id": 0}, "id": "test_id"} + ) response1 = await communicator.receive_json_from() - first_change_id = response1.get('content')['to_change_id'] + first_change_id = response1.get("content")["to_change_id"] - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': first_change_id}, 'id': 'test_id'}) + await communicator.send_json_to( + { + "type": "getElements", + "content": {"change_id": first_change_id}, + "id": "test_id", + } + ) response2 = await communicator.receive_json_from() - assert response2['type'] == 'autoupdate' - assert response2.get('content')['all_data'] + assert response2["type"] == "autoupdate" + assert response2.get("content")["all_data"] @pytest.mark.asyncio @@ -389,64 +442,73 @@ async def test_request_changed_elements_no_douple_elements(communicator, set_con Test when all_data is false """ - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() # Change element twice - await set_config('general_event_name', 'Test Event') - await set_config('general_event_name', 'Other value') + await set_config("general_event_name", "Test Event") + await set_config("general_event_name", "Other value") # Ask for all elements - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': 2}, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "getElements", "content": {"change_id": 2}, "id": "test_id"} + ) response = await communicator.receive_json_from() - type = response.get('type') - content = response.get('content') - assert type == 'autoupdate' - assert not response.get('content')['all_data'] - config_ids = [e['id'] for e in content['changed']['core/config']] + type = response.get("type") + content = response.get("content") + assert type == "autoupdate" + assert not response.get("content")["all_data"] + config_ids = [e["id"] for e in content["changed"]["core/config"]] # test that config_ids are unique assert len(config_ids) == len(set(config_ids)) @pytest.mark.asyncio async def test_send_invalid_get_elements(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'getElements', 'content': {'change_id': 'some value'}, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "getElements", "content": {"change_id": "some value"}, "id": "test_id"} + ) response = await communicator.receive_json_from() - type = response.get('type') - assert type == 'error' - assert response.get('in_response') == 'test_id' + type = response.get("type") + assert type == "error" + assert response.get("in_response") == "test_id" @pytest.mark.asyncio async def test_turn_on_autoupdate(communicator, set_config): - await set_config('general_system_enable_anonymous', True) + await set_config("general_system_enable_anonymous", True) await communicator.connect() - await communicator.send_json_to({'type': 'autoupdate', 'content': 'on', 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "autoupdate", "content": "on", "id": "test_id"} + ) await asyncio.sleep(0.01) # Change a config value - await set_config('general_event_name', 'Test Event') + await set_config("general_event_name", "Test Event") response = await communicator.receive_json_from() - id = config.get_key_to_id()['general_event_name'] - type = response.get('type') - content = response.get('content') - assert type == 'autoupdate' - assert content['changed'] == { - 'core/config': [{'id': id, 'key': 'general_event_name', 'value': 'Test Event'}]} + id = config.get_key_to_id()["general_event_name"] + type = response.get("type") + content = response.get("content") + assert type == "autoupdate" + assert content["changed"] == { + "core/config": [{"id": id, "key": "general_event_name", "value": "Test Event"}] + } @pytest.mark.asyncio async def test_turn_off_autoupdate(get_communicator, set_config): - await set_config('general_system_enable_anonymous', True) - communicator = get_communicator('autoupdate=on') + await set_config("general_system_enable_anonymous", True) + communicator = get_communicator("autoupdate=on") await communicator.connect() - await communicator.send_json_to({'type': 'autoupdate', 'content': False, 'id': 'test_id'}) + await communicator.send_json_to( + {"type": "autoupdate", "content": False, "id": "test_id"} + ) await asyncio.sleep(0.01) # Change a config value - await set_config('general_event_name', 'Test Event') + await set_config("general_event_name", "Test Event") assert await communicator.receive_nothing() diff --git a/tests/old/agenda/test_list_of_speakers.py b/tests/old/agenda/test_list_of_speakers.py index 60d56a05b..cd21d5d5a 100644 --- a/tests/old/agenda/test_list_of_speakers.py +++ b/tests/old/agenda/test_list_of_speakers.py @@ -7,23 +7,29 @@ from openslides.utils.test import TestCase class ListOfSpeakerModelTests(TestCase): def setUp(self): - self.item1 = Topic.objects.create(title='item1').agenda_item - self.item2 = Topic.objects.create(title='item2').agenda_item - self.speaker1 = User.objects.create(username='user1') - self.speaker2 = User.objects.create(username='user2') + self.item1 = Topic.objects.create(title="item1").agenda_item + self.item2 = Topic.objects.create(title="item2").agenda_item + self.speaker1 = User.objects.create(username="user1") + self.speaker2 = User.objects.create(username="user2") def test_append_speaker(self): # Append speaker1 to the list of item1 speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) - self.assertTrue(Speaker.objects.filter(user=self.speaker1, item=self.item1).exists()) + self.assertTrue( + Speaker.objects.filter(user=self.speaker1, item=self.item1).exists() + ) # Append speaker1 to the list of item2 speaker1_item2 = Speaker.objects.add(self.speaker1, self.item2) - self.assertTrue(Speaker.objects.filter(user=self.speaker1, item=self.item2).exists()) + self.assertTrue( + Speaker.objects.filter(user=self.speaker1, item=self.item2).exists() + ) # Append speaker2 to the list of item1 speaker2_item1 = Speaker.objects.add(self.speaker2, self.item1) - self.assertTrue(Speaker.objects.filter(user=self.speaker2, item=self.item1).exists()) + self.assertTrue( + Speaker.objects.filter(user=self.speaker2, item=self.item1).exists() + ) # Try to append speaker 1 again to the list of item1 with self.assertRaises(OpenSlidesError): @@ -60,5 +66,7 @@ class ListOfSpeakerModelTests(TestCase): self.assertIsNone(speaker1_item1.end_time) self.assertIsNone(speaker2_item1.begin_time) speaker2_item1.begin_speech() - self.assertIsNotNone(Speaker.objects.get(user=self.speaker1, item=self.item1).end_time) + self.assertIsNotNone( + Speaker.objects.get(user=self.speaker1, item=self.item1).end_time + ) self.assertIsNotNone(speaker2_item1.begin_time) diff --git a/tests/old/config/test_config.py b/tests/old/config/test_config.py index c57f17f3c..c3cd84a0d 100644 --- a/tests/old/config/test_config.py +++ b/tests/old/config/test_config.py @@ -1,5 +1,3 @@ - - from openslides.core.config import ConfigVariable, config from openslides.core.exceptions import ConfigError from openslides.utils.test import TestCase @@ -34,24 +32,25 @@ class HandleConfigTest(TestCase): def test_get_multiple_config_var_error(self): with self.assertRaisesMessage( - ConfigError, - 'Too many values for config variables {\'multiple_config_var\'} found.'): + ConfigError, + "Too many values for config variables {'multiple_config_var'} found.", + ): config.update_config_variables(set_simple_config_view_multiple_vars()) def test_setup_config_var(self): self.assertRaises(TypeError, ConfigVariable) - self.assertRaises(TypeError, ConfigVariable, name='foo') - self.assertRaises(TypeError, ConfigVariable, default_value='foo') + self.assertRaises(TypeError, ConfigVariable, name="foo") + self.assertRaises(TypeError, ConfigVariable, default_value="foo") def test_config_exists(self): - self.assertTrue(config.exists('string_var')) - self.assertFalse(config.exists('unknown_config_var')) + self.assertTrue(config.exists("string_var")) + self.assertFalse(config.exists("unknown_config_var")) def test_set_value_before_getting_it(self): """ Try to call __setitem__ before __getitem__. """ - config['additional_config_var'] = 'value' + config["additional_config_var"] = "value" def test_on_change(self): """ @@ -59,15 +58,18 @@ class HandleConfigTest(TestCase): message. """ with self.assertRaisesMessage( - TTestConfigException, - 'Change callback dhcnfg34dlg06kdg successfully called.'): + TTestConfigException, + "Change callback dhcnfg34dlg06kdg successfully called.", + ): self.set_config_var( - key='var_with_callback_ghvnfjd5768gdfkwg0hm2', - value='new_string_kbmbnfhdgibkdjshg452bc') + key="var_with_callback_ghvnfjd5768gdfkwg0hm2", + value="new_string_kbmbnfhdgibkdjshg452bc", + ) self.assertEqual( - config['var_with_callback_ghvnfjd5768gdfkwg0hm2'], - 'new_string_kbmbnfhdgibkdjshg452bc') + config["var_with_callback_ghvnfjd5768gdfkwg0hm2"], + "new_string_kbmbnfhdgibkdjshg452bc", + ) def set_grouped_config_view(): @@ -78,37 +80,43 @@ def set_grouped_config_view(): variable. These variables are grouped in two subgroups. """ yield ConfigVariable( - name='string_var', - default_value='default_string_rien4ooCZieng6ah', - group='Config vars for testing 1', - subgroup='Group 1 aiYeix2mCieQuae3') + name="string_var", + default_value="default_string_rien4ooCZieng6ah", + group="Config vars for testing 1", + subgroup="Group 1 aiYeix2mCieQuae3", + ) yield ConfigVariable( - name='bool_var', + name="bool_var", default_value=True, - input_type='boolean', - group='Config vars for testing 1', - subgroup='Group 1 aiYeix2mCieQuae3') + input_type="boolean", + group="Config vars for testing 1", + subgroup="Group 1 aiYeix2mCieQuae3", + ) yield ConfigVariable( - name='integer_var', + name="integer_var", default_value=3, - input_type='integer', - group='Config vars for testing 1', - subgroup='Group 1 aiYeix2mCieQuae3') + input_type="integer", + group="Config vars for testing 1", + subgroup="Group 1 aiYeix2mCieQuae3", + ) yield ConfigVariable( - name='hidden_var', - default_value='hidden_value', - group='Config vars for testing 1', - subgroup='Group 2 Toongai7ahyahy7B') + name="hidden_var", + default_value="hidden_value", + group="Config vars for testing 1", + subgroup="Group 2 Toongai7ahyahy7B", + ) yield ConfigVariable( - name='choices_var', - default_value='1', - input_type='choice', + name="choices_var", + default_value="1", + input_type="choice", choices=( - {'value': '1', 'display_name': 'Choice One Ughoch4ocoche6Ee'}, - {'value': '2', 'display_name': 'Choice Two Vahnoh5yalohv5Eb'}), - group='Config vars for testing 1', - subgroup='Group 2 Toongai7ahyahy7B') + {"value": "1", "display_name": "Choice One Ughoch4ocoche6Ee"}, + {"value": "2", "display_name": "Choice Two Vahnoh5yalohv5Eb"}, + ), + group="Config vars for testing 1", + subgroup="Group 2 Toongai7ahyahy7B", + ) def set_simple_config_view(): @@ -116,27 +124,31 @@ def set_simple_config_view(): Sets a simple config view with some config variables but without grouping. """ - yield ConfigVariable(name='additional_config_var', default_value='BaeB0ahcMae3feem') - yield ConfigVariable(name='additional_config_var_2', default_value='') - yield ConfigVariable(name='none_config_var', default_value=None) + yield ConfigVariable(name="additional_config_var", default_value="BaeB0ahcMae3feem") + yield ConfigVariable(name="additional_config_var_2", default_value="") + yield ConfigVariable(name="none_config_var", default_value=None) def set_simple_config_view_multiple_vars(): """ Sets a bad config view with some multiple config vars. """ - yield ConfigVariable(name='multiple_config_var', default_value='foobar1') - yield ConfigVariable(name='multiple_config_var', default_value='foobar2') + yield ConfigVariable(name="multiple_config_var", default_value="foobar1") + yield ConfigVariable(name="multiple_config_var", default_value="foobar2") def set_simple_config_collection_disabled_view(): - yield ConfigVariable(name='hidden_config_var_2', default_value='') + yield ConfigVariable(name="hidden_config_var_2", default_value="") def set_simple_config_collection_with_callback(): def callback(): - raise TTestConfigException('Change callback dhcnfg34dlg06kdg successfully called.') + raise TTestConfigException( + "Change callback dhcnfg34dlg06kdg successfully called." + ) + yield ConfigVariable( - name='var_with_callback_ghvnfjd5768gdfkwg0hm2', - default_value='', - on_change=callback) + name="var_with_callback_ghvnfjd5768gdfkwg0hm2", + default_value="", + on_change=callback, + ) diff --git a/tests/old/motions/test_models.py b/tests/old/motions/test_models.py index b7133b026..c0c1dfa93 100644 --- a/tests/old/motions/test_models.py +++ b/tests/old/motions/test_models.py @@ -7,8 +7,8 @@ from openslides.utils.test import TestCase class ModelTest(TestCase): def setUp(self): - self.motion = Motion.objects.create(title='v1') - self.test_user = User.objects.create(username='blub') + self.motion = Motion.objects.create(title="v1") + self.test_user = User.objects.create(username="blub") # Use the simple workflow self.workflow = Workflow.objects.get(pk=1) @@ -21,26 +21,26 @@ class ModelTest(TestCase): def test_state(self): self.motion.reset_state() - self.assertEqual(self.motion.state.name, 'submitted') + self.assertEqual(self.motion.state.name, "submitted") self.motion.state = State.objects.get(pk=5) - self.assertEqual(self.motion.state.name, 'published') + self.assertEqual(self.motion.state.name, "published") with self.assertRaises(WorkflowError): self.motion.create_poll() self.motion.state = State.objects.get(pk=6) - self.assertEqual(self.motion.state.name, 'permitted') + self.assertEqual(self.motion.state.name, "permitted") def test_new_states_or_workflows(self): - workflow_1 = Workflow.objects.create(name='W1') - state_1 = State.objects.create(name='S1', workflow=workflow_1) + workflow_1 = Workflow.objects.create(name="W1") + state_1 = State.objects.create(name="S1", workflow=workflow_1) workflow_1.first_state = state_1 workflow_1.save() - workflow_2 = Workflow.objects.create(name='W2') - state_2 = State.objects.create(name='S2', workflow=workflow_2) + workflow_2 = Workflow.objects.create(name="W2") + state_2 = State.objects.create(name="S2", workflow=workflow_2) workflow_2.first_state = state_2 workflow_2.save() - state_3 = State.objects.create(name='S3', workflow=workflow_1) + state_3 = State.objects.create(name="S3", workflow=workflow_1) with self.assertRaises(WorkflowError): workflow_2.first_state = state_3 @@ -51,12 +51,12 @@ class ModelTest(TestCase): state_1.save() def test_two_empty_identifiers(self): - Motion.objects.create(title='foo', text='bar', identifier='') - Motion.objects.create(title='foo2', text='bar2', identifier='') + Motion.objects.create(title="foo", text="bar", identifier="") + Motion.objects.create(title="foo2", text="bar2", identifier="") def test_is_amendment(self): - config['motions_amendments_enabled'] = True - amendment = Motion.objects.create(title='amendment', parent=self.motion) + config["motions_amendments_enabled"] = True + amendment = Motion.objects.create(title="amendment", parent=self.motion) self.assertTrue(amendment.is_amendment()) self.assertFalse(self.motion.is_amendment()) @@ -65,17 +65,17 @@ class ModelTest(TestCase): """ If the motion already has a identifier, the method does nothing. """ - motion = Motion(identifier='My test identifier') + motion = Motion(identifier="My test identifier") motion.set_identifier() - self.assertEqual(motion.identifier, 'My test identifier') + self.assertEqual(motion.identifier, "My test identifier") def test_set_identifier_manually(self): """ If the config is set to manually, the method does nothing. """ - config['motions_identifier'] = 'manually' + config["motions_identifier"] = "manually" motion = Motion() motion.set_identifier() @@ -88,31 +88,31 @@ class ModelTest(TestCase): If the motion is an amendment, the identifier is the identifier from the parent + a suffix. """ - config['motions_amendments_enabled'] = True - self.motion.identifier = 'Parent identifier' + config["motions_amendments_enabled"] = True + self.motion.identifier = "Parent identifier" self.motion.save() motion = Motion(parent=self.motion) motion.set_identifier() - self.assertEqual(motion.identifier, 'Parent identifier - 1') + self.assertEqual(motion.identifier, "Parent identifier - 1") def test_set_identifier_second_amendment(self): """ If a motion has already an amendment, the second motion gets another identifier. """ - config['motions_amendments_enabled'] = True - self.motion.identifier = 'Parent identifier' + config["motions_amendments_enabled"] = True + self.motion.identifier = "Parent identifier" self.motion.save() - Motion.objects.create(title='Amendment1', parent=self.motion) + Motion.objects.create(title="Amendment1", parent=self.motion) motion = Motion(parent=self.motion) motion.set_identifier() - self.assertEqual(motion.identifier, 'Parent identifier - 2') + self.assertEqual(motion.identifier, "Parent identifier - 2") class ConfigTest(TestCase): def test_stop_submitting(self): - self.assertFalse(config['motions_stop_submitting']) + self.assertFalse(config["motions_stop_submitting"]) diff --git a/tests/old/utils/test_main.py b/tests/old/utils/test_main.py index 3988b01c7..d2cec2e61 100644 --- a/tests/old/utils/test_main.py +++ b/tests/old/utils/test_main.py @@ -7,81 +7,97 @@ from openslides.utils.test import TestCase class TestFunctions(TestCase): - @patch('openslides.utils.main.sys') + @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' + mock_sys.platform = "linux" self.assertEqual(main.detect_openslides_type(), main.UNIX_VERSION) - @patch('openslides.utils.main.os.path.basename') - @patch('openslides.utils.main.sys') + @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' + 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') + @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' + 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') + @patch("openslides.utils.main.detect_openslides_type") + @patch("openslides.utils.main.os.path.expanduser") def test_get_default_settings_dir_unix(self, mock_expanduser, mock_detect): - mock_expanduser.return_value = '/home/test/.config' - self.assertEqual(main.get_default_settings_dir(main.UNIX_VERSION), - '/home/test/.config/openslides') + mock_expanduser.return_value = "/home/test/.config" + self.assertEqual( + main.get_default_settings_dir(main.UNIX_VERSION), + "/home/test/.config/openslides", + ) - @patch('openslides.utils.main.get_win32_app_data_dir') + @patch("openslides.utils.main.get_win32_app_data_dir") def test_get_default_settings_dir_win(self, mock_win): - mock_win.return_value = 'win32' - self.assertEqual(main.get_default_settings_dir(main.WINDOWS_VERSION), - 'win32/openslides') + mock_win.return_value = "win32" + self.assertEqual( + main.get_default_settings_dir(main.WINDOWS_VERSION), "win32/openslides" + ) - @patch('openslides.utils.main.get_win32_portable_dir') + @patch("openslides.utils.main.get_win32_portable_dir") def test_get_default_settings_dir_portable(self, mock_portable): - mock_portable.return_value = 'portable' - self.assertEqual(main.get_default_settings_dir(main.WINDOWS_PORTABLE_VERSION), - 'portable/openslides') + mock_portable.return_value = "portable" + self.assertEqual( + main.get_default_settings_dir(main.WINDOWS_PORTABLE_VERSION), + "portable/openslides", + ) def test_get_local_settings_dir(self): - self.assertEqual(main.get_local_settings_dir(), os.sep.join(('personal_data', 'var'))) + self.assertEqual( + main.get_local_settings_dir(), os.sep.join(("personal_data", "var")) + ) def test_setup_django_settings_module(self): - main.setup_django_settings_module('test_dir_dhvnghfjdh456fzheg2f/test_path_bngjdhc756dzwncshdfnx.py') + 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')) + 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): detect_mock.return_value = main.WINDOWS_PORTABLE_VERSION context = main.get_default_settings_context() - self.assertEqual(context['openslides_user_data_dir'], 'get_win32_portable_user_data_dir()') + self.assertEqual( + context["openslides_user_data_dir"], "get_win32_portable_user_data_dir()" + ) def test_get_default_user_data_dir(self): - self.assertIn(os.path.join('.local', 'share'), main.get_default_user_data_dir(main.UNIX_VERSION)) + self.assertIn( + os.path.join(".local", "share"), + main.get_default_user_data_dir(main.UNIX_VERSION), + ) - @patch('openslides.utils.main.threading.Thread') - @patch('openslides.utils.main.time') - @patch('openslides.utils.main.webbrowser') - def test_start_browser(self, mock_webbrowser, mock_time, mock_Thread): + @patch("openslides.utils.main.threading.Thread") + @patch("openslides.utils.main.time") + @patch("openslides.utils.main.webbrowser") + def test_start_browser(self, mock_webbrowser, mock_time, mock_Thread): browser_mock = MagicMock() mock_webbrowser.get.return_value = browser_mock - main.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 = mock_Thread.call_args[1]["target"] inner_function() - browser_mock.open.assert_called_with('http://localhost:8234') + browser_mock.open.assert_called_with("http://localhost:8234") diff --git a/tests/settings.py b/tests/settings.py index 4e3911bd2..7c0ee23b0 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -11,15 +11,13 @@ from openslides.global_settings import * # noqa OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__))) -EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # OpenSlides plugins # Add plugins to this list. -INSTALLED_PLUGINS += ( # noqa - 'tests.integration.test_plugin', -) +INSTALLED_PLUGINS += ("tests.integration.test_plugin",) # noqa INSTALLED_APPS += INSTALLED_PLUGINS # noqa @@ -27,7 +25,7 @@ INSTALLED_APPS += INSTALLED_PLUGINS # noqa # Important settings for production use # https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ -SECRET_KEY = 'secret' +SECRET_KEY = "secret" DEBUG = False @@ -40,30 +38,26 @@ DEBUG = False # Change this setting to use e. g. PostgreSQL or MySQL. -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - } -} +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}} SESSION_ENGINE = "django.contrib.sessions.backends.cache" # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ -TIME_ZONE = 'Europe/Berlin' +TIME_ZONE = "Europe/Berlin" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ -STATICFILES_DIRS.insert(0, os.path.join(OPENSLIDES_USER_DATA_PATH, 'static')) # noqa +STATICFILES_DIRS.insert(0, os.path.join(OPENSLIDES_USER_DATA_PATH, "static")) # noqa # Files # https://docs.djangoproject.com/en/1.10/topics/files/ -MEDIA_ROOT = os.path.join(OPENSLIDES_USER_DATA_PATH, '') +MEDIA_ROOT = os.path.join(OPENSLIDES_USER_DATA_PATH, "") # Customization of OpenSlides apps @@ -75,9 +69,7 @@ MOTION_IDENTIFIER_MIN_DIGITS = 1 # Use a faster password hasher. -PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.MD5PasswordHasher', -] +PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] # Deactivate restricted_data_cache RESTRICTED_DATA_CACHE = False diff --git a/tests/unit/agenda/test_models.py b/tests/unit/agenda/test_models.py index 1ea20dcaa..3d0f9e572 100644 --- a/tests/unit/agenda/test_models.py +++ b/tests/unit/agenda/test_models.py @@ -5,19 +5,17 @@ from openslides.agenda.models import Item class TestItemTitle(TestCase): - @patch('openslides.agenda.models.Item.content_object') + @patch("openslides.agenda.models.Item.content_object") def test_title_from_content_object(self, content_object): item = Item() - content_object.get_agenda_title.return_value = 'related_title' + content_object.get_agenda_title.return_value = "related_title" - self.assertEqual( - item.title, - 'related_title') + self.assertEqual(item.title, "related_title") - @patch('openslides.agenda.models.Item.content_object') + @patch("openslides.agenda.models.Item.content_object") def test_title_invalid_related(self, content_object): item = Item() - content_object.get_agenda_title.return_value = 'related_title' + content_object.get_agenda_title.return_value = "related_title" del content_object.get_agenda_title with self.assertRaises(NotImplementedError): diff --git a/tests/unit/agenda/test_views.py b/tests/unit/agenda/test_views.py index de4fdd9c6..f17efc65c 100644 --- a/tests/unit/agenda/test_views.py +++ b/tests/unit/agenda/test_views.py @@ -8,6 +8,7 @@ class ItemViewSetManageSpeaker(TestCase): """ Tests views of ItemViewSet to manage speakers. """ + def setUp(self): self.request = MagicMock() self.view_instance = ItemViewSet() @@ -15,11 +16,11 @@ class ItemViewSetManageSpeaker(TestCase): self.view_instance.get_object = get_object_mock = MagicMock() get_object_mock.return_value = self.mock_item = MagicMock() - @patch('openslides.agenda.views.inform_changed_data') - @patch('openslides.agenda.views.has_perm') - @patch('openslides.agenda.views.Speaker') + @patch("openslides.agenda.views.inform_changed_data") + @patch("openslides.agenda.views.has_perm") + @patch("openslides.agenda.views.Speaker") def test_add_oneself_as_speaker(self, mock_speaker, mock_has_perm, mock_icd): - self.request.method = 'POST' + self.request.method = "POST" self.request.user = 1 mock_has_perm.return_value = True self.request.data = {} @@ -29,14 +30,18 @@ class ItemViewSetManageSpeaker(TestCase): mock_speaker.objects.add.assert_called_with(self.request.user, self.mock_item) - @patch('openslides.agenda.views.inform_changed_data') - @patch('openslides.agenda.views.has_perm') - @patch('openslides.agenda.views.get_user_model') - @patch('openslides.agenda.views.Speaker') - def test_add_someone_else_as_speaker(self, mock_speaker, mock_get_user_model, mock_has_perm, mock_icd): - self.request.method = 'POST' + @patch("openslides.agenda.views.inform_changed_data") + @patch("openslides.agenda.views.has_perm") + @patch("openslides.agenda.views.get_user_model") + @patch("openslides.agenda.views.Speaker") + def test_add_someone_else_as_speaker( + self, mock_speaker, mock_get_user_model, mock_has_perm, mock_icd + ): + self.request.method = "POST" self.request.user = 1 - self.request.data = {'user': '2'} # It is assumed that the request user has pk!=2. + self.request.data = { + "user": "2" + } # It is assumed that the request user has pk!=2. mock_get_user_model.return_value = MockUser = MagicMock() MockUser.objects.get.return_value = mock_user = MagicMock() mock_has_perm.return_value = True @@ -46,27 +51,31 @@ class ItemViewSetManageSpeaker(TestCase): MockUser.objects.get.assert_called_with(pk=2) mock_speaker.objects.add.assert_called_with(mock_user, self.mock_item) - @patch('openslides.agenda.views.Speaker') + @patch("openslides.agenda.views.Speaker") def test_remove_oneself(self, mock_speaker): - self.request.method = 'DELETE' + self.request.method = "DELETE" self.request.data = {} self.view_instance.manage_speaker(self.request) mock_queryset = mock_speaker.objects.filter.return_value.exclude.return_value mock_queryset.get.return_value.delete.assert_called_with() - @patch('openslides.agenda.views.inform_changed_data') - @patch('openslides.agenda.views.has_perm') - @patch('openslides.agenda.views.Speaker') - def test_remove_someone_else(self, mock_speaker, mock_has_perm, mock_inform_changed_data): - self.request.method = 'DELETE' + @patch("openslides.agenda.views.inform_changed_data") + @patch("openslides.agenda.views.has_perm") + @patch("openslides.agenda.views.Speaker") + def test_remove_someone_else( + self, mock_speaker, mock_has_perm, mock_inform_changed_data + ): + self.request.method = "DELETE" self.request.user = 1 - self.request.data = {'speaker': '1'} + self.request.data = {"speaker": "1"} mock_has_perm.return_value = True self.view_instance.manage_speaker(self.request) mock_speaker.objects.get.assert_called_with(pk=1) - mock_speaker.objects.get.return_value.delete.assert_called_with(skip_autoupdate=True) + mock_speaker.objects.get.return_value.delete.assert_called_with( + skip_autoupdate=True + ) mock_inform_changed_data.assert_called_with(self.mock_item) @@ -74,6 +83,7 @@ class ItemViewSetSpeak(TestCase): """ Tests views of ItemViewSet to begin and end speech. """ + def setUp(self): self.request = MagicMock() self.view_instance = ItemViewSet() @@ -82,26 +92,28 @@ class ItemViewSetSpeak(TestCase): get_object_mock.return_value = self.mock_item = MagicMock() def test_begin_speech(self): - self.request.method = 'PUT' + self.request.method = "PUT" self.request.user.has_perm.return_value = True self.request.data = {} self.mock_item.get_next_speaker.return_value = mock_next_speaker = MagicMock() self.view_instance.speak(self.request) mock_next_speaker.begin_speech.assert_called_with() - @patch('openslides.agenda.views.Speaker') + @patch("openslides.agenda.views.Speaker") def test_begin_speech_specific_speaker(self, mock_speaker): - self.request.method = 'PUT' + self.request.method = "PUT" self.request.user.has_perm.return_value = True - self.request.data = {'speaker': '1'} + self.request.data = {"speaker": "1"} mock_speaker.objects.get.return_value = mock_next_speaker = MagicMock() self.view_instance.speak(self.request) mock_next_speaker.begin_speech.assert_called_with() - @patch('openslides.agenda.views.Speaker') + @patch("openslides.agenda.views.Speaker") def test_end_speech(self, mock_speaker): - self.request.method = 'DELETE' + self.request.method = "DELETE" self.request.user.has_perm.return_value = True - mock_speaker.objects.filter.return_value.exclude.return_value.get.return_value = mock_speaker = MagicMock() + mock_speaker.objects.filter.return_value.exclude.return_value.get.return_value = ( + mock_speaker + ) = MagicMock() self.view_instance.speak(self.request) mock_speaker.end_speech.assert_called_with() diff --git a/tests/unit/config/test_api.py b/tests/unit/config/test_api.py index 3d57f9975..c03337b6d 100644 --- a/tests/unit/config/test_api.py +++ b/tests/unit/config/test_api.py @@ -6,29 +6,30 @@ from openslides.core.exceptions import ConfigNotFound class TestConfigVariable(TestCase): - @patch('openslides.core.config.config', {'test_variable': None}) + @patch("openslides.core.config.config", {"test_variable": None}) def test_default_value_in_data(self): """ Tests, that the default_value attribute is in the 'data' property of a ConfigVariable instance. """ - config_variable = ConfigVariable('test_variable', 'test_default_value') + config_variable = ConfigVariable("test_variable", "test_default_value") self.assertIn( - 'default_value', + "default_value", config_variable.data, - "Config_varialbe.data should have a key 'default_value'") + "Config_varialbe.data should have a key 'default_value'", + ) self.assertEqual( - config_variable.data['default_value'], - 'test_default_value', + config_variable.data["default_value"], + "test_default_value", "The value of config_variable.data['default_value'] should be the same " - "as set as second argument of ConfigVariable()") + "as set as second argument of ConfigVariable()", + ) class TestConfigHandler(TestCase): - @patch('openslides.core.config.ConfigHandler.save_default_values') + @patch("openslides.core.config.ConfigHandler.save_default_values") def test_get_not_found(self, mock_save_default_values): self.assertRaises( - ConfigNotFound, - config.__getitem__, - 'key_leehah4Sho4ee7aCohbn') + ConfigNotFound, config.__getitem__, "key_leehah4Sho4ee7aCohbn" + ) diff --git a/tests/unit/core/test_views.py b/tests/unit/core/test_views.py index e0fb2c7ea..9dc7b6223 100644 --- a/tests/unit/core/test_views.py +++ b/tests/unit/core/test_views.py @@ -5,7 +5,7 @@ from openslides.core import views from openslides.utils.rest_api import ValidationError -@patch('openslides.core.views.ProjectorViewSet.get_object') +@patch("openslides.core.views.ProjectorViewSet.get_object") class ProjectorAPI(TestCase): def setUp(self): self.viewset = views.ProjectorViewSet() @@ -13,22 +13,26 @@ class ProjectorAPI(TestCase): def test_activate_elements_no_list(self, mock_object): mock_object.return_value.config = { - '3979c9fc3bee432fb25f354d6b4868b3': { - 'name': 'test_projector_element_ahshaiTie8xie3eeThu9', - 'test_key_ohwa7ooze2angoogieM9': 'test_value_raiL2ohsheij1seiqua5'}} + "3979c9fc3bee432fb25f354d6b4868b3": { + "name": "test_projector_element_ahshaiTie8xie3eeThu9", + "test_key_ohwa7ooze2angoogieM9": "test_value_raiL2ohsheij1seiqua5", + } + } request = MagicMock() - request.data = {'name': 'new_test_projector_element_buuDohphahWeeR2eeQu0'} + request.data = {"name": "new_test_projector_element_buuDohphahWeeR2eeQu0"} self.viewset.request = request with self.assertRaises(ValidationError): self.viewset.activate_elements(request=request, pk=MagicMock()) def test_activate_elements_bad_element(self, mock_object): mock_object.return_value.config = { - '374000ee236a41e09cce22ffad29b455': { - 'name': 'test_projector_element_ieroa7eu3aechaip3eeD', - 'test_key_mie3Eeroh9rooKeinga6': 'test_value_gee1Uitae6aithaiphoo'}} + "374000ee236a41e09cce22ffad29b455": { + "name": "test_projector_element_ieroa7eu3aechaip3eeD", + "test_key_mie3Eeroh9rooKeinga6": "test_value_gee1Uitae6aithaiphoo", + } + } request = MagicMock() - request.data = [{'bad_quangah1ahoo6oKaeBai': 'value_doh8ahwe0Zooc1eefu0o'}] + request.data = [{"bad_quangah1ahoo6oKaeBai": "value_doh8ahwe0Zooc1eefu0o"}] self.viewset.request = request with self.assertRaises(ValidationError): self.viewset.activate_elements(request=request, pk=MagicMock()) diff --git a/tests/unit/core/test_websocket.py b/tests/unit/core/test_websocket.py index 9cf3992b8..7ad28e25d 100644 --- a/tests/unit/core/test_websocket.py +++ b/tests/unit/core/test_websocket.py @@ -6,24 +6,15 @@ from openslides.utils.websocket import schema def test_notify_schema_validation(): # This raises a validaten error if it fails - message = { - 'id': 'test-message', - 'type': 'notify', - 'content': [{ - 'users': [5], - }] - } + message = {"id": "test-message", "type": "notify", "content": [{"users": [5]}]} jsonschema.validate(message, schema) def test_notify_schema_invalid_str_in_list(): message = { - 'type': 'notify', - 'content': [ - {}, - 'testmessage' - ], - 'id': 'test_send_invalid_notify_str_in_list', + "type": "notify", + "content": [{}, "testmessage"], + "id": "test_send_invalid_notify_str_in_list", } with pytest.raises(jsonschema.ValidationError): jsonschema.validate(message, schema) @@ -31,9 +22,9 @@ def test_notify_schema_invalid_str_in_list(): def test_notify_schema_invalid_no_elements(): message = { - 'type': 'notify', - 'content': [], - 'id': 'test_send_invalid_notify_str_in_list', + "type": "notify", + "content": [], + "id": "test_send_invalid_notify_str_in_list", } with pytest.raises(jsonschema.ValidationError): jsonschema.validate(message, schema) @@ -41,9 +32,9 @@ def test_notify_schema_invalid_no_elements(): def test_notify_schema_invalid_not_a_list(): message = { - 'type': 'notify', - 'content': {'testmessage': 'foobar, what else.'}, - 'id': 'test_send_invalid_notify_str_in_list', + "type": "notify", + "content": {"testmessage": "foobar, what else."}, + "id": "test_send_invalid_notify_str_in_list", } with pytest.raises(jsonschema.ValidationError): jsonschema.validate(message, schema) diff --git a/tests/unit/motions/test_models.py b/tests/unit/motions/test_models.py index 42fb72d04..992fc80cc 100644 --- a/tests/unit/motions/test_models.py +++ b/tests/unit/motions/test_models.py @@ -19,23 +19,31 @@ class MotionChangeRecommendationTest(TestCase): new_recommendation1 = MotionChangeRecommendation() new_recommendation1.line_from = 3 new_recommendation1.line_to = 5 - collides = new_recommendation1.collides_with_other_recommendation(other_recommendations) + collides = new_recommendation1.collides_with_other_recommendation( + other_recommendations + ) self.assertFalse(collides) new_recommendation2 = MotionChangeRecommendation() new_recommendation2.line_from = 3 new_recommendation2.line_to = 6 - collides = new_recommendation2.collides_with_other_recommendation(other_recommendations) + collides = new_recommendation2.collides_with_other_recommendation( + other_recommendations + ) self.assertTrue(collides) new_recommendation3 = MotionChangeRecommendation() new_recommendation3.line_from = 6 new_recommendation3.line_to = 8 - collides = new_recommendation3.collides_with_other_recommendation(other_recommendations) + collides = new_recommendation3.collides_with_other_recommendation( + other_recommendations + ) self.assertTrue(collides) new_recommendation4 = MotionChangeRecommendation() new_recommendation4.line_from = 7 new_recommendation4.line_to = 9 - collides = new_recommendation4.collides_with_other_recommendation(other_recommendations) + collides = new_recommendation4.collides_with_other_recommendation( + other_recommendations + ) self.assertFalse(collides) diff --git a/tests/unit/motions/test_views.py b/tests/unit/motions/test_views.py index f42638c82..5583c556f 100644 --- a/tests/unit/motions/test_views.py +++ b/tests/unit/motions/test_views.py @@ -8,6 +8,7 @@ class MotionViewSetUpdate(TestCase): """ Tests update view of MotionViewSet. """ + def setUp(self): self.request = MagicMock() self.view_instance = MotionViewSet() @@ -17,9 +18,9 @@ class MotionViewSetUpdate(TestCase): self.view_instance.get_serializer = get_serializer_mock = MagicMock() get_serializer_mock.return_value = self.mock_serializer = MagicMock() - @patch('openslides.motions.views.inform_changed_data') - @patch('openslides.motions.views.has_perm') - @patch('openslides.motions.views.config') + @patch("openslides.motions.views.inform_changed_data") + @patch("openslides.motions.views.has_perm") + @patch("openslides.motions.views.config") def test_simple_update(self, mock_config, mock_has_perm, mock_icd): self.request.user = MagicMock() self.request.user.pk = 1 diff --git a/tests/unit/users/test_models.py b/tests/unit/users/test_models.py index 59ab834df..58da44c02 100644 --- a/tests/unit/users/test_models.py +++ b/tests/unit/users/test_models.py @@ -14,23 +14,27 @@ class UserManagerTest(TestCase): user = MagicMock() user_manager = UserManager() user_manager.model = MagicMock(return_value=user) - user_manager._db = 'my_test_db' + user_manager._db = "my_test_db" - return_user = user_manager.create_user('test_username', 'test_password', test_kwarg='test_kwarg') + return_user = user_manager.create_user( + "test_username", "test_password", test_kwarg="test_kwarg" + ) - user_manager.model.assert_called_once_with(username='test_username', test_kwarg='test_kwarg') - user.set_password.assert_called_once_with('test_password') - user.save.assert_called_once_with(using='my_test_db', skip_autoupdate=False) + user_manager.model.assert_called_once_with( + username="test_username", test_kwarg="test_kwarg" + ) + user.set_password.assert_called_once_with("test_password") + user.save.assert_called_once_with(using="my_test_db", skip_autoupdate=False) self.assertEqual( - return_user, - user, - "The returned user is not the created user.") + return_user, user, "The returned user is not the created user." + ) class UserManagerGenerateUsername(TestCase): """ Tests for the manager method generate_username. """ + def setUp(self): self.exists_mock = MagicMock() self.filter_mock = MagicMock(return_value=self.exists_mock) @@ -41,87 +45,96 @@ class UserManagerGenerateUsername(TestCase): self.exists_mock.exists.return_value = False self.assertEqual( - self.manager.generate_username('wiaf9eecu9mooJiZ3Lah', 'ieHaVe9ci7mooPhe0AuY'), - 'wiaf9eecu9mooJiZ3Lah ieHaVe9ci7mooPhe0AuY') + self.manager.generate_username( + "wiaf9eecu9mooJiZ3Lah", "ieHaVe9ci7mooPhe0AuY" + ), + "wiaf9eecu9mooJiZ3Lah ieHaVe9ci7mooPhe0AuY", + ) def test_unstripped_strings(self): self.exists_mock.exists.return_value = False self.assertEqual( - self.manager.generate_username('ouYeuwai0pheukeeShah ', ' Waefa8gahj8ohRaeroca\n'), - 'ouYeuwai0pheukeeShah Waefa8gahj8ohRaeroca', - "The returned value should only have one whitespace between the " - "names.") + self.manager.generate_username( + "ouYeuwai0pheukeeShah ", " Waefa8gahj8ohRaeroca\n" + ), + "ouYeuwai0pheukeeShah Waefa8gahj8ohRaeroca", + "The returned value should only have one whitespace between the " "names.", + ) def test_empty_second_string(self): self.exists_mock.exists.return_value = False self.assertEqual( - self.manager.generate_username('foobar', ''), - 'foobar', - "The returned value should not have whitespaces at the end.") + self.manager.generate_username("foobar", ""), + "foobar", + "The returned value should not have whitespaces at the end.", + ) def test_empty_first_string(self): self.exists_mock.exists.return_value = False self.assertEqual( - self.manager.generate_username('', 'foobar'), - 'foobar', - "The returned value should not have whitespaces at the beginning.") + self.manager.generate_username("", "foobar"), + "foobar", + "The returned value should not have whitespaces at the beginning.", + ) def test_two_empty_strings(self): self.exists_mock.exists.return_value = False - with self.assertRaises(ValueError, - msg="A ValueError should be raised."): - self.manager.generate_username('', '') + with self.assertRaises(ValueError, msg="A ValueError should be raised."): + self.manager.generate_username("", "") def test_used_username(self): self.exists_mock.exists.side_effect = (True, False) self.assertEqual( - self.manager.generate_username('user', 'name'), - 'user name 1', - "If the username already exists, a number should be added to the " - "name.") + self.manager.generate_username("user", "name"), + "user name 1", + "If the username already exists, a number should be added to the " "name.", + ) def test_two_used_username(self): self.exists_mock.exists.side_effect = (True, True, False) self.assertEqual( - self.manager.generate_username('user', 'name'), - 'user name 2', + self.manager.generate_username("user", "name"), + "user name 2", "If the username with a number already exists, a higher number " - "should be added to the name.") + "should be added to the name.", + ) def test_umlauts(self): self.exists_mock.exists.return_value = False self.assertEqual( - self.manager.generate_username('äöü', 'ßüäö'), - 'äöü ßüäö', - "The method gen_username has also to work with umlauts.") + self.manager.generate_username("äöü", "ßüäö"), + "äöü ßüäö", + "The method gen_username has also to work with umlauts.", + ) -@patch('openslides.users.models.choice') +@patch("openslides.users.models.choice") class UserManagerGeneratePassword(TestCase): def test_normal(self, mock_choice): """ Test normal run of the method. """ - mock_choice.side_effect = tuple('test_password') + mock_choice.side_effect = tuple("test_password") - self.assertEqual( - UserManager().generate_password(), - 'test_pas') + self.assertEqual(UserManager().generate_password(), "test_pas") # choice has to be called 8 times mock_choice.assert_has_calls( - [call("abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") - for _ in range(8)]) + [ + call("abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") + for _ in range(8) + ] + ) -@patch('openslides.users.models.Permission') -@patch('openslides.users.models.Group') +@patch("openslides.users.models.Permission") +@patch("openslides.users.models.Group") class UserManagerCreateOrResetAdminUser(TestCase): def test_add_admin_group(self, mock_group, mock_permission): """ @@ -133,7 +146,9 @@ class UserManagerCreateOrResetAdminUser(TestCase): manager.create_or_reset_admin_user() - admin_user.groups.add.assert_called_once_with(2) # the admin should be added to the admin group with pk=2 + admin_user.groups.add.assert_called_once_with( + 2 + ) # the admin should be added to the admin group with pk=2 def test_password_set_to_admin(self, mock_group, mock_permission): """ @@ -149,12 +164,10 @@ class UserManagerCreateOrResetAdminUser(TestCase): manager.create_or_reset_admin_user() - self.assertEqual( - admin_user.default_password, - 'admin') + self.assertEqual(admin_user.default_password, "admin") admin_user.save.assert_called_once_with(skip_autoupdate=True) - @patch('openslides.users.models.User') + @patch("openslides.users.models.User") def test_return_value(self, mock_user, mock_group, mock_permission): """ Tests that the method returns True when a user is created. @@ -171,14 +184,15 @@ class UserManagerCreateOrResetAdminUser(TestCase): manager.create_or_reset_admin_user(), True, "The method create_or_reset_admin_user should return True when a " - "new user is created.") + "new user is created.", + ) - @patch('openslides.users.models.User') + @patch("openslides.users.models.User") def test_attributes_of_created_user(self, mock_user, mock_group, mock_permission): """ Tests username and last_name of the created admin user. """ - admin_user = MagicMock(username='admin', last_name='Administrator') + admin_user = MagicMock(username="admin", last_name="Administrator") manager = UserManager() manager.get = MagicMock(side_effect=ObjectDoesNotExist) manager.model = mock_user @@ -191,9 +205,11 @@ class UserManagerCreateOrResetAdminUser(TestCase): self.assertEqual( admin_user.username, - 'admin', - "The username of a new created admin should be 'admin'.") + "admin", + "The username of a new created admin should be 'admin'.", + ) self.assertEqual( admin_user.last_name, - 'Administrator', - "The last_name of a new created admin should be 'Administrator'.") + "Administrator", + "The last_name of a new created admin should be 'Administrator'.", + ) diff --git a/tests/unit/users/test_serializers.py b/tests/unit/users/test_serializers.py index 1b2655a6e..57d15a507 100644 --- a/tests/unit/users/test_serializers.py +++ b/tests/unit/users/test_serializers.py @@ -16,27 +16,27 @@ class UserCreateUpdateSerializerTest(TestCase): with self.assertRaises(ValidationError): serializer.validate(data) - @patch('openslides.users.serializers.User.objects.generate_username') + @patch("openslides.users.serializers.User.objects.generate_username") def test_validate_no_username(self, generate_username): """ Tests, that an empty username is generated. """ - generate_username.return_value = 'test_value' + generate_username.return_value = "test_value" serializer = UserFullSerializer() - data = {'first_name': 'TestName'} + data = {"first_name": "TestName"} new_data = serializer.validate(data) - self.assertEqual(new_data['username'], 'test_value') + self.assertEqual(new_data["username"], "test_value") def test_validate_no_username_in_patch_request(self): """ Tests, that an empty username is not set in a patch request context. """ - view = MagicMock(action='partial_update') - serializer = UserFullSerializer(context={'view': view}) - data = {'first_name': 'TestName'} + view = MagicMock(action="partial_update") + serializer = UserFullSerializer(context={"view": view}) + data = {"first_name": "TestName"} new_data = serializer.validate(data) - self.assertIsNone(new_data.get('username')) + self.assertIsNone(new_data.get("username")) diff --git a/tests/unit/utils/cache_provider.py b/tests/unit/utils/cache_provider.py index 13a8c3aa8..7684c0914 100644 --- a/tests/unit/utils/cache_provider.py +++ b/tests/unit/utils/cache_provider.py @@ -12,41 +12,43 @@ def restrict_elements(elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: for element in elements: restricted_element = {} for key, value in element.items(): - if key == 'id': + if key == "id": restricted_element[key] = value else: - restricted_element[key] = 'restricted_{}'.format(value) + restricted_element[key] = "restricted_{}".format(value) out.append(restricted_element) return out class Collection1: def get_collection_string(self) -> str: - return 'app/collection1' + return "app/collection1" def get_elements(self) -> List[Dict[str, Any]]: - return [ - {'id': 1, 'value': 'value1'}, - {'id': 2, 'value': 'value2'}] + return [{"id": 1, "value": "value1"}, {"id": 2, "value": "value2"}] - async def restrict_elements(self, user_id: int, elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + async def restrict_elements( + self, user_id: int, elements: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: return restrict_elements(elements) class Collection2: def get_collection_string(self) -> str: - return 'app/collection2' + return "app/collection2" def get_elements(self) -> List[Dict[str, Any]]: - return [ - {'id': 1, 'key': 'value1'}, - {'id': 2, 'key': 'value2'}] + return [{"id": 1, "key": "value1"}, {"id": 2, "key": "value2"}] - async def restrict_elements(self, user_id: int, elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + async def restrict_elements( + self, user_id: int, elements: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: return restrict_elements(elements) -def get_cachable_provider(cachables: List[Cachable] = [Collection1(), Collection2()]) -> Callable[[], List[Cachable]]: +def get_cachable_provider( + cachables: List[Cachable] = [Collection1(), Collection2()] +) -> Callable[[], List[Cachable]]: """ Returns a cachable_provider. """ @@ -55,12 +57,9 @@ def get_cachable_provider(cachables: List[Cachable] = [Collection1(), Collection def example_data(): return { - 'app/collection1': [ - {'id': 1, 'value': 'value1'}, - {'id': 2, 'value': 'value2'}], - 'app/collection2': [ - {'id': 1, 'key': 'value1'}, - {'id': 2, 'key': 'value2'}]} + "app/collection1": [{"id": 1, "value": "value1"}, {"id": 2, "value": "value2"}], + "app/collection2": [{"id": 1, "key": "value1"}, {"id": 2, "key": "value2"}], + } class TTestCacheProvider(MemmoryCacheProvider): @@ -69,11 +68,15 @@ class TTestCacheProvider(MemmoryCacheProvider): testing. """ - async def del_lock_after_wait(self, lock_name: str, future: asyncio.Future = None) -> None: + async def del_lock_after_wait( + self, lock_name: str, future: asyncio.Future = None + ) -> None: if future is None: asyncio.ensure_future(self.del_lock(lock_name)) else: + async def set_future() -> None: await self.del_lock(lock_name) future.set_result(1) # type: ignore + asyncio.ensure_future(set_future()) diff --git a/tests/unit/utils/test_cache.py b/tests/unit/utils/test_cache.py index a96bdb8fe..f9f08e947 100644 --- a/tests/unit/utils/test_cache.py +++ b/tests/unit/utils/test_cache.py @@ -6,11 +6,7 @@ import pytest from openslides.utils.cache import ElementCache -from .cache_provider import ( - TTestCacheProvider, - example_data, - get_cachable_provider, -) +from .cache_provider import TTestCacheProvider, example_data, get_cachable_provider def decode_dict(encoded_dict: Dict[str, str]) -> Dict[str, Any]: @@ -20,11 +16,15 @@ def decode_dict(encoded_dict: Dict[str, str]) -> Dict[str, Any]: return {key: json.loads(value) for key, value in encoded_dict.items()} -def sort_dict(encoded_dict: Dict[str, List[Dict[str, Any]]]) -> Dict[str, List[Dict[str, Any]]]: +def sort_dict( + encoded_dict: Dict[str, List[Dict[str, Any]]] +) -> Dict[str, List[Dict[str, Any]]]: """ Helper function that sorts the value of a dict. """ - return {key: sorted(value, key=lambda x: x['id']) for key, value in encoded_dict.items()} + return { + key: sorted(value, key=lambda x: x["id"]) for key, value in encoded_dict.items() + } @pytest.fixture @@ -32,7 +32,8 @@ def element_cache(): element_cache = ElementCache( cache_provider_class=TTestCacheProvider, cachable_provider=get_cachable_provider(), - start_time=0) + start_time=0, + ) element_cache.ensure_cache() return element_cache @@ -40,52 +41,65 @@ def element_cache(): @pytest.mark.asyncio async def test_change_elements(element_cache): input_data = { - 'app/collection1:1': {"id": 1, "value": "updated"}, - 'app/collection1:2': {"id": 2, "value": "new"}, - 'app/collection2:1': {"id": 1, "key": "updated"}, - 'app/collection2:2': None} + "app/collection1:1": {"id": 1, "value": "updated"}, + "app/collection1:2": {"id": 2, "value": "new"}, + "app/collection2:1": {"id": 1, "key": "updated"}, + "app/collection2:2": None, + } element_cache.cache_provider.full_data = { - 'app/collection1:1': '{"id": 1, "value": "old"}', - 'app/collection2:1': '{"id": 1, "key": "old"}', - 'app/collection2:2': '{"id": 2, "key": "old"}'} + "app/collection1:1": '{"id": 1, "value": "old"}', + "app/collection2:1": '{"id": 1, "key": "old"}', + "app/collection2:2": '{"id": 2, "key": "old"}', + } result = await element_cache.change_elements(input_data) assert result == 1 # first change_id - assert decode_dict(element_cache.cache_provider.full_data) == decode_dict({ - 'app/collection1:1': '{"id": 1, "value": "updated"}', - 'app/collection1:2': '{"id": 2, "value": "new"}', - 'app/collection2:1': '{"id": 1, "key": "updated"}'}) + assert decode_dict(element_cache.cache_provider.full_data) == decode_dict( + { + "app/collection1:1": '{"id": 1, "value": "updated"}', + "app/collection1:2": '{"id": 2, "value": "new"}', + "app/collection2:1": '{"id": 1, "key": "updated"}', + } + ) assert element_cache.cache_provider.change_id_data == { 1: { - 'app/collection1:1', - 'app/collection1:2', - 'app/collection2:1', - 'app/collection2:2'}} + "app/collection1:1", + "app/collection1:2", + "app/collection2:1", + "app/collection2:2", + } + } @pytest.mark.asyncio async def test_change_elements_with_no_data_in_redis(element_cache): input_data = { - 'app/collection1:1': {"id": 1, "value": "updated"}, - 'app/collection1:2': {"id": 2, "value": "new"}, - 'app/collection2:1': {"id": 1, "key": "updated"}, - 'app/collection2:2': None} + "app/collection1:1": {"id": 1, "value": "updated"}, + "app/collection1:2": {"id": 2, "value": "new"}, + "app/collection2:1": {"id": 1, "key": "updated"}, + "app/collection2:2": None, + } result = await element_cache.change_elements(input_data) assert result == 1 # first change_id - assert decode_dict(element_cache.cache_provider.full_data) == decode_dict({ - 'app/collection1:1': '{"id": 1, "value": "updated"}', - 'app/collection1:2': '{"id": 2, "value": "new"}', - 'app/collection2:1': '{"id": 1, "key": "updated"}'}) + assert decode_dict(element_cache.cache_provider.full_data) == decode_dict( + { + "app/collection1:1": '{"id": 1, "value": "updated"}', + "app/collection1:2": '{"id": 2, "value": "new"}', + "app/collection2:1": '{"id": 1, "key": "updated"}', + } + ) assert element_cache.cache_provider.change_id_data == { 1: { - 'app/collection1:1', - 'app/collection1:2', - 'app/collection2:1', - 'app/collection2:2'}} + "app/collection1:1", + "app/collection1:2", + "app/collection2:1", + "app/collection2:2", + } + } @pytest.mark.asyncio @@ -94,20 +108,24 @@ async def test_get_all_full_data_from_db(element_cache): assert result == example_data() # Test that elements are written to redis - assert decode_dict(element_cache.cache_provider.full_data) == decode_dict({ - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'}) + assert decode_dict(element_cache.cache_provider.full_data) == decode_dict( + { + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } + ) @pytest.mark.asyncio async def test_get_all_full_data_from_redis(element_cache): element_cache.cache_provider.full_data = { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'} + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } result = await element_cache.get_all_full_data() @@ -118,10 +136,11 @@ async def test_get_all_full_data_from_redis(element_cache): @pytest.mark.asyncio async def test_get_full_data_change_id_0(element_cache): element_cache.cache_provider.full_data = { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'} + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } result = await element_cache.get_full_data(0) @@ -131,12 +150,12 @@ async def test_get_full_data_change_id_0(element_cache): @pytest.mark.asyncio async def test_get_full_data_change_id_lower_then_in_redis(element_cache): element_cache.cache_provider.full_data = { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'} - element_cache.cache_provider.change_id_data = { - 2: {'app/collection1:1'}} + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } + element_cache.cache_provider.change_id_data = {2: {"app/collection1:1"}} with pytest.raises(RuntimeError): await element_cache.get_full_data(1) @@ -144,30 +163,35 @@ async def test_get_full_data_change_id_lower_then_in_redis(element_cache): @pytest.mark.asyncio async def test_get_full_data_change_id_data_in_redis(element_cache): element_cache.cache_provider.full_data = { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'} + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } element_cache.cache_provider.change_id_data = { - 1: {'app/collection1:1', 'app/collection1:3'}} + 1: {"app/collection1:1", "app/collection1:3"} + } result = await element_cache.get_full_data(1) assert result == ( - {'app/collection1': [{"id": 1, "value": "value1"}]}, - ['app/collection1:3']) + {"app/collection1": [{"id": 1, "value": "value1"}]}, + ["app/collection1:3"], + ) @pytest.mark.asyncio async def test_get_full_data_change_id_data_in_db(element_cache): element_cache.cache_provider.change_id_data = { - 1: {'app/collection1:1', 'app/collection1:3'}} + 1: {"app/collection1:1", "app/collection1:3"} + } result = await element_cache.get_full_data(1) assert result == ( - {'app/collection1': [{"id": 1, "value": "value1"}]}, - ['app/collection1:3']) + {"app/collection1": [{"id": 1, "value": "value1"}]}, + ["app/collection1:3"], + ) @pytest.mark.asyncio @@ -178,14 +202,14 @@ async def test_get_full_data_change_id_data_in_db_empty_change_id(element_cache) @pytest.mark.asyncio async def test_get_element_full_data_empty_redis(element_cache): - result = await element_cache.get_element_full_data('app/collection1', 1) + result = await element_cache.get_element_full_data("app/collection1", 1) - assert result == {'id': 1, 'value': 'value1'} + assert result == {"id": 1, "value": "value1"} @pytest.mark.asyncio async def test_get_element_full_data_empty_redis_does_not_exist(element_cache): - result = await element_cache.get_element_full_data('app/collection1', 3) + result = await element_cache.get_element_full_data("app/collection1", 3) assert result is None @@ -193,24 +217,28 @@ async def test_get_element_full_data_empty_redis_does_not_exist(element_cache): @pytest.mark.asyncio async def test_get_element_full_data_full_redis(element_cache): element_cache.cache_provider.full_data = { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'} + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } - result = await element_cache.get_element_full_data('app/collection1', 1) + result = await element_cache.get_element_full_data("app/collection1", 1) - assert result == {'id': 1, 'value': 'value1'} + assert result == {"id": 1, "value": "value1"} @pytest.mark.asyncio async def test_exists_restricted_data(element_cache): element_cache.use_restricted_data_cache = True - element_cache.cache_provider.restricted_data = {0: { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'}} + element_cache.cache_provider.restricted_data = { + 0: { + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } + } result = await element_cache.exists_restricted_data(0) @@ -220,11 +248,14 @@ async def test_exists_restricted_data(element_cache): @pytest.mark.asyncio async def test_exists_restricted_data_do_not_use_restricted_data(element_cache): element_cache.use_restricted_data_cache = False - element_cache.cache_provider.restricted_data = {0: { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'}} + element_cache.cache_provider.restricted_data = { + 0: { + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } + } result = await element_cache.exists_restricted_data(0) @@ -234,11 +265,14 @@ async def test_exists_restricted_data_do_not_use_restricted_data(element_cache): @pytest.mark.asyncio async def test_del_user(element_cache): element_cache.use_restricted_data_cache = True - element_cache.cache_provider.restricted_data = {0: { - 'app/collection1:1': '{"id": 1, "value": "value1"}', - 'app/collection1:2': '{"id": 2, "value": "value2"}', - 'app/collection2:1': '{"id": 1, "key": "value1"}', - 'app/collection2:2': '{"id": 2, "key": "value2"}'}} + element_cache.cache_provider.restricted_data = { + 0: { + "app/collection1:1": '{"id": 1, "value": "value1"}', + "app/collection1:2": '{"id": 2, "value": "value2"}', + "app/collection2:1": '{"id": 1, "key": "value1"}', + "app/collection2:2": '{"id": 2, "key": "value2"}', + } + } await element_cache.del_user(0) @@ -260,12 +294,15 @@ async def test_update_restricted_data(element_cache): await element_cache.update_restricted_data(0) - assert decode_dict(element_cache.cache_provider.restricted_data[0]) == decode_dict({ - 'app/collection1:1': '{"id": 1, "value": "restricted_value1"}', - 'app/collection1:2': '{"id": 2, "value": "restricted_value2"}', - 'app/collection2:1': '{"id": 1, "key": "restricted_value1"}', - 'app/collection2:2': '{"id": 2, "key": "restricted_value2"}', - '_config:change_id': '0'}) + assert decode_dict(element_cache.cache_provider.restricted_data[0]) == decode_dict( + { + "app/collection1:1": '{"id": 1, "value": "restricted_value1"}', + "app/collection1:2": '{"id": 2, "value": "restricted_value2"}', + "app/collection2:1": '{"id": 1, "key": "restricted_value1"}', + "app/collection2:2": '{"id": 2, "key": "restricted_value2"}', + "_config:change_id": "0", + } + ) # Make sure the lock is deleted assert not await element_cache.cache_provider.get_lock("restricted_data_0") # And the future is done @@ -284,49 +321,46 @@ async def test_update_restricted_data_disabled_restricted_data(element_cache): @pytest.mark.asyncio async def test_update_restricted_data_to_low_change_id(element_cache): element_cache.use_restricted_data_cache = True - element_cache.cache_provider.restricted_data[0] = { - '_config:change_id': '1'} - element_cache.cache_provider.change_id_data = { - 3: {'app/collection1:1'}} + element_cache.cache_provider.restricted_data[0] = {"_config:change_id": "1"} + element_cache.cache_provider.change_id_data = {3: {"app/collection1:1"}} await element_cache.update_restricted_data(0) - assert decode_dict(element_cache.cache_provider.restricted_data[0]) == decode_dict({ - 'app/collection1:1': '{"id": 1, "value": "restricted_value1"}', - 'app/collection1:2': '{"id": 2, "value": "restricted_value2"}', - 'app/collection2:1': '{"id": 1, "key": "restricted_value1"}', - 'app/collection2:2': '{"id": 2, "key": "restricted_value2"}', - '_config:change_id': '3'}) + assert decode_dict(element_cache.cache_provider.restricted_data[0]) == decode_dict( + { + "app/collection1:1": '{"id": 1, "value": "restricted_value1"}', + "app/collection1:2": '{"id": 2, "value": "restricted_value2"}', + "app/collection2:1": '{"id": 1, "key": "restricted_value1"}', + "app/collection2:2": '{"id": 2, "key": "restricted_value2"}', + "_config:change_id": "3", + } + ) @pytest.mark.asyncio async def test_update_restricted_data_with_same_id(element_cache): element_cache.use_restricted_data_cache = True - element_cache.cache_provider.restricted_data[0] = { - '_config:change_id': '1'} - element_cache.cache_provider.change_id_data = { - 1: {'app/collection1:1'}} + element_cache.cache_provider.restricted_data[0] = {"_config:change_id": "1"} + element_cache.cache_provider.change_id_data = {1: {"app/collection1:1"}} await element_cache.update_restricted_data(0) # Same id means, there is nothing to do - assert element_cache.cache_provider.restricted_data[0] == { - '_config:change_id': '1'} + assert element_cache.cache_provider.restricted_data[0] == {"_config:change_id": "1"} @pytest.mark.asyncio async def test_update_restricted_data_with_deleted_elements(element_cache): element_cache.use_restricted_data_cache = True element_cache.cache_provider.restricted_data[0] = { - 'app/collection1:3': '{"id": 1, "value": "restricted_value1"}', - '_config:change_id': '1'} - element_cache.cache_provider.change_id_data = { - 2: {'app/collection1:3'}} + "app/collection1:3": '{"id": 1, "value": "restricted_value1"}', + "_config:change_id": "1", + } + element_cache.cache_provider.change_id_data = {2: {"app/collection1:3"}} await element_cache.update_restricted_data(0) - assert element_cache.cache_provider.restricted_data[0] == { - '_config:change_id': '2'} + assert element_cache.cache_provider.restricted_data[0] == {"_config:change_id": "2"} @pytest.mark.asyncio @@ -373,9 +407,18 @@ async def test_get_all_restricted_data(element_cache): result = await element_cache.get_all_restricted_data(0) - assert sort_dict(result) == sort_dict({ - 'app/collection1': [{"id": 1, "value": "restricted_value1"}, {"id": 2, "value": "restricted_value2"}], - 'app/collection2': [{"id": 1, "key": "restricted_value1"}, {"id": 2, "key": "restricted_value2"}]}) + assert sort_dict(result) == sort_dict( + { + "app/collection1": [ + {"id": 1, "value": "restricted_value1"}, + {"id": 2, "value": "restricted_value2"}, + ], + "app/collection2": [ + {"id": 1, "key": "restricted_value1"}, + {"id": 2, "key": "restricted_value2"}, + ], + } + ) @pytest.mark.asyncio @@ -383,9 +426,18 @@ async def test_get_all_restricted_data_disabled_restricted_data_cache(element_ca element_cache.use_restricted_data_cache = False result = await element_cache.get_all_restricted_data(0) - assert sort_dict(result) == sort_dict({ - 'app/collection1': [{"id": 1, "value": "restricted_value1"}, {"id": 2, "value": "restricted_value2"}], - 'app/collection2': [{"id": 1, "key": "restricted_value1"}, {"id": 2, "key": "restricted_value2"}]}) + assert sort_dict(result) == sort_dict( + { + "app/collection1": [ + {"id": 1, "value": "restricted_value1"}, + {"id": 2, "value": "restricted_value2"}, + ], + "app/collection2": [ + {"id": 1, "key": "restricted_value1"}, + {"id": 2, "key": "restricted_value2"}, + ], + } + ) @pytest.mark.asyncio @@ -394,27 +446,39 @@ async def test_get_restricted_data_change_id_0(element_cache): result = await element_cache.get_restricted_data(0, 0) - assert sort_dict(result[0]) == sort_dict({ - 'app/collection1': [{"id": 1, "value": "restricted_value1"}, {"id": 2, "value": "restricted_value2"}], - 'app/collection2': [{"id": 1, "key": "restricted_value1"}, {"id": 2, "key": "restricted_value2"}]}) + assert sort_dict(result[0]) == sort_dict( + { + "app/collection1": [ + {"id": 1, "value": "restricted_value1"}, + {"id": 2, "value": "restricted_value2"}, + ], + "app/collection2": [ + {"id": 1, "key": "restricted_value1"}, + {"id": 2, "key": "restricted_value2"}, + ], + } + ) @pytest.mark.asyncio async def test_get_restricted_data_disabled_restricted_data_cache(element_cache): element_cache.use_restricted_data_cache = False - element_cache.cache_provider.change_id_data = {1: {'app/collection1:1', 'app/collection1:3'}} + element_cache.cache_provider.change_id_data = { + 1: {"app/collection1:1", "app/collection1:3"} + } result = await element_cache.get_restricted_data(0, 1) assert result == ( - {'app/collection1': [{"id": 1, "value": "restricted_value1"}]}, - ['app/collection1:3']) + {"app/collection1": [{"id": 1, "value": "restricted_value1"}]}, + ["app/collection1:3"], + ) @pytest.mark.asyncio async def test_get_restricted_data_change_id_lower_then_in_redis(element_cache): element_cache.use_restricted_data_cache = True - element_cache.cache_provider.change_id_data = {2: {'app/collection1:1'}} + element_cache.cache_provider.change_id_data = {2: {"app/collection1:1"}} with pytest.raises(RuntimeError): await element_cache.get_restricted_data(0, 1) @@ -423,19 +487,26 @@ async def test_get_restricted_data_change_id_lower_then_in_redis(element_cache): @pytest.mark.asyncio async def test_get_restricted_data_change_with_id(element_cache): element_cache.use_restricted_data_cache = True - element_cache.cache_provider.change_id_data = {2: {'app/collection1:1'}} + element_cache.cache_provider.change_id_data = {2: {"app/collection1:1"}} result = await element_cache.get_restricted_data(0, 2) - assert result == ({'app/collection1': [{"id": 1, "value": "restricted_value1"}]}, []) + assert result == ( + {"app/collection1": [{"id": 1, "value": "restricted_value1"}]}, + [], + ) @pytest.mark.asyncio async def test_lowest_change_id_after_updating_lowest_element(element_cache): - await element_cache.change_elements({'app/collection1:1': {"id": 1, "value": "updated1"}}) + await element_cache.change_elements( + {"app/collection1:1": {"id": 1, "value": "updated1"}} + ) first_lowest_change_id = await element_cache.get_lowest_change_id() # Alter same element again - await element_cache.change_elements({'app/collection1:1': {"id": 1, "value": "updated2"}}) + await element_cache.change_elements( + {"app/collection1:1": {"id": 1, "value": "updated2"}} + ) second_lowest_change_id = await element_cache.get_lowest_change_id() assert first_lowest_change_id == 1 diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 3af9622d8..26ea172e4 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -5,18 +5,18 @@ from openslides.utils import utils def test_to_roman_result(): - assert utils.to_roman(3) == 'III' + assert utils.to_roman(3) == "III" def test_to_roman_none(): - assert utils.to_roman(-3) == '-3' + assert utils.to_roman(-3) == "-3" def test_get_model_from_collection_string_known_app(): - projector_model = utils.get_model_from_collection_string('core/projector') + projector_model = utils.get_model_from_collection_string("core/projector") assert projector_model == Projector def test_get_model_from_collection_string_unknown_app(): with pytest.raises(ValueError): - utils.get_model_from_collection_string('invalid/model') + utils.get_model_from_collection_string("invalid/model") diff --git a/tests/unit/utils/test_validate.py b/tests/unit/utils/test_validate.py index 7ac25390d..77e95d142 100644 --- a/tests/unit/utils/test_validate.py +++ b/tests/unit/utils/test_validate.py @@ -5,7 +5,8 @@ from openslides.utils.validate import validate_html class ValidatorTest(TestCase): def test_XSS_protection(self): - data = 'tuveegi2Ho

tuveegi2Ho

Boovai7esu
ee4Yaiw0ei' + data = "tuveegi2Ho

tuveegi2Ho

Boovai7esu
ee4Yaiw0ei" self.assertEqual( validate_html(data), - 'tuveegi2Ho

tuveegi2Ho<script>kekj9(djwk</script>

Boovai7esu
ee4Yaiw0ei') + "tuveegi2Ho

tuveegi2Ho<script>kekj9(djwk</script>

Boovai7esu
ee4Yaiw0ei", + ) diff --git a/tests/unit/utils/test_views.py b/tests/unit/utils/test_views.py index 21424bc0b..2b011c456 100644 --- a/tests/unit/utils/test_views.py +++ b/tests/unit/utils/test_views.py @@ -8,11 +8,15 @@ class TestAPIView(TestCase): """ Tests that the APIView has all relevant methods """ - http_methods = set(('get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace')) + http_methods = set( + ("get", "post", "put", "patch", "delete", "head", "options", "trace") + ) self.assertTrue( http_methods.issubset(views.APIView.__dict__), - "All http methods should be defined in the APIView") + "All http methods should be defined in the APIView", + ) self.assertFalse( - hasattr(views.APIView, 'method_call'), - "The APIView should not have the method 'method_call'") + hasattr(views.APIView, "method_call"), + "The APIView should not have the method 'method_call'", + )